Browse Source

Restructure into a standard extension rather than a mimerenderer.

Ian Rose 6 years ago
parent
commit
d7a0b9df77

+ 4 - 2
dev_mode/package.json

@@ -39,7 +39,8 @@
     "@jupyterlab/fileeditor": "^0.19.1",
     "@jupyterlab/fileeditor-extension": "^0.19.1",
     "@jupyterlab/help-extension": "^0.19.1",
-    "@jupyterlab/htmlviewer-extension": "^0.1.4",
+    "@jupyterlab/htmlviewer": "^0.19.0",
+    "@jupyterlab/htmlviewer-extension": "^0.19.0",
     "@jupyterlab/imageviewer": "^0.19.1",
     "@jupyterlab/imageviewer-extension": "^0.19.1",
     "@jupyterlab/inspector": "^0.19.1",
@@ -149,6 +150,7 @@
       "@jupyterlab/filebrowser-extension": "",
       "@jupyterlab/fileeditor-extension": "",
       "@jupyterlab/help-extension": "",
+      "@jupyterlab/htmlviewer-extension": "",
       "@jupyterlab/imageviewer-extension": "",
       "@jupyterlab/inspector-extension": "",
       "@jupyterlab/launcher-extension": "",
@@ -167,7 +169,6 @@
       "@jupyterlab/tooltip-extension": ""
     },
     "mimeExtensions": {
-      "@jupyterlab/htmlviewer-extension": "",
       "@jupyterlab/javascript-extension": "",
       "@jupyterlab/json-extension": "",
       "@jupyterlab/markdownviewer-extension": "",
@@ -256,6 +257,7 @@
       "@jupyterlab/fileeditor": "../packages/fileeditor",
       "@jupyterlab/fileeditor-extension": "../packages/fileeditor-extension",
       "@jupyterlab/help-extension": "../packages/help-extension",
+      "@jupyterlab/htmlviewer": "../packages/htmlviewer",
       "@jupyterlab/htmlviewer-extension": "../packages/htmlviewer-extension",
       "@jupyterlab/imageviewer": "../packages/imageviewer",
       "@jupyterlab/imageviewer-extension": "../packages/imageviewer-extension",

+ 5 - 4
packages/htmlviewer-extension/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@jupyterlab/htmlviewer-extension",
-  "version": "0.1.4",
+  "version": "0.19.0",
   "description": "JupyterLab extension to render HTML files",
   "keywords": [
     "jupyter",
@@ -30,15 +30,16 @@
     "watch": "tsc -w"
   },
   "dependencies": {
+    "@jupyterlab/application": "^0.19.1",
     "@jupyterlab/apputils": "^0.19.1",
-    "@jupyterlab/rendermime-interfaces": "^1.2.1",
-    "@phosphor/widgets": "^1.6.0"
+    "@jupyterlab/docregistry": "^0.19.1",
+    "@jupyterlab/htmlviewer": "^0.19.0"
   },
   "devDependencies": {
     "rimraf": "~2.6.2",
     "typescript": "~3.1.1"
   },
   "jupyterlab": {
-    "mimeExtension": true
+    "extension": true
   }
 }

+ 68 - 114
packages/htmlviewer-extension/src/index.tsx

@@ -1,23 +1,19 @@
-// Distributed under the terms of the Modified BSD License.
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
 
 import {
-  PanelLayout, Widget
-} from '@phosphor/widgets';
+  ILayoutRestorer,
+  JupyterLab,
+  JupyterLabPlugin
+} from '@jupyterlab/application';
 
-import {
-  IFrame
-} from '@jupyterlab/apputils';
-
-import {
-  IRenderMime
-} from '@jupyterlab/rendermime-interfaces';
+import { InstanceTracker } from '@jupyterlab/apputils';
 
-import '../style/index.css';
+import { DocumentRegistry } from '@jupyterlab/docregistry';
 
