CKEditor 4 Table Resize 插件对「仅有表头/表尾」表格的列宽拖拽支持与 417 回归验证

原创2026-10-15 22:53:441,436 阅读
文章标签:前端UI组件

CKEditor 4 Table Resize 插件对「仅有表头/表尾」表格的列宽拖拽支持与 #417 回归验证

导读

本文围绕 CKEditor 4 仓库中 Table Resize(表格列宽拖拽)插件的 Bender 手动回归用例 tests/plugins/tableresize/manual/headeronly.md,讲解插件在「表格只有 <thead> 表头行(或只有 <tfoot> 表尾行)」这种特殊结构下的行为边界,回溯 bug #417 的修复背景,并下沉到 plugins/tableresize/plugin.js 源码剖析其「列支柱(pillar)」定位机制、事件绑定与宽度换算逻辑。读完你将掌握:如何用 Bender 手动用例复现该场景、如何读懂插件的核心源码路径、以及配套的自动化测试如何验证该修复。


一、场景背景:bug #417 与 Table Resize 插件

Table Resize 是 CKEditor 4 的表格增强插件,功能是当鼠标悬停在表格列边界时出现横向拖拽光标,允许用户直接拖动改变列宽,而无需打开单元格属性对话框。它在 CHANGES.md 中拥有较长的修复历史,其中与本用例直接相关的一条记录是(4.7.1 版本):

#417: Fixed: The Table Resize plugin throws an error when used with a table with only header or footer rows.

即:当文档中的表格只有 <thead>(表头)行,或只有 <tfoot>(表尾)行、没有任何 <tbody> 行时,旧版本插件会在鼠标悬停表格时抛出 JavaScript 异常,导致无法拖拽调整列宽。headeronly.md 正是这一修复的回归测试用例,同时配套的 footeronly.md 覆盖 tfoot 场景。

值得注意,这一族问题还有更早的记录——trac #10472("Unable to use Table Resize on table header and footer",修复于 4.7.0):在包含表头/表尾行的表格上,按住首行或末行的列边界拖拽时,拖拽指示条(pillar)高度不正确、甚至无法拖动。可见「表头/表尾行」一直是该插件边界处理的关注点。

二、手动测试用例逐行解读

2.1 Bender 元数据声明

headeronly.md 的前三行是 Bender 测试框架的指令标签:

@bender-ui: collapsed
@bender-tags: bug, 4.7.1, 417
@bender-ckeditor-plugins: wysiwygarea, toolbar, table, tableresize
  • @bender-ui: collapsed:手动测试页面(headeronly.html)默认以折叠状态展示,需要用户主动展开查看编辑器。
  • @bender-tags: bug, 4.7.1, 417:声明该用例属于 bug 回归类,关联 4.7.1 版本与 issue 编号 417。
  • @bender-ckeditor-plugins: wysiwygarea, toolbar, table, tableresize:手动测试页会加载的插件集合。这里既包括编辑器基础(wysiwygarea、toolbar)、表格插件 table,也明确加载了被测对象 tableresize。注意 tableresize 依赖 tabletools(见 plugins/tableresize/plugin.js 中的 requires: 'tabletools'),该依赖由 Bender 自动解析加载。

2.2 测试步骤与验收标准

用例正文只有三个步骤,但每步都对应明确的「期望 / 非期望」判定:

1. Focus editor.
2. Move mouse over the table.
   * **Expected**: No error is thrown in the browser console.
3. Try to resize one of the table columns.
   * **Expected**: The cursor changes to horizontal resize cursor when hovering over table borders.
     It is possible to resize table. The `column pillar` is visible while resizing table.

**Unexpected**: An error in the browser console occurs. Not possible to resize table.

翻译并拆解其验收要点:

步骤 操作 期望(Expected) 非期望(Unexpected)
1 聚焦编辑器 — —
2 鼠标移到表格上 浏览器控制台无任何报错 控制台出现异常
3 拖拽某一列的边界 悬停列边界时光标变为横向调整光标(col-resize);可正常调整列宽;调整过程中可见半透明的「column pillar」列指示条 无法拖拽、列宽不变

三个断言分别对应插件运行链路的三道关卡:不抛异常(悬停阶段的事件处理)、光标反馈正确(命中列边界时的视觉反馈)、拖拽可完成且指示条可见(resizer 的吸附与移动)。

三、复现页面结构:headeronly.html

