Editor.js Caret 模块完全指南:从 Block 间光标定位到焦点导航
2026/9/19 13:30:48 网站建设 项目流程

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 模块的setToBlocksetToTheLastBlock等方法如何在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:把光标放进指定 Block

setToBlock是 Caret 模块最核心的方法,官方文档定义其签名为:

Caret.setToBlock(block, position, offset)

Method gets Block instance and puts caret to the text node with offset

方法接收一个 Block 实例,将光标放入其文本节点并应用偏移。三个参数的含义如下表(继承自 docs/caret.md):

ParamTypeDescription
blockObjectBlock instance that BlockManager created
positionStringCan 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的实现主要分三步:

  1. 清除旧选区:调用BlockSelection.clearSelection(),避免残留选中状态影响新定位。
  2. 处理不可聚焦的 Block:如果block.focusable为 false(例如 Delimiter 分隔线这类无输入的工具),则移除当前选区、高亮该 Block(BlockSelection.selectBlock(block))并更新currentBlock,而不是强行放入光标。
  3. 定位输入元素:根据 position 选取目标元素:
    • startblock.firstInput
    • endblock.lastInput
    • default(含其他任意值)→block.currentInput

随后针对三种 position 计算精确的节点与偏移:

  • START:取输入元素内最深的第一个节点($.getDeepestNode(element, false)),偏移固定为 0;
  • END:取最深的最后一个节点($.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 分支使用的$.getNodeByOffset(src/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 initial

setToTheLastBlock会把光标放到最后一个 Block 的末尾;如果最后一个 Block 非空,则先追加一个新的空 Block 再放入光标。源码见 src/components/modules/caret.ts:

public 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()追加新块(新块会被渲染成初始工具),再把光标放进去。这一设计保证了编辑区底部永远有一个可输入的空块,避免光标落在不可输入的工具上——这也是块编辑器“永远能继续写下去”的关键机制。

四、公共 API:editor.caret

文档描述的 Caret 模块是内部模块,但对开发者而言,日常使用的是暴露给外部的editor.caretAPI。其类型定义见 types/api/caret.d.ts,实现位于 src/components/modules/api/caret.ts。可用方法如下:

方法参数说明
setToFirstBlockposition?,offset?光标置于第一个 Block
setToLastBlockposition?,offset?光标置于最后一个 Block
setToPreviousBlockposition?,offset?光标置于上一个 Block
setToNextBlockposition?,offset?光标置于下一个 Block
setToBlockblockOrIdOrIndex,position?,offset?光标置于指定 Block(见下文)
focusatEnd?聚焦编辑器;atEnd=true时置于末尾

所有方法都返回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.ts:atEnd为真时定位到最后一个 Block 的end,否则定位到第一个 Block 的start

关于offset的边界行为,test/cypress/tests/api/caret.cy.ts 提供了几个有价值的实测断言:

  • 纯文本'Plain text content.'中传offset=5,光标精确落在第 5 个字符处(range.startOffset === 5);
  • 含 HTML 的内容'1234<b>567</b>!'中传offset=6,光标会落在<b>内的文本节点'567'上、偏移 2(即“12345”之后);
  • 偏移超过内容长度时(如contentLength + 10),光标被安全钳制在内容末尾,不会越界报错;
  • 嵌套结构'123<b>456<i>789</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;
  • force=true时无条件导航(用于 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 / Shift+Tab(L204-L221):Caret.navigateNext(true)/Caret.navigatePrevious(true),导航成功则preventDefault(),否则保留浏览器原生 Tab 行为(跳出编辑器);
  • 方向键(L563-L628):DOWN/RIGHT(非 RTL)触发navigateNext(),UP/LEFT(非 RTL)触发navigatePrevious(),并支持 RTL 方向翻转;
  • Enter 在特定输入间跳转:当首个输入未聚焦时按 Enter 跳转到下一输入(L379-L382)、末个输入已聚焦时调用navigateNext()(L463-L466)。

六、其他工具方法

除上述定位与导航方法外,Caret 模块还提供若干辅助能力(均在 src/components/modules/caret.ts):

  • setToInput(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:提供firstBlocklastBlockpreviousBlocknextBlockcurrentBlock等导航上下文,并承担insertAtEnd()等新增块操作;
  • BlockSelectionsetToBlock前会clearSelection(),遇到不可聚焦块则selectBlock(block)高亮;
  • DOM 工具(src/components/dom.ts):getDeepestNodegetContentLengthgetNodeByOffset是定位算法的基石;
  • Selection 工具(src/components/selection.ts):setCursor完成最终的 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),仅供参考

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

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

立即咨询