CKEditor 4 拖拽复制节点问题修复剖析:以 Trac 13011 手动回归测试为线索 前端UI组件【免费下载链接】ckeditor4The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.项目地址https://gitcode.com/gh_mirrors/ck/ckeditor4点击查看免费下载CKEditor 4 的 clipboard 插件在 4.5.0 版本中重构了拖拽Drag Drop实现并随之引入了一批回归测试。仓库tests/tickets/13011/1.md正是其中一例它记录了 IE8 下锚点anchor拖到自身之前会被复制一份的缺陷修复后所需的人工验证步骤。本文以该测试说明为线索结合 plugins/clipboard/plugin.js 的源码与 tests/plugins/clipboard/drop.js 的自动化用例讲清缺陷成因、修复原理fixSplitNodesAfterDrop节点合并算法以及如何复现与回归验证。一、测试说明文档解读三类拖到自身之前场景tests/tickets/13011/1.md 是一份 Bender 手动测试用例头部元数据声明如下bender-tags: bug, 4.5.0, trac13011, clipboard bender-ui: collapsed bender-ckeditor-plugins: floatingspace, toolbar, wysiwygarea, link, clipboard, basicstyles, image2它声明了这是一个 4.5.0 修复的 bug 回归用例并且只允许floatingspace、toolbar、wysiwygarea、link、clipboard、basicstyles、image2这几个插件参与测试其中link用于生成锚点a idquotes namequotesclipboard是拖拽实现主体image2用于测试增强图片 widget 的拖拽手柄。文档还特别提示第二个用例没有自动化测试务必人工确认通过Second test doesnt have an automatic test. Be sure it pass.。配套的 tests/tickets/13011/1.html 提供了测试页面的初始内容pBroadcasting and a idquotes namequotes/a/p pfoo bar strongbaz/strong ban/p pFoo bar baz img altCKEditor logo src%BASE_PATH%_assets/logo.png / ban./p页面通过CKEDITOR.replace( editor )创建编辑器三个段落恰好对应三个测试场景。场景一锚点拖到自身之前Unexpected 用例操作步骤将锚点拖拽到它自己之前。此场景是复现缺陷的错误表现用例修复前在 IE8 下锚点会被复制一份Anchors are duplicated这正是 CHANGES.md 中 4.5.0 修复记录的原文描述[IE8] Fixed: Anchors are duplicated on dragdrop in specific locations.验证要点锚点本身没有可见内容属于空元素 命名锚name anchor拖拽后应当移动而非复制。场景二选中文本拖到自身之前Expected 用例操作步骤选中编辑器中的文本r **baz** b即foo bar strongbaz/strong ban中从 r 到 b 的部分再把它拖到自身之前。预期结果拖拽正常工作DD works properly即内容应当被移动先删除再插入不会留下重复片段或损坏加粗结构。该场景覆盖跨元素选择文本节点 加粗元素 在选中区之前落点的组合是最容易触发 IE8 文本节点分裂问题的路径。场景三同行动图片 widget 拖到自身之前Expected 用例操作步骤不要点击 widget直接用鼠标按住拖拽手柄drag handler把同一行内的图片在自身之前放下。预期结果图片被正确移动Image is moved properly。该场景覆盖 widget 级拖拽image2插件的增强图片使用拖拽手柄触发拖拽落点与源位置同处一行、且位于源之前验证的是块内 widget 的移动而非复制。二、缺陷根因IE8 拖拽过程中文本节点被分裂为什么拖到自身之前会复制从源码实现可以还原成因。在 plugins/clipboard/plugin.js 的initDragDrop中dragstart事件处理链保存拖拽范围editable.attachListener( editor, dragstart, function() { // Save drag range globally for cross editor DD. var dragRange clipboard.dragRange editor.getSelection().getRanges()[ 0 ]; // Store number of children, so we can later tell if any text node was split on drop. (https://dev.ckeditor.com/ticket/13011, https://dev.ckeditor.com/ticket/13447) if ( CKEDITOR.env.ie CKEDITOR.env.version 10 ) { clipboard.dragStartContainerChildCount dragRange ? getContainerChildCount( dragRange.startContainer ) : null; clipboard.dragEndContainerChildCount dragRange ? getContainerChildCount( dragRange.endContainer ) : null; } }, null, null, 100 );关键点有两处dragRange被保存为全局属性供后续 drop 时删除源内容使用仅当浏览器为IE 且版本 10即 IE6/IE7/IE8时记录拖拽范围起止容器在拖拽前的子节点数量dragStartContainerChildCount/dragEndContainerChildCount。辅助函数getContainerChildCountplugins/clipboard/plugin.js实现为容器若是文本节点则取其父元素再返回getChildCount()。记录这个数量的目的是IE8 在执行拖拽动作时会自行将文本节点拦腰截断——例如段落foo bar strongbaz/strong ban中的文本节点可能在拖拽起点/终点处被拆成两个文本节点。当 drop 落点在拖拽源之前时删除源内容与插入新内容的顺序一旦交错被拆开的多余文本节点就会被残留下来最终表现为锚点、文字甚至图片被复制。因此修复策略很直接在 drop 时对比拖前记录的子节点数与当前实际子节点数若不一致说明 IE8 分裂了节点就先把分裂出的相邻文本节点合并回去。这正是 4.5.0 引入的公开但仅供测试使用的fixSplitNodesAfterDrop方法。三、修复核心fixSplitNodesAfterDrop 节点合并算法fixSplitNodesAfterDrop定义在 plugins/clipboard/plugin.js是本次修复的算法主体fixSplitNodesAfterDrop: function( dragRange, dropRange, preDragStartContainerChildCount, preDragEndContainerChildCount ) { var dropContainer dropRange.startContainer; // 拖拽前数量缺失则直接返回如非 IE8 环境。 if ( typeof preDragEndContainerChildCount ! number || typeof preDragStartContainerChildCount ! number ) { return; } // 只关心锚定在元素上的范围。 if ( dropContainer.type ! CKEDITOR.NODE_ELEMENT ) { return; } if ( handleContainer( dragRange.startContainer, dropContainer, preDragStartContainerChildCount ) ) { return; } if ( handleContainer( dragRange.endContainer, dropContainer, preDragEndContainerChildCount ) ) { return; } function handleContainer( dragContainer, dropContainer, preChildCount ) { var dragElement dragContainer; if ( dragElement.type CKEDITOR.NODE_TEXT ) { dragElement dragContainer.getParent(); } // 拖拽容器与落点容器是同一个元素且子节点数发生了变化 IE8 分裂了文本节点。 if ( dragElement.equals( dropContainer ) preChildCount ! dropContainer.getChildCount() ) { applyFix( dropRange ); return true; } } function applyFix( dropRange ) { var nodeBefore dropRange.startContainer.getChild( dropRange.startOffset - 1 ), nodeAfter dropRange.startContainer.getChild( dropRange.startOffset ); // 落点两侧恰好都是文本节点时把它们合并为一个。 if ( nodeBefore nodeBefore.type CKEDITOR.NODE_TEXT nodeAfter nodeAfter.type CKEDITOR.NODE_TEXT ) { var offset nodeBefore.getLength(); nodeBefore.setText( nodeBefore.getText() nodeAfter.getText() ); nodeAfter.remove(); dropRange.setStart( nodeBefore, offset ); dropRange.collapse( true ); } } }算法要点可概括为三步前置校验拖前子节点数不是数字非 IE8 或未记录直接退出落点容器必须是元素节点因为只有元素容器才有getChild()/getChildCount()语义。定位把拖拽范围的起/止容器若是文本节点则上溯到父元素分别与落点容器比较若同一容器且子节点数前后不一致判定发生了节点分裂。合并读取落点前后相邻节点若二者都是文本节点就把后一个的文本拼接到前一个末尾、删除后一个并把 drop range 重定位到合并后文本的衔接偏移处。这样 IE8 分裂出来的半截文本节点被消除随后删除源内容时不会残留副本。值得说明的是合并只在两侧都是文本节点时执行所以场景三中涉及img的 widget 拖拽走的是另一条路径——fixSplitNodesAfterDrop文档注释plugins/clipboard/plugin.js中给出的示意图表明img元素本身就是分隔符分裂修复仅针对纯文本相邻的情形。四、修复的挂载点internalDrop 与快照机制修复并非独立调用而是嵌入在内部拖拽的internalDrop流程中。plugins/clipboard/plugin.js 的internalDrop是同一编辑器内拖拽的执行入口在 drop 事件确认数据为内部传输dataTransfer.getTransferType( editor ) CKEDITOR.DATA_TRANSFER_INTERNAL见 plugins/clipboard/plugin.js后经setTimeout调用。其关键片段internalDrop: function( dragRange, dropRange, dataTransfer, editor ) { var clipboard CKEDITOR.plugins.clipboard; // 保存并锁定快照使删除源 插入新内容只产生一次 undo 快照。 editor.fire( saveSnapshot ); editor.fire( lockSnapshot, { dontUpdate: 1 } ); if ( CKEDITOR.env.ie CKEDITOR.env.version 10 ) { this.fixSplitNodesAfterDrop( dragRange, dropRange, clipboard.dragStartContainerChildCount, clipboard.dragEndContainerChildCount ); } // 根据两个范围的相对顺序决定先为哪个范围创建 bookmark // 避免先建的书签破坏后建书签的偏移量。 isDropRangeAffected this.isDropRangeAffectedByDragRange( dragRange, dropRange ); if ( !isDropRangeAffected ) { dragBookmark dragRange.createBookmark( false ); } dropBookmark dropRange.clone().createBookmark( false ); if ( isDropRangeAffected ) { dragBookmark dragRange.createBookmark( false ); } // ... 处理落点在拖拽范围内的边界情况随后 // dragRange 的内容被 extractHtmlFromRange 提取删除 // 再以 dropRange 为位置 firePasteEvents 粘贴内容。 editor.fire( unlockSnapshot ); }这段代码揭示了 4.5.0 拖拽实现的三层防护快照锁saveSnapshotlockSnapshot保证删除与插入作为一个整体进入 undo 历史避免中间态被记录书签顺序isDropRangeAffectedByDragRangeplugins/clipboard/plugin.js判断 drop 落点是否会被 drag 范围的书签干扰——当落点在拖拽源之后或与源共享容器时必须先为 dropRange 建书签再为 dragRange 建书签否则先插入的书签会改变后续偏移落点兜底若 drop 落点恰好落在 drag 范围内部例如拖到可编辑区边距/内边距上被浏览器解析进源范围内见 #13453 相关处理会把落点书签前移到源起点之前保证拖到自身不会产生空操作或复制。fixSplitNodesAfterDrop在书签创建之前执行因此合并后的文本节点不会再干扰后续的书签偏移计算这是它必须放在internalDrop最前端的结构原因。五、自动化测试佐证分裂节点的可复现断言除手动测试外tests/plugins/clipboard/drop.js 提供了针对该缺陷的自动化单元测试文件头/* bender-tags: clipboard, 13468, 13015, 13140, 12806, 13011, 13453 */声明覆盖多个拖拽相关 ticket。核心用例test fix split nodes 2 (https://dev.ckeditor.com/ticket/13011)tests/plugins/clipboard/drop.js// p idp f o o b a r img / /p // ^ [ ] // drop drag var p editor.document.getById( p ); p.appendText( foo ); p.appendText( bar ); var img new CKEDITOR.dom.element( img ); p.append( img ); // 落点在 p 的第 1 个子节点前两个文本节点之间。 dropRange.setStart( p, 1 ); dropRange.collapse( true ); // 拖拽范围覆盖 p 的第 2~3 个子节点bar 文本 img。 dragRange.setStart( p, 2 ); dragRange.setEnd( p, 3 ); assert.areSame( 3, p.getChildCount() ); CKEDITOR.plugins.clipboard.fixSplitNodesAfterDrop( dragRange, dropRange, 2, 2 ); assert.areSame( 2, p.getChildCount() ); assert.areSame( foobar, p.getChild( 0 ).getText() );该用例模拟的正是IE8 把文本节点分裂后落点两侧各有一截文本的形态调用修复后foo与bar两个文本节点应合并为foobar段落子节点数从 3 恢复到 2。同文件中的相邻用例还覆盖了不应修复的反例——test fix split nodes 3tests/plugins/clipboard/drop.js构造fooimgbar结构落点在两个文本节点之间但中间隔着图片断言子节点数保持 3、drop range 不变验证了仅合并相邻文本节点的边界条件。而test fix split nodestests/plugins/clipboard/drop.js则验证了合并后两个 range 的选区 HTML 均保持正确。正是因为分裂修复有自动化断言覆盖手动测试文档才特别强调场景二没有自动测试务必人工确认形成自动化测算法 人工测整体拖拽手感的双轨回归体系。六、复现与验证方法若需在本仓库中复现并验证该缺陷修复可按以下路径操作阅读修复记录在 CHANGES.md 的 4.5.0 版本段查找#13011条目确认修复目标为 IE8 锚点拖拽复制。运行手动测试以 Bender 启动测试环境仓库根目录的 bender.js 为测试运行器配置将tests/tickets/13011/1.html作为用例页面加载按1.md中的三步分别操作重点确认锚点拖到自身之前不产生副本选中r **baz** b拖到自身之前内容正常移动用拖拽手柄移动同行图片 widget 不产生复制。运行自动化用例执行 clipboard 插件的 drop 测试套件对应 tests/plugins/clipboard/drop.js其中test fix split nodes 2直接断言分裂修复的正确性由于修复仅针对CKEDITOR.env.ie CKEDITOR.env.version 10在 IE8 环境或通过CKEDITOR.envmock 模拟下才能完整走到internalDrop中的修复分支。断点跟踪在internalDrop的fixSplitNodesAfterDrop调用处打断点观察dragStartContainerChildCount与dropContainer.getChildCount()的差异即可直观看到 IE8 分裂节点与合并节点的全过程。七、小结tests/tickets/13011/1.md看似只是三行操作说明背后却串联起 CKEditor 4.5.0 拖拽架构的一整套设计dragstart阶段为 IE8 预记录容器子节点数getContainerChildCountdrop阶段在internalDrop内通过fixSplitNodesAfterDrop合并被浏览器分裂的文本节点再配合书签顺序判定isDropRangeAffectedByDragRange与快照锁完成先删后插的原子移动。锚点复制、文本残留、widget 复制这三类现象本质都是同一根因在不同内容形态下的表现。理解这个修复链条对排查任何拖拽后内容重复类问题无论是 IE 兼容还是自定义 widget 拖拽都具有直接的参考价值。赞分享前端UI组件【免费下载链接】ckeditor4The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.项目地址https://gitcode.com/gh_mirrors/ck/ckeditor4点击查看免费下载相关推荐CKEditor 4 Media Embed 组件拖拽手工测试与底层实现以 embed/dnd 回归用例为例CKEditor 4 Media Embed 组件拖拽手工测试与底层实现以 embed/dnd 回归用例为例 本篇技术指南围绕 CKEditor 4 仓库中前端UI组件CKEditor 4 表格 Tab 键导航焦点修复4.19.0 回归测试剖析与源码解读CKEditor 4 表格 Tab 键导航焦点修复4.19.0 回归测试剖析与源码解读 在 CKEditor 4 的表格编辑中 Tab 键承担着在单元格间前端UI组件CKEditor 4 菜单右键点击焦点回归测试指南基于 2858 修复的 Bender.js 手动测试解析CKEditor 4 菜单右键点击焦点回归测试指南基于 2858 修复的 Bender.js 手动测试解析 本篇技术指南以 CKEditor 4 官方仓库中的前端UI组件上一篇ESP8266_MP3_DECODER扩展开发如何添加蓝牙音频输入与本地文件播放功能下一篇Claude Code终极指南如何用自然语言命令加速你的开发工作流创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考