import React from "react"; /* -------------------------------------------------------------------------- */ /* Libraries */ /* -------------------------------------------------------------------------- */ import { useTranslation } from "react-i18next"; import { matchPath, PathMatch, useLocation } from "react-router-dom"; import { useRoutesDefinition } from "./definition"; /* -------------------------------------------------------------------------- */ /* Types */ /* -------------------------------------------------------------------------- */ import { ActiveRoutePath, ActiveRoutePathTitleCallbackParams, RoutePathDefinition, WithRouteProps, } from "./types"; /* -------------------------------------------------------------------------- */ /* Misc */ /* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */ /* Helpers */ /* -------------------------------------------------------------------------- */ function joinPaths(paths: string[]): string { return paths.join("/").replace(/\/\/+/g, "/"); } function concatPaths(parent: string, current: string) { const jointPaths = joinPaths([parent, current]); return jointPaths; } function addActiveRoutePathIfPossible( activeRoutePaths: ActiveRoutePath[], activePath: ActiveRoutePath ) { if (canBeAddedToActiveRoutes(activeRoutePaths, activePath.match)) { activeRoutePaths.push(activePath); } } function isResolvedAsActive( toPathname: string, locationPathname: string, definition: RoutePathDefinition, ) { return ( isPathActiveForLocation(toPathname, locationPathname) && isNotCatchAll(definition.path || "") ); } function canBeAddedToActiveRoutes( activeRoutePaths: ActiveRoutePath[], match: PathMatch ) { return ( isNotSameAsPreviousMatch(activeRoutePaths, match) && isMoreSpecificThanPreviousMatch(activeRoutePaths, match.pathname) ); } function getPreviousMatch( previousMatches: ActiveRoutePath[] ): ActiveRoutePath | undefined { return previousMatches[previousMatches.length - 1]; } function isNotSameAsPreviousMatch( previousMatches: ActiveRoutePath[], match: PathMatch ): boolean { const previousMatchedPathname = getPreviousMatch(previousMatches)?.match.pattern ?? ""; return previousMatchedPathname !== match.pattern; } function isMoreSpecificThanPreviousMatch( previousMatches: ActiveRoutePath[], toPathname: string ): boolean { const previousMatchedPathname = getPreviousMatch(previousMatches)?.match.pathname ?? ""; return toPathname.length > previousMatchedPathname.length; } function isNotCatchAll(path: string) { return path !== "*"; } function isPathActiveForLocation( pathName: string, locationPathname: string, ) { return ( locationPathname === pathName || (locationPathname.startsWith(pathName) && locationPathname.charAt(pathName.length) === "/") ); } function matchPatternInPath( pathPattern: string, locationPathname: string, requireExactMatch: boolean = false ): PathMatch | null { return matchPath( { path: pathPattern, end: requireExactMatch }, locationPathname ); } export function mapActivePathBranch( siblings: RoutePathDefinition[], locationPathname: string, translation: ActiveRoutePathTitleCallbackParams["translation"], parentPath: string = "" ) { if (siblings.length === 0) { return []; } const activeBranch: ActiveRoutePath[] = []; siblings.forEach((definition) => { const pathPatternWithParent = concatPaths(parentPath, definition.path); if (pathPatternWithParent === "/") return; const match = matchPatternInPath(pathPatternWithParent, locationPathname); if (!match) return; const activeRoutePath: ActiveRoutePath = mapRouteDefinitionToActivePath( definition, [], match, locationPathname, parentPath, translation ); addActiveRoutePathIfPossible(activeBranch, activeRoutePath); }); return activeBranch; } export function mapRouteDefinitionToActivePath( definition: RoutePathDefinition, siblings: RoutePathDefinition[], match: PathMatch, locationPathname: string, parentPath: string = "", transition: ActiveRoutePathTitleCallbackParams["translation"] ): ActiveRoutePath { return { definition: definition, title: typeof definition.title === "function" ? definition.title({ definition, match, locationPathname: locationPathname, translation: transition, }) : definition.title, match: match, siblings: mapActivePathBranch( siblings, locationPathname, transition, parentPath ), }; } export function mapDefinitionToActivePath( definitions: RoutePathDefinition[], locationPathname: string, translation: ActiveRoutePathTitleCallbackParams["translation"], parentPath: string = "" ): ActiveRoutePath[] { const activeRoutePaths: ActiveRoutePath[] = []; definitions.forEach((definition, index) => { const pathPatternWithParent = concatPaths(parentPath, definition.path); const match = matchPatternInPath(pathPatternWithParent, locationPathname); if (!match) { return; } if (isResolvedAsActive(match.pathname, locationPathname, definition)) { const activeRoutePath: ActiveRoutePath = mapRouteDefinitionToActivePath( definition, definitions, match, locationPathname, parentPath, translation ); addActiveRoutePathIfPossible(activeRoutePaths, activeRoutePath); if (definition.children) { const nestedMatches = mapDefinitionToActivePath( definition.children, locationPathname, translation, pathPatternWithParent ); nestedMatches.forEach((activePath) => { addActiveRoutePathIfPossible(activeRoutePaths, activePath); }); } } }); return activeRoutePaths; } /* -------------------------------------------------------------------------- */ /* HOC with route params */ /* -------------------------------------------------------------------------- */ const withRouteParams = ( Component: React.ComponentType, componentName = Component.displayName ?? Component.name ): { (props: Omit>): JSX.Element; displayName: string; } => { function WithRouteParams( props: Omit> ) { const { t } = useTranslation(); const withRouteProps: WithRouteProps = { activePath: mapDefinitionToActivePath( useRoutesDefinition(), useLocation().pathname, t ), path: props.path, name: props.name, }; return ; } WithRouteParams.displayName = `withRouteParams(${componentName})`; return WithRouteParams; }; export { withRouteParams };