CKEditor 4 内联编辑器 Copy Formatting 粘滞模式(Sticky Mode)完整指南:原理、配置与手动验证
CKEditor 4 内联编辑器 Copy Formatting 粘滞模式(Sticky Mode)完整指南:原理、配置与手动验证
本文围绕 tests/plugins/copyformatting/manual/stickyinline.md 这份手动测试文档展开,深入讲解 CKEditor 4 中 Copy Formatting 插件的「粘滞模式」(sticky mode)在**内联编辑器(inline editor)**下的行为、底层实现、可配置项与验证方法。读完本文,你将掌握:如何通过双击工具栏按钮进入粘滞模式并连续给多处文本套用格式、粘滞模式与普通模式在命令状态与光标反馈上的差异、如何用
Escape或再次点击按钮退出粘滞模式,以及如何结合源码与 Bender 测试验证这一功能。
一、什么是 Copy Formatting 的粘滞模式
Copy Formatting 是 CKEditor 4 从 4.6.0 版本起提供的「格式刷」类插件,位于 plugins/copyformatting/plugin.js。它允许用户从一个已格式化区域提取样式(如加粗、删除线、字体颜色、背景色、内联样式与属性),再通过鼠标点击或键盘快捷键把这些样式应用到其他文本上。
其中,「粘滞模式」(sticky mode)是一种可连续复用的复制格式状态:在普通模式下,每应用一次格式,复制格式随即自动关闭;而在粘滞模式下,应用一次格式后复制格式不会自动退出,可以反复点击多处文本持续套用同一组样式。
stickyinline.md 这份手动测试用例专门验证内联编辑器(通过 CKEDITOR.inline() 创建、可编辑区域直接嵌在页面 DOM 中)下的粘滞模式行为,其测试页面为 stickyinline.html,页面内嵌的示例内容是一段包含 strong、u、s、带内联样式 span 和链接的“Apollo 11”段落,非常适合用来观察样式提取与套用。
二、手动验证步骤(继承自原文档)
stickyinline.md 给出的操作流程如下(测试环境需加载插件:copyformatting, toolbar, wysiwygarea, floatingspace, basicstyles):
- 将光标放置到文本的已格式化部分内部(例如
strong/u/带color样式的span中); - 双击工具栏上的「Copy Formatting」按钮,切换到粘滞(sticky)模式;
- 用鼠标点击或选中文本中另一处未格式化的部分;
- 再点击另一处未格式化文本。
预期结果(Expected):
- 应用样式后粘滞模式不会被关闭;
- 其他工具栏按钮保持正确的状态。
非预期结果(Unexpected):
- 应用样式后粘滞模式被关闭;
- 其他工具栏按钮出现错误状态。
该用例对应的兄弟用例 sticky.md 适用于经典(iframe 型)编辑器,其「Things to check」清单还补充了粘滞模式的其他验收点:样式仅用鼠标即可连续复制到多个元素;可通过再次点击「Copy Formatting」按钮关闭粘滞模式;可通过按 Escape 键关闭;应用样式后按钮状态不变;页面级光标在点击按钮前保持特殊形态、点击按钮后才恢复默认光标;可编辑区域的光标同样在点击按钮后才恢复。这些验收点在写自动化回归测试时可一并参考。
三、粘滞模式的底层实现
3.1 状态对象与 sticky 标志
每个编辑器实例都会创建独立的 CKEDITOR.plugins.copyformatting.state 状态对象,挂在 editor.copyFormatting 上(见 plugins/copyformatting/plugin.js)。该对象的关键属性包括:
styles:当前已复制的一组CKEDITOR.style[];sticky:布尔值,指示是否处于粘滞模式(plugin.js);filter:基于copyFormatting_allowRules与copyFormatting_disallowRules构建的高级内容过滤器(ACF)实例,用于约束「允许从哪些元素提取样式」与「明确禁止提取样式的元素」。
3.2 双击按钮如何开启粘滞模式
工具栏按钮的初始化逻辑位于 plugin.js(isToggle: true,命令为 copyFormatting,归入 toolbar: 'cleanup,0')。在 contentDom 阶段,插件为按钮元素注册了 dblclick 监听器(plugin.js):
editable.attachListener( copyFormattingButtonEl, 'dblclick', function() {
editor.execCommand( 'copyFormatting', { sticky: true } );
} );
也就是说,双击等价于以 { sticky: true } 数据执行 copyFormatting 命令。命令执行体(plugin.js)的逻辑是:
- 若命令已处于
TRISTATE_ON(即复制格式已激活),则再次执行会被视为取消:清空styles、置sticky = false、移除光标相关 CSS 类、恢复粘贴快捷键,并把命令状态置为TRISTATE_OFF——这正是“再次单击/双击按钮关闭粘滞模式”的实现; - 否则执行样式提取:
copyFormatting.styles = plugin._extractStylesFromElement( editor, editor.elementPath().lastElement ),把命令状态置为TRISTATE_ON,并按配置附加光标类,最后copyFormatting.sticky = isSticky。
注意一个细节:即使普通模式,若命令以 { from: 'keystrokeHandler' }(即通过快捷键 Ctrl+Shift+C)触发,isSticky 也会被计算为 true(plugin.js),这与 keystroke.md 中「用 Ctrl+Shift+C 复制、Ctrl+Shift+V 可多次粘贴」的验收点一致。
3.3 应用样式后为何粘滞模式不退出
命令 applyFormatting 的执行体(plugin.js)在完成 plugin._applyFormat( editor, copyFormatting.styles ) 后,有一个关键分支:
if ( !copyFormatting.sticky ) {
copyFormatting.styles = null;
cursorContainer.removeClass( 'cke_copyformatting_active' );
documentElement.removeClass( 'cke_copyformatting_disabled' );
documentElement.removeClass( 'cke_copyformatting_tableresize_cursor' );
cmd.setState( CKEDITOR.TRISTATE_OFF );
plugin._detachPasteKeystrokeHandler( editor );
}
只有 sticky 为假时,才清空已复制的样式、复位按钮状态并卸载粘贴快捷键。因此粘滞模式下每次套用格式后,styles 与 TRISTATE_ON 状态得以保留,用户可以继续点击下一处文本——这正是本文主题用例第一项“Expected”所验证的行为。
3.4 鼠标事件驱动:点击即可套用格式
无论是内联编辑器还是经典编辑器,插件在 contentDom 阶段都会注册两个 mouseup 监听(plugin.js):
- 在鼠标宿主元素(
mouseupHost)上的mouseup:当左键抬起且copyFormatting命令处于TRISTATE_ON时,执行applyFormatting; - 在
CKEDITOR.document上的mouseup:当点击发生在可编辑区域之外且复制格式仍处于激活态时,执行copyFormatting(即自动取消)。
关于 mouseupHost 的选取(plugin.js):对内联编辑器取 editable 本身;对经典编辑器取整个 editor.document(否则点击 body 边缘空白不会触发事件);对启用了 divarea 插件的编辑器则按内联方式处理,避免点击整个文档干扰焦点。
源码注释中还标注了与该功能相关的回归缺陷编号(#2780、#2655、#2470):仅当确实复制了样式时才执行 applyFormatting,避免在没有样式的情况下误触。
3.5 内联编辑器下的特殊处理
本主题文档面向内联编辑器,源码中有多处对内联场景的专门处理:
_getCursorContainer(plugin.js):内联编辑器的光标容器是editor.editable()本身;经典编辑器则是可编辑区域的父元素(iframe 的 documentElement)。- 样式提取时的上溯边界(plugin.js):当遍历到
element.contains( editor.editable() ) || element.equals( editor.editable() )时,事件extractFormatting会被evt.cancel(),即内联编辑器的cke_editable元素本身不参与样式提取。 - 过滤检查:提取出的每个样式都要经过
editor.copyFormatting.filter.check( new CKEDITOR.style( style ), true, true ),不合规的样式会被拒绝。
四、光标反馈:粘滞模式的视觉状态
粘滞模式(以及普通激活态)会通过 CSS 类改变光标形态,这部分由 plugins/copyformatting/styles/copyformatting.css 定义:
.cke_copyformatting_active:作用于可编辑区域/页面,将光标替换为自定义cursor.svg(带!important,用于压过 Table Resize 插件设置的行内光标样式);.cke_copyformatting_disabled:作用于整个页面,表示“此位置无法套用复制格式”,光标替换为cursor-disabled.svg(IE/Edge 的contenteditable不支持自定义光标,注释中给出了对应的微软反馈工单编号);.cke_copyformatting_tableresize_cursor:避免表格调整大小光标与复制格式光标冲突。
在命令执行中,进入激活态时向 cursorContainer 添加 cke_copyformatting_active、向 documentElement 添加 cke_copyformatting_tableresize_cursor,并按配置 copyFormatting_outerCursor(默认 true)决定是否给整个页面加 cke_copyformatting_disabled(plugin.js)。退出(取消或非粘滞模式下应用完成)时统一移除这些类。测试辅助函数 assertCopyFormattingState/assertApplyFormattingState(tests/plugins/copyformatting/_helpers/tools.js)正是通过这些 CSS 类的有无来断言“按钮处于 On/Off、粘滞模式是否保持、样式是否保留”。
五、退出粘滞模式的三种途径
综合源码与 sticky.md 的验收清单,退出粘滞模式有以下途径:
- 再次点击(或双击)「Copy Formatting」按钮:命令已处于
TRISTATE_ON,再次执行走“取消”分支; - 按
Escape键:插件在key事件中检测domEvent.getKeystroke() === 27,若命令处于TRISTATE_ON则执行copyFormatting命令完成取消(plugin.js); - 点击可编辑区域外部:全局
mouseup监听器会执行copyFormatting自动取消(见 3.4)。
另外,单元测试 tests/plugins/copyformatting/copyformatting.js 中“cancelling Copy Formatting command (Escape)”与“from keystroke (Escape)”两个用例,验证了按 Escape 后命令回到 TRISTATE_OFF、styles 置空、并输出 canceled 无障碍通知。
六、粘滞模式下“其他工具栏按钮状态正确”的含义
stickyinline.md 的 Expected 第二条是“其他工具栏按钮有正确状态”。这是因为复制格式在激活期间会持续拦截与格式相关的交互,最典型的是:
- 粘贴快捷键的动态接管:进入复制格式时,
_attachPasteKeystrokeHandler(plugin.js)会把copyFormatting_keystrokePaste(默认Ctrl+Shift+V)临时绑定到applyFormatting命令,并保存原先绑定的命令;退出时由_detachPasteKeystrokeHandler恢复。粘滞模式下applyFormatting不会触发 detach,因此快捷键会一直保持指向applyFormatting,直到取消——这正是 keystrokes.js 中 “keystroke attached on format copy (sticky) and not detached on format apply” 用例的断言内容。 - 按钮状态同步:
copyFormatting命令的状态直接驱动“Copy Formatting”按钮的 On/Off 切换(按钮声明了command: 'copyFormatting'),同时插件通过_putScreenReaderMessage(plugin.js)向aria-live容器输出copied、applied、canceled、failed等无障碍通知,辅助屏幕阅读器用户感知状态变化。
因此,“状态错误”通常表现为:非粘滞模式应用一次后按钮未复位、粘滞模式应用一次后按钮提前复位、或 Escape 未能关闭复制格式等,这些正是 copyformatting.js 中 test sticky Copy Formatting 用例(以 { sticky: true } 执行命令、断言应用后状态仍为激活)所回归保护的场景。
七、相关配置项速查(自 4.6.0 起)
以下配置均在 plugins/copyformatting/plugin.js 中声明默认值,可按需在 config.js 或初始化对象中覆盖:
| 配置项 | 默认值 | 说明 |
|---|---|---|
copyFormatting_outerCursor |
true |
复制格式激活时是否给整个页面附加“禁用”光标,提示某些区域不可套用格式 |
copyFormatting_allowRules |
'b s u i em strong span p div td th ol ul li(*)[*]{*}' |
允许提取样式的元素规则(ACF 语法);设为 true 可完全禁用过滤 |
copyFormatting_disallowRules |
'*data-cke-widget*,data-widget*,data-cke-realelement' |
明确禁止提取样式的元素规则(默认排除 widgets 与真实元素占位符) |
copyFormatting_allowedContexts |
true |
允许的套用上下文:'text'(文本)、'list'(列表)、'table'(表格);true 表示全部允许,也可传数组如 [ 'text' ] |
copyFormatting_keystrokeCopy |
CKEDITOR.CTRL + CKEDITOR.SHIFT + 67(Ctrl+Shift+C) |
复制格式快捷键;设为 false 可关闭 |
copyFormatting_keystrokePaste |
CKEDITOR.CTRL + CKEDITOR.SHIFT + 86(Ctrl+Shift+V) |
套用格式快捷键;设为 false 可关闭 |
配置示例(同时限定上下文、关闭页面级禁用光标并自定义快捷键):
CKEDITOR.inline( 'editor1', {
allowedContent: true,
copyFormatting_outerCursor: false,
copyFormatting_allowedContexts: [ 'text', 'list' ],
copyFormatting_keystrokeCopy: CKEDITOR.CTRL + CKEDITOR.SHIFT + 66 // Ctrl+Shift+B
} );
关于 ACF 过滤:状态对象构建时 this.filter = new CKEDITOR.filter( editor, editor.config.copyFormatting_allowRules ),若 copyFormatting_allowRules === true 则 this.filter.disabled = true(完全关闭过滤),若设置了 copyFormatting_disallowRules 则进一步 disallow(...)(plugin.js)。也就是说,复制/套用样式的“白名单/黑名单”均由这个运行时过滤器决定,与编辑器本身的 ACF 相互独立。
八、如何复现与回归验证
在仓库中,这套功能有完整的手动测试与自动化测试双重覆盖:
- 手动测试:用 Bender 加载 tests/plugins/copyformatting/manual/stickyinline.html(内联编辑器)或 tests/plugins/copyformatting/manual/sticky.html(经典编辑器),按第 2 节步骤操作,核对 Expected/Unexpected 清单;同一目录下的 manual 用例全集(如
keystroke、mousebutton、links、lists、tables、undointegration等)覆盖了粘滞模式与其他功能组合的边界场景。 - 自动化测试:
- tests/plugins/copyformatting/copyformatting.js:
test sticky Copy Formatting用例通过editor.execCommand( 'copyFormatting', { sticky: true } )进入粘滞模式,套用格式后断言按钮仍处于激活态、样式仍保留; - tests/plugins/copyformatting/keystrokes.js:验证粘滞模式下套用格式后粘贴快捷键不被卸载、以及取消后正确恢复;
- 测试辅助 tests/plugins/copyformatting/_helpers/tools.js 中的
assertCopyFormattingState/assertApplyFormattingState统一断言命令状态、styles数组与光标 CSS 类,可作为理解预期行为的权威参考。
- tests/plugins/copyformatting/copyformatting.js:
九、总结
粘滞模式是 CKEditor 4 Copy Formatting 插件面向“连续批量套用格式”场景的关键能力:它由 copyFormatting 命令执行时传入的 { sticky: true } 开启,通过 state.sticky 标志控制 applyFormatting 完成后是否清理样式与复位命令状态,并配以自定义光标、临时快捷键接管、无障碍通知与 CSS 类切换来提供完整的交互反馈。内联编辑器场景下,样式提取会在 cke_editable 边界停止,光标容器直接作用于可编辑元素本身。若要在自己的页面中验证或集成该能力,可依照 stickyinline.md 的步骤操作,并结合 plugin.js 中的配置项与 copyformatting.js 的测试断言来确认行为是否符合预期。