Automatic table row numbers

Hey there! I’m currently attempting to add automatic table row numbers using a NodeView around my table content. I feel like I’m really close, but I’m struggling with the number-column row heights taking 1-2 transactions to get in sync. It feels like there’s some kind of race condition between when I measure the heights of the rows vs when the offsetHeights are reflected in the DOM. I’m guessing this is due to the parent NodeView update() function being called before its children do their thing. Does anyone have any ideas on how I might achieve this?

The next thing I’m going to try is adding another plugin that could maybe attempt to reconcile the heights outside of the table plugin. My lack of knowledge around the PM render lifecycle is biting me here.

import { Node as ProseMirrorNode } from '@tiptap/pm/model';
import { EditorView, NodeView } from '@tiptap/pm/view';

import { tableClass, tableWrapperClass } from './index';
import { updateColumns } from './updateColumns';

export class TableView implements NodeView {
  node: ProseMirrorNode;
  cellMinWidth: number;
  dom: Element;
  tableWrapperInner: Element;
  table: Element;
  rowNumberTable: HTMLDivElement;
  colgroup: Element;
  contentDOM: HTMLElement;

  constructor(node: ProseMirrorNode, cellMinWidth: number, view: EditorView) {
    this.node = node;
    this.cellMinWidth = cellMinWidth;
    this.dom = document.createElement('div');
    this.dom.className = 'table-wrapper-outer relative group mt-4 first:mt-0';
    this.dom.appendChild(document.createElement('div'));
    this.tableWrapperInner = this.dom.appendChild(
      document.createElement('div')
    );
    this.tableWrapperInner.className =
      'table-wrapper-inner relative flex flex-row' + ' ' + tableWrapperClass;

    this.rowNumberTable = this.tableWrapperInner.appendChild(
      document.createElement('table')
    );
    this.rowNumberTable.contentEditable = 'false';
    this.rowNumberTable.className = 'flex flex-col';

    this.table = this.tableWrapperInner.appendChild(
      document.createElement('table')
    );
    this.table.className = tableClass;
    this.colgroup = this.table.appendChild(document.createElement('colgroup'));
    updateColumns(node, this.colgroup, this.table, cellMinWidth);
    this.contentDOM = this.table.appendChild(document.createElement('tbody'));
    this.update(node);
  }

  update(node: ProseMirrorNode) {
    if (node.type !== this.node.type) {
      return false;
    }
    this.node = node;
    updateColumns(node, this.colgroup, this.table, this.cellMinWidth);
    this.updateRowNumbers();
    return true;
  }

  updateRowNumbers() {
    // Remove all existing row numbers
    while (this.rowNumberTable.firstChild) {
      this.rowNumberTable.firstChild.remove();
    }

    let rows = this.contentDOM.querySelectorAll('tr');

    Array.from(rows).map((r, index) => {
      let rowNumberRow = this.rowNumberTable.insertRow();
      let cell = rowNumberRow.appendChild(document.createElement('div'));
      cell.textContent = (index + 1).toString();
      cell.className =
        'row-number w-8 flex justify-center items-center opacity-50 border';
      cell.style.height = r.offsetHeight + 1 + 'px';
    });
  }
}

2023-05-31 20.22.17

1 Like