@paperbits/common
Version:
Paperbits common components.
170 lines (132 loc) • 5.01 kB
text/typescript
import { EventManager } from "../events";
import { Router, RouterEvents, Route, RouteGuard } from ".";
export class DefaultRouter implements Router {
private notificationTimeout: any;
public currentRoute: Route;
public notifyListeners: boolean;
constructor(
private readonly routeGuards: RouteGuard[],
private readonly eventManager: EventManager
) {
this.notifyListeners = true;
this.currentRoute = this.getRouteFromLocation();
}
public getRouteFromLocation(): Route {
const path = location.pathname;
const hash = location.hash.startsWith("#") ? location.hash.slice(1) : null;
const url = path + (hash ? `#${hash}` : "");
const route: Route = {
url: url,
path: path,
hash: hash,
metadata: {},
previous: null
};
return route;
}
public addRouteChangeListener(eventHandler: (args?: any) => void): void {
this.eventManager.addEventListener(RouterEvents.onRouteChange, eventHandler);
}
public removeRouteChangeListener(eventHandler: (args?: any) => void): void {
this.eventManager.removeEventListener(RouterEvents.onRouteChange, eventHandler);
}
public addHistoryUpdateListener(eventHandler: (args?: any) => void): void {
this.eventManager.addEventListener(RouterEvents.onHistoryUpdate, eventHandler);
}
public removeHistoryUpdateListener(eventHandler: (args?: any) => void): void {
this.eventManager.removeEventListener(RouterEvents.onHistoryUpdate, eventHandler);
}
/**
* Navigates to specified URL.
* @param url Absolute or relative path, i.e. https://paperbits.io or /about
* @param title Destination title
* @param metadata Associated metadata
*/
public async navigateTo(url: string, title: string = null, metadata: Object = {}): Promise<void> {
const route = this.getRoute(url, title, metadata);
if (!route) {
return;
}
const canActivate = await this.canActivate(route);
if (canActivate) {
this.currentRoute = route;
if (this.notifyListeners) {
this.scheduleNotification(route);
}
}
}
private scheduleNotification(route: Route): void {
clearTimeout(this.notificationTimeout);
this.notificationTimeout = setTimeout(() => {
this.eventManager.dispatchEvent(RouterEvents.onRouteChange, route);
}, 100);
}
public updateHistory(url: string, title: string): void {
const route = this.getRoute(url, title);
if (!route) {
return;
}
if (this.notifyListeners) {
this.eventManager.dispatchEvent(RouterEvents.onHistoryUpdate, route);
}
}
private getRoute(url: string, title: string = null, metadata: Object = {}): Route {
if (!url) {
return undefined;
}
const isFullUrl = url && (url.startsWith("http://") || url.startsWith("https://"));
const isLocalUrl = url.startsWith(location.origin);
if (isFullUrl && !isLocalUrl) {
window.open(url, "_blank"); // navigating external link
return;
}
url = isFullUrl
? url.substring(location.origin.length)
: url;
const parts = url.split("#");
const route: Route = {
url: url,
path: parts.length > 1 ? parts[0] || location.pathname : parts[0],
title: title,
metadata: metadata,
hash: parts.length > 1 ? parts[1] : "",
previous: this.currentRoute
};
return route;
}
protected async canActivate(route: Route): Promise<boolean> {
for (const routeGuard of this.routeGuards) {
try {
const canActivate = await routeGuard.canActivate(route);
if (!canActivate) {
return false;
}
}
catch (error) {
throw new Error(`Unable to invoke route a guard: ${error.stack || error.message}`);
return false;
}
}
return true;
}
public getCurrentUrl(): string {
let permalink = this.currentRoute.path;
const hash = this.getHash();
if (this.currentRoute.hash) {
permalink += "#" + hash;
}
return permalink;
}
public getCurrentUrlMetadata(): Object {
return this.currentRoute.metadata;
}
public getPath(): string {
return this.currentRoute.path;
}
public getHash(): string {
return this.currentRoute.hash;
}
public getCurrentRoute(): Route {
return this.currentRoute;
}
}