Просмотр исходного кода

Merge pull request #1191 from afshin/links

Command links plugin
Steven Silvester 8 лет назад
Родитель
Сommit
0d8c1f02aa

+ 1 - 0
examples/lab/index.js

@@ -15,6 +15,7 @@ var lab = new JupyterLab();
 lab.registerPlugins([
   require('jupyterlab/lib/about/plugin').aboutExtension,
   require('jupyterlab/lib/clipboard/plugin').clipboardProvider,
+  require('jupyterlab/lib/commandlinker/plugin').commandLinkerProvider,
   require('jupyterlab/lib/commandpalette/plugin').commandPaletteProvider,
   require('jupyterlab/lib/console/plugin').consoleTrackerProvider,
   require('jupyterlab/lib/console/codemirror/plugin').rendererProvider,

+ 1 - 0
jupyterlab/extensions.js

@@ -4,6 +4,7 @@
 module.exports = [
   require('../lib/about/plugin').aboutExtension,
   require('../lib/clipboard/plugin').clipboardProvider,
+  require('../lib/commandlinker/plugin').commandLinkerProvider,
   require('../lib/commandpalette/plugin').commandPaletteProvider,
   require('../lib/console/plugin').consoleTrackerProvider,
   require('../lib/console/codemirror/plugin').rendererProvider,

+ 86 - 0
src/commandlinker/index.ts

@@ -0,0 +1,86 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+import {
+  JSONObject
+} from 'phosphor/lib/algorithm/json';
+
+import {
+  Token
+} from 'phosphor/lib/core/token';
+
+import {
+  IElementAttrs
+} from 'phosphor/lib/ui/vdom';
+
+/* tslint:disable */
+/**
+ * The command linker token.
+ */
+export
+const ICommandLinker = new Token<ICommandLinker>('jupyter.services.commandlinker');
+/* tslint:enable */
+
+
+/**
+ * A helper class to generate clickables that execute commands.
+ */
+export
+interface ICommandLinker {
+  /**
+   * Connect a command/argument pair to a given node so that when it is clicked,
+   * the command will execute.
+   *
+   * @param node - The node being connected.
+   *
+   * @param command - The command ID to execute upon click.
+   *
+   * @param args - The arguments with which to invoke the command.
+   *
+   * @returns The same node that was passed in, after it has been connected.
+   *
+   * #### Notes
+   * Only `click` events will execute the command on a connected node. So, there
+   * are two considerations that are relevant:
+   * 1. If a node is connected, the default click action will be prevented.
+   * 2. The `HTMLElement` passed in should be clickable.
+   */
+  connectNode(node: HTMLElement, command: string, args: JSONObject): HTMLElement;
+
+  /**
+   * Disconnect a node that has been connected to execute a command on click.
+   *
+   * @param node - The node being disconnected.
+   *
+   * @returns The same node that was passed in, after it has been disconnected.
+   *
+   * #### Notes
+   * This method is safe to call multiple times and is safe to call on nodes
+   * that were never connected.
+   *
+   * This method can be called on rendered virtual DOM nodes that were populated
+   * using the `populateVNodeAttributes` method in order to disconnect them from
+   * executing their command/argument pair.
+   */
+  disconnectNode(node: HTMLElement): HTMLElement;
+
+  /**
+   * Populate the attributes used to instantiate a virtual DOM node with the
+   * data set values necessary for its rendered DOM node to respond to clicks by
+   * executing a command/argument pair
+   *
+   * @param attrs - The attributes that will eventually be used to instantiate
+   * a virtual DOM node.
+   *
+   * @param command - The command ID to execute upon click.
+   *
+   * @param args - The arguments with which to invoke the command.
+   *
+   * #### Notes
+   * The attributes instance that is returned is identical to the attributes
+   * instance that was passed in, i.e., this method mutates the original.
+   */
+  populateVNodeAttrs(attrs: IElementAttrs, command: string, args: JSONObject): IElementAttrs;
+}

+ 205 - 0
src/commandlinker/plugin.ts

@@ -0,0 +1,205 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+import {
+  JSONObject
+} from 'phosphor/lib/algorithm/json';
+
+import {
+  CommandRegistry
+} from 'phosphor/lib/ui/commandregistry';
+
+import {
+  IElementAttrs
+} from 'phosphor/lib/ui/vdom';
+
+import {
+  JupyterLab, JupyterLabPlugin
+} from '../application';
+
+import {
+  ICommandLinker
+} from './';
+
+
+/**
+ * The command data attribute added to nodes that are connected.
+ */
+const COMMAND_ATTR = 'commandlinker-command';
+
+/**
+ * The args data attribute added to nodes that are connected.
+ */
+const ARGS_ATTR = 'commandlinker-args';
+
+
+/**
+ * The default commmand linker provider.
+ */
+export
+const commandLinkerProvider: JupyterLabPlugin<ICommandLinker> = {
+  id: 'jupyter.services.commandlinker',
+  provides: ICommandLinker,
+  activate: activateCommandLinker,
+  autoStart: true
+};
+
+
+/**
+ * A static class that provides helper methods to generate clickable nodes that
+ * execute registered commands with pre-populated arguments.
+ */
+class CommandLinker implements ICommandLinker {
+  /**
+   * Instantiate a new command linker class.
+   */
+  constructor(options: Linker.IOptions) {
+    this._commands = options.commands;
+    document.body.addEventListener('click', this);
+  }
+
+  /**
+   * Connect a command/argument pair to a given node so that when it is clicked,
+   * the command will execute.
+   *
+   * @param node - The node being connected.
+   *
+   * @param command - The command ID to execute upon click.
+   *
+   * @param args - The arguments with which to invoke the command.
+   *
+   * @returns The same node that was passed in, after it has been connected.
+   *
+   * #### Notes
+   * Only `click` events will execute the command on a connected node. So, there
+   * are two considerations that are relevant:
+   * 1. If a node is connected, the default click action will be prevented.
+   * 2. The `HTMLElement` passed in should be clickable.
+   */
+  connectNode(node: HTMLElement, command: string, args: JSONObject): HTMLElement {
+    let argsValue = JSON.stringify(args);
+    node.setAttribute(`data-${COMMAND_ATTR}`, command);
+    if (argsValue) {
+      node.setAttribute(`data-${ARGS_ATTR}`, argsValue);
+    }
+    return node;
+  }
+
+  /**
+   * Disconnect a node that has been connected to execute a command on click.
+   *
+   * @param node - The node being disconnected.
+   *
+   * @returns The same node that was passed in, after it has been disconnected.
+   *
+   * #### Notes
+   * This method is safe to call multiple times and is safe to call on nodes
+   * that were never connected.
+   *
+   * This method can be called on rendered virtual DOM nodes that were populated
+   * using the `populateVNodeAttributes` method in order to disconnect them from
+   * executing their command/argument pair.
+   */
+  disconnectNode(node: HTMLElement): HTMLElement {
+    node.removeAttribute(`data-${COMMAND_ATTR}`);
+    node.removeAttribute(`data-${ARGS_ATTR}`);
+    return node;
+  }
+
+  /**
+   * Handle the DOM events for the command linker helper class.
+   *
+   * @param event - The DOM event sent to the class.
+   *
+   * #### Notes
+   * This method implements the DOM `EventListener` interface and is
+   * called in response to events on the panel's DOM node. It should
+   * not be called directly by user code.
+   */
+  handleEvent(event: Event): void {
+    switch (event.type) {
+    case 'click':
+      this._evtClick(event as MouseEvent);
+      break;
+    default:
+      return;
+    }
+  }
+
+  /**
+   * Populate the attributes used to instantiate a virtual DOM node with the
+   * data set values necessary for its rendered DOM node to respond to clicks by
+   * executing a command/argument pair
+   *
+   * @param attrs - The attributes that will eventually be used to instantiate
+   * a virtual DOM node.
+   *
+   * @param command - The command ID to execute upon click.
+   *
+   * @param args - The arguments with which to invoke the command.
+   *
+   * #### Notes
+   * The attributes instance that is returned is identical to the attributes
+   * instance that was passed in, i.e., this method mutates the original.
+   */
+  populateVNodeAttrs(attrs: IElementAttrs, command: string, args: JSONObject): IElementAttrs {
+    let argsValue = JSON.stringify(args);
+    attrs.dataset = attrs.dataset || {};
+    attrs.dataset[COMMAND_ATTR] = command;
+    if (argsValue) {
+      attrs.dataset[ARGS_ATTR] = argsValue;
+    }
+    return attrs;
+  }
+
+  /**
+   * The global click handler that deploys commands/argument pairs that are
+   * attached to the node being clicked.
+   */
+  private _evtClick(event: MouseEvent): void {
+    let target = event.target as HTMLElement;
+    while (target && target.parentElement) {
+      if (target.hasAttribute(`data-${COMMAND_ATTR}`)) {
+        event.preventDefault();
+        let command = target.getAttribute(`data-${COMMAND_ATTR}`);
+        let argsValue = target.getAttribute(`data-${ARGS_ATTR}`);
+        let args: JSONObject;
+        if (argsValue) {
+          args = JSON.parse(argsValue);
+        }
+        this._commands.execute(command, args);
+        break;
+      }
+      target = target.parentElement;
+    }
+  }
+
+  private _commands: CommandRegistry = null;
+}
+
+
+/**
+ * A namespace for command linker statics.
+ */
+namespace Linker {
+  /**
+   * The instantiation options for a command linker.
+   */
+  export
+  interface IOptions {
+    /**
+     * The command registry instance that all linked commands will use.
+     */
+    commands: CommandRegistry;
+  }
+}
+
+
+/**
+ * Activate the command linker provider.
+ */
+function activateCommandLinker(app: JupyterLab): ICommandLinker {
+  return new CommandLinker({ commands: app.commands });
+}

+ 32 - 101
src/launcher/index.ts

@@ -2,7 +2,7 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  enumerate, IIterator, map, toArray
+  IIterator, map, toArray
 } from 'phosphor/lib/algorithm/iteration';
 
 import {
@@ -17,22 +17,18 @@ import {
   DisposableDelegate, IDisposable
 } from 'phosphor/lib/core/disposable';
 
-import {
-  Message
-} from 'phosphor/lib/core/messaging';
-
 import {
   Token
 } from 'phosphor/lib/core/token';
 
-import {
-  CommandRegistry
-} from 'phosphor/lib/ui/commandregistry';
-
 import {
   h, VNode
 } from 'phosphor/lib/ui/vdom';
 
+import {
+  ICommandLinker
+} from '../commandlinker';
+
 import {
   VDomModel, VDomWidget
 } from '../common/vdom';
@@ -155,19 +151,11 @@ class LauncherModel extends VDomModel implements ILauncher {
   /**
    * Create a new launcher model.
    */
-  constructor(options: LauncherModel.IOptions) {
+  constructor() {
     super();
-    this._commands = options.commands;
     this._items = new Vector<ILauncherItem>();
   }
 
-  /**
-   * The command registry.
-   */
-  get commands(): CommandRegistry {
-    return this._commands;
-  }
-
   /**
    * The path to the current working directory.
    */
@@ -209,19 +197,6 @@ class LauncherModel extends VDomModel implements ILauncher {
     });
   }
 
-  /**
-   * Execute the command of the launcher item at a specific index.
-   *
-   * @param index - The index of the launcher item to execute.
-   */
-  execute(index: number): void {
-    let item = this._items.at(index);
-    if (!item) {
-      return;
-    }
-    this.commands.execute(item.command, item.args);
-  }
-
   /**
    * Return an iterator of launcher items.
    */
@@ -229,30 +204,11 @@ class LauncherModel extends VDomModel implements ILauncher {
     return this._items.iter();
   }
 
-  private _commands: CommandRegistry = null;
   private _items: Vector<ILauncherItem> = null;
   private _path: string = 'home';
 }
 
 
-/**
- * A namespace for launcher model statics.
- */
-export
-namespace LauncherModel {
-  /**
-   * The instantiation options for a launcher model.
-   */
-  export
-  interface IOptions {
-    /**
-     * The command registry instance that all launcher commands should use.
-     */
-    commands: CommandRegistry;
-  }
-}
-
-
 /**
  * A virtual-DOM-based widget for the Launcher.
  */
@@ -261,45 +217,10 @@ class LauncherWidget extends VDomWidget<LauncherModel> {
   /**
    * Construct a new launcher widget.
    */
-  constructor() {
+  constructor(options: LauncherWidget.IOptions) {
     super();
     this.addClass(LAUNCHER_CLASS);
-  }
-
-  /**
-   * Handle the DOM events for launcher widget.
-   *
-   * @param event - The DOM event sent to the widget.
-   *
-   * #### Notes
-   * This method implements the DOM `EventListener` interface and is
-   * called in response to events on the panel's DOM node. It should
-   * not be called directly by user code.
-   */
-  handleEvent(event: Event): void {
-    switch (event.type) {
-    case 'click':
-      this._evtClick(event as MouseEvent);
-      break;
-    default:
-      return;
-    }
-  }
-
-  /**
-   * Handle `after_attach` messages for the widget.
-   */
-  protected onAfterAttach(msg: Message): void {
-    super.onAfterAttach(msg);
-    this.node.addEventListener('click', this);
-  }
-
-  /**
-   * Handle `before_detach` messages for the widget.
-   */
-  protected onBeforeDetach(msg: Message): void {
-    super.onBeforeDetach(msg);
-    this.node.removeEventListener('click', this);
+    this._linker = options.linker;
   }
 
   /**
@@ -307,13 +228,18 @@ class LauncherWidget extends VDomWidget<LauncherModel> {
    */
   protected render(): VNode | VNode[] {
     // Create an iterator that yields rendered item nodes.
-    let children = map(enumerate(this.model.items()), ([index, item]) => {
+    let children = map(this.model.items(), item => {
       let img = h.span({className: item.imgClassName + ' ' + IMAGE_CLASS});
       let text = h.span({className: TEXT_CLASS }, item.name);
-      return h.div({ className: ITEM_CLASS, dataset: { index } }, [img, text]);
+      let attrs = this._linker.populateVNodeAttrs({
+        className: ITEM_CLASS
+      }, item.command, item.args);
+      return h.div(attrs, [img, text]);
     });
 
-    let folderImage = h.span({ className: FOLDER_CLASS + ' ' + FOLDER_ICON_CLASS});
+    let folderImage = h.span({
+      className: `${FOLDER_CLASS} ${FOLDER_ICON_CLASS}`
+    });
     let p = this.model.path;
     let pathName = p.length ? `home > ${p.replace(/\//g, ' > ')}` : 'home';
     let path = h.span({ className: PATH_CLASS }, pathName );
@@ -323,18 +249,23 @@ class LauncherWidget extends VDomWidget<LauncherModel> {
     return h.div({ className: DIALOG_CLASS }, [cwd, body]);
   }
 
+  private _linker: ICommandLinker = null;
+}
+
+
+/**
+ * A namespace for launcher widget statics.
+ */
+export
+namespace LauncherWidget {
   /**
-   * Handle click events on the widget.
+   * The instantiation option for a launcher widget.
    */
-  private _evtClick(event: MouseEvent) {
-    let target = event.target as HTMLElement;
-    while (target !== this.node) {
-      if (target.classList.contains(ITEM_CLASS)) {
-        let index = parseInt(target.getAttribute('data-index'), 10);
-        this.model.execute(index);
-        return;
-      }
-      target = target.parentElement;
-    }
+  export
+  interface IOptions {
+    /**
+     * Command linker instance.
+     */
+    linker: ICommandLinker;
   }
 }

+ 8 - 4
src/launcher/plugin.ts

@@ -5,6 +5,10 @@ import {
   JupyterLab, JupyterLabPlugin
 } from '../application';
 
+import {
+  ICommandLinker
+} from '../commandlinker';
+
 import {
   ICommandPalette
 } from '../commandpalette';
@@ -28,7 +32,7 @@ import {
 export
 const launcherProvider: JupyterLabPlugin<ILauncher> = {
   id: 'jupyter.services.launcher',
-  requires: [IServiceManager, IPathTracker, ICommandPalette],
+  requires: [IServiceManager, IPathTracker, ICommandPalette, ICommandLinker],
   provides: ILauncher,
   activate: activateLauncher,
   autoStart: true
@@ -38,14 +42,14 @@ const launcherProvider: JupyterLabPlugin<ILauncher> = {
 /**
  * Activate the launcher.
  */
-function activateLauncher(app: JupyterLab, services: IServiceManager, pathTracker: IPathTracker, palette: ICommandPalette): ILauncher {
-  let model = new LauncherModel({ commands: app.commands });
+function activateLauncher(app: JupyterLab, services: IServiceManager, pathTracker: IPathTracker, palette: ICommandPalette, linker: ICommandLinker): ILauncher {
+  let model = new LauncherModel();
 
   // Set launcher path and track the path as it changes.
   model.path = pathTracker.path;
   pathTracker.pathChanged.connect(() => { model.path = pathTracker.path; });
 
-  let widget = new LauncherWidget();
+  let widget = new LauncherWidget({ linker });
 
   widget.model = model;
   widget.id = 'launcher';