@lumino/widgets#Widget TypeScript Examples

The following examples show how to use @lumino/widgets#Widget. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: layout.ts    From jupyterlab-interactive-dashboard-editor with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
/**
   * Create an iterator over the widgets in the layout.
   *
   * @returns a new iterator over the widgets in the layout.
   */
  iter(): IIterator<Widget> {
    // Is there a lazy way to iterate through the map?
    const arr = Array.from(this._items.values());
    return map(arr, item => item.widget);
  }
Example #2
Source File: dialogWidget.tsx    From ipyflex with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
class TemplateNameWidget extends Widget {
  constructor(el: HTMLInputElement) {
    super({ node: el });
  }

  getValue(): string {
    return (this.node as HTMLInputElement).value;
  }
}
Example #3
Source File: dialogWidget.tsx    From ipyflex with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
class FactoryParameterWidget extends Widget {
  constructor(signature: IDict<any>) {
    super();
    this._paramInputs = {};
    const params = Object.keys(signature);
    params.forEach((p) => {
      const id = uuid();
      const inp = document.createElement('input');
      inp.required = true;
      this._paramInputs[p] = inp;
      inp.id = id;
      inp.classList.add('ipyflex-float-input');
      const label = document.createElement('label');
      label.htmlFor = id;
      if (signature[p]['annotation']) {
        label.innerText = `${p} : ${signature[p]['annotation']}`;
      } else {
        label.innerText = `${p}`;
      }
      label.classList.add('ipyflex-float-label');

      const group = document.createElement('div');
      group.classList.add('ipyflex-input-label-group');
      group.appendChild(inp);
      group.appendChild(label);
      this.node.appendChild(group);
    });
  }

  getValue(): IDict<string> {
    const ret = {};
    for (const [key, inp] of Object.entries(this._paramInputs)) {
      ret[key] = inp.value;
    }
    return ret;
  }

  private _paramInputs: IDict<HTMLInputElement>;
}
Example #4
Source File: widget.tsx    From ipyflex with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
render() {
    super.render();
    this.setStyle();
    this.el.classList.add('custom-widget');
    const style: { [key: string]: string } = this.model.get('style');
    const editable = this.model.get('editable');
    const header = this.model.get('header');
    const widget = new ReactWidgetWrapper(
      this.send.bind(this),
      this.model,
      style,
      editable,
      header
    );
    MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
    this.el.insertBefore(widget.node, null);
    MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
  }
Example #5
Source File: widgetWrapper.tsx    From ipyflex with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
private _render_widget = (model: any) => {
    const manager = this.model.widget_manager;
    manager.create_view(model, {}).then((view) => {
      MessageLoop.sendMessage(view.pWidget, Widget.Msg.BeforeAttach);
      this.myRef.current.insertBefore(view.pWidget.node, null);
      view.displayed.then(async () => {
        await new Promise((r) => setTimeout(r, 100));
        window.dispatchEvent(new Event('resize'));
      });
      MessageLoop.sendMessage(view.pWidget, Widget.Msg.AfterAttach);
    });
  };
Example #6
Source File: searchprovider.ts    From jupyterlab-tabular-data-editor with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
/**
   * Report whether or not this provider has the ability to search on the given object
   */
  static canSearchOn(domain: Widget): domain is CSVDocumentWidget {
    // check to see if the CSVSearchProvider can search on the
    // first cell, false indicates another editor is present
    return (
      domain instanceof DocumentWidget && domain.content instanceof DSVEditor
    );
  }
Example #7
Source File: layout.ts    From jupyterlab-interactive-dashboard-editor with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
/**
   * Detach a widget from the parent's DOM node.
   *
   * @param widget - The widget to detach from the parent.
   */
  protected detachWidget(_index: number, widget: Widget): void {
    // Send a `'before-detach'` message if the parent is attached.
    if (this.parent!.isAttached) {
      MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
    }

    // Remove the widget's node from the parent.
    this.parent!.node.removeChild(widget.node);

    // Send an `'after-detach'` message if the parent is attached.
    if (this.parent!.isAttached) {
      MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
    }

    widget.parent = null;

    // Post a fit request for the parent widget.
    this.parent!.fit();
  }
Example #8
Source File: layout.ts    From jupyterlab-interactive-dashboard-editor with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
/**
   * Attach a widget to the parent's DOM node.
   *
   * @param widget - The widget to attach to the parent.
   */
  protected attachWidget(widget: Widget): void {
    // Set widget's parent.
    widget.parent = this.parent;

    // Send a `'before-attach'` message if the parent is attached.
    if (this.parent!.isAttached) {
      MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
    }

    // Add the widget's node to the parent.
    this.parent!.node.appendChild(widget.node);

    // Send an `'after-attach'` message if the parent is attached.
    if (this.parent!.isAttached) {
      MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
    }

    // Post a fit request for the parent widget.
    this.parent!.fit();
  }
Example #9
Source File: index.ts    From jupyterlab-interactive-dashboard-editor with BSD 3-Clause "New" or "Revised" License 6 votes vote down vote up
/**
   * A dialog with two boxes for setting a dashboard's width and height.
   */
  export class ResizeHandler extends Widget {
    constructor(oldWidth: number, oldHeight: number) {
      const node = document.createElement('div');
      const name = document.createElement('label');
      name.textContent = 'Enter New Width/Height';

      const width = document.createElement('input');
      const height = document.createElement('input');
      width.type = 'number';
      height.type = 'number';
      width.min = '0';
      width.max = '10000';
      height.min = '0';
      height.max = '10000';
      width.required = true;
      height.required = true;
      width.placeholder = `Width (${oldWidth})`;
      height.placeholder = `Height (${oldHeight})`;

      node.appendChild(name);
      node.appendChild(width);
      node.appendChild(height);

      super({ node });
    }

    getValue(): number[] {
      const inputs = this.node.getElementsByTagName('input');
      const widthInput = inputs[0];
      const heightInput = inputs[1];
      return [+widthInput.value, +heightInput.value];
    }
  }
Example #10
Source File: ColumnFilter.ts    From beakerx_tabledisplay with Apache License 2.0 6 votes vote down vote up
private addInputNode(options: { x; y; width; height }): void {
    this.filterWidget = new Widget();
    this.filterNode = this.filterWidget.node;

    this.filterNode.innerHTML = `<div class="input-clear">
      <span class="fa filter-icon fa-filter"></span>
      <input class="filter-input" type="text" title='${FILTER_INPUT_TOOLTIP}'>
      <span class="fa fa-times clear-filter"></span>
    </div>`;

    this.filterNode.classList.add('input-clear-growing');
    this.filterNode.style.width = `${options.width}px`;
    this.filterNode.style.height = this.getInputHeight();
    this.filterNode.style.left = `${options.x}px`;
    this.filterNode.style.top = `${options.y}px`;
    this.filterNode.style.position = 'absolute';

    this.filterIcon = this.filterNode.querySelector('.filter-icon') || new HTMLSpanElement();
    this.filterInput = this.filterNode.querySelector('input') || new HTMLInputElement();
    this.clearIcon = this.filterNode.querySelector('.clear-filter') || new HTMLSpanElement();
    this.filterInput.style.height = this.getInputHeight();

    this.filterWidget.setHidden(true);
  }
Example #11
Source File: ui_elements.ts    From jupyterlab-gitplus with GNU Affero General Public License v3.0 6 votes vote down vote up
export class CommitMessageDialog extends Widget {
  constructor() {
    const body = document.createElement('div');
    const basic = document.createElement('div');
    basic.classList.add('gitPlusDialogBody');
    body.appendChild(basic);
    basic.appendChild(Private.buildLabel('Commit message: '));
    basic.appendChild(
      Private.buildTextarea(
        'Enter your commit message',
        'gitplus-commit-message',
        'gitPlusMessageTextArea'
      )
    );
    super({ node: body });
  }

  public getCommitMessage(): string {
    const textareas = this.node.getElementsByTagName('textarea');
    for (const textarea of textareas) {
      if (textarea.id == 'gitplus-commit-message') {
        return textarea.value;
      }
    }
    return '';
  }
}
Example #12
Source File: ui_elements.ts    From jupyterlab-gitplus with GNU Affero General Public License v3.0 6 votes vote down vote up
export class SpinnerDialog extends Widget {
  constructor() {
    const spinner_style = {
      'margin-top': '6em'
    };
    const body = document.createElement('div');
    const basic = document.createElement('div');
    Private.apply_style(basic, spinner_style);
    body.appendChild(basic);
    const spinner = new Spinner();
    basic.appendChild(spinner.node);
    super({ node: body });
  }
}
Example #13
Source File: ui_elements.ts    From jupyterlab-gitplus with GNU Affero General Public License v3.0 6 votes vote down vote up
export class CheckBoxes extends Widget {
  constructor(items: string[] = []) {
    const basic = document.createElement('div');
    basic.classList.add('gitPlusDialogBody');

    for (const item of items) {
      basic.appendChild(Private.buildCheckbox(item));
    }
    super({ node: basic });
  }

