Editor.js Caret 模块完全指南从 Block 间光标定位到焦点导航【免费下载链接】editor.jsA block-style editor with clean JSON output项目地址: https://gitcode.com/gh_mirrors/ed/editor.js导读本文以 Editor.js 官方文档 docs/caret.md 为骨架系统讲解块级编辑器核心模块 Caret 的设计与使用。你将在文中看到 Caret 模块的setToBlock、setToTheLastBlock等方法如何在RangeAPI 之上工作以及如何通过editor.caret公共 API 编程式控制光标位置实现“跳到首块/末块/上一块/下一块”等实战能力并理解底层 DOM 算法与键盘导航的完整调用链。一、Caret 模块是什么Editor.js 是一个块样式编辑器其内容由多个 Block 组成。要让用户在块与块之间顺畅移动光标、让工具Tool能通过 API 精确摆放光标位置就需要一个专门负责“光标”的模块——Caret。按照 docs/caret.md 的描述TheCaretmodule contains methods working with caret. Uses Range methods to navigate caret between blocks.Caret 模块包含所有与光标caret相关的方法底层基于浏览器的 Range API 在 Block 之间导航光标。同时Caret 类实现了基础的 Module 类从而持有用户配置User configuration和默认的 Editor.js 实例引用。从源码看Caret 类继承自Module见 src/components/modules/caret.ts通过this.Editor访问 BlockManager、BlockSelection 等其他模块。Caret 类自身也定义了一组合法的位置常量public get positions(): {START: string; END: string; DEFAULT: string} { return { START: start, END: end, DEFAULT: default, }; }start光标置于 Block 起始位置end光标置于 Block 末尾default保持默认行为若传入 offset 则应用偏移二、核心方法 setToBlock把光标放进指定 BlocksetToBlock是 Caret 模块最核心的方法官方文档定义其签名为Caret.setToBlock(block, position, offset)Method gets Block instance and puts caret to the text node with offset方法接收一个 Block 实例将光标放入其文本节点并应用偏移。三个参数的含义如下表继承自 docs/caret.mdParamTypeDescriptionblockObjectBlock instance that BlockManager createdpositionStringCan be start, end or default. Other values will be treated as default. Shows position of the caret regarding to the Block.offsetNumbercaret offset regarding to the text node (Default: 0)2.1 源码级执行流程在 src/components/modules/caret.ts 中setToBlock的实现主要分三步清除旧选区调用BlockSelection.clearSelection()避免残留选中状态影响新定位。处理不可聚焦的 Block如果block.focusable为 false例如 Delimiter 分隔线这类无输入的工具则移除当前选区、高亮该 BlockBlockSelection.selectBlock(block)并更新currentBlock而不是强行放入光标。定位输入元素根据 position 选取目标元素start→block.firstInputend→block.lastInputdefault含其他任意值→block.currentInput随后针对三种 position 计算精确的节点与偏移START取输入元素内最深的第一个节点$.getDeepestNode(element, false)偏移固定为 0END取最深的最后一个节点$.getDeepestNode(element, true)偏移为节点内容长度$.getContentLength(nodeToSet)DEFAULT调用$.getNodeByOffset(element, offset)把“相对 Block 内容”的偏移换算成“某个具体文本节点内的偏移”若换算失败如空 Block则退回最深节点、偏移 0。最后调用this.set(nodeToSet, offsetToSet)真正放置光标并同步BlockManager的当前块与当前输入引用。2.2 set 方法放置光标并滚动到可视区域set是放置光标的最终动作其内部调用Selection.setCursor(element, offset)创建Range并设置选区见 src/components/selection.ts。在此基础上set还会做可视区域校正const scrollOffset 30; const { top, bottom } Selection.setCursor(element, offset); if (top 0) { window.scrollBy(0, top - scrollOffset); } else if (bottom innerHeight) { window.scrollBy(0, bottom - innerHeight scrollOffset); }如果光标新位置超出视口上方top 0或下方bottom innerHeight就通过window.scrollBy滚动窗口并预留 30px 的留白。这就是为什么用 API 把光标放到很远处的 Block 时页面会自动滚过去——用户不会“丢失光标”。2.3 两个方向的最深节点搜索setToBlock中反复出现的$.getDeepestNode定义在 src/components/dom.ts它沿 DOM 树向“第一个子节点 / nextSibling”或“最后一个子节点 / previousSibling”方向递归找到可容纳光标的最深层节点文本节点或元素节点。这保证了即便 Block 内部嵌套了b、i等格式化标签光标也能落到位。而 DEFAULT 分支使用的$.getNodeByOffsetsrc/components/dom.ts则用document.createTreeWalker遍历文本节点把“相对 Block 内容起点的总偏移”逐步累加映射到具体的{ node, offset }从而支持“把光标放到第 N 个字符”的精确操作。三、setToTheLastBlock定位到最后一个 Block官方文档给出了另一个方法Caret.setToTheLastBlock()sets Caret at the end of last Block. If last block is not empty, inserts another empty Block which is passed as initialsetToTheLastBlock会把光标放到最后一个 Block 的末尾如果最后一个 Block 非空则先追加一个新的空 Block 再放入光标。源码见 src/components/modules/caret.tspublic setToTheLastBlock(): void { const lastBlock this.Editor.BlockManager.lastBlock; if (!lastBlock) { return; } if (lastBlock.tool.isDefault lastBlock.isEmpty) { this.setToBlock(lastBlock); } else { const newBlock this.Editor.BlockManager.insertAtEnd(); this.setToBlock(newBlock); } }这里的判定逻辑值得注意只有当最后一个 Block 恰好是默认工具tool.isDefault且内容为空时才直接复用该 Block否则调用BlockManager.insertAtEnd()追加新块新块会被渲染成初始工具再把光标放进去。这一设计保证了编辑区底部永远有一个可输入的空块避免光标落在不可输入的工具上——这也是块编辑器“永远能继续写下去”的关键机制。四、公共 APIeditor.caret文档描述的 Caret 模块是内部模块但对开发者而言日常使用的是暴露给外部的editor.caretAPI。其类型定义见 types/api/caret.d.ts实现位于 src/components/modules/api/caret.ts。可用方法如下方法参数说明setToFirstBlockposition?,offset?光标置于第一个 BlocksetToLastBlockposition?,offset?光标置于最后一个 BlocksetToPreviousBlockposition?,offset?光标置于上一个 BlocksetToNextBlockposition?,offset?光标置于下一个 BlocksetToBlockblockOrIdOrIndex,position?,offset?光标置于指定 Block见下文focusatEnd?聚焦编辑器atEndtrue时置于末尾所有方法都返回boolean表示是否成功定位例如当前没有上一个/下一个 Block 时返回false。position 参数复用 Caret 模块的三种取值start、end、default默认值offset 默认 0。4.1 setToBlock 支持三种入参editor.caret.setToBlock的第一个参数是联合类型BlockAPI | BlockAPI[id] | number即可以传入Block 索引editor.caret.setToBlock(0)Block ideditor.caret.setToBlock(some-block-id)BlockAPI 实例const block editor.blocks.getById(id); editor.caret.setToBlock(block)内部通过resolveBlock统一解析见 src/components/modules/api/caret.ts解析失败返回false。这一弹性入参在测试用例中被完整覆盖见 test/cypress/tests/api/caret.cy.ts。4.2 focus 与偏移的实测行为focus(atEnd)的实现在 src/components/modules/api/caret.tsatEnd为真时定位到最后一个 Block 的end否则定位到第一个 Block 的start。关于offset的边界行为test/cypress/tests/api/caret.cy.ts 提供了几个有价值的实测断言纯文本Plain text content.中传offset5光标精确落在第 5 个字符处range.startOffset 5含 HTML 的内容1234b567/b!中传offset6光标会落在b内的文本节点567上、偏移 2即“12345”之后偏移超过内容长度时如contentLength 10光标被安全钳制在内容末尾不会越界报错嵌套结构123b456i789/i/b!同样能正确解析出目标节点与偏移。也就是说offset 是相对整个 Block 文本内容的偏移Caret 内部会把它换算到具体的文本节点超界时自动收敛到末尾。五、键盘导航navigateNext / navigatePrevious除了直接放置光标Caret 模块还承担键盘焦点导航职责。navigateNext(force)与navigatePrevious(force)src/components/modules/caret.ts负责在“当前 Block 的多个输入如标题与正文之间”以及“相邻 Block 之间”移动光标navigateNext优先聚焦当前块的nextInput若有否则跳转到nextBlock当没有下一个 Block 且当前块不是默认工具时自动insertAtEnd()追加默认块再跳入对应 issue #1103 的“从末尾非默认工具退出”场景若当前块是默认块则不做处理对应 #1414。navigatePrevious优先聚焦previousInput否则跳转previousBlock的末尾。两者的“是否允许导航”判定规则一致// navigateNext const isAtEnd currentInput ! undefined ? caretUtils.isCaretAtEndOfInput(currentInput) : undefined; const navigationAllowed force || isAtEnd || !currentBlock.focusable; // navigatePrevious const caretAtStart currentInput ! undefined ? caretUtils.isCaretAtStartOfInput(currentInput) : undefined; const navigationAllowed force || caretAtStart || !currentBlock.focusable;传forcetrue时无条件导航用于 Tab 键光标位于输入末尾或开头时允许导航Block 本身不可聚焦如 Delimiter时也允许导航。其中isCaretAtEndOfInput/isCaretAtStartOfInput来自 src/components/utils/caret.ts。它们对原生输入input/textarea直接比较selectionEnd与值长度/0对contenteditable则用checkContenteditableSliceForEmptiness判断光标左右两侧是否只剩不可见空白折叠空白从而正确处理nbsp;与普通空格的区别。5.1 键盘事件接线这些导航方法由 src/components/modules/blockEvents.ts 中的键盘处理器触发Tab / ShiftTabL204-L221Caret.navigateNext(true)/Caret.navigatePrevious(true)导航成功则preventDefault()否则保留浏览器原生 Tab 行为跳出编辑器方向键L563-L628DOWN/RIGHT非 RTL触发navigateNext()UP/LEFT非 RTL触发navigatePrevious()并支持 RTL 方向翻转Enter 在特定输入间跳转当首个输入未聚焦时按 Enter 跳转到下一输入L379-L382、末个输入已聚焦时调用navigateNext()L463-L466。六、其他工具方法除上述定位与导航方法外Caret 模块还提供若干辅助能力均在 src/components/modules/caret.tssetToInput(input, position, offset)L127-L147把光标放到指定的输入元素上支持start/end/default三种位置并在结束后更新currentBlock.currentInput。extractFragmentFromCaretPosition()L197-L233从光标位置到 Block 末尾截取内容片段。对原生输入直接基于value.substring(selectionStart)切分对 contenteditable 则克隆 Range 后extractContents()。这是“光标处拆分文本/换行拆分”类功能的基础。createShadow(element)/restoreCaret(element)L347-L382创建/恢复“影子光标”。createShadow在目标元素末尾插入一个cdx-shadow-caret的span作为占位restoreCaret通过Selection.expandToTag选中并移除该占位从而在需要暂时“隐藏”真实光标时保持位置记忆。insertContentAtCaretPosition(content)L389-L422在光标处插入 HTML 内容将内容包进DocumentFragment删除原选区内容后insertNode并把新光标放到插入内容的末尾——注意它专门处理了空 fragment 与文本节点/元素节点的差异保证跨浏览器光标位置正确。七、与其他模块的协作从源码调用关系看Caret 是编辑器中“被依赖度”极高的基础模块BlockManager提供firstBlock、lastBlock、previousBlock、nextBlock、currentBlock等导航上下文并承担insertAtEnd()等新增块操作BlockSelectionsetToBlock前会clearSelection()遇到不可聚焦块则selectBlock(block)高亮DOM 工具src/components/dom.tsgetDeepestNode、getContentLength、getNodeByOffset是定位算法的基石Selection 工具src/components/selection.tssetCursor完成最终的 Range 创建与选区设置BlockEvents键盘事件处理器把用户的 Tab/方向键/Enter 操作翻译成对 Caret 导航方法的调用。这种职责划分也解释了 docs/caret.md 中“Caret class implements basic Module class that holds User configuration and default Editor.js instances”的含义——Caret 与其他 Module 一样通过模块系统共享编辑器实例从而能无缝协调这些依赖。八、典型使用场景与示例下面给出通过公共 API 控制光标的常见用法可直接在浏览器控制台或自定义工具中运行const editor new EditorJS({ holder: editorjs, // ... 其他配置 }); // 等编辑器 ready 后再操作 editor.isReady.then(() { // 1. 聚焦编辑器开头 editor.caret.focus(); // 等价于 setToFirstBlock(start) // 2. 聚焦编辑器末尾自动保证末尾有空块可输入 editor.caret.focus(true); // 等价于 setToLastBlock(end) // 3. 跳到第一个 / 最后一个 Block editor.caret.setToFirstBlock(end); editor.caret.setToLastBlock(start); // 4. 相对当前块前后移动 editor.caret.setToNextBlock(start); editor.caret.setToPreviousBlock(end); // 5. 按索引 / id / BlockAPI 三种方式定位 editor.caret.setToBlock(0, start); // 第一个块 editor.caret.setToBlock(block-id, end); // 指定 id 的块 const block editor.blocks.getById(block-id); editor.caret.setToBlock(block, default, 5); // 第 5 个字符处 // 6. 返回值可用于判断是否定位成功 const moved editor.caret.setToNextBlock(end); if (!moved) { console.log(已经是最后一个块了); } });position 取值对照取值行为典型场景start光标置于 Block 内容最前从块首开始输入/替换end光标置于 Block 内容最后追加内容、聚焦末尾default保持默认行为可配合 offset精确定位到第 N 个字符九、小结Caret 模块是 Editor.js 光标体系的枢纽对外editor.caretAPI 让开发者能以一行代码把光标定位到任意 Block 的任意位置对内它借助Range、DOM 深度遍历与可视区域滚动校正为 Tab/方向键/Enter 等键盘导航提供了可靠的底层支撑。理解它的setToBlock三参数语义Block、position、offset、start/end/default三种位置模式以及navigateNext/navigatePrevious的“输入内 → 块间 → 自动补块”导航链就能在自定义工具和业务集成中精确掌控编辑器的光标行为。【免费下载链接】editor.jsA block-style editor with clean JSON output项目地址: https://gitcode.com/gh_mirrors/ed/editor.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