-/**
- * The CSS class to add to the Plotly Widget.
- */
-const CSS_CLASS = 'jp-RenderedIFrame';
+import { HTMLViewer, HTMLViewerFactory } from '@jupyterlab/htmlviewer';
 
 /**
  * The CSS class for an HTML5 icon.
@@ -25,107 +21,65 @@ const CSS_CLASS = 'jp-RenderedIFrame';
 const CSS_ICON_CLASS = 'jp-MaterialIcon jp-HTMLIcon';
 
 /**
- * The MIME type for HTML. We don't use `text/html` because that
- * will conflict with the already existing `text/html` mimetype
- * used for inline HTML in JupyterLab.
+ * The HTML file handler extension.
  */
-export
-const MIME_TYPE = 'application/vnd.jupyter.iframe.text';
-
-export
-const HTML_CLASS = 'jp-HTMLViewer';
-
-export
-const HTML_CONTAINER_CLASS = 'jp-HTMLContainer';
-
-export
-class RenderedIFrame extends Widget implements IRenderMime.IRenderer {
-  /**
-   * Create a new widget for rendering HTML.
-   */
-  constructor(options: IRenderMime.IRendererOptions) {
-    super();
-    const layout = this.layout = new PanelLayout();
-    this.addClass(CSS_CLASS);
-    this._iframe = new IFrame();
-    layout.addWidget(this._iframe);
-    this._mimeType = options.mimeType;
-    this._resolver = options.resolver;
-  }
-
-  /**
-   * Render HTML in IFrame into this widget's node.
-   */
-  async renderModel(model: IRenderMime.IMimeModel): Promise<void> {
-    let data = model.data[this._mimeType] as string;
-    data = await this._setBase(data);
-
-    const blob = new Blob([data], {type: "text/html"});
-    this._iframe.url = URL.createObjectURL(blob);
-    return Promise.resolve(void 0);
-  }
-
-  /**
-   * Set a <base> element in the HTML string so that the iframe
-   * can correctly dereference relative links/
-   */
-  private async _setBase(data: string): Promise<string> {
-    const doc = this._parser.parseFromString(data, 'text/html');
-    let base: HTMLBaseElement;
-    base = doc.querySelector('base');
-    if (!base) {
-      base = doc.createElement('base');
-      doc.head.insertBefore(base, doc.head.firstChild);
-    }
-    const path = await this._resolver.resolveUrl('');
-    const baseUrl = await this._resolver.getDownloadUrl(path);
-
-    // Set the base href, plus a fake name for the url of this
-    // document. The fake name doesn't really matter, as long
-    // as the document can dereference relative links to resources
-    // (e.g. CSS and scripts).
-    base.href = `${baseUrl}/__fake__.html`;
-    base.target = '_blank';
-    return doc.documentElement.innerHTML;
-  }
+const htmlPlugin: JupyterLabPlugin<void> = {
+  activate: activateHTMLViewer,
+  id: '@jupyterlab/htmlviewer-extension:plugin',
+  requires: [ILayoutRestorer],
+  autoStart: true
+};
 
-  private _parser = new DOMParser();
-  private _resolver: IRenderMime.IResolver;
-  private _iframe: IFrame;
-  private _mimeType: string;
+/**
+ * Activate the HTMLViewer extension.
+ */
+function activateHTMLViewer(app: JupyterLab, restorer: ILayoutRestorer): void {
+  // Add an HTML file type to the docregistry.
+  const ft: DocumentRegistry.IFileType = {
+    name: 'html',
+    contentType: 'file',
+    fileFormat: 'text',
+    displayName: 'HTML File',
+    extensions: ['.html'],
+    mimeTypes: ['text/html'],
+    iconClass: CSS_ICON_CLASS
+  };
+  app.docRegistry.addFileType(ft);
+
+  // Create a new viewer factory.
+  const factory = new HTMLViewerFactory({
+    name: 'HTML Viewer',
+    fileTypes: ['html'],
+    defaultFor: ['html'],
+    readOnly: true
+  });
+
+  // Create an instance tracker for HTML documents.
+  const tracker = new InstanceTracker<HTMLViewer>({
+    namespace: 'htmlviewer'
+  });
+
+  // Handle state restoration.
+  restorer.restore(tracker, {
+    command: 'docmanager:open',
+    args: widget => ({ path: widget.context.path, factory: 'HTML Viewer' }),
+    name: widget => widget.context.path
+  });
+
+  app.docRegistry.addWidgetFactory(factory);
+  factory.widgetCreated.connect((sender, widget) => {
+    // Track the widget.
+    tracker.add(widget);
+    // Notify the instance tracker if restore data needs to update.
+    widget.context.pathChanged.connect(() => {
+      tracker.save(widget);
+    });
+
+    widget.title.iconClass = ft.iconClass;
+    widget.title.iconLabel = ft.iconLabel;
+  });
 }
-
-
 /**
- * A mime renderer factory for HTML data.
+ * Export the plugins as default.
  */
-export
-const rendererFactory: IRenderMime.IRendererFactory = {
-  safe: true,
-  mimeTypes: [MIME_TYPE],
-  createRenderer: options => new RenderedIFrame(options)
-};
-
-
-const extensions: IRenderMime.IExtension | IRenderMime.IExtension[] = [
-  {
-    id: '@jupyterlab_html:factory',
-    rendererFactory,
-    rank: 0,
-    dataType: "string",
-    fileTypes: [{
-      name: 'html',
-      mimeTypes: [MIME_TYPE],
-      extensions: ['.html'],
-      iconClass: CSS_ICON_CLASS
-    }],
-    documentWidgetFactoryOptions: {
-      name: 'View HTML',
-      primaryFileType: 'html',
-      fileTypes: ['html'],
-      defaultFor: ['html']
-    }
-  }
-];
-
-export default extensions;
+export default htmlPlugin;

+ 5 - 52
packages/htmlviewer-extension/style/index.css

@@ -1,56 +1,9 @@
-/**
-  Distributed under the terms of the Modified BSD License.
-*/
-
-/* Add CSS variables to :root */
-:root {
-  --jp-icon-html5: url('./html5-icon.svg');
-}
-
-/* Document styles */
-.jp-MimeDocument .jp-RenderedIFrame {
-  overflow: hidden;
-}
-
-/* Output styles */
-.jp-OutputArea .jp-RenderedIFrame {
-  min-height: 360px;
-}
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
 
 /* Document icon */
 .jp-HTMLIcon {
-  background-image: var(--jp-icon-html5);
-}
-
-.jp-RenderedIFrame {
-  min-width: 100px;
-  min-height: 100px;
-  width: 100%;
-  height: 100%;
-}
-
-
-.jp-RenderedIFrame > iframe {
-  border: none;
-}
-
-
-/*
-When drag events occur, `p-mod-override-cursor` is added to the body.
-Because iframes steal all cursor events, the following two rules are necessary
-to suppress pointer events while resize drags are occuring. There may be a
-better solution to this problem.
-*/
-body.p-mod-override-cursor .jp-RenderedIFrame {
-  position: relative;
-}
-
-body.p-mod-override-cursor .jp-RenderedIFrame:before {
-  content: '';
-  position: absolute;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: transparent;
+  background-image: url('./html5-icon.svg');
 }

+ 20 - 0
packages/htmlviewer-extension/tdoptions.json

@@ -0,0 +1,20 @@
+{
+  "excludeNotExported": true,
+  "mode": "file",
+  "target": "es5",
+  "module": "es5",
+  "lib": [
+    "lib.es2015.d.ts",
+    "lib.es2015.collection.d.ts",
+    "lib.es2015.promise.d.ts",
+    "lib.dom.d.ts"
+  ],
+  "out": "../../docs/api/observables",
+  "baseUrl": ".",
+  "paths": {
+    "@jupyterlab/*": ["../packages/*"]
+  },
+  "esModuleInterop": true,
+  "jsx": "react",
+  "types": []
+}

+ 7 - 1
packages/htmlviewer-extension/tsconfig.json

@@ -6,11 +6,17 @@
   },
   "include": ["src/*"],
   "references": [
+    {
+      "path": "../application"
+    },
     {
       "path": "../apputils"
     },
     {
-      "path": "../rendermime-interfaces"
+      "path": "../docregistry"
+    },
+    {
+      "path": "../htmlviewer"
     }
   ]
 }

