import { css, customElement, html, LitElement, property, TemplateResult } from "lit-element"; // @ts-ignore import CodeMirrorStyle from "codemirror/lib/codemirror.css"; // @ts-ignore import CodeMirrorTheme from "codemirror/theme/monokai.css"; import { ColorStyles } from "../constants"; import { COMMON_STYLES } from "../common/styles"; export class Route { url: RegExp; private element?: TemplateResult; private callback?: (args: { [key: string]: string }) => TemplateResult; constructor(url: RegExp, element?: TemplateResult) { this.url = url; this.element = element; } redirect(to: string): Route { this.callback = () => { console.debug(`passbook/router: redirecting ${to}`); window.location.hash = `#${to}`; return html``; }; return this; } then(render: (args: { [key: string]: string }) => TemplateResult): Route { this.callback = render; return this; } render(args: { [key: string]: string }): TemplateResult { if (this.callback) { return this.callback(args); } if (this.element) { return this.element; } throw new Error("Route does not have callback or element"); } toString(): string { return ``; } } export const SLUG_REGEX = "[-a-zA-Z0-9_]+"; export const ROUTES: Route[] = [ // Prevent infinite Shell loops new Route(new RegExp("^/$")).redirect("/library/"), new Route(new RegExp("^#.*")).redirect("/library/"), new Route(new RegExp("^/library/$"), html``), new Route(new RegExp("^/administration/overview/$"), html``), new Route(new RegExp("^/applications/$"), html``), new Route(new RegExp(`^/applications/(?${SLUG_REGEX})/$`)).then((args) => { return html``; }), ]; class RouteMatch { route: Route; arguments?: RegExpExecArray; fullUrl?: string; constructor(route: Route) { this.route = route; } render(): TemplateResult { return this.route.render(this.arguments!.groups || {}); } toString(): string { return ``; } } @customElement("pb-router-outlet") export class RouterOutlet extends LitElement { @property() current?: RouteMatch; @property() defaultUrl?: string; static get styles() { return [ CodeMirrorStyle, CodeMirrorTheme, ColorStyles, css` :host { height: 100%; } `, ].concat(...COMMON_STYLES); } constructor() { super(); window.addEventListener("hashchange", () => this.navigate()); } firstUpdated(): void { this.navigate(); } navigate(): void { let activeUrl = window.location.hash.slice(1, Infinity); if (activeUrl === "") { activeUrl = this.defaultUrl!; window.location.hash = `#${activeUrl}`; console.debug(`passbook/router: set to ${window.location.hash}`); return; } let matchedRoute: RouteMatch | null = null; ROUTES.some((route) => { console.debug(`passbook/router: matching ${activeUrl} against ${route.url}`); const match = route.url.exec(activeUrl); if (match != null) { matchedRoute = new RouteMatch(route); matchedRoute.arguments = match; matchedRoute.fullUrl = activeUrl; console.debug(`passbook/router: found match ${matchedRoute}`); return true; } }); if (!matchedRoute) { console.debug(`passbook/router: route "${activeUrl}" not defined, defaulting to shell`); const route = new Route( RegExp(""), html`
` ); matchedRoute = new RouteMatch(route); matchedRoute.arguments = route.url.exec(activeUrl)!; matchedRoute.fullUrl = activeUrl; } this.current = matchedRoute; } render(): TemplateResult | undefined { // TODO: Render 404 when current Route is empty return this.current?.render(); } }