  public getSelected(): string[] {
    const result: string[] = [];
    const inputs = this.node.getElementsByTagName('input');

    for (const input of inputs) {
      if (input.checked) {
        result.push(input.id);
      }
    }

    return result;
  }
}
Example #14
Source File: ui_elements.ts    From jupyterlab-gitplus with GNU Affero General Public License v3.0 6 votes vote down vote up
export class DropDown extends Widget {
  constructor(options: string[][] = [], label = '', styles: {} = {}) {
    let body_style = {};
    let label_style = {};
    let select_style = {};

    if ('body_style' in styles) {
      body_style = styles['body_style'];
    }
    if ('label_style' in styles) {
      label_style = styles['label_style'];
    }
    if ('select_style' in styles) {
      select_style = styles['select_style'];
    }

    const body = document.createElement('div');
    Private.apply_style(body, body_style);
    const basic = document.createElement('div');
    body.appendChild(basic);
    basic.appendChild(Private.buildLabel(label, label_style));
    basic.appendChild(Private.buildSelect(options, select_style));
    super({ node: body });
  }

  get toNode(): HTMLSelectElement {
    return this.node.getElementsByTagName('select')[0] as HTMLSelectElement;
  }

  public getTo(): string {
    return this.toNode.value;
  }
}
Example #15
Source File: ui_elements.ts    From jupyterlab-gitplus with GNU Affero General Public License v3.0 6 votes vote down vote up
export class CommitPushed extends Widget {
  constructor(github_url: string, reviewnb_url: string) {
    const anchor_style = {
      color: '#106ba3',
      'text-decoration': 'underline'
    };

    const body = document.createElement('div');
    const basic = document.createElement('div');
    basic.classList.add('gitPlusDialogBody');
    body.appendChild(basic);
    basic.appendChild(Private.buildLabel('See commit on GitHub: '));
    basic.appendChild(Private.buildNewline());
    basic.appendChild(
      Private.buildAnchor(github_url, github_url, anchor_style)
    );
    basic.appendChild(Private.buildNewline());
    basic.appendChild(Private.buildNewline());
    basic.appendChild(Private.buildNewline());
    basic.appendChild(Private.buildLabel('See commit on ReviewNB: '));
    basic.appendChild(Private.buildNewline());
    basic.appendChild(
      Private.buildAnchor(reviewnb_url, reviewnb_url, anchor_style)
    );
    basic.appendChild(Private.buildNewline());
    super({ node: body });
  }
}
Example #16
Source File: ui_elements.ts    From jupyterlab-gitplus with GNU Affero General Public License v3.0 6 votes vote down vote up
export class PRCreated extends Widget {
  constructor(github_url: string, reviewnb_url: string) {
    const anchor_style = {
      color: '#106ba3',
      'text-decoration': 'underline'
    };

    const body = document.createElement('div');
    const basic = document.createElement('div');
    basic.classList.add('gitPlusDialogBody');
    body.appendChild(basic);
    basic.appendChild(Private.buildLabel('See pull request on GitHub: '));
    basic.appendChild(Private.buildNewline());
    basic.appendChild(
      Private.buildAnchor(github_url, github_url, anchor_style)
    );
    basic.appendChild(Private.buildNewline());
    basic.appendChild(Private.buildNewline());
    basic.appendChild(Private.buildNewline());
    basic.appendChild(Private.buildLabel('See pull request on ReviewNB: '));
    basic.appendChild(Private.buildNewline());
    basic.appendChild(
      Private.buildAnchor(reviewnb_url, reviewnb_url, anchor_style)
    );
    basic.appendChild(Private.buildNewline());
    super({ node: body });
  }
}
Example #17
Source File: HeaderMenu.ts    From beakerx_tabledisplay with Apache License 2.0 5 votes vote down vote up
protected viewport: Widget;
Example #18
Source File: ColumnFilter.ts    From beakerx_tabledisplay with Apache License 2.0 5 votes vote down vote up
filterWidget: Widget;
Example #19
Source File: ColumnFilter.ts    From beakerx_tabledisplay with Apache License 2.0 5 votes vote down vote up
attach(node: HTMLElement) {
    Widget.attach(this.filterWidget, node);
    this.bindEvents();
  }
Example #20
Source File: DataGridScope.ts    From beakerx_tabledisplay with Apache License 2.0 5 votes vote down vote up
render(): void {
    Widget.attach((this.dataGrid as unknown) as Widget, this.element); // todo investigate
  }
Example #21
Source File: BeakerXDataGrid.ts    From beakerx_tabledisplay with Apache License 2.0 5 votes vote down vote up
public getViewport(): Widget {
    return super.viewport;
  }
Example #22
Source File: launcher.tsx    From jlab-enhanced-launcher with BSD 3-Clause "New" or "Revised" License 5 votes vote down vote up
private _callback: (widget: Widget) => void;
Example #23
Source File: widget.ts    From jupyterlab-interactive-dashboard-editor with BSD 3-Clause "New" or "Revised" License 5 votes vote down vote up
private _content: Widget;
Example #24
Source File: ui_elements.ts    From jupyterlab-gitplus with GNU Affero General Public License v3.0 5 votes vote down vote up
export class CommitPRMessageDialog extends Widget {
  constructor() {
    const body = document.createElement('div');
    const basic = document.createElement('div');
    basic.classList.add('gitPlusDialogBody');
    body.appendChild(basic);
    basic.appendChild(Private.buildLabel('Commit message: '));
    basic.appendChild(
      Private.buildTextarea(
        'Enter your commit message',
        'gitplus-commit-message',
        'gitPlusMessageTextArea'
      )
    );
    basic.appendChild(Private.buildLabel('PR title: '));
    basic.appendChild(
      Private.buildTextarea(
        'Enter title for pull request',
        'gitplus-pr-message',
        'gitPlusMessageTextArea'
      )
    );
    super({ node: body });
  }

  public getCommitMessage(): string {
    const textareas = this.node.getElementsByTagName('textarea');
    for (const textarea of textareas) {
      if (textarea.id == 'gitplus-commit-message') {
        return textarea.value;
      }
    }
    return '';
  }

  public getPRTitle(): string {
    const textareas = this.node.getElementsByTagName('textarea');
    for (const textarea of textareas) {
      if (textarea.id == 'gitplus-pr-message') {
        return textarea.value;
      }
    }
    return '';
  }
}
Example #25
Source File: launcher.tsx    From jlab-enhanced-launcher with BSD 3-Clause "New" or "Revised" License 4 votes vote down vote up
/**
 * A pure tsx component for a launcher card.
 *
 * @param kernel - whether the item takes uses a kernel.
 *
 * @param item - the launcher item to render.
 *
 * @param launcher - the Launcher instance to which this is added.
 *
 * @param launcherCallback - a callback to call after an item has been launched.
 *
 * @returns a vdom `VirtualElement` for the launcher card.
 */
