CKEditor 4 Copy Formatting 插件手册测试全解析:从手动验证步骤到配置与源码原理

原创2026-10-15 12:18:411,604 阅读
文章标签:前端UI组件

CKEditor 4 Copy Formatting 插件手册测试全解析:从手动验证步骤到配置与源码原理

Copy Formatting 是 CKEditor 4 中一个非常实用的"格式刷"类插件:它允许用户把一处文本的样式(字体、颜色、背景、下划线、列表类型、表格单元格样式等)复制下来,再应用到文档的其他位置,而无需手动逐个设置格式。本指南以仓库中的 Copy Formatting 手动测试文档为核心,讲解该插件的手动测试流程、全部配置参数、底层实现原理,并结合仓库内的源码与自动化测试用例给出可验证的技术依据。读完本文,你将能够搭建测试环境亲手验证 Copy Formatting 的每一项行为,并理解它的样式提取、过滤、上下文识别与粘贴应用机制。

一、测试文档定位:一份 Bender 手动测试用例

本文所依据的核心文档是仓库中的 tests/plugins/copyformatting/manual/copyformatting.md,它是 CKEditor 4 测试体系(Bender.js)中的一份手动测试(manual test)说明。文件全文如下:

@bender-ui: collapsed
@bender-tags: bug, copyformatting, 4.6.0
@bender-ckeditor-plugins: copyformatting, toolbar, wysiwygarea, floatingspace, elementspath, undo

Have some fun with copy formatting!

不要被这段简洁的文本迷惑,这些以 @bender- 开头的注释行是 Bender 测试框架的元数据声明,每一行都承载着明确的测试语义:

元数据 值 含义
@bender-ui: collapsed collapsed 手动测试页面默认以折叠(不展开 UI 面板)的方式呈现,便于直接聚焦编辑器本身
@bender-tags bug, copyformatting, 4.6.0 测试标签:该插件从 4.6.0 版本引入,且包含对历史 bug 的回归验证
@bender-ckeditor-plugins copyformatting, toolbar, wysiwygarea, floatingspace, elementspath, undo 运行该测试所需加载的插件集合,其中 wysiwygarea(经典编辑区)、floatingspace(浮动工具栏)、elementspath(元素路径)、undo(撤销/重做)都是验证复制格式行为所必需的配套插件

与其配对的可视化测试页面是 tests/plugins/copyformatting/manual/copyformatting.html,页面内建了两个编辑器实例:

  • editor1:通过 CKEDITOR.replace() 创建经典(classic)编辑器,配置了 allowedContent: true(关闭内容过滤限制,保证所有样式都能被复制)与 height: 550;
  • editor2:通过 CKEDITOR.inline() 创建内联(inline)编辑器,并额外加载 dialog 与 sourcedialog 插件用于查看源码。

两个编辑器填充了相同的、精心设计的测试内容,覆盖了 Copy Formatting 最核心的三种应用上下文:

  1. 行内文本(Inline text):<strong><u><span style="color: #f00;" class="important">Apollo 11</span></u></strong>、<s>was the</s>、带 background-color 的 <span>、带 href 的链接 <a> 等,用于验证文字装饰(粗体、下划线、删除线)、颜色、背景色、class 属性乃至链接属性的整体复制;
  2. 列表(Lists):一个 start="3" 的有序列表 <ol> 与一个无序列表 <ul>,列表项中包含嵌套行内样式与 background: #ff0; 的背景样式,用于验证列表类型(ol/ul)与列表项样式的复制;
  3. 表格(Tables):两张结构不同的表格,其中一张带 <caption>、<thead>、<tbody> 结构,<th>/<td> 单元格内既有 background-color、color、text-decoration 等样式,也有 <strong><u> 行内标签,用于验证表格单元格、表头样式的复制与类型转换。

这份手动测试的任务就是"Have some fun with copy formatting!"——在页面上自由地把上述三类内容的格式互相复制、应用,观察样式是否被正确搬移。

二、搭建环境并运行手动测试

