247 lines
7.2 KiB
TypeScript
247 lines
7.2 KiB
TypeScript
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<string>
|
|
) {
|
|
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<string>
|
|
): 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<string> | 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<string>,
|
|
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 = <T extends WithRouteProps = WithRouteProps>(
|
|
Component: React.ComponentType<T>,
|
|
componentName = Component.displayName ?? Component.name
|
|
): {
|
|
(props: Omit<T, Exclude<keyof WithRouteProps, "path" | "name">>): JSX.Element;
|
|
displayName: string;
|
|
} => {
|
|
function WithRouteParams(
|
|
props: Omit<T, Exclude<keyof WithRouteProps, "path" | "name" | "routes">>
|
|
) {
|
|
const { t } = useTranslation();
|
|
const withRouteProps: WithRouteProps = {
|
|
activePath: mapDefinitionToActivePath(
|
|
useRoutesDefinition(),
|
|
useLocation().pathname,
|
|
t
|
|
),
|
|
path: props.path,
|
|
name: props.name,
|
|
};
|
|
return <Component {...withRouteProps} {...(props as T)} />;
|
|
}
|
|
WithRouteParams.displayName = `withRouteParams(${componentName})`;
|
|
|
|
return WithRouteParams;
|
|
};
|
|
|
|
export { withRouteParams };
|