function Card(
  kernel: boolean,
  items: INewLauncher.IItemOptions[],
  launcher: Launcher,
  commands: CommandRegistry,
  trans: TranslationBundle,
  launcherCallback: (widget: Widget) => void
): React.ReactElement<any> {
  const mode = launcher.model.viewMode === 'cards' ? '' : '-Table';

  // Get some properties of the first command
  const item = items[0];
  const command = item.command;
  const args = { ...item.args, cwd: launcher.cwd };
  const caption = commands.caption(command, args);
  const label = commands.label(command, args);
  const title = kernel ? label : caption || label;

  // Build the onclick handler.
  const onClickFactory = (
    item: INewLauncher.IItemOptions
  ): ((event: any) => void) => {
    const onClick = (event: Event): void => {
      event.stopPropagation();
      // If an item has already been launched,
      // don't try to launch another.
      if (launcher.pending === true) {
        return;
      }
      launcher.pending = true;
      void commands
        .execute(item.command, {
          ...item.args,
          cwd: launcher.cwd
        })
        .then(value => {
          launcher.model.useCard(item);
          launcher.pending = false;
          if (value instanceof Widget) {
            launcherCallback(value);
            launcher.dispose();
          }
        })
        .catch(err => {
          launcher.pending = false;
          void showErrorMessage(trans._p('Error', 'Launcher Error'), err);
        });
    };

    return onClick;
  };
  const mainOnClick = onClickFactory(item);

  const getOptions = (items: INewLauncher.IItemOptions[]): JSX.Element[] => {
    return items.map(item => {
      let label = 'Open';
      if (
        item.category &&
        (items.length > 1 || KERNEL_CATEGORIES.indexOf(item.category) > -1)
      ) {
        label = item.category;
      }
      return (
        <div
          className="jp-NewLauncher-option-button"
          key={label.toLowerCase()}
          onClick={onClickFactory(item)}
        >
          <span className="jp-NewLauncher-option-button-text">
            {label.toUpperCase()}
          </span>
        </div>
      );
    });
  };

  // With tabindex working, you can now pick a kernel by tabbing around and
  // pressing Enter.
  const onkeypress = (event: React.KeyboardEvent): void => {
    if (event.key === 'Enter') {
      mainOnClick(event);
    }
  };

  // DEPRECATED: remove _icon when lumino 2.0 is adopted
  // if icon is aliasing iconClass, don't use it
  const iconClass = commands.iconClass(command, args);
  const _icon = commands.icon(command, args);
  const icon = _icon === iconClass ? undefined : _icon;

  // Return the VDOM element.
  return (
    <div
      className={`jp-NewLauncher-item${mode}`}
      title={title}
      onClick={mainOnClick}
      onKeyPress={onkeypress}
      tabIndex={100}
      data-category={item.category || 'Other'}
      key={Private.keyProperty.get(item)}
    >
      <div className={`jp-NewLauncherCard-icon jp-NewLauncher${mode}-Cell`}>
        {kernel ? (
          item.kernelIconUrl ? (
            <img
              src={item.kernelIconUrl}
              className="jp-NewLauncher-kernelIcon"
            />
          ) : (
            <div className="jp-NewLauncherCard-noKernelIcon">
              {label[0].toUpperCase()}
            </div>
          )
        ) : (
          <LabIcon.resolveReact
            icon={icon}
            iconClass={classes(iconClass, 'jp-Icon-cover')}
            stylesheet="launcherCard"
          />
        )}
      </div>
      <div
        className={`jp-NewLauncher-label jp-NewLauncher${mode}-Cell`}
        title={label}
      >
        {label}
      </div>
      <div
        className={`jp-NewLauncher-options-wrapper jp-NewLauncher${mode}-Cell`}
      >
        <div className="jp-NewLauncher-options">{getOptions(items)}</div>
      </div>
    </div>
  );
}
Example #26
Source File: index.ts    From jlab-enhanced-launcher with BSD 3-Clause "New" or "Revised" License 4 votes vote down vote up
/**
 * Activate the launcher.
 */
async function activate(
  app: JupyterFrontEnd,
  translator: ITranslator,
  labShell: ILabShell | null,
  palette: ICommandPalette | null,
  settingRegistry: ISettingRegistry | null,
  state: IStateDB | null
): Promise<ILauncher> {
  const { commands, shell } = app;
  const trans = translator.load('jupyterlab');

  let settings: ISettingRegistry.ISettings | null = null;
  if (settingRegistry) {
    try {
      settings = await settingRegistry.load(EXTENSION_ID);
    } catch (reason) {
      console.log(`Failed to load settings for ${EXTENSION_ID}.`, reason);
    }
  }

  const model = new LauncherModel(settings, state);

  if (state) {
    Promise.all([
      state.fetch(`${EXTENSION_ID}:usageData`),
      state.fetch(`${EXTENSION_ID}:viewMode`),
      app.restored
    ])
      .then(([usage, mode]) => {
        model.viewMode = (mode as any) || 'cards';
        for (const key in usage as any) {
          model.usage[key] = (usage as any)[key];
        }
      })
      .catch(reason => {
        console.error('Fail to restore launcher usage data', reason);
      });
  }

  commands.addCommand(CommandIDs.create, {
    label: trans.__('New Launcher'),
    execute: (args: ReadonlyPartialJSONObject) => {
      const cwd = args['cwd'] ? String(args['cwd']) : '';
      const id = `launcher-${Private.id++}`;
      const callback = (item: Widget): void => {
        shell.add(item, 'main', { ref: id });
      };
      const launcher = new Launcher({ model, cwd, callback, commands });

      launcher.model = model;
      launcher.title.icon = launcherIcon;
      launcher.title.label = trans.__('Launcher');

      const main = new MainAreaWidget({ content: launcher });

      // If there are any other widgets open, remove the launcher close icon.
      main.title.closable = !!toArray(shell.widgets('main')).length;
      main.id = id;

      shell.add(main, 'main', {
        activate: args['activate'] as boolean,
        ref: args['ref'] as string
      });

      if (labShell) {
        labShell.layoutModified.connect(() => {
          // If there is only a launcher open, remove the close icon.
          main.title.closable = toArray(labShell.widgets('main')).length > 1;
        }, main);
      }

      return main;
    }
  });

  if (palette) {
    palette.addItem({
      command: CommandIDs.create,
      category: trans.__('Launcher')
    });
  }

  if (labShell && app.version >= '3.4.0') {
    labShell.addButtonEnabled = true;
    labShell.addRequested.connect((sender: DockPanel, arg: TabBar<Widget>) => {
      // Get the ref for the current tab of the tabbar which the add button was clicked
      const ref =
        arg.currentTitle?.owner.id ||
        arg.titles[arg.titles.length - 1].owner.id;
      if (commands.hasCommand('filebrowser:create-main-launcher')) {
        // If a file browser is defined connect the launcher to it
        return commands.execute('filebrowser:create-main-launcher', { ref });
      }
      return commands.execute(CommandIDs.create, { ref });
    });
  }

  return model;
}
Example #27
Source File: widget.ts    From jupyterlab-tabular-data-editor with BSD 3-Clause "New" or "Revised" License 4 votes vote down vote up
export class DSVEditor extends Widget {
  private _background: HTMLElement;
  private _ghostCorner: LayoutItem;
  private _ghostRow: LayoutItem;
  private _ghostColumn: LayoutItem;
  /**
   * Construct a new CSV viewer.
   */
  constructor(options: DSVEditor.IOptions) {
    super();

    const context = (this._context = options.context);
    const layout = (this.layout = new PanelLayout());

    this.addClass(CSV_CLASS);

    // Initialize the data grid.
    this._grid = new PaintedGrid({
      defaultSizes: {
        rowHeight: 21,
        columnWidth: 100,
        rowHeaderWidth: 60,
        columnHeaderHeight: 24
      },
      headerVisibility: 'all'
    });

    this._grid.addClass(CSV_GRID_CLASS);
    const keyHandler = new RichKeyHandler();
    this._grid.keyHandler = keyHandler;

    this._grid.copyConfig = {
      separator: '\t',
      format: DataGrid.copyFormatGeneric,
      headers: 'none',
      warningThreshold: 1e6
    };
    layout.addWidget(this._grid);

    // Add the mouse handler to the grid.
    const handler = new RichMouseHandler({ grid: this._grid });
    this._grid.mouseHandler = handler;

    // Connect to the mouse handler signals.
    handler.mouseUpSignal.connect(this._onMouseUp, this);
    handler.hoverSignal.connect(this._onMouseHover, this);

    // init search service to search for matches with the data grid
    this._searchService = new GridSearchService(this._grid);
    this._searchService.changed.connect(this._updateRenderer, this);

    // add the background column and row header elements
    this._background = VirtualDOM.realize(
      h.div({
        className: BACKGROUND_CLASS,
        style: {
          position: 'absolute',
          zIndex: '1'
        }
      })
    );
    this._rowHeader = VirtualDOM.realize(
      h.div({
        className: ROW_HEADER_CLASS,
        style: {
          position: 'absolute',
          zIndex: '2'
        }
      })
    );
    this._columnHeader = VirtualDOM.realize(
      h.div({
        className: COLUMN_HEADER_CLASS,
        style: {
          position: 'absolute',
          zIndex: '2'
        }
      })
    );

    // append the column and row headers to the viewport
    this._grid.viewport.node.appendChild(this._rowHeader);
    this._grid.viewport.node.appendChild(this._columnHeader);
    this._grid.viewport.node.appendChild(this._background);

    void this._context.ready.then(() => {
      this._updateGrid();
      this._revealed.resolve(undefined);
      // Throttle the rendering rate of the widget.
      this._monitor = new ActivityMonitor({
        signal: context.model.contentChanged,
        timeout: RENDER_TIMEOUT
      });
      this._monitor.activityStopped.connect(this._updateGrid, this);
    });
    this._grid.editingEnabled = true;
    this.commandSignal.connect(this._onCommand, this);
  }

