@jupyterlab/application
Version:
JupyterLab - Application
207 lines (178 loc) • 5.45 kB
text/typescript
/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { URLExt } from '@jupyterlab/coreutils';
import { CommandRegistry } from '@lumino/commands';
import { PromiseDelegate, Token } from '@lumino/coreutils';
import { DisposableDelegate, IDisposable } from '@lumino/disposable';
import { ISignal, Signal } from '@lumino/signaling';
import { IRouter } from './tokens';
/**
* A static class that routes URLs within the application.
*/
export class Router implements IRouter {
/**
* Create a URL router.
*/
constructor(options: Router.IOptions) {
this.base = options.base;
this.commands = options.commands;
}
/**
* The base URL for the router.
*/
readonly base: string;
/**
* The command registry used by the router.
*/
readonly commands: CommandRegistry;
/**
* Returns the parsed current URL of the application.
*/
get current(): IRouter.ILocation {
const { base } = this;
const parsed = URLExt.parse(window.location.href);
const { search, hash } = parsed;
const path = parsed.pathname?.replace(base, '/') ?? '';
const request = path + search + hash;
return { hash, path, request, search };
}
/**
* A signal emitted when the router routes a route.
*/
get routed(): ISignal<this, IRouter.ILocation> {
return this._routed;
}
/**
* If a matching rule's command resolves with the `stop` token during routing,
* no further matches will execute.
*/
readonly stop = new Token<void>('@jupyterlab/application:Router#stop');
/**
* Navigate to a new path within the application.
*
* @param path - The new path or empty string if redirecting to root.
*
* @param options - The navigation options.
*/
navigate(path: string, options: IRouter.INavOptions = {}): void {
const { base } = this;
const { history } = window;
const { hard } = options;
const old = document.location.href;
const url =
path && path.indexOf(base) === 0 ? path : URLExt.join(base, path);
if (url === old) {
return hard ? this.reload() : undefined;
}
history.pushState({}, '', url);
if (hard) {
return this.reload();
}
if (!options.skipRouting) {
// Because a `route()` call may still be in the stack after having received
// a `stop` token, wait for the next stack frame before calling `route()`.
requestAnimationFrame(() => {
void this.route();
});
}
}
/**
* Register to route a path pattern to a command.
*
* @param options - The route registration options.
*
* @returns A disposable that removes the registered rule from the router.
*/
register(options: IRouter.IRegisterOptions): IDisposable {
const { command, pattern } = options;
const rank = options.rank ?? 100;
const rules = this._rules;
rules.set(pattern, { command, rank });
return new DisposableDelegate(() => {
rules.delete(pattern);
});
}
/**
* Cause a hard reload of the document.
*/
reload(): void {
window.location.reload();
}
/**
* Route a specific path to an action.
*
* #### Notes
* If a pattern is matched, its command will be invoked with arguments that
* match the `IRouter.ILocation` interface.
*/
route(): Promise<void> {
const { commands, current, stop } = this;
const { request } = current;
const routed = this._routed;
const rules = this._rules;
const matches: Private.Rule[] = [];
// Collect all rules that match the URL.
rules.forEach((rule, pattern) => {
if (request?.match(pattern)) {
matches.push(rule);
}
});
// Order the matching rules by rank and enqueue them.
const queue = matches.sort((a, b) => b.rank - a.rank);
const done = new PromiseDelegate<void>();
// Process each enqueued command sequentially and short-circuit if a promise
// resolves with the `stop` token.
const next = async () => {
if (!queue.length) {
routed.emit(current);
done.resolve(undefined);
return;
}
const { command } = queue.pop()!;
try {
const request = this.current.request;
const result = await commands.execute(command, current);
if (result === stop) {
queue.length = 0;
console.debug(`Routing ${request} was short-circuited by ${command}`);
}
} catch (reason) {
console.warn(`Routing ${request} to ${command} failed`, reason);
}
void next();
};
void next();
return done.promise;
}
private _routed = new Signal<this, IRouter.ILocation>(this);
private _rules = new Map<RegExp, Private.Rule>();
}
/**
* A namespace for `Router` class statics.
*/
export namespace Router {
/**
* The options for instantiating a JupyterLab URL router.
*/
export interface IOptions {
/**
* The fully qualified base URL for the router.
*/
base: string;
/**
* The command registry used by the router.
*/
commands: CommandRegistry;
}
}
/**
* A namespace for private module data.
*/
namespace Private {
/**
* The internal representation of a routing rule.
*/
export type Rule = { command: string; rank: number };
}