123 lines
2.7 KiB
TypeScript
123 lines
2.7 KiB
TypeScript
type RouteParams = Record<string, string>;
|
|
type QueryParams = Record<string, string>;
|
|
|
|
export interface RouteContext {
|
|
params: RouteParams;
|
|
query: QueryParams
|
|
}
|
|
|
|
type NextFunction = () => void;
|
|
type RouteHandler = (ctx: RouteContext) => void;
|
|
type RouteGuard = (ctx: RouteContext, next: NextFunction) => void;
|
|
|
|
interface Route {
|
|
path: string;
|
|
regex: RegExp,
|
|
keys: string[],
|
|
handler: RouteHandler;
|
|
guards: RouteGuard[];
|
|
}
|
|
|
|
export class Router {
|
|
|
|
private routes: Route[] = [];
|
|
private rootElement: HTMLElement | null = null;
|
|
private isTransitioning = false;
|
|
|
|
private static instance: Router | null = null;
|
|
|
|
constructor(rootId: string) {
|
|
this.rootElement = document.getElementById(rootId);
|
|
|
|
Router.instance = this;
|
|
|
|
window.addEventListener('hashchange', () => this.handleRoute());
|
|
window.addEventListener('load', () => this.handleRoute())
|
|
}
|
|
|
|
public static reload() {
|
|
if (Router.instance) {
|
|
Router.instance.handleRoute();
|
|
}
|
|
}
|
|
|
|
public addRoute(path: string, handler: RouteHandler, guards: RouteGuard[] = []) {
|
|
const keys: string[] = [];
|
|
const regexpPath = path.replace(/:(\w+)/g, (_, key) => {
|
|
keys.push(key);
|
|
return '([^/]+)'
|
|
})
|
|
|
|
this.routes.push({
|
|
path,
|
|
regex: new RegExp(`^${regexpPath}$`),
|
|
keys, handler, guards
|
|
})
|
|
}
|
|
|
|
private handleRoute() {
|
|
if (this.isTransitioning) return;
|
|
|
|
const hash = window.location.hash.slice(1) || '/';
|
|
const [path, queryString] = hash.split("?");
|
|
|
|
const query: QueryParams = {};
|
|
new URLSearchParams(queryString).forEach((val, key) => {
|
|
query[key] = val
|
|
});
|
|
|
|
for (const route of this.routes) {
|
|
const match = path.match(route.regex);
|
|
|
|
if (match) {
|
|
const params: RouteParams = {};
|
|
route.keys.forEach((key, index) => params[key] = match[index + 1]);
|
|
|
|
const ctx: RouteContext = { params, query };
|
|
|
|
this.runGuards(route.guards, ctx, () => {
|
|
this.performTransition(() => {
|
|
route.handler(ctx);
|
|
})
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private runGuards(guards: RouteGuard[], ctx: RouteContext, callback: () => void) {
|
|
let index = 0;
|
|
|
|
const next = () => {
|
|
if (index < guards.length) {
|
|
guards[index++](ctx, next)
|
|
} else {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
private performTransition(updateDomFn: () => void) {
|
|
if (!this.rootElement) {
|
|
updateDomFn();
|
|
return;
|
|
}
|
|
|
|
this.isTransitioning = true;
|
|
|
|
this.rootElement.classList.add('fading');
|
|
|
|
setTimeout(() => {
|
|
updateDomFn();
|
|
this.rootElement?.classList.remove('fading');
|
|
this.isTransitioning = false;
|
|
}, 100)
|
|
}
|
|
|
|
static navigate(path: string) {
|
|
window.location.hash = path;
|
|
}
|
|
} |