探索 Tiptap V3 的最新功能

比较文档版本

Available in Team plan

Snapshot Compare 允许你并排查看两个文档版本并突出显示所有更改。使用它来跟踪编辑,审查贡献,并恢复早期状态。

Snapshot Compare 扩展为 快照 添加了额外的功能,允许你可视化比较两个版本之间的更改,以便跟踪添加、删除或修改的内容。这些比较称为 diffs

访问私有注册表

Snapshot Compare 扩展已发布在 Tiptap 的私有 npm 注册表中。通过遵循 私有注册表指南 集成该扩展。如果你已经验证了你的 Tiptap 账号,可以直接进入 #安装

安装

从我们的私有注册表安装扩展:

npm install @tiptap-pro/extension-snapshot-compare

设置

你可以使用以下选项配置 SnapshotCompare 扩展:

设置类型默认值描述
providerTiptapCollabProvidernull协作提供者实例
mapDiffToDecorationsfunction() => {}控制将 diff 映射到装饰以显示其内容
const provider = new TiptapCollabProvider({
  // ...
})

const editor = new Editor({
  // ...
  extensions: [
    // ...
    SnapshotCompare.configure({
      provider,
    }),
  ],
})

使用 mapDiffToDecorations 进行 diff 装饰

该扩展具有默认映射 (defaultMapDiffToDecorations) 来表示 diffs 作为 ProseMirror 装饰。 对于更复杂的集成和控制,你可以使用 mapDiffToDecorations 选项自定义此映射。

示例: 对内联插入应用自定义预定义背景颜色

SnapshotCompare.configure({
  mapDiffToDecorations: ({ diff, tr, editor, defaultMapDiffToDecorations }) => {
    if (diff.type === 'inline-insert') {
      // 返回 prosemirror 装饰或 null
      return Decoration.inline(
        diff.from,
        diff.to,
        {
          class: 'diff',
          style: {
            backgroundColor: diff.attribution.color.backgroundColor,
          },
        },
        // 将 diff 作为装饰的规格传递,这对于 `extractAttributeChanges` 是必需的
        { diff },
      )
    }

    // 回退到默认映射
    return defaultMapDiffToDecorations({
      diff,
      tr,
      editor,
      attributes: {
        // 为装饰添加自定义属性
        'data-tiptap-user-id': myUserIdMapping[diff.attribution.userId],
      },
    })
  },
})

存储

SnapshotCompare 存储对象包含以下属性:

类型描述
isPreviewingboolean指示 diff 视图是否处于激活状态
diffsDiff[]在 diff 视图中显示的 diffs
previousContentJSONContent | null应用 diff 视图之前的内容

使用 isPreviewing 属性检查 diff 视图是否当前处于激活状态:

if (editor.storage.snapshotCompare.isPreviewing) {
  // diff 视图当前处于激活状态
}

使用 diffs 属性访问在 diff 视图中显示的 diffs:

editor.storage.snapshotCompare.diffs

属性 previousContent 由扩展内部使用,用于在退出 diff 视图时恢复内容。通常,你不需要直接与之交互。

命令

命令描述
compareVersions比较两个版本并在编辑器中渲染 diff
showDiff给定更改跟踪变换,在编辑器中显示 diff
hideDiff隐藏 diff 并恢复之前的内容

compareVersions

使用 compareVersions 命令计算并显示两个文档版本之间的差异。

editor.chain().compareVersions({
  fromVersion: 1,
})

选项

你可以传入额外的选项以更好地控制 diff 过程:

描述
fromVersion比较的起始版本。fromVersiontoVersion 之间的顺序是灵活的
toVersion要比较的结束版本(默认:最新版本)
hydrateUserData向每个用户的更改添加上下文数据
onCompare手动处理 diff 结果
enableDebugging启用详细日志记录以进行故障排查

使用 hydrateUserData 添加元数据

每个 diff 具有 attribution 字段,允许你使用 hydrateUserData 回调函数添加附加元数据。

注意

请注意,userIdTiptapCollabProvider 填充,应该用于识别进行更改的用户。如果提供者未提供 user 字段,则 userId 将为 null有关更多信息,请参见 TiptapCollabProvider 文档

示例: 基于用户为 diffs 着色

const colorMapping = new Map([
  ['user-1', '#ff0000'],
  ['user-2', '#00ff00'],
  ['user-3', '#0000ff'],
])

editor.chain().compareVersions({
  fromVersion: 1,
  toVersion: 3,
  hydrateUserData: ({ userId }) => {
    return {
      color: {
        backgroundColor: colorMapping.get(userId),
      },
    }
  },
})

