blokhaus

TablePlugin

Table creation and management.

Import

import { TablePlugin } from "@blokhaus/core";

Overview

The TablePlugin provides table creation, cell editing, row/column management, and optional cell merging. It wraps Lexical's @lexical/table module and registers the INSERT_TABLE_COMMAND_BLOKHAUS command for creating tables with a specified number of rows and columns. The plugin handles keyboard navigation between cells using Tab and arrow keys.

Props

PropTypeDefaultDescription
hasCellMergebooleantrueEnable cell merge/unmerge functionality. Set to false for simpler table editing.
hasTabHandlerbooleantrueEnable Tab key navigation between cells. When true, Tab moves to the next cell and Shift+Tab moves to the previous cell.

Usage

Basic setup

app/editor/page.tsx
"use client";

import {
  EditorRoot,
  TablePlugin,
  InputRulePlugin,
  SlashMenu,
} from "@blokhaus/core";

export default function EditorPage() {
  return (
    <EditorRoot namespace="my-editor">
      <InputRulePlugin />
      <SlashMenu />
      <TablePlugin />
    </EditorRoot>
  );
}

Without cell merge

<TablePlugin hasCellMerge={false} hasTabHandler={true} />

Registered commands

CommandPayloadDescription
INSERT_TABLE_COMMAND_BLOKHAUS{ rows: number; columns: number }Inserts a new table at the current cursor position with the specified dimensions.

Creating a table programmatically

import { INSERT_TABLE_COMMAND_BLOKHAUS } from "@blokhaus/core";

// Insert a 3x4 table (3 rows, 4 columns)
editor.dispatchCommand(INSERT_TABLE_COMMAND_BLOKHAUS, {
  rows: 3,
  columns: 4,
});

Keyboard navigation

KeyAction
TabMove to the next cell. If at the last cell, create a new row.
Shift+TabMove to the previous cell.
Arrow keysMove to the adjacent cell in the corresponding direction.
EnterInsert a new line within the current cell.
Backspace (empty cell at 1,1)Delete the entire table if all cells are empty.
DeleteDelete selected content within the cell.

Cell merge and unmerge

When hasCellMerge is true, cells can be merged and unmerged:

import {
  TABLE_MERGE_CELLS_COMMAND,
  TABLE_UNMERGE_CELL_COMMAND,
} from "@blokhaus/core";

// Merge selected cells
editor.dispatchCommand(TABLE_MERGE_CELLS_COMMAND, undefined);

// Unmerge the current merged cell
editor.dispatchCommand(TABLE_UNMERGE_CELL_COMMAND, undefined);

Merged cells store their row and column span in the node data:

{
  "type": "tablecell",
  "colSpan": 2,
  "rowSpan": 1,
  "children": [{ "type": "paragraph", "children": [] }]
}

Row and column operations

The plugin provides context menu actions (via right-click on a table cell) for common operations:

ActionDescription
Insert row aboveAdd a new row above the current row
Insert row belowAdd a new row below the current row
Insert column leftAdd a new column to the left of the current column
Insert column rightAdd a new column to the right of the current column
Delete rowRemove the current row
Delete columnRemove the current column
Delete tableRemove the entire table

These actions are also available programmatically via Lexical's table utility functions.

Slash menu integration

When TablePlugin is mounted, a "Table" item appears in the slash menu. Selecting it opens a grid picker where the user can drag to select the desired number of rows and columns (similar to the table insertion UI in Google Docs and Notion).

Table structure in JSON

A table is represented in the Lexical JSON state as nested nodes:

{
  "type": "table",
  "children": [
    {
      "type": "tablerow",
      "children": [
        {
          "type": "tablecell",
          "headerState": 1,
          "colSpan": 1,
          "rowSpan": 1,
          "children": [
            {
              "type": "paragraph",
              "children": [{ "type": "text", "text": "Header 1" }]
            }
          ]
        }
      ]
    }
  ]
}

The TablePlugin automatically registers TableNode, TableRowNode, and TableCellNode with Lexical. You do not need to add them to ALL_NODES manually.

When hasTabHandler is true, the Tab key is captured by the table for cell navigation. This means Tab will not trigger indentation while the cursor is inside a table. Set hasTabHandler to false if you need Tab to pass through to other handlers.