tiptap-table

84 阅读6分钟

1.安装

npm install @tiptap/extension-table

使用

import { TableKit } from '@tiptap/extension-table'

const editor = useEditor({
    extensions: [
      TableKit.configure({
        table: { resizable: true },
      }),
    ],
  })

1.1 table 和 TableKit 关系

import { Editor } from '@tiptap/core'
import { Table, TableRow, TableCell, TableHeader,TableKit } from '@tiptap/extension-table'

new Editor({
  extensions: [
    Table,
    TableRow,
    TableCell,
    TableHeader,
    // 需要手动添加所有相关组件
  ],
})

new Editor({
  extensions: [
    TableKit, // 一个扩展包含所有功能
  ],
})

//Table  只提供基本的表格功能  需要手动配置其他组件
// Tabkit 包含所有表格相关功能   开箱即用,无需额外配置
组件作用使用场景
Table基础的表格节点定义表格的基本结构
TableKit完整的表格工具包包含所有表格相关功能

2. table/tableCell/tableHeader/tableRow

2.1 table

import { Table } from '@tiptap/extension-table'

table 一些方法

//修复表()
editor.commands.fixTables()
//设置单元格属性
editor.commands.setCellAttribute('customAttribute', 'value')
editor.commands.setCellAttribute('backgroundColor', '#000')

2.2. TableCell

import { TableCell } from '@tiptap/extension-table/cell'

2.3 TableHeader

  • 管理列的装饰器(decorations)
import { TableHeader } from '@tiptap/extension-table/header'

2.3.1 tableHeader 和 tableRow的关系

// 它们就像"特殊士兵""普通士兵"的关系
TableRow:      "普通士兵" - 可以包含普通单元格
TableHeader:   "特种士兵" - 特殊类型的单元格,用于表头

默认配置(包含表头)

// ✅ 默认:表行可以包含普通单元格 或 表头单元格
TableRow.extend({
  content: '(tableCell | tableHeader)*',  // 可以混合使用
})

// 这样配置后,表格可以这样结构:
<table>
  <tr>
    <th>姓名</th>    {/* 表头单元格 - 特殊样式 */}
    <th>年龄</th>
    <th>城市</th>
  </tr>
  <tr>
    <td>张三</td>    {/* 普通单元格 */}
    <td>25</td>
    <td>北京</td>
  </tr>
</table>

无表头配置

// ❌ 无表头:表行只能包含普通单元格
TableRow.extend({
  content: 'tableCell*',  // 只能使用普通单元格
})

// 这样配置后,表格结构:
<table>
  <tr>
    <td>姓名</td>    {/* 全是普通单元格 */}
    <td>年龄</td>
    <td>城市</td>
  </tr>
  <tr>
    <td>张三</td>
    <td>25</td>
    <td>北京</td>
  </tr>
</table>

2.4 TableRow

import { TableRow } from '@tiptap/extension-table/row'
组件比喻作用负责什么
Table整个学校包含所有班级的容器整个表格的容器和布局整个表格的"外壳”
TableRow一个班级包含所有学生的行每一行的样式和行为表格的"行”
TableCell一个学生座位具体的内容位置每个单元格的内容和样式表格的单元格
TableHeader班级表头显示"姓名"、"学号"等标题表格头部的特殊样式表格的头部
Table (最外层)
└── TableRow (行)
    └── TableCell (单元格)
        └── TableHeader (头部,可选)

3. TableMap 是什么?

TableMap 是 ProseMirror 提供的表格数据结构,它把表格抽象成一个二维矩阵:

3.1 参数

TableMap {
  width: 4,    // 4列
  height: 3,   // 3行
  map: [1, 5, 9, 13, 19, 23, 27, 31, 37, 41, 45, 49]
}

注意:每次加4 相当于 tiptap 固定的

关键理解点

1. 单元格索引 → 行列

const cellIndex = 9;
const row = Math.floor(9 / 4);  // 2
const col = 9 % 4;              // 1
// 结果:第2行第1列

2. 行列 → 单元格索引

const row = 2, col = 1;
const cellIndex = 2 * 4 + 1;    // 9
// 结果:单元格索引9

3. 获取单元格位置

const cellPos = tableStart + map[cellIndex];
表格: 4列 × 3行

列:  0   1   2   3
行:  ┌───┬───┬───┬───┐ 0
     │ 0 │ 1 │ 2 │ 3 │   map[0,1,2,3] = [1,5,9,13]
     ├───┼───┼───┼───┤ 1  
     │ 4 │ 5 │ 6 │ 7 │   map[4,5,6,7] = [19,23,27,31]
     ├───┼───┼───┼───┤ 2
     │ 8 │ 9 │10 │11 │   map[8,9,10,11] = [37,41,45,49]
     └───┴───┴───┴───┘
示例表格(3列 x 2行):
┌─────┬─────┬─────┐
│ A1  │ A2  │ A3  │  ← 第0行
├─────┼─────┼─────┤
│ B1  │ B2  │ B3  │  ← 第1行
└─────┴─────┴─────┘
  ↑     ↑     ↑
 列012

map.width = 3  // 列数
map.height = 2 // 行数
const { selection } = editor.state;
	// 获取表格的 TableMap 和选择的矩形区域
				const map = TableMap.get(selection.$anchorCell.node(-1));
				const start = selection.$anchorCell.start(-1);
				const rect = map.rectBetween(
					selection.$anchorCell.pos - start,
					selection.$headCell.pos - start,
				);
  1. selection.anchorCell 和 selection.anchorCell 和 selection.headCell
  • $anchorCell:选择的起点单元格(鼠标按下的位置)
  • $headCell:选择的终点单元格(鼠标释放的位置)
  • .node(-1) 获取表格节点(-1 表示向上查找父节点)
  • .start(-1) 获取表格节点的起始位置
  1. rectBetween