  /**
   * The ghost row of the grid.
   */
  get ghostRow(): LayoutItem {
    return this._ghostRow;
  }

  /**
   * The ghost column of the grid.
   */
  get ghostColumn(): LayoutItem {
    return this._ghostColumn;
  }

  /**
   * The ghost corner of the grid.
   */
  get ghostCorner(): LayoutItem {
    return this._ghostCorner;
  }

  /**
   * The CSV widget's context.
   */
  get context(): DocumentRegistry.Context {
    return this._context;
  }

  /**
   * A promise that resolves when the csv viewer is ready to be revealed.
   */
  get revealed(): Promise<void> {
    return this._revealed.promise;
  }

  /**
   * The delimiter for the file.
   */
  get delimiter(): string {
    return this._delimiter;
  }
  set delimiter(value: string) {
    if (value === this._delimiter) {
      return;
    }
    this._delimiter = value;
    this._updateGrid();
  }

  /**
   * The style used by the data grid.
   */
  get style(): DataGrid.Style {
    return this._grid.style;
  }
  set style(value: DataGrid.Style) {
    this._grid.style = value;
  }

  /**
   * The style used by the data grid.
   */
  get extraStyle(): PaintedGrid.ExtraStyle {
    return this._grid.extraStyle;
  }
  set extraStyle(value: PaintedGrid.ExtraStyle) {
    this._grid.extraStyle = value;
  }

  /**
   * The config used to create text renderer.
   */
  set rendererConfig(rendererConfig: TextRenderConfig) {
    this._baseRenderer = rendererConfig;
    this._updateRenderer();
  }

  /**
   * The search service
   */
  get searchService(): GridSearchService {
    return this._searchService;
  }

  get grid(): PaintedGrid {
    return this._grid;
  }

  /**
   * The DataModel used to render the DataGrid
   */
  get dataModel(): EditorModel {
    return this._grid.dataModel as EditorModel;
  }

  get litestore(): Litestore {
    return this._litestore;
  }

  get commandSignal(): Signal<this, DSVEditor.Commands> {
    return this._commandSignal;
  }

  get dirty(): boolean {
    return this._dirty;
  }

  /**
   * Sets the dirty boolean while also toggling the DIRTY_CLASS
   */
  set dirty(dirty: boolean) {
    this._dirty = dirty;
    if (this.dirty && !this.title.className.includes(DIRTY_CLASS)) {
      this.title.className += DIRTY_CLASS;
    } else if (!this.dirty) {
      this.title.className = this.title.className.replace(DIRTY_CLASS, '');
    }
  }

  get rowsSelected(): number {
    const selection: SelectionModel.Selection = this._grid.selectionModel.currentSelection();
    if (!selection) {
      return 0;
    }
    const { r1, r2 } = selection;
    return Math.abs(r2 - r1) + 1;
  }

  get columnsSelected(): number {
    const selection: SelectionModel.Selection = this._grid.selectionModel.currentSelection();
    if (!selection) {
      return 0;
    }
    const { c1, c2 } = selection;
    return Math.abs(c2 - c1) + 1;
  }

  /**
   * Dispose of the resources used by the widget.
   */
  dispose(): void {
    if (this._monitor) {
      this._monitor.dispose();
    }
    super.dispose();
  }

  /**
   * Go to line
   */
  goToLine(lineNumber: number): void {
    this._grid.scrollToRow(lineNumber);
  }

  /**
   * Handle `'activate-request'` messages.
   */
  protected onActivateRequest(msg: Message): void {
    this.node.tabIndex = -1;
    this.node.focus();
  }

  /**
   * Guess the row delimiter if it was not supplied.
   * This will be fooled if a different line delimiter possibility appears in the first row.
   */
  private _guessRowDelimeter(data: string): string {
    const i = data.slice(0, 5000).indexOf('\r');
    if (i === -1) {
      return '\n';
    } else if (data[i + 1] === '\n') {
      return '\r\n';
    } else {
      return '\r';
    }
  }

  /**
   * Counts the occurrences of a substring from a given string
   */
  private _countOccurrences(
    string: string,
    substring: string,
    rowDelimiter: string
  ): number {
    let numCol = 0;
    let pos = 0;
    const l = substring.length;
    const firstRow = string.slice(0, string.indexOf(rowDelimiter));

    pos = firstRow.indexOf(substring, pos);
    while (pos !== -1) {
      numCol++;
      pos += l;
      pos = firstRow.indexOf(substring, pos);
    }
    // number of columns is the amount of columns + 1
    return numCol + 1;
  }

  /**
   * Adds the a column header of alphabets to the top of the data (A..Z,AA..ZZ,AAA...)
   * @param colDelimiter The delimiter used to separated columns (commas, tabs, spaces)
   */
  protected _buildColHeader(colDelimiter: string): string {
    const rawData = this._context.model.toString();
    const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    // when the model is first created, we don't know how many columns or what the row delimeter is
    const rowDelimiter = this._guessRowDelimeter(rawData);
    const numCol = this._countOccurrences(rawData, colDelimiter, rowDelimiter);

    // if only single alphabets fix the string
    if (numCol <= 26) {
      return (
        alphabet.slice(0, numCol).split('').join(colDelimiter) + rowDelimiter
      );
    }
    // otherwise compute the column header with multi-letters (AA..)
    else {
      // get all single letters
      let columnHeader = alphabet.split('').join(colDelimiter);
      // find the rest
      for (let i = 27; i < numCol; i++) {
        columnHeader += colDelimiter + numberToCharacter(i);
      }
      return columnHeader + rowDelimiter;
    }
  }

  /**
   * Create the model for the grid.
   * TODO: is there a reason we can't just do this once in the constructor?
   */
  protected _updateGrid(): void {
    // Bail early if we already have a data model installed.
    if (this.dataModel) {
      return;
    }

    const delimiter = this.delimiter;
    const data = this._context.model.toString();
    const dataModel = (this._grid.dataModel = new EditorModel({
      data,
      delimiter
    }));

    this._grid.selectionModel = new GhostSelectionModel({ dataModel });

    // create litestore
    this._litestore = new Litestore({
      id: 0,
      schemas: [DSVEditor.DATAMODEL_SCHEMA]
    });

    // Give the litestore as a property of the model for it to read from.
    dataModel.litestore = this._litestore;

    // Define the initial update object for the litestore.
    const update: DSVEditor.ModelChangedArgs = {};

    // Define the initial state of the row and column map.
    const rowUpdate = {
      index: 0,
      remove: 0,
      values: toArray(range(0, this.dataModel.totalRows))
    };
    const columnUpdate = {
      index: 0,
      remove: 0,
      values: toArray(range(0, this.dataModel.totalColumns))
    };

    // Add the map updates to the update object.
    update.rowUpdate = rowUpdate;
    update.columnUpdate = columnUpdate;

    // Set an indicator to show that this is the initial update.
    update.type = 'init';

    // set inital status of litestore
    this.updateModel(update);

    // Connect to the the model signals.
    dataModel.onChangedSignal.connect(this._onModelSignal, this);
    dataModel.isDataFormattedChanged.connect(this._updateRenderer, this);

    // Update the div elements of the grid.
    this._updateContextElements();
  }

  /**
   * Update the renderer for the grid.
   */
  private _updateRenderer(): void {
    if (this._baseRenderer === null) {
      return;
    }
    const isDataFormatted = this.dataModel && this.dataModel.isDataFormatted;
    const rendererConfig = this._baseRenderer;
    const renderer = new TextRenderer({
      textColor: rendererConfig.textColor,
      horizontalAlignment: isDataFormatted
        ? this.cellHorizontalAlignmentRendererFunc()
        : rendererConfig.horizontalAlignment,
      backgroundColor: this._searchService.cellBackgroundColorRendererFunc(
        rendererConfig
      ),
      font: '11px sans-serif'
    });
    const rowHeaderRenderer = new TextRenderer({
      textColor: rendererConfig.textColor,
      horizontalAlignment: 'center',
      backgroundColor: this._searchService.cellBackgroundColorRendererFunc(
        rendererConfig
      ),
      font: '11px sans-serif'
    });
    const headerRenderer = new HeaderTextRenderer({
      textColor: rendererConfig.textColor,
      horizontalAlignment: isDataFormatted ? 'left' : 'center',
      backgroundColor: this._searchService.cellBackgroundColorRendererFunc(
        rendererConfig
      ),
      font: '11px sans-serif',
      indent: 25,
      dataDetection: isDataFormatted
    });

    this._grid.cellRenderers.update({
      body: renderer,
      'column-header': headerRenderer,
      'corner-header': renderer,
      'row-header': rowHeaderRenderer
    });
  }