editor.storage.snapshotCompare.diffs[0].attribution.color.backgroundColor // '#ff0000'

使用 onCompare 自定义 diff 过程

如果你需要更好地控制 diff 过程,可以使用 onCompare 选项接收结果并自行处理。

示例: 按用户过滤 diffs

editor.chain().compareVersions({
  fromVersion: 1,
  toVersion: 3,
  onCompare: (ctx) => {
    if (ctx.error) {
      // 处理 diff 过程中的错误
      console.error(ctx.error)
      return
    }

    // 过滤 diffs 以仅显示特定用户的更改
    const diffsToDisplay = ctx.diffSet.filter((diff) => diff.attribution.userId === 'user-1')

    editor.commands.showDiff(ctx.tr, { diffs: diffsToDisplay })
  },
})

showDiff

使用 showDiff 命令在编辑器中显示 diff,使用更改跟踪变换 (tr)。这代表自上一个快照以来对文档所做的所有更改。你可以使用此变换在编辑器中显示 diff。

通常,在使用 compareVersionsonCompare 自定义或过滤 diffs 后使用此命令。

showDiff 命令临时替换当前编辑器内容为 diff 视图,显示版本之间的差异。它还会缓存当前在编辑器中显示的内容,以便你稍后可以恢复它。

// 这将显示在编辑器中记录的更改跟踪变换的变更
editor.commands.showDiff(tr)

选项

你可以传入额外的选项以控制如何显示 diffs:

描述
diffs一个要可视化的 diffs 数组

示例: 显示特定的 diffs

// 这将仅显示由 ID 为 'user-1' 的用户所做的 diffs
const diffsToDisplay = tr.toDiff().filter((diff) => diff.attribution.userId === 'user-1')

editor.commands.showDiff(tr, { diffs: diffsToDisplay })

hideDiff

使用 hideDiff 命令隐藏 diff 并恢复之前的内容。

// 这将隐藏 diff 视图并恢复之前的内容
editor.commands.hideDiff()

使用 NodeView (高级)

在使用 自定义节点视图 时,默认的 diff 映射可能无法按预期工作。你可以自定义映射并直接在自定义节点视图中渲染 diffs。

使用 extractAttributeChanges 助手提取节点中的属性更改。这使你能够访问节点的先前和当前属性,从而可能突出显示自定义节点视图中的属性更改。

注意

当将 diffs 映射到装饰时,你需要将 diff 作为装饰的 spec 传递。这对于 extractAttributeChanges 的正常工作是必需的。

示例: 自定义标题节点视图以显示更改

import { extractAttributeChanges } from '@tiptap-pro/extension-snapshot-compare'

const Heading = BaseHeading.extend({
  addNodeView() {
    return ReactNodeViewRenderer(({ node, decorations }) => {
      const { before, after, isDiffing } = extractAttributeChanges(decorations)

      return (
        <NodeViewWrapper style={{ position: 'relative' }}>
          {isDiffing && before.level !== after.level && (
            <span
              style={{
                position: 'absolute',
                right: '100%',
                fontSize: '14px',
                color: '#999',
                backgroundColor: '#f0f0f070',
              }}
              // 显示级别属性的变化
            >
              #<s>{before.level}</s>
              {after.level}
            </span>
          )}
          <NodeViewContent as={`h${node.attrs.level ?? 1}`} />
        </NodeViewWrapper>
      )
    })
  },
})

技术细节

Diff

Diff 是一个表示文档中所做更改的对象。它包含以下属性:

属性类型描述
type'inline-insert' | 'inline-delete' | 'block-insert' | 'block-delete' | 'inline-update' | 'block-update'所做更改的类型
fromnumber更改的起始位置
tonumber更改的结束位置
contentFragment添加或删除的内容
attributionAttribution有关更改的元数据,例如执行更改的用户
attributes?Record<string, any>更改 的属性;仅对 'inline-update''block-update' diffs 可用
previousAttributes?Record<string, any>更改 的属性;仅对 'inline-update''block-update' diffs 可用

DiffSet

DiffSet 是一个 Diff 对象数组,每个对象对应于特定的更改,例如插入、删除或更新。你可以迭代数组以检查单个更改或基于 diff 类型应用自定义逻辑。

const diffsToDisplay = diffSet.filter((diff) => diff.attribution.userId === 'user-1')

// 在编辑器中显示过滤后的 diffs
editor.commands.showDiff(tr, { diffs: diffsToDisplay })

Attribution

Attribution 对象包含有关更改的元数据。它包括以下属性:

属性类型描述
type'added' | 'removed'指示所做更改的类型
userIdstring | undefined执行更改的用户的 ID
idY.ID | undefined执行更改的用户的 Y.js 客户端 ID

