CKEditor 4 内联编辑器 Copy Formatting 粘滞模式(Sticky Mode)完整指南:原理、配置与手动验证

原创2026-10-15 20:39:591,941 阅读
文章标签:前端UI组件

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):

  1. 将光标放置到文本的已格式化部分内部(例如 strong/u/带 color 样式的 span 中);
  2. 双击工具栏上的「Copy Formatting」按钮,切换到粘滞(sticky)模式;
  3. 用鼠标点击或选中文本中另一处未格式化的部分;
  4. 再点击另一处未格式化文本。

预期结果(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):

  1. 在鼠标宿主元素(mouseupHost)上的 mouseup:当左键抬起且 copyFormatting 命令处于 TRISTATE_ON 时,执行 applyFormatting;
  2. 在 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 的验收清单,退出粘滞模式有以下途径:

  1. 再次点击(或双击)「Copy Formatting」按钮:命令已处于 TRISTATE_ON,再次执行走“取消”分支;
  2. 按 Escape 键:插件在 key 事件中检测 domEvent.getKeystroke() === 27,若命令处于 TRISTATE_ON 则执行 copyFormatting 命令完成取消(plugin.js);
  3. 点击可编辑区域外部:全局 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 相互独立。

八、如何复现与回归验证

在仓库中,这套功能有完整的手动测试与自动化测试双重覆盖:

九、总结

粘滞模式是 CKEditor 4 Copy Formatting 插件面向“连续批量套用格式”场景的关键能力:它由 copyFormatting 命令执行时传入的 { sticky: true } 开启,通过 state.sticky 标志控制 applyFormatting 完成后是否清理样式与复位命令状态,并配以自定义光标、临时快捷键接管、无障碍通知与 CSS 类切换来提供完整的交互反馈。内联编辑器场景下,样式提取会在 cke_editable 边界停止,光标容器直接作用于可编辑元素本身。若要在自己的页面中验证或集成该能力,可依照 stickyinline.md 的步骤操作,并结合 plugin.js 中的配置项与 copyformatting.js 的测试断言来确认行为是否符合预期。

登录后查看全文
ckeditor4