  cellHorizontalAlignmentRendererFunc(): CellRenderer.ConfigOption<TextRenderer.HorizontalAlignment> {
    return ({ region, row, column }) => {
      const { type } = this.dataModel.dataTypes[column];
      if (region !== 'body' || type === 'boolean') {
        return 'center';
      }
      return type === 'number' || type === 'integer' ? 'right' : 'left';
    };
  }

  /**
   * Called every time the datamodel updates
   * Updates the file and the litestore
   * @param emitter
   * @param args The row, column, value, record update, selection model
   */
  private _onModelSignal(
    emitter: EditorModel,
    args: DSVEditor.ModelChangedArgs | null
  ): void {
    this.updateModel(args);
  }

  /**
   * Serializes and saves the file (default: asynchronous)
   * @param [exiting] - False to save asynchronously
   */
  async save(exiting = false): Promise<void> {
    const newString = this.dataModel.updateString();
    this.context.model.fromString(newString);

    exiting ? await this.context.save() : this.context.save();

    // reset boolean since no new changes exist
    this.dirty = false;
  }

  // private _cancelEditing(emitter: EditorModel): void {
  //   this._grid.editorController.cancel();
  // }

  /**
   * Handles all changes to the data model
   * @param emitter
   * @param command
   */
  private _onCommand(emitter: DSVEditor, command: DSVEditor.Commands): void {
    const selectionModel = this._grid.selectionModel;
    const selection = selectionModel.currentSelection();
    const rowSpan = this.rowsSelected;
    const colSpan = this.columnsSelected;
    let r1, r2, c1, c2: number;

    // grab selection if it exists
    if (selection) {
      // r1 and c1 are always first row/column
      r1 = Math.min(selection.r1, selection.r2);
      r2 = Math.max(selection.r1, selection.r2);
      c1 = Math.min(selection.c1, selection.c2);
      c2 = Math.max(selection.c1, selection.c2);
    }

    // Set up the update object for the litestore.
    let update: DSVEditor.ModelChangedArgs | null = null;

    switch (command) {
      case 'insert-rows-above': {
        update = this.dataModel.addRows('body', r1, rowSpan);
        break;
      }
      case 'insert-rows-below': {
        update = this.dataModel.addRows('body', r2 + 1, rowSpan);

        // move the selection down a row to account for the new row being inserted
        r1 += rowSpan;
        r2 += rowSpan;
        break;
      }
      case 'insert-columns-left': {
        update = this.dataModel.addColumns('body', c1, colSpan);
        break;
      }
      case 'insert-columns-right': {
        update = this.dataModel.addColumns('body', c2 + 1, colSpan);

        // move the selection right a column to account for the new column being inserted
        c1 += colSpan;
        c2 += colSpan;
        break;
      }
      case 'remove-rows': {
        update = this.dataModel.removeRows('body', r1, rowSpan);
        break;
      }
      case 'remove-columns': {
        update = this.dataModel.removeColumns('body', c1, colSpan);
        break;
      }
      case 'cut-cells':
        // Copy to the OS clipboard.
        this._grid.copyToClipboard();

        // Cut the cell selection.
        update = this.dataModel.cut('body', r1, c1, r2, c2);

        break;
      case 'copy-cells': {
        // Copy to the OS clipboard.
        this._grid.copyToClipboard();

        // Make a local copy of the cells.
        this.dataModel.copy('body', r1, c1, r2, c2);
        break;
      }
      case 'paste-cells': {
        // Paste the cells in the region.
        update = this.dataModel.paste('body', r1, c1);

        // By default, upper left cell get's re-edited, so we need to cancel.
        this._cancelEditing();
        break;
      }
      case 'clear-cells': {
        update = this.dataModel.clearCells('body', { r1, r2, c1, c2 });
        break;
      }
      case 'clear-rows': {
        const rowSpan = Math.abs(r1 - r2) + 1;
        update = this.dataModel.clearRows('body', r1, rowSpan);
        break;
      }
      case 'clear-columns': {
        const columnSpan = Math.abs(c1 - c2) + 1;
        update = this.dataModel.clearColumns('body', c1, columnSpan);
        break;
      }
      case 'undo': {
        // check to see if an undo exists (one undo will exist because that's the initial transaction)
        if (this._litestore.transactionStore.undoStack.length === 1) {
          return;
        }

        const { gridState, selection } = this._litestore.getRecord({
          schema: DSVEditor.DATAMODEL_SCHEMA,
          record: DSVEditor.RECORD_ID
        });

        this._litestore.undo();

        // Have the model emit the opposite change to the Grid.
        this.dataModel.emitOppositeChange(gridState);

        this._grid.selectCells(selection);

        break;
      }
      case 'redo': {
        // check to see if an redo exists (one redo will exist because that's the initial transaction)
        if (this._litestore.transactionStore.redoStack.length === 0) {
          return;
        }

        // Redo first, then get the new selection and the new grid change.
        this._litestore.redo();
        const { gridState, selection } = this._litestore.getRecord({
          schema: DSVEditor.DATAMODEL_SCHEMA,
          record: DSVEditor.RECORD_ID
        });

        // Have the data model emit the grid change to the grid.
        this.dataModel.emitCurrentChange(gridState.nextChange);

        if (!selection) {
          break;
        }
        const command = gridState.nextCommand;
        const gridChange = gridState.nextChange;

        let { r1, r2, c1, c2 } = selection;
        let move: DataModel.ChangedArgs;
        // handle special cases for selection
        if (command === 'insert-rows-below') {
          r1 += rowSpan;
          r2 += rowSpan;
        } else if (command === 'insert-columns-right') {
          c1 += colSpan;
          c2 += colSpan;
        } else if (command === 'move-rows') {
          move = gridChange as DataModel.RowsMovedArgs;
          r1 = move.destination;
          r2 = move.destination;
        } else if (command === 'move-columns') {
          move = gridChange as DataModel.ColumnsMovedArgs;
          c1 = move.destination;
          c2 = move.destination;
        }

        // Make the new selection.
        this._grid.selectCells({ r1, r2, c1, c2 });
        break;
      }
      case 'save':
        this.save();
        break;
    }
    if (update) {
      update.selection = selection;
      // Add the command to the grid state.
      update.gridStateUpdate.nextCommand = command;
      this._grid.selectCells({ r1, r2, c1, c2 });
    }
    this.updateModel(update);
  }

  /**
   * Updates the current transaction with the raw data, header, and changeArgs
   * @param update The modelChanged args for the Datagrid (may be null)
   */
  public updateModel(update?: DSVEditor.ModelChangedArgs): void {
    if (update) {
      // If no selection property was passed in, record the current selection.
      if (!update.selection) {
        update.selection = this._grid.selectionModel.currentSelection();
      }

      // Update the litestore.
      this._litestore.beginTransaction();
      this._litestore.updateRecord(
        {
          schema: DSVEditor.DATAMODEL_SCHEMA,
          record: DSVEditor.RECORD_ID
        },
        {
          rowMap: update.rowUpdate || DSVEditor.NULL_NUM_SPLICE,
          columnMap: update.columnUpdate || DSVEditor.NULL_NUM_SPLICE,
          valueMap: update.valueUpdate || null,
          selection: update.selection || null,
          gridState: update.gridStateUpdate || null
        }
      );
      this._litestore.endTransaction();

      // Bail before setting dirty if this is an init command.
      if (update.type === 'init') {
        return;
      }
      this.dirty = true;
    }

    // Recompute all of the metadata.
    // TODO: integrate the metadata with the rest of the model.
    if (this.dataModel.isDataFormatted) {
      this.dataModel.dataTypes = this.dataModel.resetMetadata();
      this._updateRenderer();
    }
  }

  protected getSelectedRange(): SelectionModel.Selection {
    const selections = toArray(this._grid.selectionModel.selections());
    if (selections.length === 0) {
      return;
    }
    return selections[0];
  }

  protected onAfterAttach(msg: Message): void {
    super.onAfterAttach(msg);
    this.node.addEventListener('paste', this._handlePaste.bind(this));
  }

  private _handlePaste(event: ClipboardEvent): void {
    const copiedText: string = event.clipboardData.getData('text/plain');
    // prevent default behavior
    event.preventDefault();
    event.stopPropagation();
    const { r1, r2, c1, c2 } = this.getSelectedRange();
    const row = Math.min(r1, r2);
    const column = Math.min(c1, c2);
    const update = this.dataModel.paste('body', row, column, copiedText);
    this._cancelEditing();
    this.updateModel(update);
  }

