Plate 导航反馈契约(Navigation Feedback)设计规范:从 TOC、脚注到搜索跳转的统一编辑体验
2026/9/15 17:53:20 网站建设 项目流程

Plate 导航反馈契约(Navigation Feedback)设计规范:从 TOC、脚注到搜索跳转的统一编辑体验

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

导读

本文围绕 Plate 富文本编辑器项目中的共享「导航反馈」规范展开,系统讲解docs/plans/2026-04-06-navigation-feedback-spec.md所定义的跨界面编辑定律:任何成功的导航跳转都应移动焦点/光标、将目标滚动进视口、并对落点目标做短暂高亮。这份规范的目标是把当前 TOC(目录)、脚注导航、搜索跳转等功能各自重复实现的"闪烁/滚动/选中修复"逻辑,收敛为一个编辑器级共享原语。读完本文,你将掌握该契约的完整 API 设计(flashTarget/navigate)、目标模型、分层架构(lib 层 + React 层)、分阶段落地计划与测试矩阵,并能结合仓库源码看清它在packages/corepackages/footnotepackages/toc中的实际落地形态。

本文为文档解读 + 源码印证文章,文中引用的文件路径均以仓库根目录为基准。


1. 为什么需要一个共享导航反馈契约

1.1 现状:每个跳转功能都在"各写各的"

在 Plate 的编辑器行为体系中,以下场景都需要在跳转后给出视觉反馈:

  • TOC 点击跳转:点击目录条目后滚动到对应标题;
  • 脚注导航:从参考文献跳转到定义、或从定义跳回引用处;
  • 搜索跳转:跳转到搜索命中的文本位置(本规范中属于延迟项)。

每个功能实现"跳转反馈"时,都需要反复处理同一组逻辑:

  • 目标解析与交接(target resolution handoff);
  • 瞬时高亮的计时(transient highlight timing);
  • 上一次目标状态的新旧替换与清理(replacement/clearing of prior target state);
  • 视觉 token 的逐步漂移(visual-token drift)。

正如契约文档 docs/plans/2026-04-06-navigation-feedback-contract.md 指出的:没有共享契约,就会出现重复的定时器、重复的状态、重复的渲染逻辑,长期维护与 DX 体验都是最差的。

1.2 已有律法:三条"编辑器定律"

规范中明确,导航反馈的三条规则已经在多份文档中作为跨界面定律存在:

  • 成功的导航应让焦点/光标落在目标处(move focus or caret);
  • 将目标滚动进视口(scroll target into view);
  • 短暂高亮落点目标(briefly highlight the landed target)。

这些规则分布在:

  • markdown-editing-spec.md(其中EDIT-NAV-FEEDBACK-*已标记为locked,即该行为定律已被锁定);
  • editor-protocol-matrix.md(协议矩阵中为 footnote definition / footnote reference / search target / heading target 四类场景各登记了一行跨界面导航反馈协议);
  • editor-behavior-architecture.md(架构文档将导航反馈作为共享编辑器领域对待)。

规范的核心判断是:定律已经存在,但缺失的是共享运行时契约(shared runtime contract)。这正是本规范要填补的空缺。

1.3 两个真实消费者的差异:不能强行抽象成同一个原语

契约文档特别强调:当前两个消费者"相关但不同质":

  • 脚注导航是"选中驱动"的:它拥有具体的光标/选区点,属于 selection-driven caret movement + focus/scroll;
  • TOC 点击是"DOM 滚动优先"的:先滚动,之后才附加块级选中装饰(block-selection chrome),属于 DOM-scroll-first。

因此契约应当标准化两者的交集(目标替换、自动清理、节点高亮语义),而不是假装两个流程今天已经是同一个原语。设计原则第一条即:"只标准化当前消费者已经挣得的交集(overlap)"。


2. 架构决策:契约放哪、怎么分层

2.1 ADR:永久契约落在@platejs/core