+ 42 - 0
packages/htmlviewer/package.json

@@ -0,0 +1,42 @@
+{
+  "name": "@jupyterlab/htmlviewer",
+  "version": "0.19.0",
+  "description": "A viewer for HTML documents.",
+  "homepage": "https://github.com/jupyterlab/jupyterlab",
+  "bugs": {
+    "url": "https://github.com/jupyterlab/jupyterlab/issues"
+  },
+  "license": "BSD-3-Clause",
+  "author": "Project Jupyter",
+  "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",
+  "directories": {
+    "lib": "lib/"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/jupyterlab/jupyterlab.git"
+  },
+  "scripts": {
+    "build": "tsc",
+    "clean": "rimraf lib",
+    "prepublishOnly": "npm run build",
+    "watch": "tsc -w --listEmittedFiles"
+  },
+  "dependencies": {
+    "@jupyterlab/apputils": "^0.19.1",
+    "@jupyterlab/coreutils": "^2.2.1",
+    "@jupyterlab/docregistry": "^0.19.1"
+  },
+  "devDependencies": {
+    "rimraf": "~2.6.2",
+    "typescript": "~3.1.1"
+  },
+  "publishConfig": {
+    "access": "public"
+  }
+}

+ 130 - 0
packages/htmlviewer/src/index.ts

