diff --git a/packages/htmlviewer-extension/package.json b/packages/htmlviewer-extension/package.json index 4ab471395c96..8f6830b3d5ff 100644 --- a/packages/htmlviewer-extension/package.json +++ b/packages/htmlviewer-extension/package.json @@ -24,6 +24,7 @@ "style": "style/index.css", "files": [ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", + "schema/*.json", "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}", "style/index.js" ], @@ -38,6 +39,7 @@ "@jupyterlab/apputils": "^3.1.0-beta.0", "@jupyterlab/docregistry": "^3.1.0-beta.0", "@jupyterlab/htmlviewer": "^3.1.0-beta.0", + "@jupyterlab/settingregistry": "^3.1.0-beta.0", "@jupyterlab/translation": "^3.1.0-beta.0", "@jupyterlab/ui-components": "^3.1.0-beta.0" }, @@ -49,7 +51,8 @@ "access": "public" }, "jupyterlab": { - "extension": true + "extension": true, + "schemaDir": "schema" }, "styleModule": "style/index.js" } diff --git a/packages/htmlviewer-extension/schema/plugin.json b/packages/htmlviewer-extension/schema/plugin.json new file mode 100644 index 000000000000..2a373d535b07 --- /dev/null +++ b/packages/htmlviewer-extension/schema/plugin.json @@ -0,0 +1,71 @@ +{ + "title": "HTML Viewer", + "description": "HTML Viewer settings.", + "jupyter.lab.toolbars": { + "HTML Viewer": [ + { "name": "refresh", "rank": 10 }, + { "name": "trust", "rank": 20 } + ] + }, + "jupyter.lab.transform": true, + "properties": { + "toolbar": { + "title": "HTML viewer toolbar items", + "description": "Note: To disable a toolbar item,\ncopy it to User Preferences and add the\n\"disabled\" key. The following example will disable the refresh item:\n{\n \"toolbar\": [\n {\n \"name\": \"refresh\",\n \"disabled\": true\n }\n ]\n}\n\nToolbar description:", + "items": { + "$ref": "#/definitions/toolbarItem" + }, + "type": "array", + "default": [] + } + }, + "additionalProperties": false, + "type": "object", + "definitions": { + "toolbarItem": { + "properties": { + "name": { + "title": "Unique name", + "type": "string" + }, + "args": { + "title": "Command arguments", + "type": "object" + }, + "command": { + "title": "Command id", + "type": "string", + "default": "" + }, + "disabled": { + "title": "Whether the item is disabled or not", + "type": "boolean", + "default": false + }, + "icon": { + "title": "Item icon id", + "description": "If defined, it will override the command icon", + "type": "string" + }, + "label": { + "title": "Item icon label", + "description": "If defined, it will override the command label", + "type": "string" + }, + "type": { + "title": "Item type", + "type": "string", + "enum": ["command", "spacer"] + }, + "rank": { + "title": "Item rank", + "type": "number", + "minimum": 0 + } + }, + "required": ["name"], + "additionalProperties": false, + "type": "object" + } + } +} diff --git a/packages/htmlviewer-extension/src/index.tsx b/packages/htmlviewer-extension/src/index.tsx index 25a012c53567..64401818504a 100644 --- a/packages/htmlviewer-extension/src/index.tsx +++ b/packages/htmlviewer-extension/src/index.tsx @@ -12,16 +12,28 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; -import { ICommandPalette, WidgetTracker } from '@jupyterlab/apputils'; +import { + createToolbarFactory, + ICommandPalette, + IToolbarWidgetRegistry, + WidgetTracker +} from '@jupyterlab/apputils'; import { DocumentRegistry } from '@jupyterlab/docregistry'; import { HTMLViewer, HTMLViewerFactory, - IHTMLViewerTracker + IHTMLViewerTracker, + ToolbarItems } from '@jupyterlab/htmlviewer'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ITranslator } from '@jupyterlab/translation'; import { html5Icon } from '@jupyterlab/ui-components'; +/** + * Factory name + */ +const FACTORY = 'HTML Viewer'; + /** * Command IDs used by the plugin. */ @@ -37,7 +49,12 @@ const htmlPlugin: JupyterFrontEndPlugin = { id: '@jupyterlab/htmlviewer-extension:plugin', provides: IHTMLViewerTracker, requires: [ITranslator], - optional: [ICommandPalette, ILayoutRestorer], + optional: [ + ICommandPalette, + ILayoutRestorer, + ISettingRegistry, + IToolbarWidgetRegistry + ], autoStart: true }; @@ -48,10 +65,35 @@ function activateHTMLViewer( app: JupyterFrontEnd, translator: ITranslator, palette: ICommandPalette | null, - restorer: ILayoutRestorer | null + restorer: ILayoutRestorer | null, + settingRegistry: ISettingRegistry | null, + toolbarRegistry: IToolbarWidgetRegistry | null ): IHTMLViewerTracker { - // Add an HTML file type to the docregistry. + let toolbarFactory: + | ((widget: HTMLViewer) => DocumentRegistry.IToolbarItem[]) + | undefined; const trans = translator.load('jupyterlab'); + + if (toolbarRegistry) { + toolbarRegistry.registerFactory(FACTORY, 'refresh', widget => + ToolbarItems.createRefreshButton(widget, translator) + ); + toolbarRegistry.registerFactory(FACTORY, 'trust', widget => + ToolbarItems.createTrustButton(widget, translator) + ); + + if (settingRegistry) { + toolbarFactory = createToolbarFactory( + toolbarRegistry, + settingRegistry, + FACTORY, + htmlPlugin.id, + translator + ); + } + } + + // Add an HTML file type to the docregistry. const ft: DocumentRegistry.IFileType = { name: 'html', contentType: 'file', @@ -65,10 +107,12 @@ function activateHTMLViewer( // Create a new viewer factory. const factory = new HTMLViewerFactory({ - name: trans.__('HTML Viewer'), + name: FACTORY, fileTypes: ['html'], defaultFor: ['html'], - readOnly: true + readOnly: true, + toolbarFactory, + translator }); // Create a widget tracker for HTML documents. @@ -108,19 +152,22 @@ function activateHTMLViewer( // allowing script executions in its context. app.commands.addCommand(CommandIDs.trustHTML, { label: trans.__('Trust HTML File'), + caption: trans.__(`Whether the HTML file is trusted. + Trusting the file allows scripts to run in it, + which may result in security risks. + Only enable for files you trust.`), isEnabled: () => !!tracker.currentWidget, isToggled: () => { const current = tracker.currentWidget; if (!current) { return false; } - const sandbox = current.content.sandbox; - return sandbox.indexOf('allow-scripts') !== -1; + return current.trusted; }, execute: () => { const current = tracker.currentWidget; if (!current) { - return false; + return; } current.trusted = !current.trusted; } diff --git a/packages/htmlviewer-extension/tsconfig.json b/packages/htmlviewer-extension/tsconfig.json index dcdf5f091711..cd580eae9f37 100644 --- a/packages/htmlviewer-extension/tsconfig.json +++ b/packages/htmlviewer-extension/tsconfig.json @@ -4,7 +4,9 @@ "outDir": "lib", "rootDir": "src" }, - "include": ["src/*"], + "include": [ + "src/*" + ], "references": [ { "path": "../application" @@ -18,6 +20,9 @@ { "path": "../htmlviewer" }, + { + "path": "../settingregistry" + }, { "path": "../translation" }, diff --git a/packages/htmlviewer/package.json b/packages/htmlviewer/package.json index 5f8f05a392ee..f965367867a2 100644 --- a/packages/htmlviewer/package.json +++ b/packages/htmlviewer/package.json @@ -40,6 +40,7 @@ "@jupyterlab/ui-components": "^3.1.0-beta.0", "@lumino/coreutils": "^1.5.3", "@lumino/signaling": "^1.4.3", + "@lumino/widgets": "^1.19.0", "react": "^17.0.1" }, "devDependencies": { diff --git a/packages/htmlviewer/src/index.tsx b/packages/htmlviewer/src/index.tsx index f1ada47d1647..fed8fca83320 100644 --- a/packages/htmlviewer/src/index.tsx +++ b/packages/htmlviewer/src/index.tsx @@ -26,6 +26,7 @@ import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { refreshIcon } from '@jupyterlab/ui-components'; import { Token } from '@lumino/coreutils'; import { ISignal, Signal } from '@lumino/signaling'; +import { Widget } from '@lumino/widgets'; import * as React from 'react'; /** @@ -74,7 +75,6 @@ export class HTMLViewer content: new IFrame({ sandbox: ['allow-same-origin'] }) }); this.translator = options.translator || nullTranslator; - const trans = this.translator.load('jupyterlab'); this.content.addClass(CSS_CLASS); void this.context.ready.then(() => { @@ -86,31 +86,6 @@ export class HTMLViewer }); this._monitor.activityStopped.connect(this.update, this); }); - - // Make a refresh button for the toolbar. - this.toolbar.addItem( - 'refresh', - new ToolbarButton({ - icon: refreshIcon, - onClick: async () => { - if (!this.context.model.dirty) { - await this.context.revert(); - this.update(); - } - }, - tooltip: trans.__('Rerender HTML Document') - }) - ); - // Make a trust button for the toolbar. - this.toolbar.addItem( - 'trust', - ReactWidget.create( - - ) - ); } /** @@ -234,6 +209,73 @@ export class HTMLViewerFactory extends ABCWidgetFactory { protected createNewWidget(context: DocumentRegistry.Context): HTMLViewer { return new HTMLViewer({ context }); } + + /** + * Default factory for toolbar items to be added after the widget is created. + */ + protected defaultToolbarFactory( + widget: HTMLViewer + ): DocumentRegistry.IToolbarItem[] { + return [ + // Make a refresh button for the toolbar. + { + name: 'refresh', + widget: ToolbarItems.createRefreshButton(widget, this.translator) + }, + // Make a trust button for the toolbar. + { + name: 'trust', + widget: ToolbarItems.createTrustButton(widget, this.translator) + } + ]; + } +} + +/** + * A namespace for toolbar items generator + */ +export namespace ToolbarItems { + /** + * Create the refresh button + * + * @param widget HTML viewer widget + * @param translator Application translator object + * @returns Toolbar item button + */ + export function createRefreshButton( + widget: HTMLViewer, + translator?: ITranslator + ): Widget { + const trans = (translator ?? nullTranslator).load('jupyterlab'); + return new ToolbarButton({ + icon: refreshIcon, + onClick: async () => { + if (!widget.context.model.dirty) { + await widget.context.revert(); + widget.update(); + } + }, + tooltip: trans.__('Rerender HTML Document') + }); + } + /** + * Create the trust button + * + * @param document HTML viewer widget + * @param translator Application translator object + * @returns Toolbar item button + */ + export function createTrustButton( + document: HTMLViewer, + translator: ITranslator + ): Widget { + return ReactWidget.create( + + ); + } } /** @@ -270,9 +312,11 @@ namespace Private { /** * React component for a trusted button. * - * This wraps the ToolbarButtonComponent and watches for trust chagnes. + * This wraps the ToolbarButtonComponent and watches for trust changes. */ - export function TrustButtonComponent(props: TrustButtonComponent.IProps) { + export function TrustButtonComponent( + props: TrustButtonComponent.IProps + ): JSX.Element { const translator = props.translator || nullTranslator; const trans = translator.load('jupyterlab'); return ( @@ -280,7 +324,7 @@ namespace Private { signal={props.htmlDocument.trustedChanged} initialSender={props.htmlDocument} > - {session => ( + {() => ( diff --git a/packages/htmlviewer/style/index.css b/packages/htmlviewer/style/index.css index c845303e874b..98d5e9c91227 100644 --- a/packages/htmlviewer/style/index.css +++ b/packages/htmlviewer/style/index.css @@ -4,6 +4,7 @@ |----------------------------------------------------------------------------*/ /* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ +@import url('~@lumino/widgets/style/index.css'); @import url('~@jupyterlab/ui-components/style/index.css'); @import url('~@jupyterlab/apputils/style/index.css'); @import url('~@jupyterlab/docregistry/style/index.css'); diff --git a/packages/htmlviewer/style/index.js b/packages/htmlviewer/style/index.js index b3e161e5ff8c..698de7a5d912 100644 --- a/packages/htmlviewer/style/index.js +++ b/packages/htmlviewer/style/index.js @@ -4,6 +4,7 @@ |----------------------------------------------------------------------------*/ /* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */ +import '@lumino/widgets/style/index.js'; import '@jupyterlab/ui-components/style/index.js'; import '@jupyterlab/apputils/style/index.js'; import '@jupyterlab/docregistry/style/index.js';