  private _cancelEditing(): void {
    this._grid.editorController.cancel();
  }

  /**
   * Updates the context menu elements.
   */
  private _updateContextElements(): void {
    // calculate dimensions for the ghost row/column
    const ghostRow = this._grid.rowSize(
      'body',
      this._grid.rowCount('body') - 1
    );
    const ghostColumn = this._grid.columnSize(
      'body',
      this._grid.columnCount('body') - 1
    );
    // Update the column header, row header, and background elements.
    this._background.style.width = `${this._grid.bodyWidth - ghostColumn}px`;
    this._background.style.height = `${this._grid.bodyHeight - ghostRow}px`;
    this._background.style.left = `${this._grid.headerWidth}px`;
    this._background.style.top = `${this._grid.headerHeight}px`;
    this._columnHeader.style.left = `${this._grid.headerWidth}px`;
    this._columnHeader.style.height = `${this._grid.headerHeight}px`;
    this._columnHeader.style.width = `${this._grid.bodyWidth}px`;
    this._rowHeader.style.top = `${this._grid.headerHeight}px`;
    this._rowHeader.style.width = `${this._grid.headerWidth}px`;
    this._rowHeader.style.height = `${this._grid.bodyHeight}px`;
  }

  /**
   * Handles a mouse up signal.
   */
  private _onMouseUp(
    emitter: RichMouseHandler,
    hit: DataGrid.HitTestResult
  ): void {
    // Update the context menu elements as they may have moved.
    this._updateContextElements();
  }

  /**
   * A handler for the on mouse up signal
   */
  private _onMouseHover(
    emitter: RichMouseHandler,
    hoverRegion: 'ghost-row' | 'ghost-column' | null
  ): void {
    // Switch both to non-hovered state.
    const style = { ...this._grid.extraStyle } as PaintedGrid.ExtraStyle;
    if (this.grid.style.voidColor === '#F3F3F3') {
      style.ghostColumnColor = LIGHT_EXTRA_STYLE.ghostColumnColor;
      style.ghostRowColor = LIGHT_EXTRA_STYLE.ghostRowColor;
    } else {
      style.ghostColumnColor = DARK_EXTRA_STYLE.ghostColumnColor;
      style.ghostRowColor = DARK_EXTRA_STYLE.ghostRowColor;
    }
    switch (hoverRegion) {
      case null: {
        break;
      }
      case 'ghost-row': {
        style.ghostRowColor = 'rgba(0, 0, 0, 0)';
        break;
      }
      case 'ghost-column': {
        style.ghostColumnColor = 'rgba(0, 0, 0, 0)';
        break;
      }
    }
    // Schedule a repaint of the grid.
    this._grid.extraStyle = style;
  }

  private _context: DocumentRegistry.Context;
  private _grid: PaintedGrid;
  private _searchService: GridSearchService;
  private _monitor: ActivityMonitor<
    DocumentRegistry.IModel,
    void
  > | null = null;
  private _delimiter = ',';
  private _revealed = new PromiseDelegate<void>();
  private _baseRenderer: TextRenderConfig | null = null;
  private _litestore: Litestore;
  private _dirty = false;

  // Signals for basic editing functionality
  private _commandSignal = new Signal<this, DSVEditor.Commands>(this);
  private _columnHeader: HTMLElement;
  private _rowHeader: HTMLElement;
}
Example #28
Source File: widget.ts    From jupyterlab-interactive-dashboard-editor with BSD 3-Clause "New" or "Revised" License 4 votes vote down vote up
/**
 * Widget to wrap delete/move/etc functionality of widgets in a dashboard (future).
 */
export class DashboardWidget extends Widget {
  constructor(options: DashboardWidget.IOptions) {
    super();

    const { notebook, cell, cellId, notebookId, fit } = options;

    this._notebook = notebook || null;
    this._cell = cell || null;
    this.id = DashboardWidget.createDashboardWidgetId();

    // Makes widget focusable.
    this.node.setAttribute('tabindex', '-1');

    const _cellId = getCellId(cell);
    const _notebookId = getNotebookId(notebook);

    if (_notebookId === undefined) {
      this.node.style.background = 'red';
      if (notebookId === undefined) {
        console.warn('DashboardWidget has no notebook or notebookId');
      }
      this._notebookId = notebookId;
    } else if (_cellId === undefined) {
      this.node.style.background = 'yellow';
      if (cellId === undefined) {
        console.warn('DashboardWidget has no cell or cellId');
      }
      this._cellId = cellId;
    } else {
      if (notebookId && _notebookId !== notebookId) {
        console.warn(`DashboardWidget notebookId ('${notebookId}') and id of
                      notebook ('${_notebookId}') don't match. 
                      Using ${_notebookId}.`);
      }
      if (cellId && _cellId !== cellId) {
        console.warn(`DashboardWidget cellId ('${cellId}') and id of cell
                      ('${_cellId}') don't match. Using ${_cellId}.`);
      }

      this._cellId = _cellId;
      this._notebookId = _notebookId;

      void this._notebook.context.ready.then(() => {
        let clone: Widget;
        const cellType = cell.model.type;
        const container = document.createElement('div');
        let cloneNode: HTMLElement;
        let nodes: HTMLCollectionOf<Element>;
        let node: HTMLElement;

        switch (cellType) {
          case 'markdown':
            cloneNode = (cell as MarkdownCell).clone().node;
            nodes = cloneNode.getElementsByClassName('jp-MarkdownOutput');
            node = nodes[0] as HTMLElement;
            node.style.paddingRight = '0';
            clone = new Widget({ node });
            container.classList.add(MARKDOWN_OUTPUT_CLASS);
            break;
          case 'code':
            clone = (cell as CodeCell).cloneOutputArea();
            break;
          default:
            throw new Error('Cell is not a code or markdown cell.');
        }

        // Make widget invisible until it's properly loaded/sized.
        this.node.style.opacity = '0';

        container.classList.add(DASHBOARD_WIDGET_CHILD_CLASS);

        // Fake an attach in order to render LaTeX properly.
        // Note: This is not how you should use Lumino widgets.
        if (this.parent) {
          if (this.parent!.isAttached) {
            MessageLoop.sendMessage(clone, Widget.Msg.BeforeAttach);
            container.appendChild(clone.node);
            this.node.appendChild(container);
            if (this.parent!.isAttached) {
              MessageLoop.sendMessage(clone, Widget.Msg.AfterAttach);
            }
          }
        }

        this._content = clone;

        const done = (): void => {
          if (fit) {
            this.fitContent();
          }
          // Make widget visible again.
          this.node.style.opacity = null;
          // Emit the ready signal.
          this._ready.emit(undefined);
        };

        // Wait a moment then fit content. This allows all components to load
        // and for their width/height to adjust before fitting.
        setTimeout(done.bind(this), 2);
      });
    }

    const resizerTopLeft = DashboardWidget.createResizer('top-left');
    const resizerTopRight = DashboardWidget.createResizer('top-right');
    const resizerBottomLeft = DashboardWidget.createResizer('bottom-left');
    const resizerBottomRight = DashboardWidget.createResizer('bottom-right');
    this.node.appendChild(resizerTopLeft);
    this.node.appendChild(resizerTopRight);
    this.node.appendChild(resizerBottomLeft);
    this.node.appendChild(resizerBottomRight);

    this.addClass(DASHBOARD_WIDGET_CLASS);
    this.addClass(EDITABLE_WIDGET_CLASS);
  }

  /**
   * Create click listeners on attach
   */
  onAfterAttach(msg: Message): void {
    super.onAfterAttach(msg);
    this.node.addEventListener('click', this);
    this.node.addEventListener('contextmenu', this);
    this.node.addEventListener('mousedown', this);
    this.node.addEventListener('dblclick', this);
    this.node.addEventListener('keydown', this);
  }

  /**
   * Remove click listeners on detach
   */
  onBeforeDetach(msg: Message): void {
    super.onBeforeDetach(msg);
    this.node.removeEventListener('click', this);
    this.node.removeEventListener('contextmenu', this);
    this.node.removeEventListener('mousedown', this);
    this.node.removeEventListener('dblclick', this);
    this.node.removeEventListener('keydown', this);
  }

