CKEditor 4 表格单元格类型切换实战:基于 tabletools 插件与 Cell Properties 对话框的 scoped 表头验证指南

原创2026-10-16 20:24:571,339 阅读
文章标签:前端UI组件

CKEditor 4 表格单元格类型切换实战:基于 tabletools 插件与 Cell Properties 对话框的 scoped 表头验证指南

导读

本文以 CKEditor 4 仓库中的手动测试用例 cellpropertiescelltype.md 为骨架,深入讲解“单元格属性(Cell Properties)”对话框中 Cell Type(单元格类型) 字段的实现原理与验证方法。该功能由 tabletools 插件提供,并在 CKEditor 4.20.0(issue #5084)中引入 config.tabletools_scopedHeaders 配置,用于区分 Data、Column Header、Row Header 三种单元格类型及其底层 td / th + scope 属性映射。读完本文,你将掌握该手动测试的完整操作步骤、预期结果判读方式,以及从 tableCell.js 源码层面理解类型字段的读写(setup/commit)机制和存量内容迁移方法。

一、功能背景:单元格类型为什么需要“作用域表头”

在 HTML 表格语义中,th 元素用于表头单元格,而 scope 属性用于显式声明该表头作用于整列(scope="col")、整行(scope="row")还是列组/行组,这对屏幕阅读器等无障碍工具正确建立“表头—数据”关联至关重要。

CKEditor 4 的 tabletools 插件(requires: 'table,dialog,contextmenu',见 plugin.js)在其“单元格属性”对话框中提供了一个 Cell Type(单元格类型) 下拉框。在未开启 scoped 表头功能时,它只提供两个选项:Data(td)与 Header(th)。而从 4.20.0 开始,当配置项 config.tabletools_scopedHeaders 设为 true 后,下拉框扩展为三个选项:

  • Data(td,无 scope 属性)
  • Column Header(th scope="col")
  • Row Header(th scope="row")

这一行为由 CHANGES.md 的 4.20.0 发布说明明确记载:新增 config.tabletools_scopedHeaders 配置项,用于控制带/不带 [scope] 属性的表头单元格的行为。

二、手动测试用例结构与运行环境

本文对应的关联文档位于 tests/plugins/tabletools/manual/cellpropertiescelltype.md,配套的测试夹具为 cellpropertiescelltype.html。该 HTML 文件头部声明了 Bender 测试所需的信息:

@bender-tags: 4.20.0, 5084, feature
@bender-ui: collapsed
@bender-ckeditor-plugins: wysiwygarea, toolbar, table, tabletools, sourcearea
  • 版本标签:4.20.0 与 5084 表明该用例对应 4.20.0 版本引入的 issue #5084 功能。
  • 插件集合:wysiwygarea(所见即所得编辑区)、toolbar(工具栏)、table(表格基础)、tabletools(表格工具)、sourcearea(源码模式,用于核验生成的 HTML 结构)。
  • 编辑器初始化:CKEDITOR.replace( 'editor', { language: 'en', tabletools_scopedHeaders: true } ),即显式开启 scoped 表头功能,这是本用例与默认行为最关键的差异点。
  • 初始内容:一个 border="1" 的 2×2 表格,四个单元格均为 td(Cell 1~Cell 4),没有任何 th。
  • 移动端处理:代码中通过 bender.tools.env.mobile 检测到移动环境时执行 bender.ignore() 跳过该用例,因此本用例只针对桌面端浏览器运行。

该测试在 Bender 手动测试框架(bender-ui: collapsed)下运行,操作路径是:右键点击任意单元格 → 弹出上下文菜单 → Cell → Cell Properties 打开对话框。

三、逐步操作与预期结果(完整继承原文档)

原文档的验证流程共七步,其核心思路是:在“单元格属性”对话框中反复切换单元格类型,并通过打开/关闭对话框与源码模式核验每一次类型切换后底层 HTML 是否与预期一致。完整步骤如下:

  1. 右键点击任意单元格,选择 Cell → Cell Properties。
    • 预期:Cell Type 字段当前值为 Data(因为初始单元格是 td)。
  2. 将单元格类型改为 Column Header。
    • 预期:该单元格变为 th 类型,且带有 scope 属性,其值为 col。
  3. 再次右键该单元格,选择 Cell → Cell Properties 重新打开对话框。
    • 预期:Cell Type 字段回显为 Column Header(说明修改已被正确保存并可被重新读取)。
  4. 将单元格类型改为 Row Header。
    • 预期:该单元格变为 th 类型,且带有 scope 属性,其值为 row。
  5. 再次右键该单元格,打开 Cell Properties。
    • 预期:Cell Type 字段回显为 Row Header。
  6. 将单元格类型改回 Data。
    • 预期:该单元格变为 td 类型,且不再带有 scope 属性。
  7. 继续在各种单元格类型之间来回切换,逐一确认显示的类型与实际生成的 HTML 结构始终一致。

原文档特别给出了核验手段的提示:“Please verify cell structure via toggling Source mode.” 即每次操作完成后,切换到源码模式(sourcearea 插件提供)查看实际输出,例如 Column Header 应对应 <th scope="col">,Row Header 应对应 <th scope="row">,Data 应对应 <td> 且无 scope。这是本用例区别于纯 UI 测试的关键——它要求从“对话框显示值”与“真实 DOM 结构”两个层面双重复核。

四、源码级原理:Cell Type 字段的读写映射

为什么对话框能正确回显 Column Header / Row Header,并在提交时写出 th scope="col" / th scope="row"?答案在“单元格属性”对话框定义文件 plugins/tabletools/dialogs/tableCell.js 中。

4.1 选项列表由配置决定

对话框通过 getAvailableCellTypes( editor ) 函数(tableCell.js)生成 Cell Type 下拉框的选项:

function getAvailableCellTypes( editor ) {
	if ( editor.config.tabletools_scopedHeaders ) {
		return [
			[ langCell.data, 'td' ],
			[ langCell.columnHeader, 'thc' ],
			[ langCell.rowHeader, 'thr' ]
		];
	}

	return [
		[ langCell.data, 'td' ],
		[ langCell.header, 'th' ]
	];
}

可以看到:

  • 开启 tabletools_scopedHeaders 时:三个选项的内部值分别为 td、thc(Column Header)、thr(Row Header);
  • 未开启时:仅两个选项 td 与 th(普通 Header)。

这正解释了为什么本用例(开启了 scoped 配置)中能够看到 Column Header 和 Row Header 两个独立选项,而默认配置下只会看到单一的 Header。

4.2 setup:把 DOM 读成对话框值

Cell Type 字段的 setup 回调(tableCell.js)负责在对话框打开时把选中单元格的真实状态回显为下拉框的值:

setup: setupCells( function( selectedCell ) {
	var cellName = selectedCell.getName(),
		scope = selectedCell.getAttribute( 'scope' );

	if ( cellName === 'td' ) {
		return 'td';
	}

	switch ( scope ) {
		case 'row':
			return 'thr';
		case 'col':
			return 'thc';
		default:
			return 'th';
	}
} ),

映射规则非常清晰:

单元格实际状态 setup 返回值 下拉框显示
td td Data
th + scope="row" thr Row Header
th + scope="col" thc Column Header
th(无 scope) th Header(scoped 开启时对应空值场景,详见下文 4.4)

4.3 commit:把对话框值写回 DOM

commit 回调(tableCell.js)负责在点击对话框“确定”后把选择的值写回每个选中的单元格:

commit: function( selectedCell ) {
	var nameToProps = {
			'td':  { name: 'td' },
			'th':  { name: 'th' },
			'thc': { name: 'th', scope: 'col' },
			'thr': { name: 'th', scope: 'row' }
		},
		selectedProps = nameToProps[ this.getValue() ];

	selectedCell.renameNode( selectedProps.name );

	if ( selectedProps.scope ) {
		selectedCell.setAttribute( 'scope', selectedProps.scope );
	} else {
		selectedCell.removeAttribute( 'scope' );
	}
}

关键细节有三点:

  1. 改名用 renameNode:td 与 th 之间通过 DOM 节点重命名完成,而不是删除重建;
  2. 写 scope 用 setAttribute:thc / thr 分别写出 scope="col" / scope="row";
  3. 切回 Data 时必须删除 scope:td 选项在 nameToProps 中没有 scope 属性,因此 commit 会执行 removeAttribute( 'scope' )——这正是原文档第 6 步“改为 Data 后不带有 scope 属性”的底层保证。

4.4 无 scope 的存量 th 与迁移方案

tabletools_scopedHeaders 配置项的默认值为 false,其完整文档位于 plugin.js:

CKEDITOR.config.tabletools_scopedHeaders = false;

文档注释(plugin.js)明确指出一个重要注意事项:如果启用该配置,而编辑器内容中恰好存在不带 scope 属性的 th 元素,那么这些单元格的 “Cell Type” 值会被显示为空值。为避免该问题,带 th 元素的旧内容需要被“迁移”——即手动或自动为其补上 scope 属性。

仓库给出的推荐迁移方式是使用内容过滤器变换(filter transformations)为所有无 scope 的 th 自动补上 scope="col":

editor.filter.addTransformations( [
	[
		{
			element: 'th',
			left: function( el ) {
				return !el.attributes.scope;
			},
			right: function( el ) {
				el.attributes.scope = 'col';
			}
		}
	]
] );

该变换通过 CKEDITOR.filter#addTransformations 注册,left 判断“当前 th 没有 scope”,right 执行“补上 scope="col"”。配套的手动测试 scopedheaders.md 正是验证这一边界场景:开启 scoped 表头后,无 scope 的 th 在 Cell Type 下拉框中显示为空选项,且可用选项仅为 Data、Column Header、Row Header。

五、对话框的多单元格处理与提交流程

除了类型字段本身的映射,tabletools 的单元格属性对话框对“多选单元格”场景也有专门设计,理解这一点有助于更完整地执行本文的验证:

  • 读取模型:对话框通过 CKEDITOR.plugins.tabletools.getSelectedCells( editor.getSelection() ) 获取当前选中的所有单元格(tableCell.js)。
  • 多单元格回显:字段的 setup 统一由 setupCells 包装(tableCell.js)。其逻辑是:取第一个单元格的值,若其余单元格该属性值与之不同,则该字段置空,避免误导用户“所有选中单元格都相同”。这也意味着在验证第 2、4、6 步时,若一次性选中了多个单元格,只有它们类型一致时下拉框才会显示具体值。
  • 只提交变更:对话框 onLoad 会把每个字段的 setup/commit 包装为“记录打开时的值,仅当提交值与打开值不同时才真正写回”(tableCell.js,对应历史 ticket 11439 的修复)。因此重复打开对话框而不改动任何值,不会对内容造成多余写入。
  • 提交后刷新选择:onOk 中对所有选中单元格逐一 commitContent,随后执行 editor.forceNextSelectionCheck()、selection.selectBookmarks( bookmarks ) 与 editor.selectionChange()(tableCell.js),确保操作后选择状态与工具栏命令状态同步刷新。
  • 命令注册:cellProperties 命令在 plugin.js 注册为对话框命令,声明了 allowedContent: 'td th{width,height,border-color,background-color,white-space,vertical-align,text-align}<a href="https://link.gitcode.com/i/8881462683faab9cbf76d513cc82b16e" target="_blank">colspan,rowspan]' 与包含 th 的 requiredContent,并设置了 contextSensitive 刷新逻辑——只有当光标路径位于 td 或 th 内时才处于可用状态([plugin.js)。

六、自动化测试佐证

除了手动测试用例,仓库还提供了对应的自动化测试 tests/plugins/tabletools/cellproperties.js。该测试文件同时注册了三个编辑器实例:

bender.editors = {
	basic: { name: 'basic' },
	scopedHeadersOn: {
		name: 'scopedHeadersOn',
		config: { tabletools_scopedHeaders: true }
	},
	scopedHeadersOff: {
		name: 'scopedHeadersOff',
		config: { tabletools_scopedHeaders: false }
	}
};

即分别用“默认”“开启 scoped”“关闭 scoped”三种配置验证对话框各字段(宽高、单位、自动换行、行列合并、对齐、颜色等)的加载与提交行为,与手动用例形成互补。手动用例聚焦 UI 交互与 HTML 结构核验,自动化用例则从 API 层面断言 dialog.getValueOf / setValueOf 的结果。

七、如何在本地复现与验证

要复现本手动测试,需要在仓库根目录下基于 Bender 测试框架运行手动测试集。仓库根目录提供了 bender.js、bender.ci.js 与 bender-runner.config.json 等配置文件。运行方式概述如下:

  1. 按仓库文档安装 Bender 相关依赖并启动 Bender 服务器;
  2. 在 Bender 界面中定位到 tests/plugins/tabletools/manual 目录下的 cellpropertiescelltype 用例(桌面端浏览器);
  3. 按第三节的七步流程操作,每步切换类型后打开 Source 模式核对 HTML:
    • Column Header → <th scope="col">
    • Row Header → <th scope="row">
    • Data → <td>(无 scope)
  4. 如需验证默认配置的对照行为,可参照 scopedheaders.html,该用例在同一页面分别用 tabletools_scopedHeaders: true 与 false 初始化多个编辑器进行对比。

八、总结

围绕 cellpropertiescelltype.md 这份手动测试,可以提炼出本功能的完整技术画像:

  • 功能入口:右键单元格 → Cell → Cell Properties 对话框中的 Cell Type 下拉框;
  • 配置开关:config.tabletools_scopedHeaders,默认 false,开启后提供 Data / Column Header / Row Header 三种类型(plugin.js);
  • 底层映射:td、th、th scope="col"、th scope="row" 四态与对话框内部值 td / th / thc / thr 一一对应,由 tableCell.js 的 setup/commit 保证双向一致;
  • 存量迁移:无 scope 的 th 在 scoped 模式下会被显示为空选项,需要借助 filter.addTransformations 补写 scope 属性完成迁移;
  • 验证手段:通过源码模式核验真实 HTML,是确保“对话框显示”与“实际 DOM”一致的最直接手段。

对于任何需要提升表格无障碍性(让屏幕阅读器准确理解表头与数据单元格的关联)的 CKEditor 4 集成场景,本文的配置说明、源码映射与验证流程均可直接作为实施与测试依据。

登录后查看全文
ckeditor4