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

Merge pull request #6327 from fcollonval/6326-input-dialog

Add input dialogs
Steven Silvester 6 лет назад
Родитель
Сommit
5211238d72

+ 1 - 0
packages/apputils/src/index.ts

@@ -12,6 +12,7 @@ export * from './dialog';
 export * from './domutils';
 export * from './hoverbox';
 export * from './iframe';
+export * from './inputdialog';
 export * from './instancetracker';
 export * from './mainareawidget';
 export * from './sanitizer';

+ 274 - 0
packages/apputils/src/inputdialog.ts

@@ -0,0 +1,274 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { Widget } from '@phosphor/widgets';
+
+import { Dialog } from './dialog';
+import { Styling } from './styling';
+
+const INPUT_DIALOG_CLASS = 'jp-Input-Dialog';
+
+/**
+ * Create and show a input dialog for a number.
+ *
+ * @param options - The dialog setup options.
+ *
+ * @returns A promise that resolves with whether the dialog was accepted
+ */
+export function getNumber(
+  options: InputDialog.INumberOptions
+): Promise<Dialog.IResult<number>> {
+  let dialog = new Dialog({
+    ...options,
+    body: new InputNumberDialog(options)
+  });
+  return dialog.launch();
+}
+
+/**
+ * Create and show a input dialog for a choice.
+ *
+ * @param options - The dialog setup options.
+ *
+ * @returns A promise that resolves with whether the dialog was accepted
+ */
+export function getItem(
+  options: InputDialog.IItemOptions
+): Promise<Dialog.IResult<string>> {
+  let dialog = new Dialog({
+    ...options,
+    body: new InputItemsDialog(options)
+  });
+  return dialog.launch();
+}
+
+/**
+ * Create and show a input dialog for a text.
+ *
+ * @param options - The dialog setup options.
+ *
+ * @returns A promise that resolves with whether the dialog was accepted
+ */
+export function getText(
+  options: InputDialog.ITextOptions
+): Promise<Dialog.IResult<string>> {
+  let dialog = new Dialog({
+    ...options,
+    body: new InputTextDialog(options)
+  });
+  return dialog.launch();
+}
+
+export namespace InputDialog {
+  export interface IOptions<T>
+    extends Partial<
+      Pick<
+        Dialog.IOptions<T>,
+        Exclude<keyof Dialog.IOptions<T>, 'body' | 'buttons' | 'defaultButton'>
+      >
+    > {
+    /**
+     * Label of the requested input
+     */
+    label: string;
+  }
+
+  export interface INumberOptions extends IOptions<Number> {
+    /**
+     * Default value
+     */
+    value?: number;
+  }
+
+  export interface IItemOptions extends IOptions<string> {
+    /**
+     * List of choices
+     */
+    items: Array<string>;
+    /**
+     * Default choice
+     *
+     * If the list is editable a string with a default value can be provided
+     * otherwise the index of the default choice should be given.
+     */
+    current?: number | string;
+    /**
+     * Is the item editable?
+     */
+    editable?: boolean;
+    /**
+     * Placeholder text for editable input
+     */
+    placeholder?: string;
+  }
+
+  export interface ITextOptions extends IOptions<string> {
+    /**
+     * Default input text
+     */
+    text?: string;
+    /**
+     * Placeholder text
+     */
+    placeholder?: string;
+  }
+}
+
+/**
+ * Base widget for input dialog body
+ */
+class InputDialog<T> extends Widget implements Dialog.IBodyWidget<T> {
+  /**
+   * InputDialog constructor
+   *
+   * @param label Input field label
+   */
+  constructor(label: string) {
+    super();
+    this.addClass(INPUT_DIALOG_CLASS);
+
+    let labelElement = document.createElement('label');
+    labelElement.textContent = label;
+
+    // Initialize the node
+    this.node.appendChild(labelElement);
+  }
+
+  /** Input HTML node */
+  protected _input: HTMLInputElement;
+}
+
+/**
+ * Widget body for input number dialog
+ */
+class InputNumberDialog extends InputDialog<number> {
+  /**
+   * InputNumberDialog constructor
+   *
+   * @param options Constructor options
+   */
+  constructor(options: InputDialog.INumberOptions) {
+    super(options.label);
+
+    this._input = document.createElement('input', {});
+    this._input.classList.add('jp-mod-styled');
+    this._input.type = 'number';
+    this._input.value = options.value ? options.value.toString() : '0';
+
+    // Initialize the node
+    this.node.appendChild(this._input);
+  }
+
+  /**
+   * Get the number specified by the user.
+   */
+  getValue(): number {
+    if (this._input.value) {
+      return Number(this._input.value);
+    } else {
+      return Number.NaN;
+    }
+  }
+}
+
+/**
+ * Widget body for input text dialog
+ */
+class InputTextDialog extends InputDialog<string> {
+  /**
+   * InputTextDialog constructor
+   *
+   * @param options Constructor options
+   */
+  constructor(options: InputDialog.ITextOptions) {
+    super(options.label);
+
+    this._input = document.createElement('input', {});
+    this._input.classList.add('jp-mod-styled');
+    this._input.type = 'text';
+    this._input.value = options.text ? options.text : '';
+    if (options.placeholder) {
+      this._input.placeholder = options.placeholder;
+    }
+
+    // Initialize the node
+    this.node.appendChild(this._input);
+  }
+
+  /**
+   * Get the text specified by the user
+   */
+  getValue(): string {
+    return this._input.value;
+  }
+}
+
+/**
+ * Widget body for input list dialog
+ */
+class InputItemsDialog extends InputDialog<string> {
+  /**
+   * InputItemsDialog constructor
+   *
+   * @param options Constructor options
+   */
+  constructor(options: InputDialog.IItemOptions) {
+    super(options.label);
+
+    this._editable = options.editable || false;
+
+    let current = options.current || 0;
+    let defaultIndex: number;
+    if (typeof current === 'number') {
+      defaultIndex = Math.max(0, Math.min(current, options.items.length - 1));
+      current = '';
+    }
+
+    this._list = document.createElement('select');
+    options.items.forEach((item, index) => {
+      let option = document.createElement('option');
+      if (index === defaultIndex) {
+        option.selected = true;
+        current = item;
+      }
+      option.value = item;
+      option.textContent = item;
+      this._list.appendChild(option);
+    });
+
+    if (options.editable) {
+      /* Use of list and datalist */
+      let data = document.createElement('datalist');
+      data.id = 'input-dialog-items';
+      data.appendChild(this._list);
+
+      this._input = document.createElement('input', {});
+      this._input.classList.add('jp-mod-styled');
+      this._input.type = 'list';
+      this._input.value = current;
+      this._input.setAttribute('list', data.id);
+      if (options.placeholder) {
+        this._input.placeholder = options.placeholder;
+      }
+      this.node.appendChild(this._input);
+      this.node.appendChild(data);
+    } else {
+      /* Use select directly */
+      this.node.appendChild(Styling.wrapSelect(this._list));
+    }
+  }
+
+  /**
+   * Get the user choice
+   */
+  getValue(): string {
+    if (this._editable) {
+      return this._input.value;
+    } else {
+      return this._list.value;
+    }
+  }
+
+  private _list: HTMLSelectElement;
+  private _editable: boolean;
+}

