高级用法

Paid add-on

Tracked Changes 扩展的高级行为和集成模式。

建议分组

当用户连续输入或连续删除多个字符时,这些编辑会被分组成一个单一的建议,而不是针对每个字符创建一个建议。分组发生在:

  1. 新的编辑与现有建议是相邻的(紧接在前面或后面)
  2. 现有建议是由相同用户创建的
  3. 现有建议具有相同类型(都是插入或都是删除)
  4. 距离上一次编辑的时间在分组超时时间窗口内(默认:2000 毫秒)

当编辑被分组时,建议的时间戳会被更新以保持分组窗口活跃状态。这意味着连续输入会持续扩展同一建议,直到用户暂停的时间超过超时设置。

// 完全禁止分组
TrackedChanges.configure({
  suggestionGroupingTimeout: 0,
})

// 延长分组窗口(5 秒)
TrackedChanges.configure({
  suggestionGroupingTimeout: 5000,
})

删除合并

当用户删除的内容与现有的删除建议重叠或相邻时,这些标记会自动合并,防止嵌套或重叠的删除。合并后的建议会使用范围内最早创建的原有删除标记的元数据(ID、用户、时间戳)。

这样保证了:

  • 文档永远不会包含嵌套的删除标记
  • 相关删除在视觉上分组显示
  • 保留最初删除操作的原作者信息

混合内容处理

当删除或替换包含原始文本和之前建议插入内容混合的文本时:

  • 建议插入(带有 add 标记的文本)会立即从文档中移除
  • 原始内容(不带 add 标记的文本)会被标记为 delete 建议

这种行为符合用户预期:删除仅为建议的文本(尚未被接受的)时,文本直接消失。原始文档内容的删除需要明确的接受操作。

标记追踪

Tracked Changes 还可以将现有文本上的标记级编辑追踪为 markChange 建议。这包括简单的格式标记,如 bolditalic,以及带有属性的标记,如 linkhighlight

标记追踪的行为如下:

  • 在现有文本上添加或移除标记会创建 markChange 建议
  • 应用于新插入内容的格式保留为插入建议的一部分,而不是创建第二个标记建议
  • 标记变更建议存储更改的标记名称和属性,以便在接受和拒绝期间正确恢复或移除带属性的标记

标记追踪的工作原理

在现有文本上,追踪的标记变更记录实际提出的标记变更:

  • 添加标记会创建带有 added 操作的 markChange 建议
  • 移除标记会创建带有 removed 操作的 markChange 建议

例如,如果用户对单词应用 bold,建议会存储 bold 已被添加。如果用户从已格式化的文本中移除 bold,建议会存储 bold 已被移除。

带属性的标记工作方式相同,但属性是追踪变更的一部分。这对于 link 等标记很重要,因为确切的属性标识了正在更改的标记变体:

// 建议添加特定链接
editor.commands.addTrackedMark({
  from: 7,
  to: 12,
  markName: 'link',
  markAttrs: {
    href: 'https://tiptap.dev',
    target: '_blank',
  },
})

在这种情况下,建议存储标记名称(link)和标记属性。接受或拒绝建议后可以保留、恢复或移除确切的链接标记,而不是将所有链接视为等同。

如果用户应用了一个标记,然后在同一文本上撤销了相同的标记更改,追踪的标记更改可以相互抵消,而不是留下冗余的建议。这有助于在审查期间保持嵌套和重叠的标记建议稳定。

默认情况下,Tracked Changes 忽略 suggestion 标记本身和内联评论/线程标记,因此这些内部标记不会创建递归的追踪建议。你也可以选择退出追踪额外的标记:

TrackedChanges.configure({
  ignoredTrackingMarks: ['highlight'],
})

当你的应用程序具有装饰性或特定于集成的标记且绝不应出现在追踪变更中时,使用此选项。

表格支持

该扩展会追踪表格内的结构性变更,包括行、单元格和列操作。

单元格选择删除 — 当用户通过 CellSelection 删除单元格时(即选中多个单元格后按删除键),每个被选中的单元格都会提升为节点级删除建议,而不是立即被移除。所选中的所有单元格共享相同的 suggestionId,因此它们会在建议列表中显示为一个分组变更。

列删除 — 删除某一列时,会生成一个覆盖该列所有单元格的单个分组建议,而不是每个单元格各生成一个建议。

接受和拒绝 — 接受或拒绝表格建议时,会正确处理底层的表格节点结构,防止在操作涉及列节点时出错。

要启用表格追踪,请将表格扩展与 Tracked Changes 一起引入:

import { Table } from '@tiptap/extension-table'
import { TableRow } from '@tiptap/extension-table-row'
import { TableCell } from '@tiptap/extension-table-cell'
import { TableHeader } from '@tiptap/extension-table-header'
import { TrackedChanges } from '@tiptap-pro/extension-tracked-changes'

const editor = new Editor({
  extensions: [
    Table.configure({ resizable: true }),
    TableRow,
    TableCell,
    TableHeader,
    TrackedChanges.configure({ enabled: true, userId: 'user-123' }),
  ],
})

无需额外配置——一旦两个扩展都处于激活状态,表格变更就会自动被追踪。

原子节点追踪

扩展会自动追踪对原子节点(如图片、水平线和其他非文本内容)的更改。这些节点不能拥有标记,因此扩展使用节点属性(suggestionIdsuggestionTypesuggestionUserIdsuggestionCreatedAtsuggestionUserMetadata)代替。这一过程是透明的——原子节点会在相同的建议查询中出现,并且可以像其他建议一样被接受或拒绝。

协作兼容性

该扩展设计兼容基于 Yjs 的协作。它会自动忽略:

  • 来自 Yjs 的远程同步事务(带有 y-sync$ 元数据)
  • 历史事务(撤销/重做由 ProseMirror 本地处理)
  • addToHistory: false 的事务(通常为远程变更)

意味着仅跟踪本地用户的编辑作为建议。

评论集成

Tracked Changes 扩展与 评论 扩展集成,支持将建议和评论线程一起处理的复审流程。该集成基于两个扩展提供的事件系统构建。

当两个扩展同时激活时,评论线程会自动通过线程元数据与建议关联,实现双向同步:

  • 解决与建议关联的评论线程会自动接受该建议
  • 删除与建议关联的评论线程会自动拒绝该建议
  • 接受建议会自动解决其关联的评论线程
  • 拒绝建议会自动删除其关联的评论线程

当建议内容发生变化(例如用户在跟踪插入中继续输入)时,关联线程的元数据会更新以保持同步。

这意味着用户可以通过建议 UI 或评论面板审阅变更,两者保持一致。

代码块兼容性

默认情况下,Tiptap 代码块不允许使用标记,这阻碍了建议标记在代码内容内部的应用。该扩展导出辅助函数来解决此问题:

import { CodeBlock } from '@tiptap/extension-code-block'
import { TrackedChanges, withSuggestionMarkOnCodeBlock } from '@tiptap-pro/extension-tracked-changes'
import StarterKit from '@tiptap/starter-kit'

const TrackableCodeBlock = withSuggestionMarkOnCodeBlock(CodeBlock)

const editor = new Editor({
  extensions: [
    StarterKit.configure({
      codeBlock: false, // 禁用默认代码块
    }),
    TrackableCodeBlock, // 使用修补后的版本
    TrackedChanges.configure({ enabled: true, userId: 'user-123' }),
  ],
})

这同样适用于 CodeBlockLowlight 或其他代码块扩展。