diff --git a/package.json b/package.json new file mode 100644 index 0000000..d4b82b7 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "HTMLViewer", + "version": "0.1.0", + "description": "A JupyterLab extension.", + "keywords": [ + "jupyter", + "jupyterlab", + "jupyterlab-extension" + ], + "homepage": "https://github.com/my_name/myextension", + "bugs": { + "url": "https://github.com/my_name/myextension/issues" + }, + "license": "BSD-3-Clause", + "author": "logilab", + "files": [ + "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", + "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/my_name/myextension.git" + }, + "scripts": { + "build": "tsc", + "clean": "rimraf lib", + "watch": "tsc -w", + "prepare": "npm run clean && npm run build" + }, + "dependencies": { + "@jupyterlab/application": "^0.18.5", + "@jupyterlab/apputils": "^0.18.4", + "@jupyterlab/docmanager": "^0.18.4", + "@jupyterlab/docregistry": "^0.18.4", + "@jupyterlab/notebook": "^0.18.4", + "@phosphor/disposable": "^1.1.2", + "@types/react": "^16.4.14", + "@types/react-dom": "^16.0.7" + }, + "devDependencies": { + "rimraf": "^2.6.1", + "typescript": "~2.9.2" + }, + "jupyterlab": { + "extension": true + } +} diff --git a/src/htmlviewer.ts b/src/htmlviewer.ts new file mode 100644 index 0000000..6c5dc43 --- /dev/null +++ b/src/htmlviewer.ts @@ -0,0 +1,24 @@ +import { IInstanceTracker } from '@jupyterlab/apputils'; + +import { IDocumentWidget } from '@jupyterlab/docregistry'; + +import { Token } from '@phosphor/coreutils'; + +import { HtmlViewer } from './widget'; + +export * from './widget'; + +/** + * A class that tracks editor widgets. + */ +export interface IHtmlTracker +extends IInstanceTracker> {} + +/* tslint:disable */ +/** + * The editor tracker token. + */ +export const IHtmlTracker = new Token( + '@jupyterlab/htmlviewer:IHtmlTracker' +); +/* tslint:enable */ diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..3c6d14c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,91 @@ + +import { + ILayoutRestorer, + JupyterLab, + JupyterLabPlugin +} from '@jupyterlab/application'; + +import { ICommandPalette, InstanceTracker } from '@jupyterlab/apputils'; + +import { IDocumentWidget } from '@jupyterlab/docregistry'; + +import { + HtmlViewer, + HtmlViewerFactory, + IHtmlTracker +} from './htmlviewer'; + +/** + * The list of file types for html. + */ +const FILE_TYPES = ['html', 'xhtml']; + +/** + * The name of the factory that creates html widgets. + */ +const FACTORY = 'Html'; + +/** + * The html file handler extension. + */ +const plugin: JupyterLabPlugin = { + activate, + id: '@jupyterlab/htmlviewer-extension:plugin', + provides: IHtmlTracker, + requires: [ICommandPalette, ILayoutRestorer], + autoStart: true +}; + +/** + * Export the plugin as default. + */ +export default plugin; + +/** + * Activate the html widget extension. + */ +function activate( + app: JupyterLab, + palette: ICommandPalette, + restorer: ILayoutRestorer +): IHtmlTracker { + const namespace = 'html-widget'; + const factory = new HtmlViewerFactory({ + name: FACTORY, + modelName: 'base64', + fileTypes: FILE_TYPES, + defaultFor: FILE_TYPES, + readOnly: true + }); + const tracker = new InstanceTracker>({ + namespace + }); + + // Handle state restoration. + restorer.restore(tracker, { + command: 'docmanager:open', + args: widget => ({ path: widget.context.path, factory: FACTORY }), + name: widget => widget.context.path + }); + + app.docRegistry.addWidgetFactory(factory); + + factory.widgetCreated.connect((sender, widget) => { + // Notify the instance tracker if restore data needs to update. + widget.context.pathChanged.connect(() => { + tracker.save(widget); + }); + tracker.add(widget); + + const types = app.docRegistry.getFileTypesForPath(widget.context.path); + + if (types.length > 0) { + widget.title.iconClass = types[0].iconClass; + widget.title.iconLabel = types[0].iconLabel; + } + }); + + return tracker; +} + + diff --git a/src/widget.ts b/src/widget.ts new file mode 100644 index 0000000..1a049b8 --- /dev/null +++ b/src/widget.ts @@ -0,0 +1,131 @@ +import { PathExt } from '@jupyterlab/coreutils'; + +import { + ABCWidgetFactory, + DocumentRegistry, + IDocumentWidget, + DocumentWidget +} from '@jupyterlab/docregistry'; + +import { PromiseDelegate } from '@phosphor/coreutils'; + +import { Message } from '@phosphor/messaging'; + +import { Widget } from '@phosphor/widgets'; + +/** + * The class name added to a htmlviewer. + */ +const HTML_CLASS = 'jp-HtmlViewer'; + +/** + * A widget for HTML. + */ +export class HtmlViewer extends Widget { + /** + * Construct a new html widget. + */ + constructor(context: DocumentRegistry.Context) { + super(); + this.context = context; + this.node.tabIndex = -1; + this.addClass(HTML_CLASS); + + this._html = document.createElement('iframe'); + this._html.setAttribute('height', "100%"); + this._html.setAttribute('width', "100%"); + this.node.appendChild(this._html); + + this._onTitleChanged(); + context.pathChanged.connect( + this._onTitleChanged, + this + ); + + context.ready.then(() => { + if (this.isDisposed) { + return; + } + this._render(); + context.model.contentChanged.connect( + this.update, + this + ); + context.fileChanged.connect( + this.update, + this + ); + this._ready.resolve(void 0); + }); + } + + /** + * The html widget's context. + */ + readonly context: DocumentRegistry.Context; + + /** + * A promise that resolves when the html viewer is ready. + */ + get ready(): Promise { + return this._ready.promise; + } + + /** + * Handle `update-request` messages for the widget. + */ + protected onUpdateRequest(msg: Message): void { + if (this.isDisposed || !this.context.isReady) { + return; + } + this._render(); + } + + /** + * Handle `'activate-request'` messages. + */ + protected onActivateRequest(msg: Message): void { + this.node.focus(); + } + + /** + * Handle a change to the title. + */ + private _onTitleChanged(): void { + this.title.label = PathExt.basename(this.context.localPath); + } + + /** + * Render the widget content. + */ + private _render(): void { + let context = this.context; + let cm = context.contentsModel; + if (!cm) { + return; + } + let url = this.node.baseURI.replace("/lab", "/files/")+ this.context.path; + this._html.setAttribute('src', url); + } + + private _ready = new PromiseDelegate(); + private _html: HTMLElement; +} + +/** + * A widget factory for html + */ +export class HtmlViewerFactory extends ABCWidgetFactory< + IDocumentWidget + > { + /** + * Create a new widget given a context. + */ + protected createNewWidget( + context: DocumentRegistry.IContext + ): IDocumentWidget { + const content = new HtmlViewer(context); + const widget = new DocumentWidget({ content, context }); + return widget; + } + } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5c51033 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "declaration": true, + "lib": ["es2015", "dom"], + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": true, + "noUnusedLocals": true, + "outDir": "./lib", + "target": "es2015", + "strict": true, + "strictNullChecks": false, + "types": [] + }, + "include": ["src/*"] +}