+ 197 - 0
tests/test-apputils/src/inputdialog.spec.ts

@@ -0,0 +1,197 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import { getItem, getText, getNumber } from '@jupyterlab/apputils';
+
+import {
+  acceptDialog,
+  dismissDialog,
+  waitForDialog
+} from '@jupyterlab/testutils';
+
+describe('@jupyterlab/apputils', () => {
+  describe('getItem()', () => {
+    it('should accept at least two arguments', async () => {
+      const dialog = getItem({
+        label: 'list',
+        items: ['item1']
+      });
+
+      await dismissDialog();
+      expect((await dialog).button.accept).toBe(false);
+    });
+
+    it('should accept options', async () => {
+      const dialog = getItem({
+        label: 'list',
+        items: ['item1', 'item2'],
+        current: 1,
+        editable: false,
+        title: 'Pick a choice',
+        placeholder: 'item'
+      });
+
+      await acceptDialog();
+
+      const result = await dialog;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe('item2');
+    });
+
+    it('should be editable', async () => {
+      const node = document.createElement('div');
+
+      document.body.appendChild(node);
+
+      const prompt = getItem({
+        label: 'list',
+        items: ['item1', 'item2'],
+        title: 'Pick a choice',
+        placeholder: 'item',
+        editable: true,
+        host: node
+      });
+
+      await waitForDialog(node);
+      const body = node.getElementsByClassName('jp-Input-Dialog').item(0);
+      const input = body.getElementsByTagName('input').item(0);
+      input.value = 'item3';
+
+      await acceptDialog();
+
+      const result = await prompt;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe('item3');
+      document.body.removeChild(node);
+    });
+  });
+
+  describe('getText()', () => {
+    it('should accept at least one argument', async () => {
+      const dialog = getText({
+        label: 'text'
+      });
+
+      await dismissDialog();
+      expect((await dialog).button.accept).toBe(false);
+    });
+
+    it('should accept options', async () => {
+      const dialog = getText({
+        label: 'text',
+        title: 'Give a text',
+        placeholder: 'your text',
+        text: 'answer'
+      });
+
+      await acceptDialog();
+
+      const result = await dialog;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe('answer');
+    });
+
+    it('should be editable', async () => {
+      const node = document.createElement('div');
+
+      document.body.appendChild(node);
+
+      const prompt = getText({
+        label: 'text',
+        host: node
+      });
+
+      await waitForDialog(node);
+      const body = node.getElementsByClassName('jp-Input-Dialog').item(0);
+      const input = body.getElementsByTagName('input').item(0);
+      input.value = 'my answer';
+
+      await acceptDialog();
+
+      const result = await prompt;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe('my answer');
+      document.body.removeChild(node);
+    });
+  });
+
+  describe('getNumber()', () => {
+    it('should accept at least one argument', async () => {
+      const dialog = getNumber({
+        label: 'number'
+      });
+
+      await dismissDialog();
+      expect((await dialog).button.accept).toBe(false);
+    });
+
+    it('should accept options', async () => {
+      const dialog = getNumber({
+        label: 'number',
+        title: 'Pick a number',
+        value: 10
+      });
+
+      await acceptDialog();
+
+      const result = await dialog;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe(10);
+    });
+
+    it('should be editable', async () => {
+      const node = document.createElement('div');
+
+      document.body.appendChild(node);
+
+      const prompt = getNumber({
+        label: 'text',
+        title: 'Pick a number',
+        host: node
+      });
+
+      await waitForDialog(node);
+      const body = node.getElementsByClassName('jp-Input-Dialog').item(0);
+      const input = body.getElementsByTagName('input').item(0);
+      input.value = '25';
+
+      await acceptDialog();
+
+      const result = await prompt;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe(25);
+      document.body.removeChild(node);
+    });
+
+    it('should return NaN if empty', async () => {
+      const node = document.createElement('div');
+
+      document.body.appendChild(node);
+
+      const prompt = getNumber({
+        label: 'text',
+        title: 'Pick a number',
+        host: node
+      });
+
+      await waitForDialog(node);
+      const body = node.getElementsByClassName('jp-Input-Dialog').item(0);
+      const input = body.getElementsByTagName('input').item(0);
+      input.value = '';
+
+      await acceptDialog();
+
+      const result = await prompt;
+
+      expect(result.button.accept).toBe(true);
+      expect(result.value).toBe(Number.NaN);
+      document.body.removeChild(node);
+    });
+  });
+});