使用 Frontity 实现弹出菜单
在前一篇文章中我们介绍了如何创建一个基于 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);