CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-tiptap--pm

Comprehensive wrapper around ProseMirror packages providing unified entry point for rich text editing functionality in Tiptap framework

Pending
Overview
Eval results
Files

tables.mddocs/

Tables

The tables system provides comprehensive support for table editing with advanced features like cell selection, column resizing, and table manipulation commands. It handles complex table operations while maintaining document consistency.

Capabilities

Table Structure Classes

Core classes for table representation and manipulation.

/**
 * Maps table structure for efficient navigation and manipulation
 */
class TableMap {
  /**
   * Create table map from a table node
   */
  static get(table: Node): TableMap;
  
  /**
   * Width of the table (number of columns)
   */
  width: number;
  
  /**
   * Height of the table (number of rows)
   */
  height: number;
  
  /**
   * Find the cell at given coordinates
   */
  findCell(pos: ResolvedPos): Rect;
  
  /**
   * Get position of cell at coordinates
   */
  positionAt(row: number, col: number, table: Node): number;
}

/**
 * Represents table cell selection
 */
class CellSelection extends Selection {
  /**
   * Create cell selection between two cells
   */
  static create(doc: Node, anchorCell: number, headCell?: number): CellSelection;
  
  /**
   * Selected rectangle coordinates
   */
  $anchorCell: ResolvedPos;
  $headCell: ResolvedPos;
  
  /**
   * Check if selection is a single cell
   */
  isColSelection(): boolean;
  isRowSelection(): boolean;
}

/**
 * Table resize state management
 */
class ResizeState {
  constructor(
    activeHandle: number,
    dragging: boolean,
    startX: number,
    startWidth: number
  );
  
  /**
   * Apply resize to the table
   */
  apply(tr: Transaction): Transaction;
}

Table Node Definitions

Schema definitions for table structures.

/**
 * Get table node specifications
 */
function tableNodes(options?: TableNodesOptions): {
  table: NodeSpec;
  table_row: NodeSpec; 
  table_cell: NodeSpec;
  table_header: NodeSpec;
};

/**
 * Get node types from schema for table operations
 */
function tableNodeTypes(schema: Schema): {
  table: NodeType;
  table_row: NodeType;
  table_cell: NodeType; 
  table_header: NodeType;
};

Table Plugins

Plugins for table functionality.

/**
 * Create table editing plugin
 */
function tableEditing(): Plugin;

/**
 * Create column resizing plugin
 */
function columnResizing(options?: ColumnResizingOptions): Plugin;

Table Manipulation Commands

Commands for modifying table structure.

/**
 * Add a column after the current selection
 */
