高级用法

Paid add-on

跟踪更改扩展的高级行为和集成模式。

建议分组

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

  1. 新编辑与现有建议相邻(紧挨其前或其后)
  2. 现有建议由同一用户创建
  3. 现有建议是相同类型(都是插入或都是删除)

满足这三个条件的连续编辑会合并为一个单一建议。

删除合并

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

这样保证了:

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

混合内容处理

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

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

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

块拆分

扩展使用 splitKind 区分两种块拆分:

拆分类型发生时机接受拒绝
insertion在块末尾按 Enter保留新块移除新块
structural在已有内容中间按 Enter保留两个块再次合并两个块

两种拆分的 suggestion.type 都是 'split'。建议面板会使用受影响的 nodeType 生成标签,例如末尾拆分显示 new paragraph,中间拆分显示 split heading。

可以在修订记录演示中尝试这些场景。

末尾拆分是插入拆分

在段落末尾按 Enter 时,扩展会创建一个带有 split 建议的新段落:

<p>First paragraph.</p>
<p data-suggestion data-suggestion-type="blockSplit">Second paragraph.</p>

返回的 findSuggestions() 建议如下:

{
  type: 'split',
  splitKind: 'insertion',
  nodeType: 'paragraph',
  text: 'Second paragraph.',
  fullText: 'Second paragraph.',
}

拆分后输入的文本会保留在同一个插入拆分中,建议文本会随着新块变化。如果再次在该块末尾按 Enter,第二次拆分会获得独立的建议和 ID。

在标题或列表项末尾拆分时规则相同。对于列表项,新的 <li> 就是插入的块。拆分标记会出现在它之前的段落末尾,也就是前一个列表项内。

中间拆分是结构拆分

在已有内容中按 Enter 时,扩展会跟踪新的边界,但不会将第二个块视为新插入的内容。例如,将 <p>HelloWorld</p> 在 Hello 和 World 之间拆分会创建一个结构拆分:

<p>Hello</p>
<p data-suggestion data-suggestion-type="blockSplit">World</p>

该建议的 splitKind 为 'structural',nodeType 为 'paragraph'。在这个例子中,其 text 和 fullText 都包含第二个块中的文本,即 "World"。

对于标题,中间拆分使用 nodeType: 'heading',并获得诸如 split heading 的标签。列表项内部的中间拆分会保留列表项的替换行为,而不会创建结构 split 建议。

独立接受和拒绝拆分

接受任一种拆分都会保留两个块并清除建议。拒绝插入拆分会移除新块;拒绝结构拆分会将两个块重新合并。

连续的末尾拆分保持独立。接受或拒绝一个拆分不会影响其他拆分。扩展不会将所有连续的末尾拆分合并为一个建议。

拆分标记会渲染在拆分前一个块的末尾。请参阅 blockSplitMarker 进行配置或移除。

split 与 blockSplit

查询 API 返回 suggestion.type: 'split'。节点属性和 data-suggestion-type DOM 属性使用 'blockSplit'。使用 findSuggestions() 时请按 'split' 筛选和切换;只有在针对渲染节点编写 CSS 时才使用 'blockSplit'。完整映射请参阅 SuggestionNodeType。

嵌套和堆叠建议

当一个用户在另一个用户尚未处理的建议内部进行编辑时,这个编辑会变成一个独立的建议,叠加在原始建议之上,并归属于编辑该内容的用户——而不是修改或丢弃原始建议。这样,多个审阅者就可以在同一文本范围上提出修改,而不会互相覆盖。

嵌套适用于行内插入、删除和替换。同一作者的编辑不会改变:如果同一个用户继续在自己的建议内部输入内容,它仍然会合并为一个单一建议(参见 建议分组)。嵌套只会发生在不同作者之间。

当建议被堆叠时,外层建议的 text 和 fullText 字段会出现差异:text 是作者单独贡献的文本,而 fullText 则包含叠加在其上方的嵌套建议插入的文本。详情和示例请参见 text 与 fullText。

接受和拒绝规则

操作结果
接受一个内层建议内层建议被应用;外层建议保持不变。
拒绝一个内层建议内层建议被丢弃;外层建议保持不变。
接受一个外层建议外层建议被应用;其中的嵌套建议会保留,并继续作为各自独立的待处理建议存在。
拒绝一个外层建议外层建议会被丢弃,其中的嵌套编辑也会一并丢弃,因为它们所依附的内容被移除了。

仅支持行内

仅支持行内建议的嵌套。块级嵌套建议(例如跨整块的嵌套建议)不在支持范围内。

标记追踪

修订还可以将现有文本上的标记级编辑追踪为 markChange 建议。这包括简单的格式标记,如 bold 和 italic,以及带有属性的标记,如 link 或 highlight。

标记追踪的行为如下:

  • 在现有文本上添加或移除标记会创建 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)和标记属性。接受或拒绝建议后可以保留、恢复或移除确切的链接标记,而不是将所有链接视为等同。

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

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

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

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

节点属性追踪

更新现有节点的属性会创建 attributeChange 建议,而不是替换整个节点。这适用于图片等原子节点,也适用于带有自定义属性的非原子节点。

建议待处理期间,文档中包含提议的属性值。建议通过 attributeChanges 暴露之前的值和提议的值,因此审阅界面可以解释这次变更:

{
  type: 'attributeChange',
  nodeType: 'image',
  attributeChanges: [
    {
      attributeName: 'src',
      oldValue: 'https://example.com/old.png',
      newValue: 'https://example.com/new.png',
    },
  ],
}

接受建议会保留提议值。拒绝建议会恢复每个 oldValue。如果同一作者在审阅前再次修改属性,建议会保留原始的 oldValue 并更新 newValue。将属性改回原始值会从待处理建议中移除该属性;当这是最后一个变更属性时,扩展会取消 attributeChange 建议。

使用 [data-suggestion-type="attributeChange"] 为受影响的节点设置样式。有关 DOM 和 API 详情,请参阅样式和 SuggestionAttributeChange。

表格支持

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

单元格选择删除 — 当用户通过 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' }),
  ],
})

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

原子节点追踪

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

协作兼容性

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

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

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

评论集成

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

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

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

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

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

代码块兼容性

默认情况下,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 或其他代码块扩展。