你可以扩展 Attribution 接口以包括其他属性:

declare module '@tiptap-pro/extension-snapshot-compare' {
  interface Attribution {
    userName: string
  }
}

ChangeTrackingTransform

ChangeTrackingTransform 是一个记录对文档所做更改的类(基于 ProseMirror 的 Transform)。 它表示一个变换,其步骤描述了从文档的一个版本到另一个版本所做的所有更改。它具有以下属性:

属性类型描述
stepsChangeTrackingStep[]表示对文档所做更改的步骤数组
docNode应用更改后文档的状态
beforeNode应用更改前文档的状态

ChangeTrackingStep

ChangeTrackingStep 是一个基于 ProseMirror 的 ReplaceStep 类表示的类,代表对文档所做的单个更改。它具有以下属性:

属性类型描述
attributionAttribution有关更改的元数据,例如执行更改的用户

类型

以下是 SnapshotCompare 扩展的完整 TypeScript 定义:

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    snapshotCompare: {
      /**
       * 给定更改跟踪变换,在编辑器内显示 diff
       */
      showDiff: (tr: ChangeTrackingTransform, options?: { diffs?: DiffSet }) => ReturnType
      /**
       * 隐藏 diff 并恢复之前的内容
       */
      hideDiff: () => ReturnType
      /**
       * 比较两个版本并将 diff 渲染到编辑器中
       */
      compareVersions: <
        T extends Pick<Attribution, 'color'> & Record<string, any> = Pick<Attribution, 'color'> &
          Record<string, any>,
      >(options: {
        /**
         * 开始 diff 的版本
         */
        fromVersion: number
        /**
         * 结束 diff 的版本
         * 如果未提供,将使用最新快照
         */
        toVersion?: number
        /**
         * 允许为每个用户的更改添加上下文数据
         */
        hydrateUserData?: (context: {
          /**
           * 事件的类型
           */
          type: 'added' | 'removed'
          /**
           * 操作此内容的 userId
           */
          userId: string | undefined
          /**
           * 内容的 yjs 标识符
           */
          id?: y.ID
        }) => T
        /**
         * 如果提供,允许自定义渲染 diffs 到编辑器的行为。
         * @note 默认行为是立即显示 diff。
         */
        onCompare?: (
          context:
            | {
                error?: undefined
                /**
                 * 编辑器实例
                 */
                editor: Editor
                /**
                 * 带有归属元数据的变换所有更改
                 */
                tr: ChangeTrackingTransform<Attribution & T>
                /**
                 * 作为 diffs 数组表示的更改
                 */
                diffSet: DiffSet<Attribution & T>
              }
            | {
                error: Error
              },
        ) => void
        /**
         * 详细记录 diffing 过程以帮助追踪错误
         */
        enableDebugging?: boolean
      }) => ReturnType
    }
  }
}

export type SnapshotCompareOptions = {
  /**
   * tiptap 提供者实例。这对于扩展示计算 diffs 是必需的,但不是必需的以显示它们。
   * 也可以传递 TiptapCollabProvider 实例。
   */
  provider: TiptapCollabProvider | null
  /**
   * 这允许你控制将 diff 映射到装饰以显示该 diff 内容的方式
   */
  mapDiffToDecorations?: (options: {
    /**
     * 要映射到装饰的 diff
     */
    diff: Diff
    /**
     * 编辑器实例
     */
    editor: Editor
    /**
     * 更改跟踪变换
     */
    tr: ChangeTrackingTransform
    /**
     * 将 diff 映射到装饰的默认实现
     */
    defaultMapDiffToDecorations: typeof defaultMapDiffToDecorations
  }) => ReturnType<typeof defaultMapDiffToDecorations>
}

export type SnapshotCompareStorageInactive = {
  /**
   * diff 视图当前是否处于激活状态
   */
  isPreviewing: false
  /**
   * diff 视图应用之前的内容
   */
  previousContent: null
  /**
   * 已应用的更改跟踪变换
   * 因为 diff 视图没有激活,它当前是空的
   */
  diffs: DiffSet
  /**
   * 已应用的更改跟踪变换
   */
  tr: null
}

export type SnapshotCompareStorageActive = {
  /**
   * diff 视图当前是否处于激活状态
   */
  isPreviewing: true
  /**
   * diff 视图应用之前的内容
   */
  previousContent: JSONContent
  /**
   * 已应用的更改跟踪变换
   */
  diffs: DiffSet
  /**
   * 已应用的更改跟踪变换
   */
  tr: ChangeTrackingTransform
}

export type SnapshotCompareStorage = SnapshotCompareStorageInactive | SnapshotCompareStorageActive