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 接收editor和TResolvedSuggestion(内含suggestionId和keyId),整体包在editor.tf.withoutNormalizing中执行,内部按三步推进:
合并换行删除建议。先找出所有
type === 'remove'且isLineBreak且id匹配的块级建议节点,按 path 倒序执行mergeNodes({ at: PathApi.next(path) })。语义是:删除换行的建议被接受后,两个段落应真正合并。清除“已接受”的痕迹。对全树
unsetNodes掉keyId、suggestion、瞬态 key,但只匹配满足条件的节点:- 文本/行内元素:若
dataList中存在type === 'update'的建议,则以该节点是否含有目标id为准;否则仅当建议是type === 'insert'且 id 匹配时清除(即插入文本转正、update 的 mark 定稿); - 块级建议节点:
isLineBreak的建议只要 id 匹配就清除,非换行建议要求type === 'insert'且 id 匹配。
- 文本/行内元素:若
物理删除“拒绝后仍被接受的删除项”。
removeNodes匹配type === 'remove'且 id 匹配的文本、行内元素,以及type === 'remove'、非isLineBreak的块级建议节点——删除操作被接受意味着内容真正消失(换行删除已在第 1 步通过合并处理)。
组合起来的效果:接受的 insert 保留文本、去掉建议元数据;接受的 remove 直接删掉内容;接受的 update 让新 mark 属性落地。
五、rejectSuggestion:把痕迹原样回滚
rejectSuggestion.ts 结构与 accept 互为镜像,额外多两个步骤:
- 记录行内“insert 元素”入口(如被建议插入的行内 link 等),用于最后删除。
- 合并换行插入建议:匹配
type === 'insert'且isLineBreak的块级建议节点并mergeNodes——拒绝“加一个换行”就是把段落还原成原来的一段。 - 恢复被建议删除的内容:对文本/行内元素中
type === 'remove'且 id 匹配的建议、以及块级type === 'remove'(含换行)建议节点,unsetNodes掉建议元数据,让原文恢复可见。 - 物理删除被建议插入的内容:
removeNodes清除type === 'insert'的文本节点、行内 insert 元素,以及type === 'insert'且非换行的块级建议节点;行内 insert 元素在最后按记录的路径倒序removeNodes。 - 回滚 update 建议:找出
dataList中含type === 'update'且 id 匹配的文本节点,把newProperties中为真的属性unsetNodes掉(撤销新 mark),把properties中为假的属性重新setNodes为true(恢复旧 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类型:TSuggestionData的type只有'insert' | 'remove',所以对整块的样式修改不走建议流程;rejectSuggestion的 update 回滚分支只作用于文本节点。 currentUserId必填:文档明确它是建议归属(attribution)的要求,缺失时建议无法正确记录创建者。- 接受/拒绝按
suggestionId精确生效:两条 transform 的所有匹配条件都带 id 等值判断,逐条处理互不串扰;这也是“多次撤销/重入”能完整支持建议变更的前提(文档 Features 一节列出的能力)。 - 仓库中
acceptSuggestion.spec.tsx、rejectSuggestion.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),仅供参考