契约文档给出了三个候选方案并逐一评估:

方案优点缺点结论
Option A@platejs/core共享导航插件跨界面契约的天然永久归宿;与现有共享插件、DOM、node-prop 缝对齐;API 对人类与 Agent 最可发现需触碰 core 插件架构;过早过度泛化会抬高回退成本推荐
Option B@platejs/selection作为主宿主已拥有选区邻近 UI 与 overlay 面语义上错误(导航不是选区功能);会把廉价节点高亮伪装成选区特性;可能把 overlay 假设拖入基础契约
Option Ctoc/footnote/search各自本地实现每个功能本地 diff 最小必然漂移;重复定时器/状态/渲染逻辑;长期 DX 与维护最差

ADR 的最终结论是:packages/core中实现共享导航插件表面(shared plugin surface),包含共享 transforms 与共享渲染状态注入。决策驱动因素为:跨界面复用、低渲染成本、低概念成本、可预测的归属、面向更广目标类型的未来路径。selection将来可以作为可选的 range/overlay 适配器宿主,但不是导航契约的主要归属。

文档还明确排除了把契约放在floating(浮动层)或新建独立导航包,理由是契约"不属于单一功能族、本质上不是 overlay 几何、不单纯是选区问题、且被多个当前与未来界面所需"。

2.2 两层形态:lib 层管契约,React 层只做适配

规范与契约文档共同强调:不能让 React store 悄悄成为真正的契约。因此采用两层结构:

  • Lib 层:在packages/core内新增一个编辑器作用域的 lib 插件,拥有当前导航目标(current nav target)、请求/脉冲 id(nav request id / pulse id)、前一目标替换(replacement)、自动清理定时器(auto-clear timer);
  • React 层:只提供把活跃导航目标暴露给渲染器与 hooks 所需的薄适配表面。

约束条件:如果插件/编辑器状态可以干净地驱动inject.nodeProps与渲染 hooks,就不要默认新建独立的NavigationFeedbackStore;只有真实渲染器约束逼不得已时才引入专用 store。

硬性要求:必须证明导航目标变化能触发渲染更新,使inject.nodeProps无需依赖无关的选区变化即可增删高亮属性。

2.3 两类一等消费者模式

Phase 1 明确支持两种模式,且不允许为了让抽象看起来干净而把 TOC 强扭成脚注的形状

  1. Selection-driven navigate(选中驱动导航):面向脚注跳转这类拥有具体 caret/selection 点的消费者;
  2. Flash-only target feedback(仅闪烁目标反馈):面向 TOC 这类应保留当前"非文本选区"导航行为、但复用共享闪烁计时与替换语义的消费者。

3. 渲染策略:node 属性优先,overlay 是后路

3.1 为什么选择>editor.tf.navigation.flashTarget({ target: { type: "node", path }, variant: "navigated", });
editor.tf.navigation.navigate({ target: { type: "node", path }, flash: { variant: "navigated" }, focus: true, scroll: true, select: { anchor: point, focus: point, }, });

设计意图非常明确:

  • flashTarget(...)是一等公民,不是 fallback 辅助函数;
  • navigate(...)面向选中驱动流程,负责把 select、focus、scroll、flash 协调在一起;
  • 不要求每个消费者都必须提供选区语义。

4.2 初始目标类型(target kinds)

  • Phase 1A 支持node目标(按 Slate path 定位);
  • 可选但延迟block-idrange、自定义 DOM rect 或虚拟目标;
  • 明确不加:不在早期引入更通用的目标代数(target algebra),除非确实出现需要它的消费者。

这意味着search 被延迟:要么团队有意将range提升进目标模型,要么证明真实搜索跳转只需要 node 目标语义。


5. 精确文件落点与分阶段路线

5.1 Core 包内文件清单