function addColumnAfter(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Add a column before the current selection
 */
function addColumnBefore(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Add a row after the current selection
 */
function addRowAfter(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Add a row before the current selection
 */
function addRowBefore(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Delete the selected column
 */
function deleteColumn(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Delete the selected row
 */
function deleteRow(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Delete the entire table
 */
function deleteTable(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Merge selected cells
 */
function mergeCells(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Split the current cell
 */
function splitCell(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Split cell with specific cell type
 */
function splitCellWithType(getCellType: (schema: Schema) => NodeType): Command;

Table Navigation Commands

Commands for moving within tables.

/**
 * Move to the next cell
 */
function goToNextCell(direction: number): Command;

/**
 * Move cell selection forward
 */
function moveCellForward(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Select the next cell
 */
function nextCell(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

Header Toggle Commands

Commands for toggling header states.

/**
 * Toggle header state of selection
 */
function toggleHeader(type: "column" | "row" | "cell"): Command;

/**
 * Toggle header state of selected cells
 */
function toggleHeaderCell(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Toggle header state of selected column
 */
function toggleHeaderColumn(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

/**
 * Toggle header state of selected row
 */
function toggleHeaderRow(state: EditorState, dispatch?: (tr: Transaction) => void): boolean;

Table Query Functions

Utility functions for table inspection.

/**
 * Find cell around the given position
 */
function cellAround(pos: ResolvedPos): ResolvedPos | null;

/**
 * Find cell near the given position
 */
function cellNear(pos: ResolvedPos): ResolvedPos | null;

/**
 * Find cell at the given position
 */
function findCell(pos: ResolvedPos): Rect | null;

/**
 * Get number of columns in table
 */
function colCount(pos: ResolvedPos): number;

/**
 * Check if row is a header row
 */
function rowIsHeader(map: TableMap, table: Node, row: number): boolean;

/**
 * Check if column is a header column
 */
function columnIsHeader(map: TableMap, table: Node, col: number): boolean;

/**
 * Check if position is inside a table
 */
function isInTable(state: EditorState): boolean;

/**
 * Check if two positions are in the same table
 */
function inSameTable(a: ResolvedPos, b: ResolvedPos): boolean;

/**
 * Get selected rectangle in table
 */
function selectedRect(state: EditorState): Rect;

/**
 * Check if position points directly at a cell
 */
function pointsAtCell(pos: ResolvedPos): boolean;

Usage Examples:

import {
  tableNodes,
  tableEditing,
  columnResizing,
  addColumnAfter,
  addRowAfter,
  deleteColumn,
  deleteRow,
  mergeCells,
  splitCell,
  toggleHeaderRow,
  CellSelection
} from "@tiptap/pm/tables";
import { keymap } from "@tiptap/pm/keymap";

// Create schema with table nodes
const nodes = {
  ...baseNodes,
  ...tableNodes({
    cellContent: "block+",
    cellAttributes: {
      background: { default: null }
    }
  })
};

const schema = new Schema({ nodes, marks });

// Table plugins
const tablePlugins = [
  tableEditing(),
  columnResizing({
    handleWidth: 5,
    cellMinWidth: 50,
    lastColumnResizable: true
  })
];

// Table keymap
const tableKeymap = keymap({
  "Tab": goToNextCell(1),
  "Shift-Tab": goToNextCell(-1),
  "Mod-Shift-\\": addColumnAfter,
  "Mod-Shift-|": addRowAfter,
  "Mod-Shift-Backspace": deleteColumn,
  "Mod-Alt-Backspace": deleteRow,
  "Mod-Shift-m": mergeCells,
  "Mod-Shift-s": splitCell,
  "Mod-Shift-h": toggleHeaderRow
});

// Create editor with table support
const state = EditorState.create({
  schema,
  plugins: [...tablePlugins, tableKeymap]
});

// Create a simple table
function insertTable(rows: number, cols: number) {
  return (state: EditorState, dispatch?: (tr: Transaction) => void) => {
    const { table, table_row, table_cell } = schema.nodes;
    
    const cells = [];
    for (let i = 0; i < cols; i++) {
      cells.push(table_cell.createAndFill());
    }
    
    const tableRows = [];
    for (let i = 0; i < rows; i++) {
      tableRows.push(table_row.create(null, cells));
    }
    
    const tableNode = table.create(null, tableRows);
    
    if (dispatch) {
      dispatch(state.tr.replaceSelectionWith(tableNode));
    }
    
    return true;
  };
}

// Table manipulation helper
class TableEditor {
  constructor(private view: EditorView) {}
  
  insertTable(rows: number = 3, cols: number = 3) {
    const command = insertTable(rows, cols);
    command(this.view.state, this.view.dispatch);
  }
  
  addColumn(after: boolean = true) {
    const command = after ? addColumnAfter : addColumnBefore;
    command(this.view.state, this.view.dispatch);
  }
  
  addRow(after: boolean = true) {
    const command = after ? addRowAfter : addRowBefore;
    command(this.view.state, this.view.dispatch);
  }
  
  deleteColumn() {
    deleteColumn(this.view.state, this.view.dispatch);
  }
  
  deleteRow() {
    deleteRow(this.view.state, this.view.dispatch);
  }
  
  mergeCells() {
    mergeCells(this.view.state, this.view.dispatch);
  }
  
  splitCell() {
    splitCell(this.view.state, this.view.dispatch);
  }
  
  selectColumn(col: number) {
    const table = this.findTable();
    if (table) {
      const map = TableMap.get(table.node);
      const anchor = table.start + map.positionAt(0, col, table.node) + 1;
      const head = table.start + map.positionAt(map.height - 1, col, table.node) + 1;
      
      const selection = CellSelection.create(this.view.state.doc, anchor, head);
      this.view.dispatch(this.view.state.tr.setSelection(selection));
    }
  }
  
  private findTable() {
    const { $from } = this.view.state.selection;
    for (let d = $from.depth; d > 0; d--) {
      const node = $from.node(d);
      if (node.type.name === "table") {
        return { node, start: $from.start(d) };
      }
    }
    return null;
  }
}

Advanced Table Features

Custom Table Rendering

Create custom table views with enhanced functionality.

import { TableView } from "@tiptap/pm/tables";

class CustomTableView extends TableView {
  constructor(node: Node, cellMinWidth: number) {
    super(node, cellMinWidth);
    this.addCustomFeatures();
  }
  
  private addCustomFeatures() {
    // Add row numbers
    this.addRowNumbers();
    
    // Add column letters
    this.addColumnHeaders();
    
    // Add context menu
    this.addContextMenu();
  }
  
  private addRowNumbers() {
    const rows = this.table.querySelectorAll("tr");
    rows.forEach((row, index) => {
      const numberCell = document.createElement("td");
      numberCell.className = "row-number";
      numberCell.textContent = String(index + 1);
      row.insertBefore(numberCell, row.firstChild);
    });
  }
  
  private addColumnHeaders() {
    const firstRow = this.table.querySelector("tr");
    if (firstRow) {
      const cellCount = firstRow.children.length;
      const headerRow = document.createElement("tr");
      headerRow.className = "column-headers";
      
      for (let i = 0; i < cellCount; i++) {
        const headerCell = document.createElement("th");
        headerCell.textContent = String.fromCharCode(65 + i); // A, B, C...
        headerRow.appendChild(headerCell);
      }
      
      this.table.insertBefore(headerRow, this.table.firstChild);
    }
  }
  
  private addContextMenu() {
    this.table.addEventListener("contextmenu", (event) => {
      event.preventDefault();
      this.showContextMenu(event.clientX, event.clientY);
    });
  }
  
  private showContextMenu(x: number, y: number) {
    const menu = document.createElement("div");
    menu.className = "table-context-menu";
    menu.style.position = "fixed";
    menu.style.left = x + "px";
    menu.style.top = y + "px";
    
    const menuItems = [
      { label: "Add Column After", action: () => addColumnAfter },
      { label: "Add Row After", action: () => addRowAfter },
      { label: "Delete Column", action: () => deleteColumn },
      { label: "Delete Row", action: () => deleteRow },
      { label: "Merge Cells", action: () => mergeCells }
    ];
    
    menuItems.forEach(item => {
      const menuItem = document.createElement("div");
      menuItem.textContent = item.label;
      menuItem.onclick = () => {
        item.action()(this.view.state, this.view.dispatch);
        menu.remove();
      };
      menu.appendChild(menuItem);
    });
    
    document.body.appendChild(menu);
    
    // Remove menu on outside click
    setTimeout(() => {
      document.addEventListener("click", () => menu.remove(), { once: true });
    });
  }
}

Table Import/Export

Handle table data conversion for different formats.

class TableConverter {
  // Import from CSV
  static fromCSV(csv: string, schema: Schema): Node {
    const { table, table_row, table_cell } = schema.nodes;
    const rows = csv.split("\n").filter(row => row.trim());
    
    const tableRows = rows.map(rowText => {
      const cells = rowText.split(",").map(cellText => {
        const content = cellText.trim();
        return table_cell.create(null, content ? schema.text(content) : null);
      });
      return table_row.create(null, cells);
    });
    
    return table.create(null, tableRows);
  }
  
  // Export to CSV
  static toCSV(tableNode: Node): string {
    const rows: string[] = [];
    
    tableNode.forEach(row => {
      const cellTexts: string[] = [];
      row.forEach(cell => {
        cellTexts.push(cell.textContent.replace(/,/g, '\\,'));
      });
      rows.push(cellTexts.join(","));
    });
    
    return rows.join("\n");
  }
  
  // Import from HTML table
  static fromHTML(html: string, schema: Schema): Node {
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, "text/html");
    const htmlTable = doc.querySelector("table");
    
    if (!htmlTable) throw new Error("No table found in HTML");
    
    const domParser = DOMParser.fromSchema(schema);
    return domParser.parse(htmlTable);
  }
  
  // Export to HTML
  static toHTML(tableNode: Node, schema: Schema): string {
    const serializer = DOMSerializer.fromSchema(schema);
    const dom = serializer.serializeNode(tableNode);
    return dom.outerHTML;
  }
}

Table Analytics

Track and analyze table usage patterns.

class TableAnalytics {
  private metrics = {
    cellCount: 0,
    rowCount: 0,
    columnCount: 0,
    mergedCells: 0,
    headerCells: 0
  };
  
  analyzeTable(tableNode: Node): TableMetrics {
    const map = TableMap.get(tableNode);
    
    this.metrics = {
      cellCount: 0,
      rowCount: map.height,
      columnCount: map.width,
      mergedCells: 0,
      headerCells: 0
    };
    
    tableNode.descendants((node, pos) => {
      if (node.type.name === "table_cell" || node.type.name === "table_header") {
        this.metrics.cellCount++;
        
        if (node.type.name === "table_header") {
          this.metrics.headerCells++;
        }
        
        const colspan = node.attrs.colspan || 1;
        const rowspan = node.attrs.rowspan || 1;
        if (colspan > 1 || rowspan > 1) {
          this.metrics.mergedCells++;
        }
      }
    });
    
    return { ...this.metrics };
  }
  
  getComplexityScore(tableNode: Node): number {
    const metrics = this.analyzeTable(tableNode);
    let score = 0;
    
    // Base complexity from size
    score += metrics.rowCount * metrics.columnCount * 0.1;
    
    // Penalty for merged cells
    score += metrics.mergedCells * 2;
    
    // Bonus for proper headers
    if (metrics.headerCells > 0) {
      score += 1;
    }
    
    return Math.round(score * 10) / 10;
  }
}

interface TableMetrics {
  cellCount: number;
  rowCount: number;
  columnCount: number;
  mergedCells: number;
  headerCells: number;
}

Types

/**
 * Table node options
 */
interface TableNodesOptions {
  cellContent?: string;
  cellAttributes?: { [key: string]: AttributeSpec };
}

/**
 * Column resizing options
 */
interface ColumnResizingOptions {
  handleWidth?: number;
  cellMinWidth?: number;
  lastColumnResizable?: boolean;
}

/**
 * Rectangle representing table selection
 */
interface Rect {
  left: number;
  right: number;
  top: number;
  bottom: number;
}

/**
 * Table cell position information
 */
interface CellInfo {
  pos: number;
  start: number;
  node: Node;
}

Install with Tessl CLI

npx tessl i tessl/npm-tiptap--pm

docs

collaboration.md

commands-and-editing.md

cursors-and-enhancements.md

history.md

index.md

input-and-keymaps.md

markdown.md

menus-and-ui.md

model-and-schema.md

schema-definitions.md

state-management.md

tables.md

transformations.md

view-and-rendering.md

tile.json