@@ -0,0 +1,130 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+import { IFrame } from '@jupyterlab/apputils';
+
+import { ActivityMonitor } from '@jupyterlab/coreutils';
+
+import {
+  ABCWidgetFactory,
+  DocumentRegistry,
+  DocumentWidget,
+  IDocumentWidget
+} from '@jupyterlab/docregistry';
+
+import '../style/index.css';
+
+/**
+ * The timeout to wait for change activity to have ceased before rendering.
+ */
+const RENDER_TIMEOUT = 1000;
+
+/**
+ * The CSS class to add to the HTMLViewer Widget.
+ */
+const CSS_CLASS = 'jp-HTMLViewer';
+
+/**
+ * A viewer widget for HTML documents.
+ */
+export class HTMLViewer extends DocumentWidget<IFrame>
+  implements IDocumentWidget<IFrame> {
+  /**
+   * Create a new widget for rendering HTML.
+   */
+  constructor(options: DocumentWidget.IOptionsOptionalContent) {
+    super({ ...options, content: new IFrame() });
+    this.content.addClass(CSS_CLASS);
+
+    this.context.ready.then(() => {
+      this.update();
+      // Throttle the rendering rate of the widget.
+      this._monitor = new ActivityMonitor({
+        signal: this.context.model.contentChanged,
+        timeout: RENDER_TIMEOUT
+      });
+      this._monitor.activityStopped.connect(
+        this.update,
+        this
+      );
+    });
+  }
+
+  /**
+   * Handle and update request.
+   */
+  protected onUpdateRequest(): void {
+    if (this._renderPending) {
+      return;
+    }
+    this._renderPending = true;
+    this._renderModel().then(() => (this._renderPending = false));
+  }
+
+  /**
+   * Render HTML in IFrame into this widget's node.
+   */
+  private async _renderModel(): Promise<void> {
+    let data = this.context.model.toString();
+    data = await this._setBase(data);
+
+    // Set the new iframe url.
+    const blob = new Blob([data], { type: 'text/html' });
+    const oldUrl = this._objectUrl;
+    this._objectUrl = URL.createObjectURL(blob);
+    this.content.url = this._objectUrl;
+
+    // Release reference to any previous object url.
+    if (oldUrl) {
+      try {
+        URL.revokeObjectURL(oldUrl);
+      } catch (error) {
+        /* no-op */
+      }
+    }
+    return;
+  }
+
+  /**
+   * Set a <base> element in the HTML string so that the iframe
+   * can correctly dereference relative links.
+   */
+  private async _setBase(data: string): Promise<string> {
+    const doc = this._parser.parseFromString(data, 'text/html');
+    let base: HTMLBaseElement;
+    base = doc.querySelector('base');
+    if (!base) {
+      base = doc.createElement('base');
+      doc.head.insertBefore(base, doc.head.firstChild);
+    }
+    const path = this.context.path;
+    const baseUrl = await this.context.urlResolver.getDownloadUrl(path);
+
+    // Set the base href, plus a fake name for the url of this
+    // document. The fake name doesn't really matter, as long
+    // as the document can dereference relative links to resources
+    // (e.g. CSS and scripts).
+    base.href = baseUrl;
+    base.target = '_blank';
+    return doc.documentElement.innerHTML;
+  }
+
+  private _renderPending = false;
+  private _parser = new DOMParser();
+  private _monitor: ActivityMonitor<any, any> | null = null;
+  private _objectUrl: string = '';
+}
+
+/**
+ * A widget factory for HTMLViewers.
+ */
+export class HTMLViewerFactory extends ABCWidgetFactory<HTMLViewer> {
+  /**
+   * Create a new widget given a context.
+   */
+  protected createNewWidget(context: DocumentRegistry.Context): HTMLViewer {
+    return new HTMLViewer({ context });
+  }
+}

