react-view-router
Version:
react-view-router
244 lines (243 loc) • 9.41 kB
JavaScript
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;