使用 Frontity 实现弹出菜单

frontity popup menu

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

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

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

  1. const chakraTheme = {
  2. name: "frontity-chakra-theme",
  3. ...
  4. state: {
  5. isSubMenuPostsOpen: false, //菜单是否开启的标志
  6. currentTaxId: 36, //当前的弹出菜单项所在目录id
  7. taxIdWithSlug: [ //定义需要弹出菜单项的数组
  8. {id:4, slug:"most-popular-posts"}, //slug 是获取文章用的 URL
  9. {id:36, slug:"most-popular-posts"}
  10. ]
  11. ...
  12. actions: {
  13. theme: {
  14. openSubMenuPosts: ({ state }) => { //显示菜单
  15. state.theme.isSubMenuPostsOpen = true;
  16. },
  17. closeSubMenuPosts: ({ state }) => { //关闭菜单
  18. state.theme.isSubMenuPostsOpen = false;
  19. },
  20. ...

第二步增加一个获取 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

  1. const PopularPostArchive = {
  2. pattern: "/most-popular-posts/:aid(\\d+)", //如匹配most-popular-posts/36
  3. name: "most popular post archive",
  4. priority: 10,
  5. //调用WordPress REST API获取文章数据
  6. func: async ({ link, params, state, libraries, force }) => {
  7. const { api, populate, parse, getTotal, getTotalPages } = libraries.source;
  8. const { page, query, route } = parse(link);
  9. const response = await api.get({
  10. endpoint: "posts", //返回 post
  11. params: {
  12. _embed: true,
  13. page,
  14. categories: params.aid, //post 限定目录id,url中传入
  15. orderby: 'wpb_post_views_count', //根据文章点击数排序
  16. order: "desc",
  17. ...state.source.params
  18. },
  19. })
  20. const items = await populate({
  21. response, state, force,
  22. });
  23. if (page > 1 && items.length === 0) throw new ServerError(`post archive doesn't have page ${page}`, 404);
  24. const total = getTotal(response, items.length);
  25. const totalPages = getTotalPages(response, 0);
  26. const hasNewerPosts = page < totalPages;
  27. const hasOlderPosts = page > 1;
  28. const getPageLink = (page) =>
  29. libraries.source.stringify({
  30. route,
  31. query,
  32. page,
  33. });
  34. const currentPageData = state.source.data[link];
  35. const newPageData = {
  36. type: "post",
  37. items,
  38. total,
  39. totalPages,
  40. isArchive: true,
  41. isPostArchive: true,
  42. isPostTypeArchive: true,
  43. ...(hasOlderPosts && { previous: getPageLink(page - 1) }),
  44. ...(hasNewerPosts && { next: getPageLink(page + 1) }),
  45. };
  46. Object.assign(currentPageData, newPageData); //返回结果,console中可查看
  47. }
  48. }
  49. export default PopularPostArchive;

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

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

  1. ...
  2. const chakraTheme = {
  3. name: "frontity-chakra-theme",
  4. ...
  5. libraries: {
  6. html2react: {
  7. // Add a processor to html2react so it processes the <img> tags
  8. // inside the content HTML. You can add your own processors too.
  9. processors: [image, ...processors]
  10. },
  11. source: { //声明Handler
  12. handlers: [HomePagePostArchive, PopularPostArchive, PostRelatedArchive, PostFormatArchive]
  13. }
  14. }
  15. };

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

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

  1. import { Box } from "@chakra-ui/react";
  2. import { connect } from "frontity";
  3. import React from "react";
  4. import { omitConnectProps } from "./helpers";
  5. const Link = ({
  6. state,
  7. actions,
  8. link,
  9. className,
  10. children,
  11. rel,
  12. "aria-current": ariaCurrent,
  13. ...props
  14. }) => {
  15. const isDisabled = props["aria-disabled"];
  16. // If we're not in a frontity environment, let's just render the children
  17. if (state == null)
  18. return (
  19. <a className={className} href={isDisabled ? undefined : "#"} {...props}>
  20. {children}
  21. </a>
  22. );
  23. // Check if the link is an external or internal link
  24. const isExternal = link && link.startsWith("http");
  25. const onClick = event => {
  26. // Do nothing if it's an external link
  27. if (isExternal || isDisabled) return;
  28. event.preventDefault();
  29. event.stopPropagation();
  30. // Set the router to the new url.
  31. actions.router.set(link);
  32. // Scroll the page to the top
  33. window.scrollTo(0, 0);
  34. // if the menu modal is open, close it so it doesn't block rendering
  35. if (state.theme.isMobileMenuOpen) {
  36. actions.theme.closeMobileMenu();
  37. }
  38. // 当点击主菜单项时关闭弹出菜单
  39. if(state.theme.isSubMenuPostsOpen) {
  40. actions.theme.closeSubMenuPosts();
  41. }
  42. if (props.onClick) {
  43. props.onClick(event);
  44. }
  45. };
  46. return (
  47. <Box
  48. as="a"
  49. href={isDisabled ? undefined : link}
  50. onClick={onClick}
  51. className={className}
  52. aria-current={ariaCurrent}
  53. rel={rel}
  54. target={isExternal ? "_blank" : undefined}
  55. onMouseEnter={event => {
  56. // Prefetch the link's content when the user hovers on the link
  57. if (!isExternal) {
  58. if(props.idx) {
  59. state.theme.taxIdWithSlug.map((item) => {actions.source.fetch(`/${item.slug}/${item.id}`);}); //子菜单数据预取
  60. if(props.idx==1 || props.idx==2) { //鼠标滑过这两项时显示子菜单
  61. props.idx==1?(state.theme.currentTaxId = 36)&&actions.theme.openSubMenuPosts():(state.theme.currentTaxId = 4)&&actions.theme.openSubMenuPosts();
  62. }
  63. else actions.theme.closeSubMenuPosts();
  64. }
  65. actions.source.fetch(link);
  66. }
  67. if (props.onMouseEnter) props.onMouseEnter(event);
  68. }}
  69. {...omitConnectProps(props)}
  70. >
  71. {children}
  72. </Box>
  73. );
  74. };
  75. export default connect(Link);

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

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

  1. import { Box, Popover, PopoverContent, SimpleGrid, SkeletonText, Flex, Heading } from "@chakra-ui/react";
  2. import React from "react";
  3. import { connect } from "frontity";
  4. import Switch from "@frontity/components/switch";
  5. import Image from "@frontity/components/image";
  6. import { formatPostData } from "../helpers";
  7. import Link from "../link";
  8. const SubMenuPosts = ({state, actions}) => {
  9. //构造获取子菜单的Url,在第一步中定义了相关的数据
  10. const link = "/" + state.theme.taxIdWithSlug.filter(function(item){return item && item.id==state.theme.currentTaxId;})[0].slug + "/" + state.theme.currentTaxId;
  11. //从状态中提取子菜单数据,此时如果没有,会到WordPress后台提取。
  12. const data = state.source.get(link);
  13. return (
  14. <Popover
  15. isOpen={state.theme.isSubMenuPostsOpen}
  16. onClose={actions.theme.closeSubMenuPosts}
  17. autoFocus={true}
  18. trigger="hover"
  19. >
  20. <PopoverContent
  21. width="70vw"
  22. pos="fixed"
  23. top="70px"
  24. left="50px"
  25. transition="transform ease .25s"
  26. maxWidth="100%"
  27. bg="rgba(251,251,251,0.98)"
  28. css={{ backdropFilter: "blur(1px)" }}
  29. >
  30. <Switch>
  31. <SkeletonText mt="8" noOfLines={5} spacing="8" when={data.isFetching} />
  32. <SimpleGrid
  33. columns={5} spacing={1}
  34. when={data.isReady}
  35. >
  36. {data.items && data.items.map(({ type, id }, index) => {
  37. const item = state.source[type][id];
  38. const datafmt = formatPostData(state, item);
  39. const { title, featured_media, link } = datafmt;
  40. const { src, alt, srcSet } = featured_media;
  41. return (
  42. <Flex
  43. direction="column"
  44. position="relative"
  45. bg="white"
  46. as="article"
  47. key={index}
  48. >
  49. {featured_media && featured_media.src && (
  50. <Link link={link} >
  51. <Box
  52. role="group"
  53. cursor="pointer"
  54. height="90px"
  55. width="100%"
  56. pos="relative"
  57. >
  58. <Box
  59. as={Image}
  60. width="900"
  61. height="550"
  62. position="absolute"
  63. boxSize="100%"
  64. objectFit="cover"
  65. top="0"
  66. left="0"
  67. maxWidth="100%"
  68. src={src}
  69. alt={alt}
  70. srcSet={srcSet}
  71. />
  72. </Box>
  73. </Link>
  74. )}
  75. <Flex p="10px" flexGrow="1" direction="column">
  76. <Heading fontSize="small" as="h6" textTransform="uppercase">
  77. <Link link={link} dangerouslySetInnerHTML={{ __html: title }} ></Link>
  78. </Heading>
  79. </Flex>
  80. </Flex>
  81. );
  82. })}
  83. </SimpleGrid>
  84. </Switch>
  85. </PopoverContent>
  86. </Popover>
  87. );
  88. }
  89. export default connect(SubMenuPosts);

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

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

  1. import { connect } from "frontity";
  2. import React from "react";
  3. import MainHeader from "./header";
  4. import Navigation from "./navigation";
  5. import SocialNav from "./social-menu";
  6. import { SearchButton, SearchModal, SearchForm } from "../search";
  7. import SubMenuPosts from "../menu/submenu-posts";
  8. const Header = ({ state, actions }) => (
  9. <MainHeader>
  10. <Navigation menu={state.theme.menu} />
  11. {state.theme.showSocialLinks && (
  12. <SocialNav menu={state.theme.socialLinks} />
  13. )}
  14. <SearchButton onClick={actions.theme.openSearchModal} />
  15. <SearchModal
  16. isOpen={state.theme.isSearchModalOpen}
  17. onClose={actions.theme.closeSearchModal}
  18. >
  19. <SearchForm />
  20. </SearchModal>
  21. //加入弹出子菜单显示组件
  22. <SubMenuPosts/>
  23. </MainHeader>
  24. );
  25. export default connect(Header);
frontity popup menu
Frontity 显示弹出菜单

发表评论

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