CKEditor 5 typing 功能深度解析:输入与删除管道、撤销粒度与自动文本转换
2026/9/16 9:31:12 网站建设 项目流程

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包实现两大能力:

  1. 文本输入与删除——处理用户的键入(inputting)与删除(deleting)操作;
  2. 自动文本转换(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):处理DeleteBackspace等删除操作。

通常由 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)负责监听视图层insertTextbeforeinput等事件,把浏览器的原生输入行为翻译成编辑器可感知的事件;
  • 同时注册insertText命令(并保留input别名),其底层实现见 src/inserttextcommand.ts。

3.2 基于 beforeinput 的输入队列

Input插件的核心机制是TypingQueue(输入队列)。由于浏览器在beforeinput事件触发时尚未真正修改 DOM,编辑器不能立即把字符写入模型,而是需要"等浏览器先把 DOM 改了,再验证并同步到模型"。

从源码可以看到以下事件处理链(src/input.ts):

  1. 监听到beforeinput时,以high优先级冲刷上一次排队的内容;
  2. 监听到insertText事件时,把文本与选区(以ModelLiveRange形式存储,防止模型变化后选区失效)压入队列;
  3. 通过MutationObserver检测到相关 DOM 变化(mutations事件)时冲刷队列,把insertText真正执行到模型;
  4. 内置 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)遵循"先删后插"的两步式替换:

  1. 取出选区上原有的格式属性(如加粗、链接),保证替换后格式不丢失;
  2. model.deleteContent( selection )删除旧内容;
  3. model.insertContent( ... )以保留的属性插入新文本。

命令还支持textselectionrangeresultRange四个参数(src/inserttextcommand.ts),其中selectionrange二选一,resultRange用于控制插入后光标落点。

四、删除管道:Delete 插件与删除命令

Delete插件(src/delete.ts)处理DeleteBackspace以及其他导致内容删除的用户动作。

4.1 方向与命令注册

DeleteCommand在创建时需要指定方向(src/delete.ts):

  • forward(向前,即Delete)→ 注册为deleteForward命令(别名forwardDelete,向后兼容);
  • backward(向后,即Backspace)→ 注册为delete命令。

4.2 删除的单位与序列

DeleteCommand.execute()支持三个关键选项(src/deletecommand.ts):

选项说明
unit删除粒度:charactercodePointword,默认为按字符删除
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

  • oneHalf1/2½
  • oneThird1/3
  • twoThirds2/3
  • oneForth1/4¼
  • threeQuarters3/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):先把includeextra合并,再过滤掉remove命中的项,最后展开组名并去重。值得注意的一个细节是:字符串形式的规则若在预定义表中找不到,会被静默过滤掉(见 src/texttransformation.ts),所以拼写组名/转换名时要格外小心。

示例一:使用include只保留指定组,并追加自定义规则

ClassicEditor .create( { // ... 其他配置 ... typing: { transformations: { include: [ // 只使用 'quotes' 与 'typography' 两组。 'quotes', 'typography', // 再加上一条自定义转换。 { from: 'CKE', to: 'CKEditor' } ], } } } ) .then( /* ... */ ) .catch( /* ... */ );

示例二:使用removeextra精确裁剪并扩展

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

一条转换规则由fromto两个字段构成(src/typingconfig.ts)。

from:字符串或正则

  • 字符串:直接检查输入结尾是否与之匹配(内部会被转义并包装为(from)$的正则,见normalizeFrom(),src/texttransformation.ts);
  • 正则:整个正则必须全部由捕获组覆盖,并且必须以$结尾(因为它是与输入结尾做比较的)。

to:字符串、数组或函数

  • 字符串:原样替换,但只适用于from也是字符串的情况;
  • 数组:长度必须与from正则的捕获组数量一致,null表示"该捕获组原样保留、不替换";
  • 函数:接收正则匹配数组作为参数,返回上述数组;可用于实现大小写转换等动态逻辑。

to在内部会被规范化为"输入匹配数组、输出替换数组"的函数(见normalizeTo(),src/texttransformation.ts)。

5.5 底层执行流程与边界

从 src/texttransformation.ts 可以还原完整的运行机制:

  1. 动态开关:插件监听模型选区变化,当光标位于代码块(codeBlock)或行内代码(code属性)内时自动禁用自身,避免误转换代码内容;
  2. 文本监听:创建TextWatcher(src/textwatcher.ts)监听键入与选区事件,通过testCallback逐个测试规范化后的转换规则;
  3. 触发替换:命中matched:data事件且当前批次属于键入操作(batch.isTyping)时,按捕获组把匹配文本替换为规则指定的内容,并继承原位置上的文本格式属性(如加粗);
  4. 撤销联动:替换完成后调用deletePlugin.requestUndoOnBackspace(),让一次退格即可撤销整个自动转换。

六、撤销粒度:typing.undoStep 配置

输入与删除的变更如何分组、何时产生一个"可撤销步骤"?答案在TypingChangeBuffer(src/utils/changebuffer.ts)中。

  • 每个**批次(batch)**对应一个撤销步骤;TypingChangeBuffer会把连续的小变更累积到同一批次里;
  • 当累积的原子变更数超过limit时,自动开启新批次;
  • limit即配置项typing.undoStep,默认值为20——大致意思是"每输入或删除约 20 个字符产生一个新的撤销步骤"。

undoStepInput插件初始化时被读取(src/input.ts),DeleteCommand构造函数同样读取该配置(src/deletecommand.ts)。配置示例:

ClassicEditor .create( { typing: { // 值越小,撤销粒度越细(撤销步数越多);值越大,一次撤销删除/输入的内容越多。 undoStep: 50 } } ) .then( /* ... */ ) .catch( /* ... */ );

七、TypingConfig 配置参考

TypingConfig是 typing 各特性的统一配置入口(src/typingconfig.ts),完整字段如下:

配置项类型默认值说明
typing.undoStepnumber20输入/删除的撤销粒度:约每累计 N 个字符产生一个新撤销步骤
typing.transformationsTextTransformationConfig四组预定义转换自动文本转换配置(include/remove/extra

八、相关能力与验证途径

  • 可运行演示:特性文档对应的交互演示位于 docs/_snippets/features/(text-transformation.htmltext-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 testpnpm 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),仅供参考

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

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

立即咨询