Frontend/src/routes/withRoute.tsx
2022-07-18 11:38:44 +03:00

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 };