Plate 如何实现 Suggestion 修改建议与接受拒绝操作?
2026/9/15 18:46:18 网站建设 项目流程

Plate 如何实现 Suggestion 修改建议与接受拒绝操作?

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

如果你的多人协作编辑器需要“Google Docs 式”的修改建议——先留下改动痕迹,再由审阅人逐条接受或拒绝——Plate 的@platejs/suggestion插件就是完成这件事的组件。它支持两类建议:行内文本建议(以 mark 加注解的形式附着在文本上)和块级建议(针对整个内容块),并对建议的变更提供完整的撤销/重入支持。本文按“接入插件 → 开启建议模式 → 建议数据如何落库 → accept/reject 两条 transform 如何把痕迹还原成最终文本”的顺序,讲清这条链路在 Plate 中是如何实现的。

一、接入 SuggestionPlugin

接入有两条路径,任选其一。

方式一:使用 SuggestionKit(最快)

SuggestionKit包含预配置好的SuggestionPlugin及配套 Plate UI 组件,直接展开进 plugins 数组:

import { createPlateEditor } from 'platejs/react'; import { SuggestionKit } from '@/components/editor/plugins/suggestion-kit'; const editor = createPlateEditor({ plugins: [ // ...otherPlugins, ...SuggestionKit, ], });

Kit 内已包含三个渲染组件:SuggestionLeaf(渲染建议文本 mark)、BlockSuggestion(渲染块级建议)、SuggestionLineBreak(处理建议中的换行)。

方式二:手动安装并扩展插件

npm install @platejs/suggestion

拿到BaseSuggestionPlugin后用toTPlatePlugin扩展,关键配置和渲染如下(currentUserId是建议归属的必需项,示例中的'alice'需替换为实际用户 ID):

import { type ExtendConfig, isSlateEditor, isSlateElement, isSlateString, } from 'platejs'; import { type BaseSuggestionConfig, BaseSuggestionPlugin, } from '@platejs/suggestion'; import { toTPlatePlugin } from 'platejs/react'; import { BlockSuggestion } from '@/components/ui/block-suggestion'; import { SuggestionLeaf } from '@/components/ui/suggestion-node'; export type SuggestionConfig = ExtendConfig< BaseSuggestionConfig, { activeId: string | null; hoverId: string | null; } >; export const suggestionPlugin = toTPlatePlugin<SuggestionConfig>( BaseSuggestionPlugin, ({ editor }) => ({ options: { activeId: null, currentUserId: 'alice', // 设为当前用户 ID hoverId: null, }, render: { node: SuggestionLeaf, belowRootNodes: ({ api, element }) => { if (!api.suggestion!.isBlockSuggestion(element)) { return null; } return <BlockSuggestion element={element} />; }, }, }) );

各配置项的作用(引自官方插件文档):

  • options.activeId:当前激活的建议 ID,用于视觉高亮;
  • options.currentUserId:创建建议的当前用户 ID,缺少它无法正确归属建议;
  • options.hoverId:悬停中的建议 ID,用于 hover 效果;
  • render.node:用SuggestionLeaf渲染建议文本 mark;
  • render.belowRootNodes:对块级建议渲染BlockSuggestion

此外还需要一个suggestionLineBreakPlugin来处理换行建议,并和suggestionPlugin一起挂到编辑器上:

import { createPlatePlugin } from 'platejs/react'; import { SuggestionLineBreak } from '@/components/ui/suggestion-node'; const suggestionLineBreakPlugin = createPlatePlugin({ key: 'suggestionLineBreak', render: { belowNodes: SuggestionLineBreak as any }, }); const editor = createPlateEditor({ plugins: [ // ...otherPlugins, suggestionPlugin, suggestionLineBreakPlugin, ], });

如果编辑器要做完整的协作场景,官方文档建议同时接入 discussion 插件(discussionPlugin负责存储用户与讨论线程状态,纯 UI 状态插件),把discussionPlugin放在suggestionPlugin之前即可。

二、开启建议模式与创建建议

isSuggesting选项决定编辑器是否处于建议模式。文档给出的用法是通过插件选项切换:

import { useEditorRef, usePluginOption } from 'platejs/react'; function SuggestionToolbar() { const editor = useEditorRef(); const isSuggesting = usePluginOption(suggestionPlugin, 'isSuggesting'); const toggleSuggesting = () => { editor.setOption(suggestionPlugin, 'isSuggesting', !isSuggesting); }; return ( <button onClick={toggleSuggesting}> {isSuggesting ? 'Stop Suggesting' : 'Start Suggesting'} </button> ); }

也可以把官方的SuggestionToolbarButton挂到 Toolbar 上完成同样的切换。建议模式下对选中文本执行Cmd + Shift + S快捷键即可在选区上添加一条建议。

三、建议数据如何落库

理解 accept/reject 之前,先看数据形状,这是 插件文档/(collaboration)/suggestion.mdx) Types 一节定义的:

行内建议数据TInlineSuggestionData挂在文本节点上,一个文本节点可以带多条建议(以suggestion_<id>键存放):

  • id:建议唯一标识;
  • userId:创建者;
  • createdAt:创建时间戳;
  • type'insert' | 'remove' | 'update'
  • newProperties/properties(可选):update类型时分别记录“新 mark 属性”和“原 mark 属性”。

块级建议数据TSuggestionData挂在元素节点的suggestion字段上,字段同上,但type只有'insert' | 'remove',另有一个可选的isLineBreak表示这条建议是一次换行插入。

也就是说:行内支持三种操作(插入、删除、改 mark),块级只支持两种(插入、删除),块的“修改”不会以 update 形式出现——这个差异直接决定了下面两条 transform 的分支结构。

四、acceptSuggestion:把插入定稿、把删除清掉

acceptSuggestion.ts 接收editorTResolvedSuggestion(内含suggestionIdkeyId),整体包在editor.tf.withoutNormalizing中执行,内部按三步推进:

  1. 合并换行删除建议。先找出所有type === 'remove'isLineBreakid匹配的块级建议节点,按 path 倒序执行mergeNodes({ at: PathApi.next(path) })。语义是:删除换行的建议被接受后,两个段落应真正合并。

  2. 清除“已接受”的痕迹。对全树unsetNodeskeyIdsuggestion、瞬态 key,但只匹配满足条件的节点:

    • 文本/行内元素:若dataList中存在type === 'update'的建议,则以该节点是否含有目标id为准;否则仅当建议是type === 'insert'且 id 匹配时清除(即插入文本转正、update 的 mark 定稿);
    • 块级建议节点:isLineBreak的建议只要 id 匹配就清除,非换行建议要求type === 'insert'且 id 匹配。
  3. 物理删除“拒绝后仍被接受的删除项”removeNodes匹配type === 'remove'且 id 匹配的文本、行内元素,以及type === 'remove'、非isLineBreak的块级建议节点——删除操作被接受意味着内容真正消失(换行删除已在第 1 步通过合并处理)。

组合起来的效果:接受的 insert 保留文本、去掉建议元数据;接受的 remove 直接删掉内容;接受的 update 让新 mark 属性落地。

五、rejectSuggestion:把痕迹原样回滚

rejectSuggestion.ts 结构与 accept 互为镜像,额外多两个步骤:

  1. 记录行内“insert 元素”入口(如被建议插入的行内 link 等),用于最后删除。
  2. 合并换行插入建议:匹配type === 'insert'isLineBreak的块级建议节点并mergeNodes——拒绝“加一个换行”就是把段落还原成原来的一段。
  3. 恢复被建议删除的内容:对文本/行内元素中type === 'remove'且 id 匹配的建议、以及块级type === 'remove'(含换行)建议节点,unsetNodes掉建议元数据,让原文恢复可见。
  4. 物理删除被建议插入的内容removeNodes清除type === 'insert'的文本节点、行内 insert 元素,以及type === 'insert'且非换行的块级建议节点;行内 insert 元素在最后按记录的路径倒序removeNodes
  5. 回滚 update 建议:找出dataList中含type === 'update'且 id 匹配的文本节点,把newProperties中为真的属性unsetNodes掉(撤销新 mark),把properties中为假的属性重新setNodestrue(恢复旧 mark),最后unsetNodes([getSuggestionKey(id)])移除这条建议数据。

净效果:拒绝 insert → 内容消失;拒绝 remove → 原文回来;拒绝 update → mark 属性回到properties记录的旧值。每一步只按suggestionId精确匹配,同一段文本上并存的其它建议不受影响。

六、状态管理与辅助 API

文档列出的api.suggestion方法在这条链路里各有分工:

  • api.suggestion.node/nodes:按 id、isText等条件找建议节点,点击处理里就是靠node({ isText: true })找到点击位置所属的建议;
  • api.suggestion.nodeId(node):从节点反查建议 ID,点击处理器用它设置activeId
  • api.suggestion.dataList(node):取一个文本节点上的全部建议数据,accept/reject 内部匹配update类型时用它;
  • api.suggestion.isBlockSuggestion(node):判断节点是否块级建议,belowRootNodes渲染和 transform 里的分支都依赖它;
  • api.suggestion.withoutSuggestions(fn):在执行某段逻辑期间临时禁用建议行为,避免程序化操作误产生建议痕迹。

手动接入文档中的onClickhandler 演示了状态维护的完整写法:点击后沿 DOM 向上遍历,若落在带slate-<type>class 的建议节点上,就通过api.suggestion.nodeId把对应 ID 写入activeId;点空处或点不到建议则setOption('activeId', null),从而驱动 hover/active 样式。

七、边界与限制

  • 块级建议没有update类型TSuggestionDatatype只有'insert' | 'remove',所以对整块的样式修改不走建议流程;rejectSuggestion的 update 回滚分支只作用于文本节点。
  • currentUserId必填:文档明确它是建议归属(attribution)的要求,缺失时建议无法正确记录创建者。
  • 接受/拒绝按suggestionId精确生效:两条 transform 的所有匹配条件都带 id 等值判断,逐条处理互不串扰;这也是“多次撤销/重入”能完整支持建议变更的前提(文档 Features 一节列出的能力)。
  • 仓库中acceptSuggestion.spec.tsxrejectSuggestion.spec.tsx(transforms 目录)对两条 transform 有独立的单元测试覆盖,行为细节可以以这两个 spec 为准对照。

下一步

文档把 suggestion 与 discussion 的组合列为完整协作形态:discussionPlugin提供用户与讨论线程状态,suggestionPlugin.configure({ options: { currentUserId } })叠加在上面。接入 discussion 后即可在建议基础上继续搭评论线程,参考 Discussion 插件文档/(collaboration)/discussion.mdx)。

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询