  handleEvent(event: Event): void {
    // Just do the default behavior in present mode.
    if (this._mode === 'present') {
      return;
    }

    switch (event.type) {
      case 'keydown':
        this._evtKeyDown(event as KeyboardEvent);
        break;
      case 'mousedown':
        this._evtMouseDown(event as MouseEvent);
        break;
      case 'mouseup':
        this._evtMouseUp(event as MouseEvent);
        break;
      case 'mousemove':
        this._evtMouseMove(event as MouseEvent);
        break;
      case 'click':
      case 'contextmenu':
        // Focuses on clicked output and blurs all others
        // Is there a more efficient way to blur other outputs?
        Array.from(document.getElementsByClassName(DASHBOARD_WIDGET_CLASS)).map(
          blur
        );
        this.node.focus();
        break;
      case 'dblclick':
        this._evtDblClick(event as MouseEvent);
        break;
    }
  }

  /**
   * Handle the `'keydown'` event for the widget.
   */
  private _evtKeyDown(event: KeyboardEvent): void {
    event.preventDefault();
    event.stopPropagation();

    const pos = this.pos;
    const oldPos = { ...pos };

    const bumpDistance = event.altKey ? 1 : DashboardWidget.BUMP_DISTANCE;

    switch (event.keyCode) {
      // Left arrow key
      case 37:
        pos.left -= bumpDistance;
        break;
      // Up arrow key
      case 38:
        pos.top -= bumpDistance;
        break;
      // Right arrow key
      case 39:
        pos.left += bumpDistance;
        break;
      // Down arrow key
      case 40:
        pos.top += bumpDistance;
        break;
    }

    if (pos !== oldPos) {
      (this.parent as Dashboard).updateWidget(this, pos);
    }
  }

  /**
   * Handle the `'dblclick'` event for the widget. Currently a no-op.
   */
  private _evtDblClick(event: MouseEvent): void {
    // Do nothing if it's not a left mouse press.
    if (event.button !== 0) {
      return;
    }

    // Do nothing if any modifier keys are pressed.
    if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) {
      return;
    }

