2019-07-25 13:24:57 +02:00
|
|
|
import { Env } from "../component/component";
|
|
|
|
|
import { QWeb } from "../qweb/index";
|
|
|
|
|
import { makeDirective } from "./directive";
|
|
|
|
|
import { LINK_TEMPLATE, LINK_TEMPLATE_NAME } from "./Link";
|
|
|
|
|
|
|
|
|
|
interface Route {
|
|
|
|
|
name: string;
|
|
|
|
|
path: string;
|
|
|
|
|
component?: any;
|
|
|
|
|
redirect?: Destination;
|
|
|
|
|
params: string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type RouteParams = { [key: string]: string | number };
|
|
|
|
|
|
|
|
|
|
export interface RouterEnv extends Env {
|
|
|
|
|
router: Router;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Destination {
|
2019-07-25 23:06:15 +02:00
|
|
|
path?: string;
|
2019-07-26 08:31:22 +02:00
|
|
|
to?: string;
|
2019-07-25 13:24:57 +02:00
|
|
|
params?: RouteParams;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Options {
|
|
|
|
|
mode: Router["mode"];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const paramRegexp = /\{\{(.*?)\}\}/;
|
|
|
|
|
|
|
|
|
|
export class Router {
|
|
|
|
|
currentRoute: Route | null = null;
|
|
|
|
|
currentParams: RouteParams | null = null;
|
|
|
|
|
mode: "history" | "hash";
|
|
|
|
|
|
|
|
|
|
routes: { [id: string]: Route };
|
|
|
|
|
routeIds: string[];
|
|
|
|
|
env: RouterEnv;
|
|
|
|
|
|
|
|
|
|
constructor(env: Env, routes: Partial<Route>[], options: Options = { mode: "history" }) {
|
|
|
|
|
env.router = this;
|
|
|
|
|
this.mode = options.mode;
|
|
|
|
|
this.env = env as RouterEnv;
|
|
|
|
|
|
|
|
|
|
this.routes = {};
|
|
|
|
|
this.routeIds = [];
|
|
|
|
|
let nextId = 1;
|
|
|
|
|
for (let partialRoute of routes) {
|
|
|
|
|
if (!partialRoute.name) {
|
|
|
|
|
partialRoute.name = "__route__" + nextId++;
|
|
|
|
|
}
|
|
|
|
|
if (partialRoute.component) {
|
|
|
|
|
QWeb.register("__component__" + partialRoute.name, partialRoute.component);
|
|
|
|
|
}
|
2019-07-26 08:31:22 +02:00
|
|
|
if (partialRoute.redirect) {
|
|
|
|
|
this.validateDestination(partialRoute.redirect);
|
|
|
|
|
}
|
2019-07-25 13:24:57 +02:00
|
|
|
partialRoute.params = partialRoute.path ? findParams(partialRoute.path) : [];
|
|
|
|
|
this.routes[partialRoute.name] = partialRoute as Route;
|
|
|
|
|
this.routeIds.push(partialRoute.name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.checkRoute();
|
|
|
|
|
|
2019-07-25 23:17:34 +02:00
|
|
|
this.checkAndUpdateRoute = this.checkAndUpdateRoute.bind(this);
|
|
|
|
|
window.addEventListener("popstate", this.checkAndUpdateRoute);
|
2019-07-25 13:24:57 +02:00
|
|
|
|
|
|
|
|
// setup link and directive
|
|
|
|
|
env.qweb.addTemplate(LINK_TEMPLATE_NAME, LINK_TEMPLATE);
|
|
|
|
|
QWeb.addDirective(makeDirective(<RouterEnv>env));
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-26 08:22:31 +02:00
|
|
|
navigate(to: Destination): void {
|
|
|
|
|
const url = this.destToUrl(to);
|
|
|
|
|
history.pushState({}, url, location.origin + url);
|
2019-07-25 13:24:57 +02:00
|
|
|
this.checkAndUpdateRoute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destToUrl(dest: Destination): string {
|
2019-07-26 08:31:22 +02:00
|
|
|
this.validateDestination(dest);
|
|
|
|
|
return dest.path || this.routeToURL(this.routes[dest.to!].path, dest.params!);
|
2019-07-25 13:24:57 +02:00
|
|
|
}
|
|
|
|
|
|
2019-07-25 23:17:34 +02:00
|
|
|
get currentRouteName(): string | null {
|
|
|
|
|
return this.currentRoute && this.currentRoute.name;
|
2019-07-25 13:24:57 +02:00
|
|
|
}
|
|
|
|
|
|
2019-07-26 08:31:22 +02:00
|
|
|
private validateDestination(dest: Destination) {
|
|
|
|
|
if ((!dest.path && !dest.to) || (dest.path && dest.to)) {
|
|
|
|
|
throw new Error(`Invalid destination: ${JSON.stringify(dest)}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-25 13:24:57 +02:00
|
|
|
private routeToURL(path: string, params: RouteParams): string {
|
|
|
|
|
const parts = path.split("/");
|
|
|
|
|
const l = parts.length;
|
|
|
|
|
for (let i = 0; i < l; i++) {
|
|
|
|
|
const part = parts[i];
|
|
|
|
|
const match = part.match(paramRegexp);
|
|
|
|
|
if (match) {
|
|
|
|
|
const key = match[1].split(".")[0];
|
|
|
|
|
parts[i] = <string>params[key];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return parts.join("/");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private checkRoute(): void {
|
|
|
|
|
let currentPath =
|
|
|
|
|
this.mode === "history" ? window.location.pathname : window.location.hash.slice(1);
|
|
|
|
|
currentPath = currentPath || "/";
|
|
|
|
|
for (let routeId of this.routeIds) {
|
|
|
|
|
let route = this.routes[routeId];
|
|
|
|
|
let params = this.matchRoute(route.path, currentPath);
|
|
|
|
|
if (params) {
|
2019-07-26 08:22:31 +02:00
|
|
|
if (route.redirect) {
|
|
|
|
|
this.navigate(route.redirect);
|
|
|
|
|
this.checkRoute();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2019-07-25 13:24:57 +02:00
|
|
|
this.currentRoute = route;
|
|
|
|
|
this.currentParams = params;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.currentRoute = null;
|
|
|
|
|
this.currentParams = {};
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-25 23:17:34 +02:00
|
|
|
protected checkAndUpdateRoute(): void {
|
2019-07-25 13:24:57 +02:00
|
|
|
const initialName = this.currentRoute ? this.currentRoute.name : null;
|
|
|
|
|
this.checkRoute();
|
|
|
|
|
const currentName = this.currentRoute ? this.currentRoute.name : null;
|
|
|
|
|
|
|
|
|
|
if (currentName !== initialName) {
|
|
|
|
|
this.env.qweb.forceUpdate();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private matchRoute(routePath: string, path: string): RouteParams | false {
|
|
|
|
|
if (routePath === "*") {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
const descrParts = routePath.split("/");
|
|
|
|
|
const targetParts = path.split("/");
|
|
|
|
|
const l = descrParts.length;
|
|
|
|
|
if (l !== targetParts.length) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const result = {};
|
|
|
|
|
for (let i = 0; i < l; i++) {
|
|
|
|
|
const descr = descrParts[i];
|
|
|
|
|
let target: string | number = targetParts[i];
|
|
|
|
|
const match = descr.match(paramRegexp);
|
|
|
|
|
if (match) {
|
|
|
|
|
const [key, suffix] = match[1].split(".");
|
|
|
|
|
if (suffix === "number") {
|
|
|
|
|
target = parseInt(target, 10);
|
|
|
|
|
}
|
|
|
|
|
result[key] = target;
|
|
|
|
|
} else if (descr !== target) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findParams(str: string): string[] {
|
|
|
|
|
const globalParamRegexp = /\{\{(.*?)\}\}/g;
|
|
|
|
|
const result: string[] = [];
|
|
|
|
|
let m;
|
|
|
|
|
do {
|
|
|
|
|
m = globalParamRegexp.exec(str);
|
|
|
|
|
if (m) {
|
|
|
|
|
result.push(m[1].split(".")[0]);
|
|
|
|
|
}
|
|
|
|
|
} while (m);
|
|
|
|
|
return result;
|
|
|
|
|
}
|