copyformatting.md 属于 Bender.js 手动测试(manual test),运行方式与自动化测试不同:它不会自动断言结果,而是打开一个浏览器页面,由测试人员按步骤手动操作并对照预期。

  1. 安装依赖:在仓库根目录执行 npm install,安装 Bender 测试框架所需的全部依赖(项目依赖清单见 package.json)。
  2. 启动 Bender 服务器:运行 node node_modules/bender/bender.js server run(Bender 的主配置文件为仓库根目录的 bender.js,其中声明了 applications.ckeditor 指向根目录的 ckeditor.js,端口与主机等参数均在此配置)。
  3. 打开手动测试列表:浏览器访问 Bender 提供的手动测试入口,定位到 tests/plugins/copyformatting/manual/ 目录下的 copyformatting.html 页面。由于文档声明了 @bender-ckeditor-plugins,Bender 会自动为页面加载 copyformatting 及其配套插件。

说明:手动测试依赖真实浏览器环境(尤其涉及鼠标事件、光标样式与 contenteditable 行为),因此必须在浏览器中人工操作,而非命令行断言。这正是本目录下所有 manual/*.html 与 manual/*.md 成对存在的原因——.md 描述操作步骤与预期,.html 提供可交互的测试页面。

三、Copy Formatting 的核心交互流程

结合同目录下的 keystroke.md、sticky.md 等手动测试文档,Copy Formatting 的完整交互流程可以概括为"复制格式 → 移动选区 → 应用格式"三个阶段:

基本流程(非粘性模式):

  1. 将光标放入带有样式的文本中(如 Apo^llo 11,即 Apollo 中间);
  2. 点击工具栏的 Copy Formatting 按钮(或按 Ctrl + Shift + C)复制格式;
  3. 用鼠标点击(或键盘移动选区)到另一处未样式化的文本;
  4. 再次点击目标位置或按 Ctrl + Shift + V 应用格式。

需要验证的关键行为(来自 keystroke.md):

  • 复制样式后,工具栏按钮进入 "On"(按下)状态;
  • 全部文本样式都被复制:文字装饰(text decoration)、字体(style)、字重(weight)、颜色(color)、背景色(background color);
  • 全部属性(attributes)都被复制;
  • 新应用的样式会覆盖目标位置原有的样式;
  • 用 Ctrl + Shift + C 复制的样式,可以多次按 Ctrl + Shift + V 重复粘贴;
  • 按 Escape 键可随时取消复制,按钮回到 "Off" 状态;
  • 若从无样式元素复制格式,则应用到目标元素的效果是移除其原有格式。

粘性模式(sticky)(来自 sticky.md):

  1. 将光标放入带样式文本;
  2. 双击工具栏的 Copy Formatting 按钮,进入 sticky 模式;
  3. 用鼠标依次点击多处未样式化文本,格式会被持续应用;
  4. 再次单击按钮或按 Escape 键退出粘性模式。

粘性模式下按钮状态在每次应用后保持不变,页面光标会一直呈现 Copy Formatting 专用光标,直到手动退出。

鼠标按键约束(来自 mousebutton.md):只有左键点击才会触发格式应用;使用中键或右键点击时,编辑器不应执行任何动作,而应保留浏览器默认行为(如中键滚动)。

四、插件全部配置参数详解

所有配置均以 copyFormatting_ 为前缀,在编辑器初始化时作为 config 选项传入,其默认值与完整注释定义在 plugins/copyformatting/plugin.js 底部。下面逐一说明。

4.1 copyFormatting_allowRules:允许复制哪些样式(ACF 规则)

config.copyFormatting_allowRules = 'b s u i em strong span p div td th ol ul li(*)[*]{*}';

默认值为 'b s u i em strong span p div td th ol ul li(*)[*]{*}',使用 CKEditor 4 的 Advanced Content Filter(ACF)语法描述"允许从哪些元素提取样式"。

  • 设为一个 ACF 规则字符串时,例如 'span(*)[*]{*}',表示只允许复制 <span> 元素(任意类、任意属性、任意内联样式);
  • 设为 true 时,彻底禁用过滤,允许复制任意元素上的任意样式。

源码中该配置在 State 构造函数里被用来构造过滤实例(见 plugin.js#L264):this.filter = new CKEDITOR.filter( editor, editor.config.copyFormatting_allowRules );。当配置为 true 时,过滤实例被直接禁用(this.filter.disabled = true)。

4.2 copyFormatting_disallowRules:禁止复制哪些样式

config.copyFormatting_disallowRules = '*data-cke-widget*,data-widget*,data-cke-realelement';

默认规则禁止从 widget 相关元素上提取样式,防止把 CKEditor 4 widget(如图片、嵌入内容)的内部标记当成普通格式复制走。同样采用 ACF 语法,例如 'span(important)' 表示禁止复制带 important 类的 <span>。源码中通过 this.filter.disallow( editor.config.copyFormatting_disallowRules ) 叠加到允许规则之上(见 plugin.js#L270)。

4.3 copyFormatting_allowedContexts:允许在哪些上下文工作

config.copyFormatting_allowedContexts = true; // 默认:全部上下文可用
// 或
config.copyFormatting_allowedContexts = [ 'text', 'list' ];

插件把编辑内容划分为三种上下文:

  • 'text':普通文本段落;
  • 'list':列表(ul/ol/li);
  • 'table':表格(table/tr/td/th 等)。

设为 true 时三种上下文全部启用;设为数组时只启用列出的上下文。判定逻辑在 _determineContext()(plugin.js#L797,实际路径 plugins/copyformatting/plugin.js#L797):先检测选区范围内是否存在 ul/ol 祖先,其次检测 table,否则判定为 text;随后 _isContextAllowed()(plugin.js#L284)根据配置决定该上下文是否放行。

这一行为有专门的手动测试 allowedcontexts.md 与 allowedcontexts.html 验证:例如配置 [ 'text', 'list' ] 的编辑器,可以正常在文本与列表间复制/应用格式,但从表格复制或向表格应用格式则"什么都不发生"。该 HTML 中通过 evt.editor.config.copyFormatting_allowedContexts = [ ... ] 动态配置了 5 个不同上下文组合的编辑器实例,覆盖 ['text']、['text','list']、['text','table']、['list','table'] 与 false 五种情况。

4.4 copyFormatting_keystrokeCopy:复制格式快捷键

config.copyFormatting_keystrokeCopy = CKEDITOR.CTRL + CKEDITOR.SHIFT + 67; // 默认 Ctrl+Shift+C
// 自定义示例:Ctrl+Shift+B
config.copyFormatting_keystrokeCopy = CKEDITOR.CTRL + CKEDITOR.SHIFT + 66;
// 关闭:
config.copyFormatting_keystrokeCopy = false;

默认值为 Ctrl + Shift + C(键码 67)。当配置为真值时,插件在初始化时调用 editor.setKeystroke( editor.config.copyFormatting_keystrokeCopy, 'copyFormatting' )(plugin.js#L133)注册快捷键。

4.5 copyFormatting_keystrokePaste:应用格式快捷键

config.copyFormatting_keystrokePaste = CKEDITOR.CTRL + CKEDITOR.SHIFT + 86; // 默认 Ctrl+Shift+V
// 自定义示例:Ctrl+Shift+M
config.copyFormatting_keystrokePaste = CKEDITOR.CTRL + CKEDITOR.SHIFT + 77;
// 关闭:
config.copyFormatting_keystrokePaste = false;

默认值为 Ctrl + Shift + V(键码 86)。注意这个默认值会与 paste 类插件的"粘贴纯文本"快捷键产生冲突,因此插件采用了临时接管策略:调用 copyFormatting 命令时,通过 _attachPasteKeystrokeHandler()(plugin.js#L1078)把该快捷键临时改绑为 applyFormatting 命令,并记住原本绑定的命令(_initialKeystrokePasteCommand);退出复制状态或完成应用后,再由 _detachPasteKeystrokeHandler()(plugin.js#L1093)恢复原绑定。prematurepaste.md 手动测试专门验证了未复制样式时按 Ctrl + Shift + V 不会误触发格式应用,而会走普通粘贴行为。

自定义快捷键的验证见 customkeystroke.md 与 customkeystroke.html:第一个编辑器配置了 Ctrl+Shift+B/Ctrl+Shift+M,应正常工作;第二个编辑器把两个快捷键都设为 false,Ctrl+Shift+C/Ctrl+Shift+V 不应再触发 Copy Formatting 功能。

4.6 copyFormatting_outerCursor:编辑器外光标是否显示"禁用"样式

config.copyFormatting_outerCursor = true; // 默认
// 关闭:
config.copyFormatting_outerCursor = false;

当 Copy Formatting 激活时,插件会给页面 documentElement 添加 cke_copyformatting_disabled 类(见 plugin.js#L405),使编辑器外的鼠标显示为"禁用"光标,提示用户当前位置无法应用格式。相关 CSS 定义在 plugins/copyformatting/styles/copyformatting.css:.cke_copyformatting_disabled 使用 cursor: url(../cursors/cursor-disabled.svg) 12 1, auto;,而编辑区内的 .cke_copyformatting_active 则使用专用光标 cursor.svg,并带有 !important 以覆盖 Table Resize 等插件的光标干扰。disabledcursor.md 手动测试验证了在 IE/Edge 中(该浏览器不支持在 contenteditable 内自定义光标)编辑器外光标不应异常显示为禁用态。

五、源码级实现原理

5.1 插件的状态对象与命令体系

插件在 init() 中为每个编辑器实例创建一个独立的状态对象 editor.copyFormatting = new plugin.state( editor )(plugin.js#L82)。State 类(plugin.js#L233)保存三个关键数据:

  • styles:当前已复制的 CKEDITOR.style[] 数组,null 表示尚未复制;
  • sticky:是否处于粘性模式;
  • filter:由 copyFormatting_allowRules / copyFormatting_disallowRules 构造的样式过滤器。

同时插件注册两个命令(plugin.js#L84-L85):

  • copyFormatting:执行时调用 _extractStylesFromElement() 从当前光标所在元素向上遍历提取样式并存入 copyFormatting.styles,命令状态切为 TRISTATE_ON,随后根据 isSticky(来自双击按钮或快捷键触发的 data.sticky)决定是否进入粘性模式,并为光标容器添加激活类(plugin.js#L371-L415);
  • applyFormatting:执行时调用 _applyFormat() 把 copyFormatting.styles 应用到当前选区;若为非粘性模式,应用后清空样式、移除光标类并把命令状态切回 TRISTATE_OFF(plugin.js#L417-L450)。

工具栏按钮 CopyFormatting 以 isToggle: true 注册(plugin.js#L87),其 mouseup 事件会被拦截(evt.data.stopPropagation()),防止点击按钮本身触发编辑器内的格式应用。

5.2 样式提取:从元素向上遍历

_extractStylesFromElement()(plugin.js#L501)从当前元素开始,沿着 DOM 祖先链向上遍历,直到到达 editable 或遇到 breakOnElements(ul、ol、table)为止。对每个元素:

  1. 触发 extractFormatting 事件;
  2. 默认监听器调用 _convertElementToStyleDef()(plugin.js#L478),把元素名、属性(排除 excludedAttributes:id、style、href、data-cke-saved-href、dir)与内联样式(parseCssText 解析的 style 属性)合并为一个 CKEDITOR.STYLE_INLINE 类型的样式定义;
  3. 用 copyFormatting.filter.check() 对样式做 ACF 过滤,被过滤掉的元素直接 evt.cancel()。

extractFormatting 事件可被开发者监听和改写,例如强制给提取的样式附加 class 或取消特定元素的提取(事件文档见 plugin.js#L1219-L1244)。

5.3 格式应用:先清旧样式,再上新样式

applyFormatting 事件注册了两个监听器(plugin.js#L169-L221):

  1. 优先级 10(先执行)——移除目标位置的旧样式:通过 _extractStylesFromRange() 收集选区内的旧样式,逐一 remove();对 preservedElements(ul、ol、li、td、th、tr、thead、tbody、table)不删除元素本身,而是用 _removeStylesFromElementInRange() 剥离其属性(如 <ul style="..."> 还原为 <ul>)。每个步骤之间用 range.createBookmark()/moveToBookmark() 恢复选区,避免列表/表格的怪异行为。
  2. 优先级 999(后执行)——应用新样式:根据 _determineContext() 判定出的上下文,分派到三个函数之一:
    • _applyStylesToTextContext()(plugin.js#L831):普通文本上下文,逐个 style.apply( editor );对于 elementsForInlineTransform(li)会把元素转换为 <span> 再应用,并剔除 value、type 属性;
    • _applyStylesToListContext()(plugin.js#L873):列表上下文,若复制的是 ol/ul 类型则把目标列表 renameNode 成对应类型并 applyToObject,复制 li 样式则应用到每个列表项;
    • _applyStylesToTableContext()(plugin.js#L918):表格上下文,table/tr 样式直接应用到节点,td/th 样式按目标单元格名适配后应用,thead/tbody 同理。

应用前还会经过 _filterStyles()(plugin.js#L763)处理:把 h1~h6、p、div 等 inlineBoundary 块级元素统一转换为 <span> 再应用,并丢弃空 <span>。

5.4 折叠选区的"单词"定位

当光标处于折叠状态(未选中文字)时,直接应用样式会破坏相邻内容。为此 _applyFormat()(plugin.js#L973)会调用 _getSelectedWordOffset()(plugin.js#L589),用正则 /\b\w+\b/ig 定位光标所在单词的起止位置,把选区扩展到整个单词后再应用样式。该函数还处理了跨节点单词(如 lu<span style="color: #f00;">n</span>ar)、跳过注释节点、处理段落/列表/表格边界等情况。getwordbetweenelements.md、spaceattheend.md、selectionfromrighttoleft.md 等手动测试均围绕该逻辑设计(例如从右往左选择文本时"BBB"也必须被正确样式化)。

5.5 无障碍与撤销集成

  • 屏幕阅读器:插件通过 _addScreenReaderContainer()(plugin.js#L1035)在页面插入一个 aria-live="polite" 的隐藏容器,复制/应用/取消时调用 _putScreenReaderMessage() 写入状态文本("Formatting copied"、"Formatting applied"、"Formatting canceled" 等,见 plugins/copyformatting/lang/en.js)。
  • 撤销集成:插件的 mouseup 处理(plugin.js#L105-L117)只在 cmd.state === TRISTATE_ON 且左键点击时才执行命令,并且点击编辑器外部会自动关闭 Copy Formatting 状态。这避免了连续点击选区产生大量无意义的历史快照。自动化测试 undointegration.js 验证了连续 20 余次改变选区后 Undo 步骤数保持不变(对应 issue #2780)。

六、围绕核心用例的完整手动测试清单

除了 copyformatting.md 本身,tests/plugins/copyformatting/manual/ 目录还提供了针对同一插件不同侧面的多份手动测试,可作为深入验证的清单:

测试文件 验证主题
keystroke.md 默认快捷键 Ctrl+Shift+C/Ctrl+Shift+V 的完整流程与按钮状态切换
customkeystroke.md 自定义/关闭快捷键后的行为
sticky.md 双击按钮进入粘性模式,多次应用与退出
allowedcontexts.md 不同 copyFormatting_allowedContexts 组合下的上下文限制
mousebutton.md 中键/右键点击不应触发格式应用
lists.md 混合上下文、列表类型转换、列表不被拆分
tables.md 表格单元格/表头样式应用、仅行内样式作用于文本
undointegration.md 多次改变选区后 Undo/Redo 按钮保持可用
selectionfromrighttoleft.md 从右到左选择文本时样式仍被应用
disabledcursor.md 编辑器外禁用光标行为(IE/Edge 特例)
prematurepaste.md 未复制样式时按 Ctrl+Shift+V 只走普通粘贴

自动化测试方面,tests/plugins/copyformatting/copyformatting.js 覆盖了折叠选区应用样式、非折叠选区应用、键盘快捷键应用、文本样式应用到表格等场景;applyformatcontext.js 验证 extractFormatting/applyFormatting 事件的参数结构;keystrokes.js 则逐条断言快捷键的挂载、临时接管与还原行为。这些测试与手动测试文档共同构成了对 Copy Formatting 插件行为最完整的事实依据。

七、小结

本文以 tests/plugins/copyformatting/manual/copyformatting.md 为骨架,完整还原了 Copy Formatting 手动测试的元数据语义与测试内容设计,并延伸讲解了插件的六项配置参数、两条命令与两个事件构成的命令体系、三种上下文的分派逻辑、折叠选区的单词定位、无障碍与撤销集成等源码实现。无论你是要在自己的 CKEditor 4 项目中启用该插件,还是希望深入理解其"格式刷"的底层机制,都可以直接以本仓库的 plugins/copyformatting/plugin.js、plugins/copyformatting/styles/copyformatting.css 以及 tests/plugins/copyformatting/ 下的手动与自动化测试为蓝本进行验证与二次开发。

登录后查看全文
ckeditor4