- 前端
- UI组件
【免费下载链接】ckeditor4
The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.
导读
本文围绕 CKEditor 4 仓库中一份名为enterdivemptypaste的 Bender 手工测试文档(tests/core/editable/manual/enterdivemptypaste.md)展开,讲解在ENTER_DIV(回车生成<div>块)模式下,空编辑器连续粘贴两次却只得到一次内容的历史 bug(issue #4301)及其修复原理。读者将了解该 bug 的复现步骤与验收标准、editable.insertHtml内部对"空编辑区"的判定逻辑,以及配套自动化测试如何锁定该回归,从而掌握排查 CKEditor 4 粘贴类缺陷的完整思路。
关联文档原文:一份最小化的手工回归测试
本仓库中与该主题直接相关的文档是一份 Bender 手工测试说明(manual test),全文如下:
@bender-tags: 4.16.1, bug, 4301 @bender-ui: collapsed @bender-ckeditor-plugins: clipboard, toolbar, wysiwygarea, sourcearea ## For each editor 1. Copy anything (e.g. text above the editor). 2. Paste it in the editor. 3. Without moving selection, paste it once more. ## Expected The text is pasted twice. ## Unexpected The text is pasted once.它同时配套了一份可运行的 HTML 用例 tests/core/editable/manual/enterdivemptypaste.html,其中只包含一个关键配置:
<div>Lorem ipsum dolor sit amet</div> <div id="editor"></div> <script> CKEDITOR.replace( 'editor', { enterMode: CKEDITOR.ENTER_DIV } ); </script>文档虽然短小,但信息密度很高:
- 版本与类型标签:
4.16.1, bug, 4301,表明该缺陷在 4.16.1 版本被发现并修复,issue 编号为 4301; - 插件依赖:
clipboard, toolbar, wysiwygarea, sourcearea——粘贴功能依赖 clipboard 插件,wysiwygarea 提供可视编辑区,sourcearea 用于查看源码结果; - 操作步骤:复制一段文本 → 粘贴 → 不移动选区 → 再次粘贴;
- 验收标准:文本应出现两次(Expected);若只出现一次(Unexpected),即 bug 复现。
Bug 根因:空编辑区的"空段落"误判
在ENTER_DIV模式下,编辑器在渲染空内容时不会像ENTER_P那样生成<p>占位块,而是生成一个空的<div>占位块。当用户第一次粘贴文本时,编辑器需要先把这个空的占位<div>从 DOM 中移除,再插入粘贴内容,否则就会产生"粘贴内容被包裹在多余 div 中"的问题(即早先的 issue #2751,见 CHANGES.md 的修复记录)。
问题的关键在于:如何判断"当前编辑区是空的"。在 core/editable.js 的insert()内部流程中,修复后的实现是这样处理的:
html = editable.getHtml(), // Instead of getData method, we directly check the HTML // due to the fact that internal getData operates on latest snapshot, // not the current content. // Checking it after clearing the range's content will give the // most correct results (#4301). isEmptyEditable = html === '' || html.match( emptyParagraphRegexp ); // When enter mode is set to div and content wrapped with div is pasted, // we must ensure that no additional divs are created (#2751). if ( editor.enterMode === CKEDITOR.ENTER_DIV && isEmptyEditable ) { clearEditable( editable, range ); }这里有两个关键设计点:
直接读取 HTML 而非调用
getData():注释明确指出,内部getData()读取的是"最新快照"(latest snapshot),而不是编辑区当前的真实内容。在连续粘贴场景下,第一次粘贴刚结束、快照尚未更新时,若用getData()判断,会把"已经有一段文本"的编辑区误判为空,从而再次触发clearEditable()把第一次粘贴的内容清掉——这正是"粘贴两次却只剩一次"的根因。判定时机在清空选区内容之后:代码先执行
prepareRangeToDataInsertion( that )(其中会range.deleteContents()删除选区内容),再读取 HTML 判断是否为空。这样"最初是空编辑器、但选区操作后 DOM 已变化"的情况也能被正确识别。
空段落正则的细节
判空所用的emptyParagraphRegexp定义在 core/editable.js:
emptyParagraphRegexp = /(^|<body\b[^>]*>)\s*<(p|div|address|h\d|center|pre)[^>]*>\s*(?:<br[^>]*>| |\u00A0| )?\s*(:?<\/\2>)?\s*(?=$|<\/body>)/gi;该正则覆盖了 CKEditor 4 会用作空占位的全部块标签(p、div、address、h1–h6、center、pre),以及占位块中可能出现的空内容形式(<br>、 、\u00A0、 ),并限定匹配位置在文档开头或</body>之前。也就是说,只有"文档末尾处孤立的一个空占位块"才被视为空编辑区,普通内容不会被误判。
clearEditable的实现位于 core/editable.js:直接移除编辑区第一个子元素,并把折叠选区重置到编辑区起点,为后续insertDataIntoRange的插入做准备:
function clearEditable( editable, range ) { var first = editable.getFirst(); first && first.remove(); range.setStartAt( editable, CKEDITOR.POSITION_AFTER_START ); range.collapse( true ); }修复的完整数据流
结合 core/editable.js 中insert()的源码,一次粘贴(内部走editable.insertHtml)的完整数据流为:
- 类型归一:
unfiltered_html被归一为html并置dontFilter = true; - 只读检查:
range.checkReadOnly()命中非可编辑区域则直接返回; - 选区准备:
prepareRangeToDataInsertion( that )删除选区内容、记录合并候选与僵尸节点,为 HTML 插入准备干净的 range(core/editable.js 起); - 空编辑区判定(修复点):读取当前真实 HTML,用
emptyParagraphRegexp判空;若enterMode === CKEDITOR.ENTER_DIV且为空,调用clearEditable移除空占位<div>; - 数据处理:
processDataForInsertion( that, data )走内容过滤(配合 clipboard 插件的pasteFilter等机制,见 plugins/clipboard/plugin.js 中一系列paste事件监听器); - 数据插入:
insertDataIntoRange( that )将 HTML 写入 DOM; - 最终清理:
cleanupAfterInsertion( that )复位选区并清理合并产生的残留。
关键点在于第 4 步判定发生在第 3 步删除了选区内容之后、且使用editable.getHtml()直接读取真实 DOM——这两处修正共同保证了"第二次粘贴时编辑区里明明已有内容"这一事实能被如实感知,从而不再错误清空。
自动化测试:如何锁定回归
该修复不仅有手工测试文档,还有对应的自动化测试文件 tests/core/editable/enterdiv.js。它通过 Bender 框架在 classic 与 divarea 两种编辑器形态下、以enterMode: CKEDITOR.ENTER_DIV配置运行。与 #4301 直接对应的用例是'test multiple paste into empty editor':
// (#4301) tests[ 'test multiple paste into empty editor' ] = function() { bender.editorBot.create( { name: 'multiple_paste', config: { enterMode: CKEDITOR.ENTER_DIV } }, function( bot ) { var html = 'Lorem ipsum', editor = bot.editor; editor.insertHtml( html ); editor.insertHtml( html ); assert.areSame( '<div>' + html + html + '</div>', editor.getData() ); } ); };它精确模拟了手工测试的场景:向空编辑器连续调用两次insertHtml( 'Lorem ipsum' ),断言最终数据为<div>Lorem ipsumLorem ipsum</div>——两次内容都在,且只被一个<div>包裹(后者同时守护了 #2751 的"不多套一层 div"约束)。同文件还包含与 #2751 相关的四个插入用例(test insert div、test insert div wrapped in another div、test insert two divs、test insert two divs wrapped in another div)以及'reinsert the current data'用例,共同覆盖"粘贴 div 包裹内容"的各种边界。
此外,generateEmptyParagraphsTests()针对p/div/address/h1~h6/center/pre十种块标签 ×br/ /\u00A0/ 四种占位内容组合,逐一验证"空占位块 + 插入<div>foo</div><div>bar</div>"后结果不被篡改——这正是emptyParagraphRegexp设计覆盖面的直接测试依据。
版本修复记录与演进
在仓库根目录的 CHANGES.md 中,该修复的官方记录为:
#4301: Fixed: Pasted content is overwritten when pasted in an initially empty editor with the
divEnter mode.
而早先的关联修复 #2751(CHANGES.md)记录为:
#2751: Fixed: An editor with
config.enterModeset toENTER_DIValters pasted content.
两条记录连起来看,正好勾勒出该区域代码的演进脉络:先解决"粘贴内容被多套一层 div"(#2751,测试中要求assert.areSame( htmlString, editor.getData() )),再解决由此引入的"空编辑器连续粘贴被覆盖"(#4301)。手工测试文档enterdivemptypaste.md中的4.16.1标签,即对应 #4301 修复落地的版本。
与 ENTER_DIV 模式相关的配置全景
要彻底理解该测试场景,还需要掌握enterMode配置体系。在 core/config.js 中定义了三个相关配置:
| 配置项 | 默认值 | 取值含义 |
|---|---|---|
config.enterMode | CKEDITOR.ENTER_P | ENTER_P(1) 按回车生成<p>;ENTER_BR(2) 生成<br>;ENTER_DIV(3) 生成<div>块 |
config.forceEnterMode | false | 为true时强制按enterMode处理回车,不再根据所在块上下文自适应 |
config.shiftEnterMode | CKEDITOR.ENTER_BR | 定义Shift+Enter的行为,可选值与enterMode相同 |
其中enterMode的注释明确提示:ENTER_P因其语义价值与正确性被推荐使用,编辑器针对该模式做了优化;ENTER_DIV属于非常规用法。而本 bug 恰恰暴露了非常规模式下"空编辑区占位块"与粘贴插入逻辑之间的耦合。
除配置外,plugins/enterkey/plugin.js 中的enterBlock()也在源码层面印证了该模式:当mode == CKEDITOR.ENTER_DIV时新建块标签取'div',否则取'p'(见 plugins/enterkey/plugin.js)。同样地,core/editable.js 的autoParagraphTag()根据activeEnterMode决定自动包裹用的标签——ENTER_DIV时用div,ENTER_P时用p,ENTER_BR或autoParagraph === false时返回false(不自动包裹)。这些是空编辑区占位块来源的底层机制。
实战排查清单
如果你在自己的 CKEditor 4 集成中遇到"粘贴内容丢失/被覆盖"类问题,可以按以下清单对照排查:
- 确认 enterMode:
config.enterMode是否为CKEDITOR.ENTER_DIV?该问题集中在 div 模式下(ENTER_P模式不受影响)。 - 复现场景:是否为空编辑器首次粘贴后、不移动选区立即再次粘贴?这是 #4301 的精确触发条件。
- 验证判定逻辑:检查是否命中
isEmptyEditable的误判——参考 core/editable.js 的实现,确认使用的是editable.getHtml()实时读取而非快照数据。 - 对照自动化测试:将 tests/core/editable/enterdiv.js 中的
multiple_paste用例复刻为集成测试,锁定回归。 - 升级检查:确认所用版本是否包含 4.16.1 及以后的修复(参见 CHANGES.md 的 #4301 记录)。
总结
enterdivemptypaste手工测试文档虽只有十余行,却精准刻画了 CKEditor 4 在ENTER_DIV模式下"空编辑区判定"与"粘贴覆盖"之间的经典缺陷:#4301。其修复核心在于插入流程中用editable.getHtml()实时判断编辑区是否为空,并将判定时机放在选区清理之后,从而避免把"已有内容的编辑区"误当空编辑区清空。配套的自动化测试(tests/core/editable/enterdiv.js)与版本记录(CHANGES.md)为读者提供了可直接复用的验证手段——理解这一案例,也就掌握了 CKEditor 4 粘贴数据流(core/editable.js)中"判空 → 清理 → 过滤 → 插入"这条核心链路的调试方法。
- 前端
- UI组件
【免费下载链接】ckeditor4
The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考