From 60be174436fa111491cb213542613dd4bd480cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Sun, 5 May 2024 16:30:49 +0200 Subject: [PATCH 1/3] Allow Application.shell to be Widget or HTMLElement --- packages/application/src/index.ts | 29 ++++++++++------ packages/application/tests/src/index.spec.ts | 36 ++++++++++++++++++++ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/packages/application/src/index.ts b/packages/application/src/index.ts index d205bcaf8..f84df09be 100644 --- a/packages/application/src/index.ts +++ b/packages/application/src/index.ts @@ -39,7 +39,7 @@ export { type IPlugin }; * UI applications with the ability to be safely extended by third * party code via plugins. */ -export class Application { +export class Application { /** * Construct a new application. * @@ -57,6 +57,7 @@ export class Application { renderer: options.contextMenuRenderer }); this.shell = options.shell; + this._hasShellWidget = this.shell instanceof Widget; } /** @@ -196,7 +197,7 @@ export class Application { * If the plugin provides a service which has already been provided * by another plugin, the new service will override the old service. */ - registerPlugin(plugin: IPlugin): void { + registerPlugin(plugin: IPlugin, any>): void { this.pluginRegistry.registerPlugin(plugin); } @@ -208,7 +209,7 @@ export class Application { * #### Notes * This calls `registerPlugin()` for each of the given plugins. */ - registerPlugins(plugins: IPlugin[]): void { + registerPlugins(plugins: IPlugin, any>[]): void { this.pluginRegistry.registerPlugins(plugins); } @@ -339,10 +340,16 @@ export class Application { * A subclass may reimplement this method as needed. */ protected attachShell(id: string): void { + if (this._hasShellWidget){ Widget.attach( - this.shell, + this.shell as Widget, (id && document.getElementById(id)) || document.body - ); + );} else { + const host = (id && document.getElementById(id)) || document.body; + if(!host.contains(this.shell as HTMLElement)) { + host.appendChild(this.shell as HTMLElement) + } + } } /** @@ -419,7 +426,7 @@ export class Application { * A subclass may reimplement this method as needed. */ protected evtResize(event: Event): void { - this.shell.update(); + if(this._hasShellWidget){(this.shell as Widget).update()} } /** @@ -429,6 +436,7 @@ export class Application { private _delegate = new PromiseDelegate(); private _started = false; private _bubblingKeydown = false; + private _hasShellWidget = true; } /** @@ -438,13 +446,12 @@ export namespace Application { /** * An options object for creating an application. */ - export interface IOptions extends PluginRegistry.IOptions { + export interface IOptions extends PluginRegistry.IOptions { /** - * The shell widget to use for the application. - * - * This should be a newly created and initialized widget. + * The shell element to use for the application. * - * The application will attach the widget to the DOM. + * If it is a {@link Widget}, this should be a newly created and initialized widget. + * and the application will attach the widget to the DOM. */ shell: T; diff --git a/packages/application/tests/src/index.spec.ts b/packages/application/tests/src/index.spec.ts index f2e535eea..e8c5a454a 100644 --- a/packages/application/tests/src/index.spec.ts +++ b/packages/application/tests/src/index.spec.ts @@ -57,6 +57,18 @@ describe('@lumino/application', () => { expect(app.hasPlugin(id1)).to.be.true; expect(app.isPluginActivated(id2)).to.be.true; }); + + it('should instantiate an application with a HTMLElement as shell', () => { + const shell = document.createElement('div'); + const app = new Application({ + shell + }); + + expect(app).to.be.instanceOf(Application); + expect(app.commands).to.be.instanceOf(CommandRegistry); + expect(app.contextMenu).to.be.instanceOf(ContextMenu); + expect(app.shell).to.equal(shell); + }); }); describe('#getPluginDescription', () => { @@ -624,5 +636,29 @@ describe('@lumino/application', () => { expect(app.isPluginActivated(id3)).to.be.false; }); }); + + describe('#start', () => { + it('should attach the shell widget to the document body', async () => { + const shell = new Widget(); + const app = new Application({ + shell + }); + + await app.start(); + + expect(document.body.contains(shell.node)).to.be.true; + }) + + it('should attach the shell HTML element to the document body', async () => { + const shell = document.createElement('div'); + const app = new Application({ + shell + }); + + await app.start(); + + expect(document.body.contains(shell)).to.be.true; + }) + }) }); }); From 9709c8668db215e2f73419c9a8cf5236b77ab8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Mon, 27 May 2024 08:18:11 +0200 Subject: [PATCH 2/3] Make the Application fully composable --- packages/application/src/index.ts | 51 ++++++++++++++------ packages/application/tests/src/index.spec.ts | 27 +++++++++-- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/packages/application/src/index.ts b/packages/application/src/index.ts index f84df09be..fe7112874 100644 --- a/packages/application/src/index.ts +++ b/packages/application/src/index.ts @@ -51,11 +51,14 @@ export class Application { this.pluginRegistry.application = this; // Initialize the application state. - this.commands = new CommandRegistry(); - this.contextMenu = new ContextMenu({ + this.commands = options.commands ?? new CommandRegistry(); + const contextMenuOptions = { commands: this.commands, renderer: options.contextMenuRenderer - }); + }; + this.contextMenu = options.contextMenuFactory + ? options.contextMenuFactory(contextMenuOptions) + : new ContextMenu(contextMenuOptions); this.shell = options.shell; this._hasShellWidget = this.shell instanceof Widget; } @@ -197,7 +200,9 @@ export class Application { * If the plugin provides a service which has already been provided * by another plugin, the new service will override the old service. */ - registerPlugin(plugin: IPlugin, any>): void { + registerPlugin( + plugin: IPlugin, any> + ): void { this.pluginRegistry.registerPlugin(plugin); } @@ -209,7 +214,9 @@ export class Application { * #### Notes * This calls `registerPlugin()` for each of the given plugins. */ - registerPlugins(plugins: IPlugin, any>[]): void { + registerPlugins( + plugins: IPlugin, any>[] + ): void { this.pluginRegistry.registerPlugins(plugins); } @@ -340,14 +347,15 @@ export class Application { * A subclass may reimplement this method as needed. */ protected attachShell(id: string): void { - if (this._hasShellWidget){ - Widget.attach( - this.shell as Widget, - (id && document.getElementById(id)) || document.body - );} else { + if (this._hasShellWidget) { + Widget.attach( + this.shell as Widget, + (id && document.getElementById(id)) || document.body + ); + } else { const host = (id && document.getElementById(id)) || document.body; - if(!host.contains(this.shell as HTMLElement)) { - host.appendChild(this.shell as HTMLElement) + if (!host.contains(this.shell as HTMLElement)) { + host.appendChild(this.shell as HTMLElement); } } } @@ -426,7 +434,9 @@ export class Application { * A subclass may reimplement this method as needed. */ protected evtResize(event: Event): void { - if(this._hasShellWidget){(this.shell as Widget).update()} + if (this._hasShellWidget) { + (this.shell as Widget).update(); + } } /** @@ -440,13 +450,14 @@ export class Application { } /** - * The namespace for the `Application` class statics. + * The namespace for the {@link Application} class statics. */ export namespace Application { /** * An options object for creating an application. */ - export interface IOptions extends PluginRegistry.IOptions { + export interface IOptions + extends PluginRegistry.IOptions { /** * The shell element to use for the application. * @@ -455,6 +466,16 @@ export namespace Application { */ shell: T; + /** + * A custom commands registry. + */ + commands?: CommandRegistry; + + /** + * A custom context menu factory. + */ + contextMenuFactory?: (options: ContextMenu.IOptions) => ContextMenu; + /** * A custom renderer for the context menu. */ diff --git a/packages/application/tests/src/index.spec.ts b/packages/application/tests/src/index.spec.ts index e8c5a454a..43b689c2c 100644 --- a/packages/application/tests/src/index.spec.ts +++ b/packages/application/tests/src/index.spec.ts @@ -69,6 +69,27 @@ describe('@lumino/application', () => { expect(app.contextMenu).to.be.instanceOf(ContextMenu); expect(app.shell).to.equal(shell); }); + + it('should instantiate an application with a custom command registry', () => { + const commands = new (class extends CommandRegistry {})(); + + const app = new Application({ shell: new Widget(), commands }); + + expect(app.commands).to.be.equal(commands); + }); + + it('should instantiate an application with a custom context menu factory', () => { + const contextMenuFactory = (options: ContextMenu.IOptions) => + new (class extends ContextMenu {})(options); + + const app = new Application({ + shell: new Widget(), + contextMenuFactory + }); + + expect(app.contextMenu).to.be.instanceOf(ContextMenu); + expect(app.contextMenu.menu.commands).to.be.equal(app.commands); + }); }); describe('#getPluginDescription', () => { @@ -647,7 +668,7 @@ describe('@lumino/application', () => { await app.start(); expect(document.body.contains(shell.node)).to.be.true; - }) + }); it('should attach the shell HTML element to the document body', async () => { const shell = document.createElement('div'); @@ -658,7 +679,7 @@ describe('@lumino/application', () => { await app.start(); expect(document.body.contains(shell)).to.be.true; - }) - }) + }); + }); }); }); From d722a134026f7f0e08dcc22ad7cb73c117865ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Mon, 27 May 2024 08:53:25 +0200 Subject: [PATCH 3/3] Update API --- review/api/application.api.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/review/api/application.api.md b/review/api/application.api.md index cc30b57e9..d7381df82 100644 --- a/review/api/application.api.md +++ b/review/api/application.api.md @@ -13,7 +13,7 @@ import { Token } from '@lumino/coreutils'; import { Widget } from '@lumino/widgets'; // @public -export class Application { +export class Application { constructor(options: Application.IOptions); activateDeferredPlugins(): Promise; activatePlugin(id: string): Promise; @@ -34,8 +34,8 @@ export class Application { isPluginActivated(id: string): boolean; listPlugins(): string[]; protected pluginRegistry: PluginRegistry; - registerPlugin(plugin: IPlugin): void; - registerPlugins(plugins: IPlugin[]): void; + registerPlugin(plugin: IPlugin, any>): void; + registerPlugins(plugins: IPlugin, any>[]): void; resolveOptionalService(token: Token): Promise; resolveRequiredService(token: Token): Promise; readonly shell: T; @@ -45,7 +45,9 @@ export class Application { // @public export namespace Application { - export interface IOptions extends PluginRegistry.IOptions { + export interface IOptions extends PluginRegistry.IOptions { + commands?: CommandRegistry; + contextMenuFactory?: (options: ContextMenu.IOptions) => ContextMenu; contextMenuRenderer?: Menu.IRenderer; pluginRegistry?: PluginRegistry; shell: T;