使用 Frontity 实现弹出菜单

frontity popup menu

前一篇文章中我们介绍了如何创建一个基于 Frontity 的网站,从本篇文章开始,将介绍如何为网站添加新的功能,以及对原 WordPress 做兼容性适配。本篇文章首先介绍如何实现弹出式子菜单,获取的菜单项是点击量最高的文章列表(降序)。

首先增加一个子菜单是否开启的状态标志,在主题的状态中声明一个菜单开关变量,以及显示和关闭菜单的函数,如下:

sample-frontity-project/packages/frontity-chakra-theme/index.js

const chakraTheme = {
   name: "frontity-chakra-theme",
...
   state: {
      isSubMenuPostsOpen: false,  //菜单是否开启的标志
      currentTaxId: 36,  //当前的弹出菜单项所在目录id
      taxIdWithSlug: [  //定义需要弹出菜单项的数组
         {id:4, slug:"most-popular-posts"},  //slug 是获取文章用的 URL
         {id:36, slug:"most-popular-posts"}
      ]
...
    actions: {
        theme: {
            openSubMenuPosts: ({ state }) => {  //显示菜单
                 state.theme.isSubMenuPostsOpen = true;
            },
            closeSubMenuPosts: ({ state }) => {  //关闭菜单
                 state.theme.isSubMenuPostsOpen = false;
            },
...

第二步增加一个获取 Post 的 Handler,该 Handler 当调用 actions.source.fetch(link)时被调用,会到 WordPress 中提取文章数据,并把结果放到 State 中。Handler 中的 pattern 参数是 fetch 文章时的 URL 匹配规则,相应的语法规则请参考说明,参数 aid 定义传入的目录 id,该参数会合并到 params 对象中,同时合并状态中定义的一些初始参数( state.source.params),然后根据 WordPress REST API 相应 Endpoint 的要求加入其它的参数。

sample-frontity-project/packages/frontity-chakra-theme/src/components/handler/popular-post-archive.js

const PopularPostArchive = {
    pattern: "/most-popular-posts/:aid(\\d+)", //如匹配most-popular-posts/36
    name: "most popular post archive",
    priority: 10,
    //调用WordPress REST API获取文章数据
    func: async ({ link, params, state, libraries, force }) => {
      const { api, populate, parse, getTotal, getTotalPages } = libraries.source;
      const { page, query, route } = parse(link);
      const response = await api.get({
        endpoint: "posts",  //返回 post
        params: {
          _embed: true,
          page,
          categories: params.aid,  //post 限定目录id,url中传入
          orderby: 'wpb_post_views_count',  //根据文章点击数排序
          order: "desc",
           ...state.source.params
        },
      })
      const items = await populate({
        response, state, force,
      });
      if (page > 1 && items.length === 0) throw new ServerError(`post archive doesn't have page ${page}`, 404);
      const total = getTotal(response, items.length);
      const totalPages = getTotalPages(response, 0);
      const hasNewerPosts = page < totalPages;
      const hasOlderPosts = page > 1;

      const getPageLink = (page) =>
        libraries.source.stringify({
        route,
        query,
        page,
      });
      const currentPageData = state.source.data[link];
      const newPageData = {
        type: "post",
        items,
        total,
        totalPages,
        isArchive: true,
        isPostArchive: true,
        isPostTypeArchive: true,
        ...(hasOlderPosts && { previous: getPageLink(page - 1) }),
        ...(hasNewerPosts && { next: getPageLink(page + 1) }),
      };
      Object.assign(currentPageData, newPageData); //返回结果,console中可查看
    }
}

export default PopularPostArchive;

需要注意的是 Handler 使用前需要声明一下,可以放在主题的 index.js 中。

sample-frontity-project/packages/frontity-chakra-theme/src/index.js

...
const chakraTheme = {
  name: "frontity-chakra-theme",
...
  libraries: {
    html2react: {
      // Add a processor to html2react so it processes the <img> tags
      // inside the content HTML. You can add your own processors too.
      processors: [image, ...processors]
    },
    source: {  //声明Handler
      handlers: [HomePagePostArchive, PopularPostArchive, PostRelatedArchive, PostFormatArchive]
    }
  }
};

第三步实现菜单数据的预取以及打开/关闭弹出菜单,比如当鼠标滑过主菜单项的时候可以提前到 WordPress 中提取文章数据。这需要到 Link 组件中的 onMouseEnter 事件中增加相关代码。

sample-frontity-project/packages/frontity-chakra-theme/src/components/link.js

import { Box } from "@chakra-ui/react";
import { connect } from "frontity";
import React from "react";
import { omitConnectProps } from "./helpers";

const Link = ({
  state,
  actions,
  link,
  className,
  children,
  rel,
  "aria-current": ariaCurrent,
  ...props
}) => {
  const isDisabled = props["aria-disabled"];
  // If we're not in a frontity environment, let's just render the children
  if (state == null)
    return (
      <a className={className} href={isDisabled ? undefined : "#"} {...props}>
        {children}
      </a>
    );

  // Check if the link is an external or internal link
  const isExternal = link && link.startsWith("http");

  const onClick = event => {
    // Do nothing if it's an external link
    if (isExternal || isDisabled) return;

    event.preventDefault();
    event.stopPropagation();

    // Set the router to the new url.
    actions.router.set(link);

    // Scroll the page to the top
    window.scrollTo(0, 0);

    // if the menu modal is open, close it so it doesn't block rendering
    if (state.theme.isMobileMenuOpen) {
      actions.theme.closeMobileMenu();
    }
    // 当点击主菜单项时关闭弹出菜单
    if(state.theme.isSubMenuPostsOpen) {
      actions.theme.closeSubMenuPosts();
    }

    if (props.onClick) {
      props.onClick(event);
    }
  };

  return (
    <Box
      as="a"
      href={isDisabled ? undefined : link}
      onClick={onClick}
      className={className}
      aria-current={ariaCurrent}
      rel={rel}
      target={isExternal ? "_blank" : undefined}
      onMouseEnter={event => {
        // Prefetch the link's content when the user hovers on the link
        if (!isExternal) {
          if(props.idx) {
            state.theme.taxIdWithSlug.map((item) => {actions.source.fetch(`/${item.slug}/${item.id}`);});  //子菜单数据预取
            if(props.idx==1 || props.idx==2) { //鼠标滑过这两项时显示子菜单
              props.idx==1?(state.theme.currentTaxId = 36)&&actions.theme.openSubMenuPosts():(state.theme.currentTaxId = 4)&&actions.theme.openSubMenuPosts();
            }
            else actions.theme.closeSubMenuPosts();
          }
          actions.source.fetch(link);
        }
        if (props.onMouseEnter) props.onMouseEnter(event);
      }}
      {...omitConnectProps(props)}
    >
      {children}
    </Box>
  );
};

export default connect(Link);

第四步显示/关闭弹出菜单,使用了ChaKra 的 Popover 组件。

sample-frontity-project/packages/frontity-chakra-theme/src/components/menu/submenu-posts.js

import { Box, Popover,  PopoverContent, SimpleGrid, SkeletonText, Flex, Heading } from "@chakra-ui/react";
import React from "react";
import { connect } from "frontity";
import Switch from "@frontity/components/switch";
import Image from "@frontity/components/image";
import { formatPostData } from "../helpers";
import Link from "../link";

const SubMenuPosts = ({state, actions}) => {
    //构造获取子菜单的Url,在第一步中定义了相关的数据
    const link = "/" + state.theme.taxIdWithSlug.filter(function(item){return item && item.id==state.theme.currentTaxId;})[0].slug + "/" + state.theme.currentTaxId;
    //从状态中提取子菜单数据,此时如果没有,会到WordPress后台提取。
    const data = state.source.get(link);
    return (
    <Popover
            isOpen={state.theme.isSubMenuPostsOpen}
            onClose={actions.theme.closeSubMenuPosts}
            autoFocus={true}
            trigger="hover"
    >
        <PopoverContent
            width="70vw"
            pos="fixed"
            top="70px"
            left="50px"
            transition="transform ease .25s"
            maxWidth="100%"
            bg="rgba(251,251,251,0.98)"
            css={{ backdropFilter: "blur(1px)" }}
        >
            <Switch>
                <SkeletonText mt="8" noOfLines={5} spacing="8" when={data.isFetching} />
                <SimpleGrid
                    columns={5} spacing={1}
                    when={data.isReady}
                >
                    {data.items && data.items.map(({ type, id }, index) => {
                        const item = state.source[type][id];
                        const datafmt = formatPostData(state, item);
                        const { title, featured_media, link } = datafmt;
                        const { src, alt, srcSet } = featured_media;
                        return (
                        <Flex
                            direction="column"
                            position="relative"
                            bg="white"
                            as="article"
                            key={index}
                        >
                            {featured_media && featured_media.src && (
                              <Link link={link} >
                                <Box
                                    role="group"
                                    cursor="pointer"
                                    height="90px"
                                    width="100%"
                                    pos="relative"
                                >
                                    <Box
                                        as={Image}
                                        width="900"
                                        height="550"
                                        position="absolute"
                                        boxSize="100%"
                                        objectFit="cover"
                                        top="0"
                                        left="0"
                                        maxWidth="100%"
                                        src={src}
                                        alt={alt}
                                        srcSet={srcSet}
                                    />
                                </Box>
                              </Link>
                            )}
                      
                            <Flex p="10px" flexGrow="1" direction="column">
                              <Heading fontSize="small" as="h6" textTransform="uppercase">
                                <Link link={link} dangerouslySetInnerHTML={{ __html: title }} ></Link>
                              </Heading>
                            </Flex>
                        </Flex>
                        );
                    })}
                </SimpleGrid>
            </Switch>
        </PopoverContent>
    </Popover>
    );
}
export default connect(SubMenuPosts);

最后,在网页的 Head 处理中加入第四步定义的子菜单组件。

sample-frontity-project/packages/frontity-chakra-theme/src/components/header/index.js

import { connect } from "frontity";
import React from "react";
import MainHeader from "./header";
import Navigation from "./navigation";
import SocialNav from "./social-menu";
import { SearchButton, SearchModal, SearchForm } from "../search";

import SubMenuPosts from "../menu/submenu-posts";

const Header = ({ state, actions }) => (
  <MainHeader>
    <Navigation menu={state.theme.menu} />
    {state.theme.showSocialLinks && (
      <SocialNav menu={state.theme.socialLinks} />
    )}
    <SearchButton onClick={actions.theme.openSearchModal} />
    <SearchModal
      isOpen={state.theme.isSearchModalOpen}
      onClose={actions.theme.closeSearchModal}
    >
      <SearchForm />
    </SearchModal>
    //加入弹出子菜单显示组件
    <SubMenuPosts/>
  </MainHeader>
);

export default connect(Header);
frontity popup menu
Frontity 显示弹出菜单

发表评论

邮箱地址不会被公开。 必填项已用*标注