UNPKG

react-view-router

Version:
244 lines (243 loc) 9.41 kB
import "core-js/modules/web.dom.iterable.js"; import { useState, useCallback, useMemo, useEffect } from 'react'; import { isCommonPage, getRouteMatched, useRouter, useMatchedRouteAndIndex, useRouteChanged, useRouteMetaChanged } from './base'; import { isRouteChanged, hasOwnProp, getRouteChildren, readRouteMeta, isString } from '../util'; const DEFAULT_TITLE_NAME = 'title'; function readRouteTitle(route, options = {}) { const ret = { visible: false, title: '' }; const visible = readRouteMeta(route, 'visible', options); if (visible === false) return ret; const titleName = options.titleName || DEFAULT_TITLE_NAME; ret.title = readRouteMeta(route, titleName, options) || ''; ret.visible = visible !== false && Boolean(ret.title); return ret; } function readRouteTitles(router, routes, options = {}) { const { refresh, filter, titleName, maxLevel = 99, level = 1 } = options; routes = getRouteChildren(routes); return routes.filter(r => r.meta.title).map(r => { const { visible, title } = readRouteTitle(r, { router, titleName, level, maxLevel, refresh }); if (visible === false) return; if (filter && filter(r, routes, { title, visible, router, level, maxLevel, refresh }) === false) return; const ret = { title, path: r.path, meta: r.meta, route: r, level }; if (level < maxLevel && Array.isArray(r.children) && r.children.length) { const children = readRouteTitles(router, r.children, { refresh, filter, titleName, maxLevel, level: level + 1 }); if (children.length) ret.children = children; } return ret; }).filter(Boolean); } function getMatchedDepth(matchedRoute) { return matchedRoute ? matchedRoute.depth + 1 : 0; } function isTitleRoute(route, titleName = DEFAULT_TITLE_NAME) { return route && hasOwnProp(route.meta, titleName); } function walkMatchedRouteList(callback, matched, depth, maxLevel, titleName = DEFAULT_TITLE_NAME) { if (!callback) return; // let baseLevel = depth; while (depth < maxLevel && matched && matched[depth]) { const matchedRouteIndex = depth++; const matchedRoute = matched[matchedRouteIndex]; if (isTitleRoute(matchedRoute, titleName)) { // const { visible } = readRouteTitle(matchedRoute.config, titleName, { // maxLevel: maxLevel - baseLevel, // level: depth - baseLevel // }); // if (!visible) break; callback(matchedRoute, matchedRouteIndex); } } } function getMatchedRouteList(matched, depth, maxLevel, titleName) { const ret = []; walkMatchedRouteList(matchedRoute => { ret.push(matchedRoute); }, matched, depth, maxLevel, titleName); return ret; } function getMatchedRoutes(router, matchedRoute, maxLevel, commonPageName, titleName) { const currentRoute = router.currentRoute || router.initialRoute; const depth = getMatchedDepth(matchedRoute); const matchedRoutes = getMatchedRouteList(getRouteMatched(router, currentRoute, commonPageName), depth, maxLevel + depth, titleName); return matchedRoutes; } function findTitleByMatchedPath(matchedPath, titles, matchedTitles) { if (!matchedPath) return undefined; let matchedTitle; titles.some(title => { if (title.path === matchedPath) { matchedTitle = title; if (matchedTitles) matchedTitles.push(title); return true; } if (title.children && title.children.length) { matchedTitle = findTitleByMatchedPath(matchedPath, title.children, matchedTitles); if (matchedTitle && matchedTitles) matchedTitles.unshift(title); } return matchedTitle; }); return matchedTitle; } function useRouteTitle(props, defaultRouter, deps = []) { if (!props) props = {}; const { maxLevel = 99, filterMetas = [], manual, matchedOffset = 0, titleName = DEFAULT_TITLE_NAME, commonPageName = 'commonPage' } = props; const [$refs] = useState({ parsed: false, mounted: false, timerIds: {} }); $refs.filter = props.filter; $refs.onNoMatchedPath = props.onNoMatchedPath; const router = useRouter(defaultRouter); if (!router) throw new Error('[useRouteTitle] router can not be null!'); const [matchedRoute, routeIndex] = defaultRouter ? [null, 0] // eslint-disable-next-line react-hooks/rules-of-hooks : useMatchedRouteAndIndex(undefined, { matchedOffset, commonPageName }); const [dirty, setDirty] = useState(false); const refreshTitles = useCallback(() => setDirty(true), [setDirty]); const refreshTabs = useCallback((routes = []) => { $refs.parsed = true; return readRouteTitles(router, routes, { refresh: refreshTitles, filter: $refs.filter, titleName, maxLevel }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [router, $refs, maxLevel, refreshTitles, ...deps]); const [titles, setTitles] = useState(() => manual ? [] : refreshTabs(defaultRouter ? router.routes : matchedRoute ? matchedRoute.config.children : [])); const [matchedRoutes, setMatchedRoutes] = useState(() => getMatchedRoutes(router, matchedRoute, maxLevel, commonPageName, titleName)); const matchedTitles = useMemo(() => { const ret = []; if (!matchedRoutes.length || !$refs.parsed) return ret; const currentRoute = matchedRoutes[matchedRoutes.length - 1]; findTitleByMatchedPath(currentRoute.path, titles, ret); return ret; }, [$refs.parsed, matchedRoutes, titles]); const currentPaths = useMemo(() => { if (!matchedRoutes.length || !$refs.parsed) return []; const currentRoute = matchedRoutes[matchedRoutes.length - 1]; const currentTitle = findTitleByMatchedPath(currentRoute.path, titles); if (!currentTitle && $refs.mounted && $refs.onNoMatchedPath) { if ($refs.timerIds.onNoMatchedPath) clearTimeout($refs.timerIds.onNoMatchedPath); $refs.timerIds.onNoMatchedPath = setTimeout(() => { $refs.timerIds.onNoMatchedPath = 0; if (!$refs.mounted || !$refs.onNoMatchedPath) return; const fallback = fallbackPath => { const toPath = isString(fallbackPath) ? fallbackPath : fallbackPath.path; const currentRoute = router.pendingRoute || router.currentRoute || router.initialRoute; if (!toPath || currentRoute && currentRoute.path === toPath) return; return router.replace(fallbackPath); }; if (isString($refs.onNoMatchedPath)) { if (!titles.length) return; return fallback($refs.onNoMatchedPath === ':first' ? titles[0].path : $refs.onNoMatchedPath); } $refs.onNoMatchedPath(currentRoute.path, titles, fallback); }, 0); } return currentTitle ? matchedRoutes.map(r => r.path) : []; }, [matchedRoutes, titles, $refs, router]); useRouteChanged(router, useCallback((currentRoute, prevRoute) => { if (!$refs.mounted) return; const routes = getMatchedRoutes(router, matchedRoute, maxLevel, commonPageName, titleName); if (routes.length !== matchedRoutes.length || matchedRoutes.some((route, i) => route.path !== routes[i].path)) { setMatchedRoutes(routes); } if (!defaultRouter) { const matched = getRouteMatched(router, currentRoute, commonPageName); if (matched) { const depth = getMatchedDepth(matchedRoute); const prevMatched = getRouteMatched(router, prevRoute, commonPageName); const matchedRouteList = getMatchedRouteList(prevMatched, depth, maxLevel, titleName); const newMatchedRouteList = getMatchedRouteList(matched, depth, maxLevel, titleName); if (matchedRouteList.length !== newMatchedRouteList.length || newMatchedRouteList.some((newMatchedRoute, i) => isRouteChanged(matchedRouteList[i], newMatchedRoute))) { const newMatchedRoute = newMatchedRouteList[0]; setTitles(refreshTabs(newMatchedRoute ? newMatchedRoute.config.parent ? newMatchedRoute.config.parent.children : router.routes : [])); } } } }, [router, matchedRoute, maxLevel, commonPageName, titleName, matchedRoutes, defaultRouter, refreshTabs, $refs])); $refs.routeMetaChangedCallback = useCallback(() => { if (!$refs.mounted) return; const matched = getRouteMatched(router, router.currentRoute, commonPageName); const matchedRoute = matched[routeIndex]; setTitles(refreshTabs(defaultRouter ? router.routes : matchedRoute ? matchedRoute.config.children : [])); }, [router, commonPageName, routeIndex, refreshTabs, defaultRouter, $refs]); useRouteMetaChanged(router, $refs.routeMetaChangedCallback, [...filterMetas, titleName, 'visible']); useMemo(() => { if (!dirty || !$refs.mounted) return; setDirty(false); if ($refs.timerIds.dirty) return; $refs.timerIds.dirty = setTimeout(() => { $refs.timerIds.dirty = 0; router.isRunning && $refs.routeMetaChangedCallback && $refs.routeMetaChangedCallback(); }, 0); }, [router, dirty, $refs]); useEffect(() => { $refs.mounted = true; return () => { $refs.mounted = false; }; }, [$refs]); return { parsed: $refs.parsed, titles, setTitles, refreshTitles, matchedRoutes, matchedTitles, currentPaths }; } export { isTitleRoute, isCommonPage, readRouteTitle, readRouteTitles }; export default useRouteTitle;