CKEditor 5 typing 功能深度解析:输入与删除管道、撤销粒度与自动文本转换
【免费下载链接】ckeditor5Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.项目地址: https://gitcode.com/GitHub_Trending/ck/ckeditor5
导读
CKEditor 5 的@ckeditor/ckeditor5-typing包是整个编辑器"书写体验"的基石:它负责处理键盘输入、内容删除以及输入法组合(composition),同时内置了自动文本转换(autocorrect)能力,让(c)自动变成©、1/2自动变成½。本文将围绕该包的 API 文档与仓库源码,从"胶水插件 Typing"的架构出发,深入剖析输入管道、删除管道、撤销粒度配置,并给出自动文本转换的完整配置示例与底层实现原理。
一、功能总览:typing 包在编辑器中扮演什么角色
根据 typing.md 的说明,@ckeditor/ckeditor5-typing包实现两大能力:
- 文本输入与删除——处理用户的键入(inputting)与删除(deleting)操作;
- 自动文本转换(autocorrect)——把预定义片段自动转成更美观、更规范的形态。
在架构上,Typing插件是一个典型的"胶水插件(glue plugin)"。查看 src/typing.ts 可以确认,它自身不实现任何逻辑,而是通过requires声明加载两个核心插件:
export class Typing extends Plugin { public static get requires(): PluginDependenciesOf<[ Input, Delete ]> { return [ Input, Delete ]; } // ... }Input(见 src/input.ts):处理来自键盘或其他输入法的文本输入;Delete(见 src/delete.ts):处理Delete、Backspace等删除操作。
通常由 Essentials 自动启用
绝大多数编辑器预设无需手动注册Typing。查看 packages/ckeditor5-essentials/src/essentials.ts 可以看到,Essentials插件的依赖列表中已经包含Typing(与 Clipboard、Enter、SelectAll、ShiftEnter、Undo 等基础功能并列)。也就是说,只要你在插件列表中启用了Essentials,输入、删除与撤销的基本能力就会一并就绪。
二、安装与引入
该包是 CKEditor 5 开源聚合包的一部分,通过安装聚合包即可使用:
npm install ckeditor5随后在构建配置中引入所需插件:
import { ClassicEditor, Essentials, Typing, TextTransformation } from 'ckeditor5'; ClassicEditor .create( document.querySelector( '#editor' ), { licenseKey: '<YOUR_LICENSE_KEY>', // 或 'GPL' plugins: [ Essentials, TextTransformation, /* ... */ ], typing: { // 输入/删除与文本转换配置 } } ) .then( /* ... */ ) .catch( /* ... */ );注意:Typing通常随Essentials自动加载,无需显式添加;而TextTransformation是一个独立插件,需要按需加入plugins数组(详见下文第五节)。
三、输入管道:Input 插件如何把字符送进编辑器
Input插件(src/input.ts)的职责是"处理来自键盘或其他输入法的文本输入"。它的实现非常值得研究,以下是几个关键环节。
3.1 观察器与命令注册
在init()中,插件完成两件基础工作(src/input.ts):
view.addObserver( InsertTextObserver ); const insertTextCommand = new InsertTextCommand( editor, editor.config.get( 'typing.undoStep' ) || 20 ); editor.commands.add( 'insertText', insertTextCommand ); editor.commands.add( 'input', insertTextCommand ); // 向后兼容的别名InsertTextObserver(src/inserttextobserver.ts)负责监听视图层insertText、beforeinput等事件,把浏览器的原生输入行为翻译成编辑器可感知的事件;- 同时注册
insertText命令(并保留input别名),其底层实现见 src/inserttextcommand.ts。
3.2 基于 beforeinput 的输入队列
Input插件的核心机制是TypingQueue(输入队列)。由于浏览器在beforeinput事件触发时尚未真正修改 DOM,编辑器不能立即把字符写入模型,而是需要"等浏览器先把 DOM 改了,再验证并同步到模型"。
从源码可以看到以下事件处理链(src/input.ts):
- 监听到
beforeinput时,以high优先级冲刷上一次排队的内容; - 监听到
insertText事件时,把文本与选区(以ModelLiveRange形式存储,防止模型变化后选区失效)压入队列; - 通过
MutationObserver检测到相关 DOM 变化(mutations事件)时冲刷队列,把insertText真正执行到模型; - 内置 50ms 的防抖冲刷作为兜底,防止突变观察器未及时触发的极端情况(
flushDebounced,见 src/input.ts)。
这种设计保证了浏览器 DOM、编辑器视图与模型三者始终一致,尤其是在 Safari 等对非组合事件处理有特殊行为的浏览器中尤为重要。
3.3 输入法组合(composition)与 Android 特判
中文、日文等输入法依赖 IME 组合(composition)事件。Input插件做了细致的处理:
- 在
compositionstart时,如果当前选区非折叠,先通过model.deleteContent()删除选中内容,防止组合输入覆盖已选中文本(src/input.ts); - 在
compositionend时,以high优先级冲刷队列,确保所有组合字符在组合结束前写入模型;再以lowest优先级触发"组合后修复",清理被忽略的 DOM 变化(如 NBSP 与普通空格的差异)(src/input.ts); - 在 Android 上,英文输入也会触发组合事件,因此插件会对比目标范围内的已有文本与待插入文本,只插入差异部分(
env.isAndroid分支,src/input.ts),避免整词重复插入。
3.4 InsertTextCommand 的执行语义
InsertTextCommand.execute()(src/inserttextcommand.ts)遵循"先删后插"的两步式替换:
- 取出选区上原有的格式属性(如加粗、链接),保证替换后格式不丢失;
model.deleteContent( selection )删除旧内容;model.insertContent( ... )以保留的属性插入新文本。
命令还支持text、selection、range、resultRange四个参数(src/inserttextcommand.ts),其中selection与range二选一,resultRange用于控制插入后光标落点。
四、删除管道:Delete 插件与删除命令
Delete插件(src/delete.ts)处理Delete、Backspace以及其他导致内容删除的用户动作。
4.1 方向与命令注册
DeleteCommand在创建时需要指定方向(src/delete.ts):
forward(向前,即Delete)→ 注册为deleteForward命令(别名forwardDelete,向后兼容);backward(向后,即Backspace)→ 注册为delete命令。
4.2 删除的单位与序列
DeleteCommand.execute()支持三个关键选项(src/deletecommand.ts):
| 选项 | 说明 |
|---|---|
unit | 删除粒度:character、codePoint、word,默认为按字符删除 |
sequence | 长按按键时第几次触发删除事件(未松键),默认1 |
selection | 要删除的选区,默认使用当前模型选区 |
执行逻辑(src/deletecommand.ts):
- 若选区折叠,先按指定方向调用
model.modifySelection()扩展选区(启用treatEmojiAsSingleUnit,把 emoji 当作单一单位处理); - 计算被删除内容的原子变更数,并通过
TypingChangeBuffer累计到当前撤销批次(详见第六节); - 调用
model.deleteContent()完成删除。
4.3 空编辑器与空块的特殊处理
源码中针对两个易出错场景做了兜底:
- 空编辑器中按下删除:若当前 limit 元素为空且允许段落存在,则把整个内容替换为一个空段落(src/deletecommand.ts),避免编辑器陷入"无内容可选"的状态;
- 在首块空块中按 Backspace:若空块是 limit 元素的第一个子元素,则将其替换为段落(src/deletecommand.ts)。
4.4 组合输入期间的删除与撤销联动
- 在组合(composition)进行中,浏览器会直接修改 DOM(渲染器被禁用),此时
Delete插件不拦截默认行为,而是交由浏览器处理(src/delete.ts); Delete插件暴露了requestUndoOnBackspace()方法:若下一次用户动作是 Backspace,则撤销上一次变更。该方法被TextTransformation在完成自动转换后调用——这正是"把(c)转成©后按退格键能一次性撤销整个转换"的实现基础(src/delete.ts、src/texttransformation.ts)。
五、自动文本转换(TextTransformation / autocorrect)
这是 typing 包中最贴近用户、最可配置的功能。官方特性文档位于 packages/ckeditor5-typing/docs/features/text-transformation.md,仓库还提供了可直接体验的交互演示(text-transformation.html 与扩展演示 text-transformation-extended.html)。
5.1 开箱即用的默认转换
输入以下片段会立刻被替换为更美观的形式:
| 输入 | 输出 |
|---|---|
(tm) | ™ |
1/2 | ½ |
-> | → |
-- | – |
"foo" | “foo” |
5.2 预定义转换的完整清单(按组)
根据 src/typingconfig.ts 的文档说明,特性默认通过transformations.include启用以下四组转换:
排版组typography
ellipsis:...→…enDash:--→–emDash:---→—
引号组quotes
quotesPrimary:"Foo bar"→“Foo bar”quotesSecondary:'Foo bar'→‘Foo bar’
符号组symbols
trademark:(tm)→™registeredTrademark:(r)→®copyright:(c)→©
数学组mathematical
oneHalf:1/2→½oneThird:1/3→⅓twoThirds:2/3→⅔oneForth:1/4→¼threeQuarters:3/4→¾lessThanOrEqual:<=→≤greaterThanOrEqual:>=→≥notEqual:!=→≠arrowLeft:<-→←arrowRight:->→→
其他(未归组,但可直接按名引用)
quotesPrimaryEnGb:'Foo bar'→‘Foo bar’quotesSecondaryEnGb:"Foo bar"→“Foo bar”quotesPrimaryPl:"Foo bar"→„Foo bar”quotesSecondaryPl:'Foo bar'→‚Foo bar’
这些定义的源码实现可以在 src/texttransformation.ts 中找到,其中组与成员的关系由TRANSFORMATION_GROUPS表维护(src/texttransformation.ts)。
5.3 配置项:include、remove、extra
TextTransformationConfig提供三个互操作的配置项(src/typingconfig.ts):
include:完全覆盖默认列表。可以引用上面的组名、转换名,或直接书写自定义规则;remove:从 include 与 extra 合并后的列表中移除指定项;extra:在既有列表基础上追加自定义转换。
三者的优先级逻辑在normalizeTransformations()中实现(src/texttransformation.ts):先把include与extra合并,再过滤掉remove命中的项,最后展开组名并去重。值得注意的一个细节是:字符串形式的规则若在预定义表中找不到,会被静默过滤掉(见 src/texttransformation.ts),所以拼写组名/转换名时要格外小心。
示例一:使用include只保留指定组,并追加自定义规则
ClassicEditor .create( { // ... 其他配置 ... typing: { transformations: { include: [ // 只使用 'quotes' 与 'typography' 两组。 'quotes', 'typography', // 再加上一条自定义转换。 { from: 'CKE', to: 'CKEditor' } ], } } } ) .then( /* ... */ ) .catch( /* ... */ );示例二:使用remove与extra精确裁剪并扩展
ClassicEditor .create( { // ... 其他配置 ... typing: { transformations: { remove: [ // 不用 'symbols' 与 'quotes' 两组。 'symbols', 'quotes', // 也不要用这两条。 'arrowLeft', 'arrowRight' ], extra: [ // 自定义 emoji 转换。 { from: ':)', to: '🙂' }, { from: ':+1:', to: '👍' }, { from: ':tada:', to: '🎉' }, // 正则模式规则:必须用 $ 结尾,且所有片段都要用捕获组包裹。 // 下面这条把 ` "foo"` 变成 ` «foo»`。 { from: /(^|\s)(")([^"]*)(")$/, to: [ null, '«', null, '»' ] }, // `to` 还可以是回调函数:把句号/问号/感叹号后的首字母自动大写。 { from: /([.?!] )([a-z])$/, to: matches => [ null, matches[ 1 ].toUpperCase() ] } ], } } } ) .then( /* ... */ ) .catch( /* ... */ );5.4 规则格式:TextTypingTransformationDescription
一条转换规则由from和to两个字段构成(src/typingconfig.ts)。
from:字符串或正则
- 字符串:直接检查输入结尾是否与之匹配(内部会被转义并包装为
(from)$的正则,见normalizeFrom(),src/texttransformation.ts); - 正则:整个正则必须全部由捕获组覆盖,并且必须以
$结尾(因为它是与输入结尾做比较的)。
to:字符串、数组或函数
- 字符串:原样替换,但只适用于
from也是字符串的情况; - 数组:长度必须与
from正则的捕获组数量一致,null表示"该捕获组原样保留、不替换"; - 函数:接收正则匹配数组作为参数,返回上述数组;可用于实现大小写转换等动态逻辑。
to在内部会被规范化为"输入匹配数组、输出替换数组"的函数(见normalizeTo(),src/texttransformation.ts)。
5.5 底层执行流程与边界
从 src/texttransformation.ts 可以还原完整的运行机制:
- 动态开关:插件监听模型选区变化,当光标位于代码块(
codeBlock)或行内代码(code属性)内时自动禁用自身,避免误转换代码内容; - 文本监听:创建
TextWatcher(src/textwatcher.ts)监听键入与选区事件,通过testCallback逐个测试规范化后的转换规则; - 触发替换:命中
matched:data事件且当前批次属于键入操作(batch.isTyping)时,按捕获组把匹配文本替换为规则指定的内容,并继承原位置上的文本格式属性(如加粗); - 撤销联动:替换完成后调用
deletePlugin.requestUndoOnBackspace(),让一次退格即可撤销整个自动转换。
六、撤销粒度:typing.undoStep 配置
输入与删除的变更如何分组、何时产生一个"可撤销步骤"?答案在TypingChangeBuffer(src/utils/changebuffer.ts)中。
- 每个**批次(batch)**对应一个撤销步骤;
TypingChangeBuffer会把连续的小变更累积到同一批次里; - 当累积的原子变更数超过
limit时,自动开启新批次; limit即配置项typing.undoStep,默认值为20——大致意思是"每输入或删除约 20 个字符产生一个新的撤销步骤"。
undoStep在Input插件初始化时被读取(src/input.ts),DeleteCommand构造函数同样读取该配置(src/deletecommand.ts)。配置示例:
ClassicEditor .create( { typing: { // 值越小,撤销粒度越细(撤销步数越多);值越大,一次撤销删除/输入的内容越多。 undoStep: 50 } } ) .then( /* ... */ ) .catch( /* ... */ );七、TypingConfig 配置参考
TypingConfig是 typing 各特性的统一配置入口(src/typingconfig.ts),完整字段如下:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
typing.undoStep | number | 20 | 输入/删除的撤销粒度:约每累计 N 个字符产生一个新撤销步骤 |
typing.transformations | TextTransformationConfig | 四组预定义转换 | 自动文本转换配置(include/remove/extra) |
八、相关能力与验证途径
- 可运行演示:特性文档对应的交互演示位于 docs/_snippets/features/(
text-transformation.html、text-transformation-extended.html及其 JS 逻辑),覆盖默认转换与自定义规则(emoji、引号样式、自动大写)场景; - 手动测试页面:
manual/目录提供了覆盖输入(input.manual.html)、删除(delete.manual.html)、beforeinput(beforeinput.manual.html)、拼写检查、RTL、unicode、两步光标等场景的手工验证页; - 自动化测试:
tests/目录包含与源码一一对应的测试(如 tests/input.js、tests/delete.js、tests/texttransformation.js、tests/typing.js 等),可运行pnpm run test或pnpm run test:browser验证行为。
此外,与 typing 配合使用的生产力特性还包括:自动格式化(packages/ckeditor5-autoformat)、自动链接(packages/ckeditor5-link)、提及智能补全(packages/ckeditor5-mention)等,它们与TextTransformation一样,都建立在输入管道与TextWatcher的基础之上。
小结
@ckeditor/ckeditor5-typing表面上是"管打字"的底层包,实际却承载了三件关键事情:以beforeinput+ 队列机制保证输入可靠落盘、以方向化命令与粒度控制提供符合直觉的删除行为、以可配置的转换规则带来开箱即用的 autocorrect 体验。理解Typing → Input + Delete的插件结构、typing.undoStep的撤销语义以及include/remove/extra的配置组合,就能在集成 CKEditor 5 时精准掌控用户的输入与编辑体验。
【免费下载链接】ckeditor5Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.项目地址: https://gitcode.com/GitHub_Trending/ck/ckeditor5
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考