Lib 插件巷packages/core/src/lib/plugins/):

  • packages/core/src/lib/plugins/navigation-feedback/NavigationFeedbackPlugin.ts
  • packages/core/src/lib/plugins/navigation-feedback/index.ts
  • packages/core/src/lib/plugins/navigation-feedback/types.ts
  • packages/core/src/lib/plugins/navigation-feedback/transforms/flashTarget.ts
  • packages/core/src/lib/plugins/navigation-feedback/transforms/navigate.ts
  • packages/core/src/lib/plugins/navigation-feedback/transforms/index.ts

并接入packages/core/src/lib/plugins/index.tspackages/core/src/lib/plugins/getCorePlugins.ts

React 侧巷packages/core/src/react/plugins/):

  • packages/core/src/react/plugins/navigation-feedback/NavigationFeedbackPlugin.ts
  • packages/core/src/react/plugins/navigation-feedback/useNavigationFeedback.ts(仓库最终落地名为useNavigationHighlight.ts,见第 6 节)
  • packages/core/src/react/plugins/navigation-feedback/index.ts

并接入packages/core/src/react/plugins/index.tspackages/core/src/react/editor/getPlateCorePlugins.ts

若 node-prop 注入需要可复用 core 帮助函数,优先复用既有注入缝:

  • packages/core/src/internal/plugin/pipeInjectNodeProps.tsx
  • packages/core/src/internal/plugin/pluginInjectNodeProps.ts

若滚动集成需要共享选项表面,可参考packages/core/src/lib/plugins/dom/DOMPlugin.ts

默认分工:lib 插件拥有 transforms 与规范契约;React 层只拥有渲染面适配器/hook 表面;除非渲染管线证明必要,否则不让 React store 成为契约本身。

5.2 功能包集成点

  • 脚注packages/footnote/src/lib/transforms/focusFootnoteDefinition.tspackages/footnote/src/lib/transforms/focusFootnoteReference.ts
  • TOCpackages/toc/src/react/hooks/useTocElement.ts(仓库实际实现位于useContentController.ts,见第 6 节);
  • 搜索:延迟,直到 range-vs-node 目标语义明确。

高亮样式初期应留在应用/编辑器 UI(如apps/www/src/app/globals.css),不要为了发布包级样式而阻塞 core 插件设计。

5.3 分阶段路线图

阶段内容关键约束
Phase 1Acore 契约 + 选中驱动消费者:lib 插件、React 插件/hook、flashTarget/navigatetransforms、data-nav-target/data-nav-highlightnode-prop 注入、定时器替换/自动清理;集成脚注 ref→def 与 def→ref目标类型仅node;先在选中驱动消费者上验证契约;不把 search 拖入本阶段
Phase 1B非选中消费者:TOC 作为 flash-first 消费者接入同一契约保留 TOC 现有滚动行为;复用共享闪烁计时/替换语义;不强制文本选区;"TOC 也应落光标"作为独立 UX 决策,不偷偷塞进基础契约
Phase 2显式目标模型扩展:决定 search 需要range还是仅node;如需range,按真正的扩展设计仅当 1A/1B 稳定后才做
Phase 3加固:统一 variant 命名与超时策略;确保替换语义确定性;增加可见目标反馈的浏览器测试
Phase 4延迟扩展:range 目标、selection中的 overlay 适配器、讨论/评论锚点消费者仅按需

5.4 测试计划

单元 / 包测试(core)

  • 插件在getPlateCorePlugins中注册;
  • flashTarget设置目标状态;
  • 新 flash 替换旧状态;
  • 自动清理定时器清理目标;
  • navigate按顺序执行 selection + scroll + flash;
  • node-prop 注入正确增删预期 data 属性;
  • 渲染失效路径无需选区变化即可更新高亮属性。

功能消费者

  • 脚注 transforms 调用共享导航 API 而非本地持有 flash;
  • TOC 点击路径复用共享闪烁计时/替换语义且不强制选区语义。

集成测试:从一个目标导航到另一个目标时干净地交换高亮;内联 void 目标高亮可用;块级目标高亮可用。