手动测试的载体页面是 tests/plugins/tableresize/manual/headeronly.html,其编辑内容刻意构造了「只有表头、没有表体」的极端表格:

<div id="editor">
	<table border="1" style="width:500px">
		<thead>
			<tr>
				<td>1</td>
				<td>2</td>
				<td>3</td>
			</tr>
		</thead>
	</table>

	<p>Foo Bar...</p>
</div>

<script>
	CKEDITOR.replace( 'editor' );
</script>

关键点:该 <table> 内只有 <thead>,没有 <tbody> 行。历史上正是这种结构会让插件在遍历行以构建 pillar 时访问到不存在的行/单元格而抛错。表格宽度固定为 500px、三列各含一个 <td>,便于验证三列的分别拖拽。

四、源码级原理:插件如何工作,为何会在空表体上出错

4.1 悬停监听与「列支柱(pillar)」构建

插件在编辑器 contentDom 时挂载 mousemove 监听(plugins/tableresize/plugin.js)。流程如下:

  1. 从事件目标向上查找表格:仅当目标是 table 本身或 thead/tbody/tfoot 内元素时继续处理。
  2. 校验表格位于编辑器内部,避免误伤编辑器外层页面中的表格(对应修复记录 trac #12825、#11603)。
  3. 若该表格尚无 pillar 缓存(自定义数据 _cke_table_pillars),调用 buildTableColumnPillars( table ) 为每个列边界计算「列支柱」,并缓存结果;同时监听 mouseout/mousedown 使缓存失效(clearPillarsCache)。
  4. 用 getPillarAtPosition 判断当前鼠标坐标是否落在某个 pillar 矩形内,命中则把半透明 resizer 吸附到该 pillar(attachTo)。

buildTableColumnPillars 是核心(plugins/tableresize/plugin.js):它把 table.$.rows 与 CKEDITOR.tools.buildTableMap( table ) 生成的「表格单元格映射」用 CKEDITOR.tools.array.zip 配对,逐行遍历单元格,依据当前单元格的 offsetWidth、边框宽度以及相邻单元格/整表位置,计算每个列边界 pillar 的 x/y/width/height。这里遍历的是 table.$.rows(<tr> 的集合),无论这些行位于 thead、tbody 还是 tfoot 中都会被枚举——这正是修复 #417 后行为正确的根本原因:插件不再假设表格一定存在 tbody 行。

4.2 buildTableMap:兼容合并单元格的表格模型

CKEDITOR.tools.buildTableMap 定义于 plugins/tabletools/plugin.js,把表格的行×列结构展开成二维数组 aMap,并处理 colSpan/rowSpan 展开(同一合并单元格会占据多个行列槽位)。Table Resize 在构建 pillars(#3961 修复,解决合并单元格导致编辑受阻的问题)以及拖拽开始阶段确定左右两侧受影响单元格时都会使用该映射,保证合并单元格场景下也能正确计算边界。

4.3 拖拽过程与边界保护

命中 pillar 后,columnResizer 对象负责完整拖拽生命周期(plugins/tableresize/plugin.js):

  • attachTo:把 resizer(一个 position:absolute;cursor:col-resize 的透明 div,见 plugin.js)定位到 pillar 位置,并将 body 光标设为 col-resize;多个对齐的 pillar(合并单元格跨行)会合并成一条贯穿整表高度的指示条(对应 pillar.md 用例)。
  • resizeStart:用 buildTableMap 找出 pillar 左右两侧的单元格集合,计算拖拽左右边界 leftShiftBoundary/rightShiftBoundary,防止把列拖到负宽度。
  • move:随鼠标更新 resizer 的 left,记录 currentShift,并把位移钳制在边界内。
  • resizeEnd:对两侧单元格执行 setStyle('width', …);单元格最终宽度不小于 1px(对应修复 trac #11626),处于最外侧时同步调整整个表格宽度;所有单元格异步调整完成后触发 saveSnapshot 以配合撤销/重做(对应修复 trac #13388)。

4.4 为何要监听 scroll

4.20.0 起插件额外监听 scroll 事件,用 200ms 防抖清除所有表格的 pillar 缓存(plugins/tableresize/plugin.js),修复 #4889「横向滚动后拖拽光标位置错位」的问题。对应手动用例见 tests/plugins/tableresize/manual/scrollupdate.md。

五、自动化测试:headeronly / footeronly 场景的代码级验证

除手动用例外,仓库还提供 Bender 自动化测试 tests/plugins/tableresize/tableresize.js,其中直接对应 #417 的两个用例:

// #417
'test resizing table with thead only': function() {
	var editor = this.editors.classic2,
		editable = editor.editable();

	editor.setData( CKEDITOR.document.findOne( '#headeronly' ).getOuterHtml(), {
		callback: function() {
			resume( function() {
				editor.document.fire( 'mousemove', new CKEDITOR.dom.event( {
					target: editable.findOne( 'table' ).$
				} ) );
				assert.pass();
			} );
		}
	} );
	wait();
},

// #417
'test resizing table with tfoot only': function() { /* …同上,取 #footeronly …*/ }

其断言方式为「不抛异常即通过」:向表格目标派发一次 mousemove,若构建 pillars 的代码路径正常完成、没有抛出异常,测试即 pass。测试页 tests/plugins/tableresize/tableresize.html 中定义了 #headeronly 与 #footeronly 两个表格,分别只有 <thead> 与 <tfoot> 行,结构与本手动用例完全一致。

同一测试文件还覆盖了其余回归点:编辑器外表格不被误伤(intable 场景)、撤销/重做表格缩放(#13388)、空表格不抛错(#14762,对应 manual/empty.md)、滚动后 pillar 缓存重置(#4889)等。测试同时运行于 classic 与 inline 编辑器,并在结束时断言临时 div[data-cke-temp] resizer 已被清理(对应 trac #11179 的 destroy 清理)。

六、同场景回归用例矩阵:Table Resize 的边界情况全览

headeronly.md 只是 tests/plugins/tableresize/manual/ 目录下十余个手动用例之一,它们共同构成该插件的边界回归矩阵:

用例 关联 issue 验证要点
headeronly.md / footeronly.md #417 仅表头 / 仅表尾表格可正常悬停与拖拽
pillar.md trac #10472 首末行拖拽时指示条应贯穿整表高度
mergedcells.md #1467 合并单元格中部悬停不应误触 resize 光标
rowspan.md #3961 跨行单元格(rowspan)拖拽时 pillar 渲染正确、可正常聚焦
empty.md trac #14762 空表格悬停不抛 Unable to get property 'cells' 异常
iebordercollapse.md #2823 IE 下 border-collapse 表格最右列可调整
scrollupdate.md #4889 横向滚动后光标位置不漂移

结合 CHANGES.md 中 tableresize 的修复记录,还可以看到更多历史边界:绝对定位容器内不可用(#909)、最大化 + divarea 组合(#1959)、IE8 滚动条上报错(trac #11823)、设置非法列宽(trac #11626)等。这些记录说明,Table Resize 看似简单,实则围绕表格结构多样性与浏览器兼容性做了大量防御性处理。

七、如何在本仓库中运行这些用例

本仓库使用 Bender 测试框架(参见根目录 bender.js、bender.ci.js 与 package.json 中的 benderjs 系列依赖)。手动用例的运行方式为:

  1. 在仓库根目录启动 Bender 服务并打开 tests 页面,通过 @bender-ui 界面导航到 plugins/tableresize/manual/headeronly.html;
  2. 按用例步骤在真实浏览器(如 Chrome/Edge)中操作,观察控制台与光标行为;
  3. 自动化用例 tableresize.js 则随 Bender 测试套件一并执行,用于持续集成回归。

运行环境注意事项:插件源码中仍保留针对旧版 IE 的分支(如 needsIEHacks 针对 IE6/7 的特殊处理,plugin.js),这些分支在现代浏览器中不会生效,但阅读源码时有助于理解 data-cke-temp resizer 的创建与清理逻辑为何如此编写。

八、小结

headeronly.md 是一个短小但信息量完整的手动回归用例:它以「仅有表头的表格」为靶场景,验证 Table Resize 插件在 bug #417 修复后能够(1)悬停不抛异常、(2)正确显示横向调整光标、(3)通过可见的 column pillar 完成列宽拖拽。配合 plugins/tableresize/plugin.js 的 pillar 构建与拖拽实现、plugins/tabletools/plugin.js 的 buildTableMap 表格模型,以及 tests/plugins/tableresize/tableresize.js 的自动化断言,读者可以完整还原这一修复的前因后果,并将其方法论迁移到对该插件其他边界用例(合并单元格、跨行、空表格、IE 兼容、滚动定位)的理解上。

登录后查看全文
ckeditor4