+ 40 - 0
packages/htmlviewer/style/index.css

@@ -0,0 +1,40 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+/* Document styles */
+.jp-HTMLViewer {
+  overflow: hidden;
+}
+
+.jp-HTMLViewer {
+  min-width: 100px;
+  min-height: 100px;
+  width: 100%;
+  height: 100%;
+}
+
+.jp-HTMLViewer > iframe {
+  border: none;
+}
+
+/*
+When drag events occur, `p-mod-override-cursor` is added to the body.
+Because iframes steal all cursor events, the following two rules are necessary
+to suppress pointer events while resize drags are occuring. There may be a
+better solution to this problem.
+*/
+body.p-mod-override-cursor .jp-HTMLViewer {
+  position: relative;
+}
+
+body.p-mod-override-cursor .jp-HTMLViewer:before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: transparent;
+}

+ 20 - 0
packages/htmlviewer/tdoptions.json

@@ -0,0 +1,20 @@
+{
+  "excludeNotExported": true,
+  "mode": "file",
+  "target": "es5",
+  "module": "es5",
+  "lib": [
+    "lib.es2015.d.ts",
+    "lib.es2015.collection.d.ts",
+    "lib.es2015.promise.d.ts",
+    "lib.dom.d.ts"
+  ],
+  "out": "../../docs/api/observables",
+  "baseUrl": ".",
+  "paths": {
+    "@jupyterlab/*": ["../packages/*"]
+  },
+  "esModuleInterop": true,
+  "jsx": "react",
+  "types": []
+}

+ 19 - 0
packages/htmlviewer/tsconfig.json

@@ -0,0 +1,19 @@
+{
+  "extends": "../../tsconfigbase",
+  "compilerOptions": {
+    "outDir": "lib",
+    "rootDir": "src"
+  },
+  "include": ["src/*"],
+  "references": [
+    {
+      "path": "../apputils"
+    },
+    {
+      "path": "../coreutils"
+    },
+    {
+      "path": "../docregistry"
+    }
+  ]
+}

+ 2 - 1
packages/metapackage/package.json

@@ -57,7 +57,8 @@
     "@jupyterlab/fileeditor": "^0.19.1",
     "@jupyterlab/fileeditor-extension": "^0.19.1",
     "@jupyterlab/help-extension": "^0.19.1",
-    "@jupyterlab/htmlviewer-extension": "^0.1.4",
+    "@jupyterlab/htmlviewer": "^0.19.0",
+    "@jupyterlab/htmlviewer-extension": "^0.19.0",
     "@jupyterlab/imageviewer": "^0.19.1",
     "@jupyterlab/imageviewer-extension": "^0.19.1",
     "@jupyterlab/inspector": "^0.19.1",

+ 3 - 0
packages/metapackage/tsconfig.json

@@ -87,6 +87,9 @@
     {
       "path": "../help-extension"
     },
+    {
+      "path": "../htmlviewer"
+    },
     {
       "path": "../htmlviewer-extension"
     },