浏览器验证:脚注 ref→def、def→ref 可见地闪烁目标;TOC 跳转可见地闪烁目标;搜索跳转验证延迟。

5.5 风险与缓解

风险缓解
过早的过度泛化抽象只从已挣得的交集起步:flashTarget+ 选中驱动navigate,目标仅node
渲染层耦合保持样式薄、基于 data 属性
core 表面膨胀先只暴露最小的flashTarget/navigate
TOC 与脚注并非同一原语在契约中显式声明两种消费者模式
功能包仍做本地闪烁显式迁移首批消费者并删除本地高亮逻辑
目标状态更新无法干净触发节点树重渲染Phase 1A 中证明渲染失效缝;仅在插件/编辑器状态 + hooks 无法重绘属性时才引入最小 React store

6. 源码印证:契约在仓库中的实际落地形态

本规范的后续计划文档 docs/plans/2026-04-10-navigation-feedback-path-ref-runtime.md 显示,导航反馈在 core 中以运行时PathRef状态落地。当前仓库中该契约已经实现,且文件布局与规范中的"精确文件落点"基本一一对应。

6.1 Lib 层:packages/core/src/lib/plugins/navigation-feedback/

仓库实际文件为:

  • NavigationFeedbackPlugin.ts
  • types.ts
  • transforms/flashTarget.ts
  • transforms/navigate.ts
  • transforms/index.ts
  • NavigationFeedbackPlugin.spec.ts

类型定义types.ts)中可以看到完整的运行时模型:

export type NavigationFeedbackTarget = { path: Path; type: 'node'; }; export type NavigationFeedbackActiveTarget = NavigationFeedbackTarget & { cycle: 0 | 1; duration: number; pulse: number; variant: string; }; export type NavigationFeedbackStoredTarget = Omit< NavigationFeedbackActiveTarget, 'path' > & { pathRef: PathRef; };

值得注意的设计细节:

  • 存储态用PathRef而非裸Path:目标在文档编辑(插入/删除)后仍能保持位置有效性,这是与规范中"替换/清理前一目标状态"直接对应的实现机制;
  • cycle: 0 | 1pulse脉冲计数:每次 flash 递增 pulse,cycle = pulse % 2,用于驱动 CSS 动画的交替触发(同一节点连续点击两次也能重新播放动画);
  • duration默认 1600msNavigationFeedbackPlugin.tsoptions.duration: 1600flashTarget内 fallback 为 800ms。

插件通过extendEditorApi暴露查询面、通过extendEditorTransforms暴露命令面:

// api 面(查询) editor.api.navigation.activeTarget() editor.api.navigation.clear() editor.api.navigation.isTarget(path) // transforms 面(命令) editor.tf.navigation.clear() editor.tf.navigation.flashTarget(options) editor.tf.navigation.navigate(options)

flashTarget的实现要点transforms/flashTarget.ts):

  • 使用WeakMap<SlateEditor, timeout>WeakMap<SlateEditor, pulse>保存每编辑器的定时器与脉冲计数,避免污染编辑器对象;
  • 新 flash 会先clearNavigationTimeout+ 清理上一目标的 DOM 属性与pathRef,保证确定性替换
  • 通过editor.api.toDOMNode(node)拿到目标 DOM 元素,直接设置四个属性:
    • data-nav-target="true"
    • data-nav-highlight={variant}(默认navigated
    • data-nav-cycle={0|1}
    • data-nav-pulse={n}
    • 以及 CSS 变量--plate-nav-feedback-duration: {duration}ms
  • 超时后调用clearNavigationFeedbackTarget(editor, pulse)带 pulse 校验——只有脉冲匹配时才清理,避免后发的 flash 被先发的定时器误清。

navigate的实现要点transforms/navigate.ts):

  • 按顺序执行:select(若提供 point/range)→focusscrollIntoView(滚动目标点优先级为scrollTarget>select.focus>select.anchor>selectpoint >editor.api.start(target.path))→flashTarget(除非flash: false);
  • 这正是契约文档中"navigate 把 select、focus、scroll、flash 协调在一起"的落地。

