remirror/remirror

View on GitHub
packages/remirror__extension-react-tables/src/views/table-view.tsx

Summary

Maintainability
A
0 mins
Test Coverage
F
0%
import { EditorView, NodeView, range, throttle, Transaction } from '@remirror/core';
import { Node as ProsemirrorNode } from '@remirror/pm/model';
import { TableMap, updateColumnsOnResize } from '@remirror/pm/tables';
import { Decoration } from '@remirror/pm/view';
import { ExtensionTablesTheme } from '@remirror/theme';

import TableInsertButton, { shouldHideInsertButton } from '../components/table-insert-button';
import { ReactTableNodeAttrs } from '../table-extensions';
import { h } from '../utils/dom';
import { setNodeAttrs } from '../utils/prosemirror';

export class TableView implements NodeView {
  readonly root: HTMLElement;
  readonly table: HTMLTableElement;
  readonly colgroup: HTMLTableColElement;
  readonly tbody: HTMLTableSectionElement;
  readonly insertButtonWrapper: HTMLElement;

  private readonly handleMouseMove: (e: MouseEvent) => void;
  private showInsertButton: boolean;
  private removeInsertButton?: (tr: Transaction) => Transaction;

  map: TableMap;

  get dom(): HTMLElement {
    return this.root;
  }

  get contentDOM(): HTMLElement {
    return this.tbody;
  }

  constructor(
    public node: ProsemirrorNode,
    public cellMinWidth: number,
    public decorations: readonly Decoration[],
    public view: EditorView,
    public getPos: () => number,
  ) {
    // console.debug('[TableView] constructor');

    this.map = TableMap.get(this.node);

    this.tbody = h('tbody', { className: ExtensionTablesTheme.TABLE_TBODY_WITH_CONTROLLERS });
    this.colgroup = h(
      'colgroup',
      { className: ExtensionTablesTheme.TABLE_COLGROUP },
      ...range(this.map.width).map(() => h('col')),
    );
    this.table = h(
      'table',
      {
        className: ExtensionTablesTheme.TABLE,
        dataset: { controllersInjected: '' },
      },
      this.colgroup,
      this.tbody,
    );
    this.insertButtonWrapper = h('div');
    this.root = h('div', null, this.table, this.insertButtonWrapper);

    this.render();

    this.showInsertButton = false;
    this.handleMouseMove = throttle(100, (e: MouseEvent) => {
      if (this.showInsertButton) {
        const attrs = this.attrs().insertButtonAttrs;

        if (attrs && shouldHideInsertButton(attrs, e)) {
          this.showInsertButton = false;
          replaceChildren(this.insertButtonWrapper, []);

          if (this.removeInsertButton) {
            this.view.dispatch(this.removeInsertButton(this.view.state.tr));
          }
        }
      }
    });

    document.addEventListener('mousemove', this.handleMouseMove);
  }

  update(node: ProsemirrorNode, decorations: readonly Decoration[]): boolean {
    // console.debug(`[TableView] update`);

    if (node.type !== this.node.type) {
      return false;
    }

    this.decorations = decorations;
    this.node = node;
    this.map = TableMap.get(this.node);

    this.render();

    return true;
  }

  private render() {
    this.renderTable();

    if (!this.attrs().isControllersInjected) {
      return;
    }

    this.renderInsertButton();
  }

  private renderTable() {
    if (this.colgroup.children.length !== this.map.width) {
      const cols = range(this.map.width).map(() => h('col'));
      replaceChildren(this.colgroup, cols);
    }

    const className = [
      ExtensionTablesTheme.TABLE,

      // Hide the table before controllers injected
      this.attrs().isControllersInjected
        ? ExtensionTablesTheme.TABLE_WITH_CONTROLLERS
        : ExtensionTablesTheme.TABLE_WAITTING_CONTROLLERS,
    ].join(' ');

    if (this.table.className !== className) {
      this.table.className = className;
    }

    updateColumnsOnResize(this.node, this.colgroup, this.table, this.cellMinWidth);
  }

  private renderInsertButton() {
    const attrs = this.attrs().insertButtonAttrs;

    if (attrs) {
      const tableRect = {
        map: this.map,
        table: this.node,
        tableStart: this.getPos() + 1,

        // The following properties are not actually used
        left: 0,
        top: 0,
        right: 0,
        bottom: 0,
      };
      this.removeInsertButton = (tr: Transaction): Transaction => {
        // Remove insertButtonAttrs from tableNode so that the TableInsertButton won't keep at the origin position.
        const attrsPatch: Partial<ReactTableNodeAttrs> = { insertButtonAttrs: null };
        return setNodeAttrs(tr, tableRect.tableStart - 1, attrsPatch);
      };
      const button = TableInsertButton({
        view: this.view,
        attrs,
        tableRect,
        removeInsertButton: this.removeInsertButton,
      });
      replaceChildren(this.insertButtonWrapper, [button]);
      this.showInsertButton = true;
    } else {
      replaceChildren(this.insertButtonWrapper, []);
      this.showInsertButton = false;
    }
  }

  private attrs() {
    return this.node.attrs as ReactTableNodeAttrs;
  }

  ignoreMutation(): boolean {
    return true;
  }

  destroy(): void {
    // console.debug('[TableView] destroy');

    document.removeEventListener('mousemove', this.handleMouseMove);
  }
}

// TODO: this function's performance should be very bad. Maybe we should use some kind of DOM-diff algorithm.
export function replaceChildren(parent: HTMLElement, children: HTMLElement[]): void {
  while (parent.firstChild) {
    parent.firstChild.remove();
  }

  for (const child of children) {
    parent.append(child);
  }
}