Pārlūkot izejas kodu

Add support for widget based dialogs

Vidar Tonaas Fauske 8 gadi atpakaļ
vecāks
revīzija
85c7370a7b
2 mainītis faili ar 231 papildinājumiem un 30 dzēšanām
  1. 191 30
      src/dialog/index.ts
  2. 40 0
      test/src/dialog/dialog.spec.ts

+ 191 - 30
src/dialog/index.ts

@@ -1,6 +1,20 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
+
+import {
+  Widget
+} from 'phosphor/lib/ui/widget';
+
+import {
+  Panel
+} from 'phosphor/lib/ui/panel';
+
+import {
+  Message
+} from 'phosphor/lib/core/messaging';
+
+
 /**
  * The class name added to dialog instances.
  */
@@ -142,7 +156,7 @@ interface IDialogOptions {
    * a `<span>`.  If an `<input>` or `<select>` element is provided,
    * they will be styled.
    */
-  body?: HTMLElement | string;
+  body?: Widget | HTMLElement | string;
 
   /**
    * The host element for the dialog (defaults to `document.body`).
@@ -150,7 +164,7 @@ interface IDialogOptions {
   host?: HTMLElement;
 
   /**
-   * A list of button times to display (defaults to [[okButton]] and
+   * A list of button types to display (defaults to [[okButton]] and
    *   [[cancelButton]]).
    */
   buttons?: IButtonItem[];
@@ -176,37 +190,44 @@ function showDialog(options?: IDialogOptions): Promise<IButtonItem> {
   options.host = host;
   options.body = options.body || '';
   okButton.text = options.okText ? options.okText : 'OK';
-  let buttons = options.buttons || [cancelButton, okButton];
-  let buttonNodes = buttons.map(createButton);
-  let dialog = createDialog(options, buttonNodes);
-  host.appendChild(dialog);
-  // Focus the ok button if given.
-  let index = buttons.indexOf(okButton);
-  if (index !== -1) {
-    buttonNodes[index].focus();
-  }
-  return new Promise<IButtonItem>((resolve, reject) => {
-    buttonNodes.map(node => {
-      node.addEventListener('click', evt => {
-        if (node.contains(evt.target as HTMLElement)) {
+  let buttons = options.buttons = options.buttons || [cancelButton, okButton];
+  if (options.body instanceof Widget) {
+    return new Promise<IButtonItem>((resolve, reject) => {
+      let dialog = new WidgetDialog(options, resolve, reject);
+      Widget.attach(dialog, host);
+    });
+  } else {
+    let buttonNodes = buttons.map(createButton);
+    let dialog = createDialog(options, buttonNodes);
+    host.appendChild(dialog);
+    // Focus the ok button if given.
+    let index = buttons.indexOf(okButton);
+    if (index !== -1) {
+      buttonNodes[index].focus();
+    }
+    return new Promise<IButtonItem>((resolve, reject) => {
+      buttonNodes.map(node => {
+        node.addEventListener('click', evt => {
+          if (node.contains(evt.target as HTMLElement)) {
+            host.removeChild(dialog);
+            let button = buttons[buttonNodes.indexOf(node)];
+            resolve(button);
+          }
+        });
+      });
+      dialog.addEventListener('keydown', evt => {
+        // Check for escape key
+        if (evt.keyCode === 27) {
           host.removeChild(dialog);
-          let button = buttons[buttonNodes.indexOf(node)];
-          resolve(button);
+          resolve(cancelButton);
         }
-      });
+      }, true);
+      dialog.addEventListener('contextmenu', evt => {
+        evt.preventDefault();
+        evt.stopPropagation();
+      }, true);
     });
-    dialog.addEventListener('keydown', evt => {
-      // Check for escape key
-      if (evt.keyCode === 27) {
-        host.removeChild(dialog);
-        resolve(cancelButton);
-      }
-    }, true);
-    dialog.addEventListener('contextmenu', evt => {
-      evt.preventDefault();
-      evt.stopPropagation();
-    }, true);
-  });
+  }
 }
 
 
@@ -259,6 +280,146 @@ function createDialog(options: IDialogOptions, buttonNodes: HTMLElement[]): HTML
   return node;
 }
 
+/**
+ * Create the dialog node.
+ */
+class WidgetDialog extends Panel {
+  /**
+   *
+   */
+  constructor(options: IDialogOptions, resolve: (value: IButtonItem) => void, reject?: (error: any) => void) {
+    super();
+
+    if (!(options.body instanceof Widget)) {
+      throw 'A widget dialog can only be created with a widget as its body.';
+    }
+
+    this.resolve = resolve;
+    this.reject = reject;
+
+    // Create the dialog nodes (except for the buttons).
+    let content = new Panel();
+    let header = new Widget({node: document.createElement('div')});
+    let body = new Panel();
+    let footer = new Widget({node: document.createElement('div')});
+    let title = document.createElement('span');
+    this.addClass(DIALOG_CLASS);
+    content.addClass(CONTENT_CLASS);
+    header.addClass(HEADER_CLASS);
+    body.addClass(BODY_CLASS);
+    footer.addClass(FOOTER_CLASS);
+    title.className = TITLE_CLASS;
+    this.addWidget(content);
+    content.addWidget(header);
+    content.addWidget(body);
+    content.addWidget(footer);
+    header.node.appendChild(title);
+
+    // Populate the nodes.
+    title.textContent = options.title || '';
+    let child = options.body as Widget;
+    child.addClass(BODY_CONTENT_CLASS);
+    body.addWidget(child);
+    this._buttons = options.buttons;
+    this._buttonNodes = options.buttons.map(createButton);
+    this._buttonNodes.map(buttonNode => {
+      footer.node.appendChild(buttonNode);
+    });
+  }
+
+  protected onAfterAttach(msg: Message): void {
+    let node = this.node;
+    node.addEventListener('keydown', this, true);
+    node.addEventListener('contextmenu', this, true);
+    node.addEventListener('click', this);
+    this._buttonNodes.map(buttonNode => {
+      buttonNode.addEventListener('click', this.evtButtonClick.bind(this));
+    });
+
+    // Focus the ok button if given.
+    let index = this._buttons.indexOf(okButton);
+    if (index !== -1) {
+      this._buttonNodes[index].focus();
+    }
+  }
+
+  /**
+   * Handle the DOM events for the directory listing.
+   *
+   * @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 'keydown':
+      this.evtKeydown(event as KeyboardEvent);
+      break;
+    case 'contextmenu':
+      this.evtContextMenu(event as MouseEvent);
+      break;
+    default:
+      break;
+    }
+  }
+
+  /**
+   * Handle the `'click'` event for a dialog button.
+   *
+   * @param event - The DOM event sent to the widget
+   */
+  protected evtButtonClick(event: MouseEvent): void {
+    for (let buttonNode of this._buttonNodes) {
+      if (buttonNode.contains(event.target as HTMLElement)) {
+        this.close();
+        let button = this._buttons[this._buttonNodes.indexOf(buttonNode)];
+        this.resolve(button);
+      }
+    }
+  }
+
+  /**
+   * Handle the `'keydown'` event for the widget.
+   *
+   * @param event - The DOM event sent to the widget
+   */
+  protected evtKeydown(event: KeyboardEvent): void {
+    // Check for escape key
+    if (event.keyCode === 27) {
+      this.close();
+      this.resolve(cancelButton);
+    }
+  }
+
+
+  /**
+   * Handle the `'contextmenu'` event for the widget.
+   *
+   * @param event - The DOM event sent to the widget
+   */
+  protected evtContextMenu(event: Event): void {
+    event.preventDefault();
+    event.stopPropagation();
+  }
+
+  /**
+   * The resolution function of the dialog Promise.
+   */
+  protected resolve: (value: IButtonItem) => void;
+
+
+  /**
+   * The rejection function of the dialog Promise.
+   */
+  protected reject: (error: any) => void;
+
+  private _buttonNodes: HTMLElement[];
+  private _buttons: IButtonItem[];
+}
+
 
 /**
  * Style the child elements of a parent element.

+ 40 - 0
test/src/dialog/dialog.spec.ts

@@ -7,6 +7,14 @@ import {
   simulate
 } from 'simulate-event';
 
+import {
+  Widget
+} from 'phosphor/lib/ui/widget';
+
+import {
+  Message
+} from 'phosphor/lib/core/messaging';
+
 import {
   showDialog, okButton
 } from '../../../lib/dialog';
@@ -82,6 +90,15 @@ describe('dialog/index', () => {
       acceptDialog();
     });
 
+    it('should accept a widget body', (done) => {
+      let body = new Widget({node: document.createElement('div')});
+      showDialog({ body }).then(result => {
+        expect(result.text).to.be('OK');
+        done();
+      });
+      acceptDialog();
+    });
+
     it('should resolve with the clicked button result', (done) => {
       let button = {
         text: 'foo',
@@ -111,6 +128,29 @@ describe('dialog/index', () => {
       });
     });
 
+    /**
+     * Class to test that onAfterAttach is called
+     */
+    class TestWidget extends Widget {
+      constructor(resolve: () => void) {
+        super();
+        this.resolve = resolve;
+      }
+      protected onAfterAttach(msg: Message): void {
+        this.resolve();
+      }
+
+      resolve: () => void;
+    }
+
+    it('should fire onAfterAttach on widget body', (done) => {
+      let promise = new Promise((resolve, reject) => {
+        let body = new TestWidget(resolve);
+        showDialog({ body });
+      });
+      promise.then(done);
+    });
+
   });
 
 });