wallet-core/packages/aml-backoffice-ui/src/route.ts
2023-06-05 10:04:09 -03:00

168 lines
4.7 KiB
TypeScript

import { createHashHistory } from "history";
import { h as create, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
const history = createHashHistory();
type PageDefinition<DynamicPart extends Record<string, string>> = {
pattern: string;
(params: DynamicPart): string;
};
function replaceAll(
pattern: string,
vars: Record<string, string>,
values: Record<string, string>,
): string {
let result = pattern;
for (const v in vars) {
result = result.replace(vars[v], !values[v] ? "" : values[v]);
}
return result;
}
export function pageDefinition<T extends Record<string, string>>(
pattern: string,
): PageDefinition<T> {
const patternParams = pattern.match(/(:[\w?]*)/g);
if (!patternParams)
throw Error(
`page definition pattern ${pattern} doesn't have any parameter`,
);
const vars = patternParams.reduce((prev, cur) => {
const pName = cur.match(/(\w+)/g);
//skip things like :? in the path pattern
if (!pName || !pName[0]) return prev;
const name = pName[0];
return { ...prev, [name]: cur };
}, {} as Record<string, string>);
const f = (values: T): string => replaceAll(pattern, vars, values);
f.pattern = pattern;
return f;
}
export type PageEntry<T = unknown> = T extends Record<string, string>
? {
url: PageDefinition<T>;
view: (props: T) => VNode;
}
: T extends unknown
? {
url: string;
view: (props: {}) => VNode;
}
: never;
export function Router({
pageList,
onNotFound,
}: {
pageList: Array<PageEntry<any>>;
onNotFound: () => VNode;
}): VNode {
const current = useCurrentLocation(pageList);
if (current !== undefined) {
return create(current.page.view, current.values);
}
return onNotFound();
}
type Location = {
page: PageEntry<any>;
path: string;
values: Record<string, string>;
};
export function useCurrentLocation(pageList: Array<PageEntry<any>>) {
const [currentLocation, setCurrentLocation] = useState<Location>();
/**
* Search path in the pageList
* get the values from the path found
* add params from searchParams
*
* @param path
* @param params
*/
function doSync(path: string, params: URLSearchParams) {
let result: typeof currentLocation;
for (let idx = 0; idx < pageList.length; idx++) {
const page = pageList[idx];
if (typeof page.url === "string") {
if (page.url === path) {
const values: Record<string, string> = {};
params.forEach((v, k) => {
values[k] = v;
});
result = { page, values, path };
break;
}
} else {
const values = doestUrlMatchToRoute(path, page.url.pattern);
if (values !== undefined) {
params.forEach((v, k) => {
values[k] = v;
});
result = { page, values, path };
break;
}
}
}
setCurrentLocation(result);
}
useEffect(() => {
doSync(window.location.hash, new URLSearchParams(window.location.search));
return history.listen(() => {
doSync(window.location.hash, new URLSearchParams(window.location.search));
});
}, []);
return currentLocation;
}
function doestUrlMatchToRoute(
url: string,
route: string,
): undefined | Record<string, string> {
const paramsPattern = /(?:\?([^#]*))?$/;
// const paramsPattern = /(?:\?([^#]*))?(#.*)?$/;
const params = url.match(paramsPattern);
const urlWithoutParams = url.replace(paramsPattern, "");
const result: Record<string, string> = {};
if (params && params[1]) {
const paramList = params[1].split("&");
for (let i = 0; i < paramList.length; i++) {
const idx = paramList[i].indexOf("=");
const name = paramList[i].substring(0, idx);
const value = paramList[i].substring(idx + 1);
result[decodeURIComponent(name)] = decodeURIComponent(value);
}
}
const urlSeg = urlWithoutParams.split("/");
const routeSeg = route.split("/");
let max = Math.max(urlSeg.length, routeSeg.length);
for (let i = 0; i < max; i++) {
if (routeSeg[i] && routeSeg[i].charAt(0) === ":") {
const param = routeSeg[i].replace(/(^:|[+*?]+$)/g, "");
const flags = (routeSeg[i].match(/[+*?]+$/) || EMPTY)[0] || "";
const plus = ~flags.indexOf("+");
const star = ~flags.indexOf("*");
const val = urlSeg[i] || "";
if (!val && !star && (flags.indexOf("?") < 0 || plus)) {
return undefined;
}
result[param] = decodeURIComponent(val);
if (plus || star) {
result[param] = urlSeg.slice(i).map(decodeURIComponent).join("/");
break;
}
} else if (routeSeg[i] !== urlSeg[i]) {
return undefined;
}
}
return result;
}
const EMPTY: Record<string, string> = {};