它计算从起点到终点之间的最小矩形区域,返回一个 Rect 对象:typescript

type Rect = {
  left: number;   // 左边界(列索引)
  right: number;  // 右边界(列索引 + 1)
  top: number;    // 上边界(行索引)
  bottom: number; // 下边界(行索引 + 1)
}

eg

012
   ┌─────┬─────┬─────┐
行0ABC  │
   ├─────┼─────┼─────┤
行1DEF  │
   ├─────┼─────┼─────┤
行2GHI  │
   └─────┴─────┴─────┘
   
   
   
   选择:BEF
rect = { left: 1, right: 3, top: 0, bottom: 2 }
     列12
   ┌─────┬─────┐
行0BC  │  ✓
   ├─────┼─────┤
行1EF  │  ✓
   └─────┴─────┘

4.Tiptap 扩展开发

Tiptap 扩展开发的两种主要方式:

  1. 使用 Node.create<Options>(...) 重新从头创建整个 Tiptap 节点。
  2. 使用 Extension.extend(...) 继承 Tiptap 官方的扩展,并在此基础上进行修改。
方面TableCell.extend() (推荐)Node.create() (不推荐)
稳定性 & 兼容性继承了官方扩展的所有内部逻辑、默认行为、依赖和修复。版本升级时更安全。需要手动复制所有核心配置(如 tableRolecontent、默认 parseHTML 等)。如果官方扩展内部有更新,您的代码可能会损坏。
代码量只需要编写你想要添加或覆盖的部分(如 addProseMirrorPlugins)。代码更简洁。必须重新编写所有内容,包括 Node 的基本结构定义、contenttableRole 等。代码冗余。
表格功能确保与 ProseMirror prosemirror-tables 库的内部交互(如合并单元格、列操作)是完全同步和正确的。容易遗漏 ProseMirror 表格所需的底层配置,可能导致合并/拆分等复杂操作出现不可预测的错误。
可维护性清晰地表明您只是在扩展一个标准功能。看起来像在重新实现一个标准功能。

4.1 (Node.create / Extension.create)

从零开始创造新积木 Tiptap 没有“警告框”节点,我们需要从零开始定义它:

import { Node, mergeAttributes } from "@tiptap/core";

// 1. 使用 Node.create 从零开始定义一个新节点
export const AlertNode = Node.create({
  name: "alert", // 节点名称,必须唯一

  // 2. 定义节点包含的内容(必须)
  content: "block+", 

  // 3. 定义如何从 HTML 解析(必须)
  parseHTML() {
    return [{ tag: "div[data-type='alert']" }];
  },

  // 4. 定义如何渲染成 HTML(必须)
  renderHTML({ HTMLAttributes }) {
    // 渲染成一个带有特定属性的 div
    return [
      "div",
      mergeAttributes(
        { "data-type": "alert", class: "bg-red-100 p-4 border-l-4 border-red-500" },
        HTMLAttributes
      ),
      0, // 0 表示内容应该插入到这里
    ];
  },

  // 5. 定义属性(可选)
  addAttributes() {
      return {
          type: {
              default: 'warning',
          }
      }
  }

  // ... 还需要定义 Commands, InputRules, Plugins 等等
});
  1. 乐高方式二:修改现有积木 (Extension.extend)

4.2. Extension.extend

修改现有积木

import { TableCell as TiptapTableCell } from "@tiptap/extension-table";
import { Plugin } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";

/**
 * 继承 Tiptap 官方的 TableCell 扩展
 * 目标:在保留所有原生表格功能的基础上,添加一个自定义的 ProseMirror 插件
 * 来绘制行选择器(Row Grip)。
 */
export const EnhancedTableCell = TiptapTableCell.extend({
  
  // 1. 覆盖 addProseMirrorPlugins 方法
  addProseMirrorPlugins() {
    return [
      // 2. 确保调用父级方法,以保留所有原生表格插件
      ...(this.parent?.() || []), 

      // 3. 添加我们自定义的插件(例如,绘制 Row Grip 的装饰器)
      new Plugin({
        props: {
          decorations: (state) => {
             // 仅在此处编写绘制 Row Grip 的逻辑
             const decorations: Decoration[] = [];
             // ... 复杂的 Row Grip 绘制代码 ...
             return DecorationSet.create(state.doc, decorations);
          },
        },
      }),
    ];
  },
  
  // 4. 只覆盖我们想修改的属性(例如,给 td 标签添加默认 class)
  addOptions() {
      return {
          ...this.parent?.(), // 继承父级所有选项
          HTMLAttributes: {
              class: 'custom-table-cell-style' // 增加自定义 class
          }
      }
  }
});

5. Decorations

  • 是 ProseMirror 的“视图层标注”,用来在不修改文档内容的前提下,给某些位置或节点附加样式/DOM 节点。

  • 作用场景:

    • 高亮、下划线、占位符、错误提示等“样式变化但不入库”的东西。
    • 浮动/绝对定位的交互控件(如表格列头 grip、行头 grip、添加列/行按钮)。
    • 选区辅助可视化(例如整列/整行被选中的描边)。

三种类型

5.1 三种类型

  • Decoration.inline(from, to, attrs)

    • 针对文本区间的内联标注,常用于语法高亮、拼写错误下划线。
  • Decoration.node(from, to, attrs)

    • 针对某个节点(块)的标注,改变该节点的 attrs(如 class、style、draggable)。
  • Decoration.widget(pos, domBuilder, options?)

    • 在某个文档位置插入“零宽”小部件(DOM 节点),不改变文档模型,最常用来放按钮、光标标记等。