@jupyterlab/application 4.0.0-alpha.9 → 4.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,180 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import { IWidgetTracker, WidgetTracker } from '@jupyterlab/apputils';
5
+ import {
6
+ DocumentRegistry,
7
+ MimeDocument,
8
+ MimeDocumentFactory
9
+ } from '@jupyterlab/docregistry';
10
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
11
+ import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
12
+ import { ITranslator } from '@jupyterlab/translation';
13
+ import { LabIcon } from '@jupyterlab/ui-components';
14
+ import { Token } from '@lumino/coreutils';
15
+ import { AttachedProperty } from '@lumino/properties';
16
+ import { JupyterFrontEnd, JupyterFrontEndPlugin } from './index';
17
+ import { ILayoutRestorer } from './layoutrestorer';
18
+
19
+ /**
20
+ * A class that tracks mime documents.
21
+ */
22
+ export interface IMimeDocumentTracker extends IWidgetTracker<MimeDocument> {}
23
+
24
+ /**
25
+ * The mime document tracker token.
26
+ */
27
+ export const IMimeDocumentTracker = new Token<IMimeDocumentTracker>(
28
+ '@jupyterlab/application:IMimeDocumentTracker'
29
+ );
30
+
31
+ /**
32
+ * Create rendermime plugins for rendermime extension modules.
33
+ */
34
+ export function createRendermimePlugins(
35
+ extensions: IRenderMime.IExtensionModule[]
36
+ ): JupyterFrontEndPlugin<void | IMimeDocumentTracker, any, any>[] {
37
+ const plugins: JupyterFrontEndPlugin<void | IMimeDocumentTracker>[] = [];
38
+
39
+ const namespace = 'application-mimedocuments';
40
+ const tracker = new WidgetTracker<MimeDocument>({ namespace });
41
+
42
+ extensions.forEach(mod => {
43
+ let data = mod.default;
44
+
45
+ // Handle CommonJS exports.
46
+ if (!mod.hasOwnProperty('__esModule')) {
47
+ data = mod as any;
48
+ }
49
+ if (!Array.isArray(data)) {
50
+ data = [data] as ReadonlyArray<IRenderMime.IExtension>;
51
+ }
52
+ (data as ReadonlyArray<IRenderMime.IExtension>).forEach(item => {
53
+ plugins.push(createRendermimePlugin(tracker, item));
54
+ });
55
+ });
56
+
57
+ // Also add a meta-plugin handling state restoration
58
+ // and exposing the mime document widget tracker.
59
+ plugins.push({
60
+ id: '@jupyterlab/application:mimedocument',
61
+ optional: [ILayoutRestorer],
62
+ provides: IMimeDocumentTracker,
63
+ autoStart: true,
64
+ activate: (app: JupyterFrontEnd, restorer: ILayoutRestorer | null) => {
65
+ if (restorer) {
66
+ void restorer.restore(tracker, {
67
+ command: 'docmanager:open',
68
+ args: widget => ({
69
+ path: widget.context.path,
70
+ factory: Private.factoryNameProperty.get(widget)
71
+ }),
72
+ name: widget =>
73
+ `${widget.context.path}:${Private.factoryNameProperty.get(widget)}`
74
+ });
75
+ }
76
+ return tracker;
77
+ }
78
+ });
79
+
80
+ return plugins;
81
+ }
82
+
83
+ /**
84
+ * Create rendermime plugins for rendermime extension modules.
85
+ */
86
+ export function createRendermimePlugin(
87
+ tracker: WidgetTracker<MimeDocument>,
88
+ item: IRenderMime.IExtension
89
+ ): JupyterFrontEndPlugin<void> {
90
+ return {
91
+ id: item.id,
92
+ requires: [IRenderMimeRegistry, ITranslator],
93
+ autoStart: true,
94
+ activate: (
95
+ app: JupyterFrontEnd,
96
+ rendermime: IRenderMimeRegistry,
97
+ translator: ITranslator
98
+ ) => {
99
+ // Add the mime renderer.
100
+ if (item.rank !== undefined) {
101
+ rendermime.addFactory(item.rendererFactory, item.rank);
102
+ } else {
103
+ rendermime.addFactory(item.rendererFactory);
104
+ }
105
+
106
+ // Handle the widget factory.
107
+ if (!item.documentWidgetFactoryOptions) {
108
+ return;
109
+ }
110
+
111
+ const registry = app.docRegistry;
112
+ let options: IRenderMime.IDocumentWidgetFactoryOptions[] = [];
113
+ if (Array.isArray(item.documentWidgetFactoryOptions)) {
114
+ options = item.documentWidgetFactoryOptions;
115
+ } else {
116
+ options = [
117
+ item.documentWidgetFactoryOptions as IRenderMime.IDocumentWidgetFactoryOptions
118
+ ];
119
+ }
120
+
121
+ if (item.fileTypes) {
122
+ item.fileTypes.forEach(ft => {
123
+ if (ft.icon) {
124
+ // upconvert the contents of the icon field to a proper LabIcon
125
+ ft = { ...ft, icon: LabIcon.resolve({ icon: ft.icon }) };
126
+ }
127
+
128
+ app.docRegistry.addFileType(ft as DocumentRegistry.IFileType);
129
+ });
130
+ }
131
+
132
+ options.forEach(option => {
133
+ const toolbarFactory = option.toolbarFactory
134
+ ? (w: MimeDocument) => option.toolbarFactory!(w.content.renderer)
135
+ : undefined;
136
+ const factory = new MimeDocumentFactory({
137
+ renderTimeout: item.renderTimeout,
138
+ dataType: item.dataType,
139
+ rendermime,
140
+ modelName: option.modelName,
141
+ name: option.name,
142
+ primaryFileType: registry.getFileType(option.primaryFileType),
143
+ fileTypes: option.fileTypes,
144
+ defaultFor: option.defaultFor,
145
+ defaultRendered: option.defaultRendered,
146
+ toolbarFactory,
147
+ translator,
148
+ factory: item.rendererFactory
149
+ });
150
+ registry.addWidgetFactory(factory);
151
+
152
+ factory.widgetCreated.connect((sender, widget) => {
153
+ Private.factoryNameProperty.set(widget, factory.name);
154
+ // Notify the widget tracker if restore data needs to update.
155
+ widget.context.pathChanged.connect(() => {
156
+ void tracker.save(widget);
157
+ });
158
+ void tracker.add(widget);
159
+ });
160
+ });
161
+ }
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Private namespace for the module.
167
+ */
168
+ namespace Private {
169
+ /**
170
+ * An attached property for keeping the factory name
171
+ * that was used to create a mimedocument.
172
+ */
173
+ export const factoryNameProperty = new AttachedProperty<
174
+ MimeDocument,
175
+ string | undefined
176
+ >({
177
+ name: 'factoryName',
178
+ create: () => undefined
179
+ });
180
+ }
package/src/router.ts ADDED
@@ -0,0 +1,206 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |----------------------------------------------------------------------------*/
5
+
6
+ import { URLExt } from '@jupyterlab/coreutils';
7
+ import { CommandRegistry } from '@lumino/commands';
8
+ import { PromiseDelegate, Token } from '@lumino/coreutils';
9
+ import { DisposableDelegate, IDisposable } from '@lumino/disposable';
10
+ import { ISignal, Signal } from '@lumino/signaling';
11
+ import { IRouter } from './tokens';
12
+
13
+ /**
14
+ * A static class that routes URLs within the application.
15
+ */
16
+ export class Router implements IRouter {
17
+ /**
18
+ * Create a URL router.
19
+ */
20
+ constructor(options: Router.IOptions) {
21
+ this.base = options.base;
22
+ this.commands = options.commands;
23
+ }
24
+
25
+ /**
26
+ * The base URL for the router.
27
+ */
28
+ readonly base: string;
29
+
30
+ /**
31
+ * The command registry used by the router.
32
+ */
33
+ readonly commands: CommandRegistry;
34
+
35
+ /**
36
+ * Returns the parsed current URL of the application.
37
+ */
38
+ get current(): IRouter.ILocation {
39
+ const { base } = this;
40
+ const parsed = URLExt.parse(window.location.href);
41
+ const { search, hash } = parsed;
42
+ const path = parsed.pathname?.replace(base, '/') ?? '';
43
+ const request = path + search + hash;
44
+
45
+ return { hash, path, request, search };
46
+ }
47
+
48
+ /**
49
+ * A signal emitted when the router routes a route.
50
+ */
51
+ get routed(): ISignal<this, IRouter.ILocation> {
52
+ return this._routed;
53
+ }
54
+
55
+ /**
56
+ * If a matching rule's command resolves with the `stop` token during routing,
57
+ * no further matches will execute.
58
+ */
59
+ readonly stop = new Token<void>('@jupyterlab/application:Router#stop');
60
+
61
+ /**
62
+ * Navigate to a new path within the application.
63
+ *
64
+ * @param path - The new path or empty string if redirecting to root.
65
+ *
66
+ * @param options - The navigation options.
67
+ */
68
+ navigate(path: string, options: IRouter.INavOptions = {}): void {
69
+ const { base } = this;
70
+ const { history } = window;
71
+ const { hard } = options;
72
+ const old = document.location.href;
73
+ const url =
74
+ path && path.indexOf(base) === 0 ? path : URLExt.join(base, path);
75
+
76
+ if (url === old) {
77
+ return hard ? this.reload() : undefined;
78
+ }
79
+
80
+ history.pushState({}, '', url);
81
+
82
+ if (hard) {
83
+ return this.reload();
84
+ }
85
+
86
+ if (!options.skipRouting) {
87
+ // Because a `route()` call may still be in the stack after having received
88
+ // a `stop` token, wait for the next stack frame before calling `route()`.
89
+ requestAnimationFrame(() => {
90
+ void this.route();
91
+ });
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Register to route a path pattern to a command.
97
+ *
98
+ * @param options - The route registration options.
99
+ *
100
+ * @returns A disposable that removes the registered rule from the router.
101
+ */
102
+ register(options: IRouter.IRegisterOptions): IDisposable {
103
+ const { command, pattern } = options;
104
+ const rank = options.rank ?? 100;
105
+ const rules = this._rules;
106
+
107
+ rules.set(pattern, { command, rank });
108
+
109
+ return new DisposableDelegate(() => {
110
+ rules.delete(pattern);
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Cause a hard reload of the document.
116
+ */
117
+ reload(): void {
118
+ window.location.reload();
119
+ }
120
+
121
+ /**
122
+ * Route a specific path to an action.
123
+ *
124
+ * #### Notes
125
+ * If a pattern is matched, its command will be invoked with arguments that
126
+ * match the `IRouter.ILocation` interface.
127
+ */
128
+ route(): Promise<void> {
129
+ const { commands, current, stop } = this;
130
+ const { request } = current;
131
+ const routed = this._routed;
132
+ const rules = this._rules;
133
+ const matches: Private.Rule[] = [];
134
+
135
+ // Collect all rules that match the URL.
136
+ rules.forEach((rule, pattern) => {
137
+ if (request?.match(pattern)) {
138
+ matches.push(rule);
139
+ }
140
+ });
141
+
142
+ // Order the matching rules by rank and enqueue them.
143
+ const queue = matches.sort((a, b) => b.rank - a.rank);
144
+ const done = new PromiseDelegate<void>();
145
+
146
+ // Process each enqueued command sequentially and short-circuit if a promise
147
+ // resolves with the `stop` token.
148
+ const next = async () => {
149
+ if (!queue.length) {
150
+ routed.emit(current);
151
+ done.resolve(undefined);
152
+ return;
153
+ }
154
+
155
+ const { command } = queue.pop()!;
156
+
157
+ try {
158
+ const request = this.current.request;
159
+ const result = await commands.execute(command, current);
160
+ if (result === stop) {
161
+ queue.length = 0;
162
+ console.debug(`Routing ${request} was short-circuited by ${command}`);
163
+ }
164
+ } catch (reason) {
165
+ console.warn(`Routing ${request} to ${command} failed`, reason);
166
+ }
167
+ void next();
168
+ };
169
+ void next();
170
+
171
+ return done.promise;
172
+ }
173
+
174
+ private _routed = new Signal<this, IRouter.ILocation>(this);
175
+ private _rules = new Map<RegExp, Private.Rule>();
176
+ }
177
+
178
+ /**
179
+ * A namespace for `Router` class statics.
180
+ */
181
+ export namespace Router {
182
+ /**
183
+ * The options for instantiating a JupyterLab URL router.
184
+ */
185
+ export interface IOptions {
186
+ /**
187
+ * The fully qualified base URL for the router.
188
+ */
189
+ base: string;
190
+
191
+ /**
192
+ * The command registry used by the router.
193
+ */
194
+ commands: CommandRegistry;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * A namespace for private module data.
200
+ */
201
+ namespace Private {
202
+ /**
203
+ * The internal representation of a routing rule.
204
+ */
205
+ export type Rule = { command: string; rank: number };
206
+ }