6.2 React 层:packages/core/src/react/plugins/navigation-feedback/

仓库实际文件为:

  • NavigationFeedbackPlugin.ts
  • useNavigationHighlight.ts
  • NavigationFeedbackPlugin.spec.tsx
  • index.ts

(规范草案中的 hook 名为useNavigationFeedback.ts,最终落地为useNavigationHighlight.ts,这是文档与实现的唯一命名差异。)该 hook 供渲染器消费活跃导航目标,配合 node-prop 注入缝完成高亮属性的渲染面适配。

6.3 脚注消费者:选中驱动 navigate

packages/footnote/src/lib/transforms/focusFootnoteDefinition.tsfocusFootnoteReference.ts中,脚注跳转已改为调用共享 API:

return editor.tf.navigation.navigate({ ... });

测试 insertFootnote.spec.ts 中也可以看到对editor.api.navigation.activeTarget()的断言(如 L307、L324、L363),验证了跳转后活跃目标状态确实被设置。

6.4 TOC 消费者:flash-first

在 useContentController.ts 中,TOC 点击路径通过editor.tf.navigation.flashTarget(...)复用共享闪烁语义(见 L74 附近),且不强制文本选区——正是规范 Phase 1B 所要求的"保留 TOC 当前滚动行为、复用共享闪烁计时/替换语义、不强制选区"。

调试记录 2026-04-07-debug-toc-demo-nav-progress.md 证实了浏览器端的实际表现:在/blocks/toc-demo点击Benefits of Using TOC后,产生了一行aria-current="location"的目录条目,以及一个data-nav-target="true"的标题高亮。

6.5 搜索:仍在延迟清单

搜索跳转的高亮仍处于延迟状态,符合规范 Phase 2 的决策——在rangevsnode目标语义明确之前不接入。相关计划(如 2026-04-09-editor-behavior-replan-next-batch.md)仍将其列为"shared navigation feedback for search jumps"待办。


7. 交接与协作指引

契约文档还给出了执行协作的参考:

  • 建议推理强度(by lane):core 契约 + 包边界为high;功能集成、测试与浏览器验证为medium
  • 按角色分工architect压测 core/插件归属与 API 形态;executor实现 core 插件与首批消费者;test-engineer补充单元/集成/浏览器覆盖;code-reviewer做最终 API 与分层评审;verifier提供完成证据;
  • 验证路径:包测试先行 → 应用集成测试其次 → 浏览器验证最后,之后才把契约视为"真实"。

8. 小结:把"跳转后高亮"沉淀为共享编辑器定律

回顾2026-04-06-navigation-feedback-spec.md的 Outcome,本规范要达成的三项目标在仓库中均有对应:

  1. EDIT-NAV-FEEDBACK-*成为共享跨界面定律——已写入 markdown-editing-spec.md(locked)与 editor-protocol-matrix.md;
  2. TOC、脚注、搜索跳转显式复用同一个瞬态导航反馈原语——脚注与 TOC 已在源码中调用editor.tf.navigation.flashTarget/navigate,搜索按计划延迟;
  3. 架构文档将导航反馈视为共享编辑器领域而非每功能各写各的 hack——editor-behavior-architecture.md 承接此定位。

对于后续想要扩展新跳转面(讨论/评论锚点、自定义大纲、未来的 range 高亮)的开发者,正确姿势是:功能包只负责目标解析(resolve target),然后调用 core 的editor.tf.navigation.flashTarget(...)editor.tf.navigation.navigate(...),让共享契约统一处理焦点、滚动、闪烁与状态替换——这就是本文从规范到源码所呈现的完整闭环。

<输出文章>

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

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

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

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

立即咨询