    // Stop the event propagation.
    event.preventDefault();
    event.stopPropagation();
  }

  /**
   * Handle `mousedown` events for the widget.
   */
  private _evtMouseDown(event: MouseEvent): void {
    const { button, shiftKey, target } = event;

    // We only handle main or secondary button actions.
    if (
      !(button === 0 || button === 2) ||
      // Shift right-click gives the browser default behavior.
      (shiftKey && button === 2)
    ) {
      return;
    }

    event.preventDefault();

    window.addEventListener('mouseup', this);
    window.addEventListener('mousemove', this);

    // this.node.style.opacity = '0.6';

    const elem = target as HTMLElement;
    // Set mode to resize if the mousedown happened on a resizer.
    if (elem.classList.contains('pr-Resizer')) {
      this._mouseMode = 'resize';
      if (elem.classList.contains('pr-ResizerTopRight')) {
        this._selectedResizer = 'top-right';
      } else if (elem.classList.contains('pr-ResizerTopLeft')) {
        this._selectedResizer = 'top-left';
      } else if (elem.classList.contains('pr-ResizerBottomLeft')) {
        this._selectedResizer = 'bottom-left';
      } else {
        this._selectedResizer = 'bottom-right';
      }
    } else {
      this._mouseMode = 'drag';
    }

    const cell = this.cell;

    const rect = this.node.getBoundingClientRect();

    const { width, height, top, left } = this.pos;

    this._clickData = {
      pressX: event.clientX,
      pressY: event.clientY,
      cell,
      origWidth: width,
      origHeight: height,
      origLeft: left,
      origTop: top,
      target: this.node.cloneNode(true) as HTMLElement,
      widgetX: rect.left,
      widgetY: rect.top
    };
  }

  /**
   * Handle `mousemove` events for the widget.
   */
  private _evtMouseMove(event: MouseEvent): void {
    switch (this._mouseMode) {
      case 'drag':
        this._dragMouseMove(event);
        break;
      case 'resize':
        this._resizeMouseMove(event);
        break;
      default:
        break;
    }
  }

  /**
   * Handle `mousemove` events when the widget mouseMode is `drag`.
   */
  private _dragMouseMove(event: MouseEvent): void {
    const data = this._clickData;
    const { clientX, clientY } = event;

    if (
      data &&
      Private.shouldStartDrag(data.pressX, data.pressY, clientX, clientY)
    ) {
      void this._startDrag(data.target, clientX, clientY);
    }
  }

  /**
   * Handle `mousemove` events when the widget mouseMode is `resize`.
   */
  private _resizeMouseMove(event: MouseEvent): void {
    const {
      pressX,
      pressY,
      origWidth,
      origHeight,
      origLeft,
      origTop
    } = this._clickData;

    const deltaX = event.clientX - pressX;
    const deltaY = event.clientY - pressY;

    let { width, height, top, left } = this.pos;

    switch (this._selectedResizer) {
      case 'bottom-right':
        width = Math.max(origWidth + deltaX, DashboardWidget.MIN_WIDTH);
        height = Math.max(origHeight + deltaY, DashboardWidget.MIN_HEIGHT);
        break;
      case 'bottom-left':
        width = Math.max(origWidth - deltaX, DashboardWidget.MIN_WIDTH);
        height = Math.max(origHeight + deltaY, DashboardWidget.MIN_HEIGHT);
        left = origLeft + deltaX;
        break;
      case 'top-right':
        width = Math.max(origWidth + deltaX, DashboardWidget.MIN_WIDTH);
        height = Math.max(origHeight - deltaY, DashboardWidget.MIN_HEIGHT);
        top = origTop + deltaY;
        break;
      case 'top-left':
        width = Math.max(origWidth - deltaX, DashboardWidget.MIN_WIDTH);
        height = Math.max(origHeight - deltaY, DashboardWidget.MIN_HEIGHT);
        top = origTop + deltaY;
        left = origLeft + deltaX;
        break;
    }

    this.pos = { width, height, top, left };

    if (this.mode === 'grid-edit') {
      (this.parent.layout as DashboardLayout).drawDropZone(this.pos, '#2b98f0');
    }
    if (this.mode === 'free-edit' && this._fitToContent && !event.altKey) {
      this.fitContent();
    }
  }

  /**
   * Fit widget width/height to the width/height of the underlying content.
   */
  fitContent(): void {
    const element = this._content.node.firstChild as HTMLElement;
    // Pixels are added to prevent weird wrapping issues. Kind of a hack.
    this.pos = {
      width: element.clientWidth + 3,
      height: element.clientHeight + 2,
      left: undefined,
      top: undefined
    };
  }

  /**
   * Determines whether the widget contains the point (left, top).
   *
   * ### Notes
   * Both `left` and `top` are relative to the dashboard.
   */
  containsPoint(left: number, top: number): boolean {
    const pos = {
      left,
      top,
      width: 0,
      height: 0
    };
    const overlap = this.overlaps(pos);
    return overlap.type !== 'none';
  }

  /**
   * Determines whether the widget overlaps an area.
   *
   * @param _pos - the position and size of the test area.
   *
   * @returns - an object containing the type of overlap and this widget.
   */
  overlaps(_pos: Widgetstore.WidgetPosition): DashboardWidget.Overlap {
    const { left, top, width, height } = _pos;
    const pos = this.pos;
    const w = 0.5 * (width + pos.width);
    const h = 0.5 * (height + pos.height);
    const dx = left + 0.5 * width - (pos.left + 0.5 * pos.width);
    const dy = top + 0.5 * height - (pos.top + 0.5 * pos.height);
    let type: DashboardWidget.Direction = 'none';

    if (Math.abs(dx) < w && Math.abs(dy) < h) {
      if (top > pos.top + pos.height / 2) {
        type = 'up';
      } else {
        type = 'down';
      }
    }

    return { type, widget: this };
  }

  /**
   * Start a drag event
   */
  private _startDrag(
    target: HTMLElement,
    clientX: number,
    clientY: number
  ): Promise<void> {
    const dragImage = target;

    dragImage.classList.add(DRAG_IMAGE_CLASS);

    this.node.style.opacity = '0';
    this.node.style.pointerEvents = 'none';

    this._drag = new Drag({
      mimeData: new MimeData(),
      dragImage,
      proposedAction: 'move',
      supportedActions: 'copy-move',
      source: this,
      dragAdjustX: this._clickData.widgetX,
      dragAdjustY: this._clickData.widgetY
    });

    this._drag.mimeData.setData(DASHBOARD_WIDGET_MIME, this);

    document.removeEventListener('mousemove', this, true);
    document.removeEventListener('mouseup', this, true);

    return this._drag.start(clientX, clientY).then(() => {
      if (this.isDisposed) {
        return;
      }
      this.node.style.opacity = null;
      this.node.style.pointerEvents = 'auto';
      this._drag = null;
      this._clickData = null;
    });
  }

  /**
   * Handle `mouseUp` events for the widget.
   */
  private _evtMouseUp(event: MouseEvent): void {
    event.stopPropagation();
    event.preventDefault();

    this.node.style.opacity = null;

    if (this._mouseMode === 'resize' && this.parent !== undefined) {
      const pos = this.pos;
      (this.parent as Dashboard).updateWidget(this, pos);
    }

    this._mouseMode = 'none';
    (this.parent.layout as DashboardLayout).clearCanvas();
    window.removeEventListener('mouseup', this);
    window.removeEventListener('mousemove', this);
  }

  /**
   * The widget's position on its dashboard.
   *
   * ### Notes
   * When setting the widget pos, fields that you don't want to modify
   * can be set as `undefined`.
   */
  get pos(): WidgetPosition {
    return {
      left: parseInt(this.node.style.left, 10),
      top: parseInt(this.node.style.top, 10),
      width: parseInt(this.node.style.width, 10),
      height: parseInt(this.node.style.height, 10)
    };
  }
  set pos(newPos: WidgetPosition) {
    const style = this.node.style;
    for (const [key, value] of Object.entries(newPos)) {
      if (value !== undefined) {
        style.setProperty(key, `${value}px`);
      }
    }
  }

  /**
   * Information sufficient to reconstruct the widget.
   */
  get info(): Widgetstore.WidgetInfo {
    const pos = this.pos;
    return {
      pos,
      widgetId: this.id,
      cellId: this.cellId,
      notebookId: this.notebookId,
      removed: false
    };
  }

  /**
   * The id of the cell the widget is generated from.
   */
  get cellId(): string {
    return this._cellId || getCellId(this._cell);
  }

  /**
   * The id of the notebook the widget is generated from.
   */
  get notebookId(): string {
    return this._notebookId || getNotebookId(this._notebook);
  }

  /**
   * Whether to constrain widget dimensions to the underlying content.
   */
  get fitToContent(): boolean {
    return this._fitToContent;
  }
  set fitToContent(newState: boolean) {
    this._fitToContent = newState;
  }

  /**
   * The cell the widget is generated from.
   */
  get cell(): CodeCell | MarkdownCell {
    return this._cell;
  }

  /**
   * The notebook the widget is generated from.
   */
  get notebook(): NotebookPanel {
    return this._notebook;
  }

  /**
   * The index of the cell in the notebook.
   */
  get index(): number {
    return this._cell
      ? ArrayExt.findFirstIndex(
          this._notebook.content.widgets,
          c => c === this._cell
        )
      : this._index;
  }

  /**
   * The path of the notebook for the cloned output area.
   */
  get path(): string {
    return this._notebook.context.path;
  }

  /**
   * The widget's display mode.
   */
  get mode(): Dashboard.Mode {
    return this._mode;
  }
  set mode(newMode: Dashboard.Mode) {
    this._mode = newMode;
    if (newMode === 'present') {
      this.removeClass(EDITABLE_WIDGET_CLASS);
    } else {
      this.addClass(EDITABLE_WIDGET_CLASS);
    }
    if (newMode === 'grid-edit') {
      if (this.parent) {
        (this.parent as Dashboard).updateWidget(this, this.pos);
      }
    }
  }

  /**
   * A signal emitted once the widget's content is added.
   */
  get ready(): ISignal<this, void> {
    return this._ready;
  }

  /**
   * Whether the widget can be moved during a resize.
   */
  get locked(): boolean {
    return this._locked;
  }
  set locked(newState: boolean) {
    this._locked = newState;
  }

  /**
   * The content of the widget.
   */
  get content(): Widget {
    return this._content;
  }
  set content(newContent: Widget) {
    this._content.dispose();
    this._content = newContent;
  }

  private _notebook: NotebookPanel;
  private _notebookId: string;
  private _index: number;
  private _cell: CodeCell | MarkdownCell | null = null;
  private _cellId: string;
  private _ready = new Signal<this, void>(this);
  private _fitToContent = false;
  private _mouseMode: DashboardWidget.MouseMode = 'none';
  private _mode: Dashboard.Mode = 'grid-edit';
  private _drag: Drag | null = null;
  private _clickData: {
    pressX: number;
    pressY: number;
    origWidth: number;
    origHeight: number;
    origLeft: number;
    origTop: number;
    target: HTMLElement;
    cell: CodeCell | MarkdownCell;
    widgetX: number;
    widgetY: number;
  } | null = null;
  private _locked = false;
  private _content: Widget;
  private _selectedResizer: DashboardWidget.ResizerCorner;
}
Example #29
Source File: widget.ts    From jupyterlab-interactive-dashboard-editor with BSD 3-Clause "New" or "Revised" License 4 votes vote down vote up
constructor(options: DashboardWidget.IOptions) {
    super();

    const { notebook, cell, cellId, notebookId, fit } = options;

    this._notebook = notebook || null;
    this._cell = cell || null;
    this.id = DashboardWidget.createDashboardWidgetId();

    // Makes widget focusable.
    this.node.setAttribute('tabindex', '-1');

    const _cellId = getCellId(cell);
    const _notebookId = getNotebookId(notebook);

    if (_notebookId === undefined) {
      this.node.style.background = 'red';
      if (notebookId === undefined) {
        console.warn('DashboardWidget has no notebook or notebookId');
      }
      this._notebookId = notebookId;
    } else if (_cellId === undefined) {
      this.node.style.background = 'yellow';
      if (cellId === undefined) {
        console.warn('DashboardWidget has no cell or cellId');
      }
      this._cellId = cellId;
    } else {
      if (notebookId && _notebookId !== notebookId) {
        console.warn(`DashboardWidget notebookId ('${notebookId}') and id of
                      notebook ('${_notebookId}') don't match. 
                      Using ${_notebookId}.`);
      }
      if (cellId && _cellId !== cellId) {
        console.warn(`DashboardWidget cellId ('${cellId}') and id of cell
                      ('${_cellId}') don't match. Using ${_cellId}.`);
      }

      this._cellId = _cellId;
      this._notebookId = _notebookId;

      void this._notebook.context.ready.then(() => {
        let clone: Widget;
        const cellType = cell.model.type;
        const container = document.createElement('div');
        let cloneNode: HTMLElement;
        let nodes: HTMLCollectionOf<Element>;
        let node: HTMLElement;

        switch (cellType) {
          case 'markdown':
            cloneNode = (cell as MarkdownCell).clone().node;
            nodes = cloneNode.getElementsByClassName('jp-MarkdownOutput');
            node = nodes[0] as HTMLElement;
            node.style.paddingRight = '0';
            clone = new Widget({ node });
            container.classList.add(MARKDOWN_OUTPUT_CLASS);
            break;
          case 'code':
            clone = (cell as CodeCell).cloneOutputArea();
            break;
          default:
            throw new Error('Cell is not a code or markdown cell.');
        }

        // Make widget invisible until it's properly loaded/sized.
        this.node.style.opacity = '0';

        container.classList.add(DASHBOARD_WIDGET_CHILD_CLASS);

        // Fake an attach in order to render LaTeX properly.
        // Note: This is not how you should use Lumino widgets.
        if (this.parent) {
          if (this.parent!.isAttached) {
            MessageLoop.sendMessage(clone, Widget.Msg.BeforeAttach);
            container.appendChild(clone.node);
            this.node.appendChild(container);
            if (this.parent!.isAttached) {
              MessageLoop.sendMessage(clone, Widget.Msg.AfterAttach);
            }
          }
        }

        this._content = clone;

        const done = (): void => {
          if (fit) {
            this.fitContent();
          }
          // Make widget visible again.
          this.node.style.opacity = null;
          // Emit the ready signal.
          this._ready.emit(undefined);
        };

        // Wait a moment then fit content. This allows all components to load
        // and for their width/height to adjust before fitting.
        setTimeout(done.bind(this), 2);
      });
    }

    const resizerTopLeft = DashboardWidget.createResizer('top-left');
    const resizerTopRight = DashboardWidget.createResizer('top-right');
    const resizerBottomLeft = DashboardWidget.createResizer('bottom-left');
    const resizerBottomRight = DashboardWidget.createResizer('bottom-right');
    this.node.appendChild(resizerTopLeft);
    this.node.appendChild(resizerTopRight);
    this.node.appendChild(resizerBottomLeft);
    this.node.appendChild(resizerBottomRight);

    this.addClass(DASHBOARD_WIDGET_CLASS);
    this.addClass(EDITABLE_WIDGET_CLASS);
  }