- 前端
- UI组件
【免费下载链接】ckeditor4
The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.
本文以 tests/plugins/table/manual/editingmode.md 手动测试用例为主线,深入剖析 CKEditor 4 自 4.13.0 起引入的对话框"创建模式(creation mode)"与"编辑模式(editing mode)"机制(对应 issue #2423)。读完本文,你将掌握
CKEDITOR.dialog#getModel、#getMode、#setModel三个核心 API 的语义与判定规则,能读懂表格插件(table/tableProperties)两个对话框的模式差异,并能基于仓库中的手动测试与自动化测试复现、验证该行为,进而为自己的插件对话框实现同样的模式区分。
一、背景:为什么对话框需要"模式"概念
在 CKEditor 4.13.0 之前,同一个对话框往往要同时承担两种职责:新建一个对象(例如插入一张新表格)与编辑一个已有对象(例如修改光标所在表格的属性)。开发者在onShow/onOk中不得不自行判断"当前有没有选中的目标元素",逻辑分散且难以复用。
从 4.13.0 开始(对应 issue #2423,见 editingmode.md 中的@bender-tags: dialog, 4.13.0, 2423, feature),对话框框架正式引入两种模式:
- 创建模式(creation mode):对话框用于向编辑器引入新内容,例如插入一张新建的表格;
- 编辑模式(editing mode):对话框用于修改编辑器现有内容,例如更新光标所在已有表格的属性。
这两个模式由静态常量定义在 plugins/dialog/plugin.js 中:
CKEDITOR.tools.extend( CKEDITOR.dialog, { /** * Indicates that the dialog is introducing new changes to the editor, for example inserting * a newly created element as a part of a feature used with this dialog. * * @static * @since 4.13.0 */ CREATION_MODE: 0, /** * Indicates that the dialog is modifying the existing editor state, for example updating * an existing element as a part of a feature used with this dialog. * * @static * @since 4.13.0 */ EDITING_MODE: 1 } );CREATION_MODE的值为0,EDITING_MODE的值为1,自动化测试中正是通过比较这两个常量来断言对话框所处模式(见下文"自动化测试验证"一节)。
二、模式判定的核心 API:getModel 与 getMode
模式并不是凭空产生的,它由对话框的"模型(model)"推导而来。这里的"模型"指的是对话框所作用的目标对象,其 API 契约定义在 plugins/dialog/dialogDefinition.js:
getModel( editor ):返回对话框的主题对象。对于table、link这类插件,通常返回一个CKEDITOR.dom.element;对于image2、placeholder这类 widget 插件,返回CKEDITOR.plugins.widget实例;如果对话框不使用模型,返回null。getMode( editor ):返回对话框当前模式,取值为CKEDITOR.dialog#EDITING_MODE或CKEDITOR.dialog#CREATION_MODE。
在 plugins/dialog/plugin.js 中,这两个方法有明确的默认实现:
getModel: function( editor ) { // Prioritize forced model. if ( this._.model ) { return this._.model; } if ( this.definition.getModel ) { return this.definition.getModel( editor ); } return null; }, getMode: function( editor ) { if ( this.definition.getMode ) { return this.definition.getMode( editor ); } var model = this.getModel( editor ); if ( !model || ( model instanceof CKEDITOR.dom.element && !model.getParent() ) ) { return CKEDITOR.dialog.CREATION_MODE; } return CKEDITOR.dialog.EDITING_MODE; }判定规则可以归纳为以下三点(从源码结构看):
- 如果对话框定义中显式提供了
getMode,则优先调用它; - 否则回退到
getModel( editor )的结果进行推断:- 返回
null(如创建新表格时),判为CREATION_MODE; - 返回一个已挂接到 DOM的
CKEDITOR.dom.element(即getParent()非空),判为EDITING_MODE; - 返回
CKEDITOR.plugins.widget实例,判为EDITING_MODE;
- 返回
- 一个值得注意的边界情况:即使
getModel返回了元素,只要该元素尚未被挂接到文档(!model.getParent()),依然判为CREATION_MODE。这避免了"拿到一个游离元素却误入编辑模式"的隐患。
此外还有配套的私有方法setModel( newModel )(见 plugins/dialog/plugin.js),允许框架在合适时机强制指定对话框模型,getModel会优先返回被强制设置的模型。
三、表格插件中的模式实现:table 与 tableProperties
table插件在 plugins/table/plugin.js 中注册了两个对话框,但它们共享同一个定义文件:
CKEDITOR.dialog.add( 'table', this.path + 'dialogs/table.js' ); CKEDITOR.dialog.add( 'tableProperties', this.path + 'dialogs/table.js' );也就是说,plugins/table/dialogs/table.js 中定义的对话框定义同时服务于"插入表格"与"表格属性"两个场景,二者的差异正是由模式机制来区分的。
对话框定义中的getModel实现如下:
getModel: function( editor ) { if ( this.dialog.getName() !== 'tableProperties' ) { return null; } var selection = editor.getSelection(), range = selection && selection.getRanges()[ 0 ]; return range ? range._getTableElement( { table: 1 } ) : null; },这段代码的语义非常清晰:
- 当对话框名称不是
tableProperties(即用户通过工具栏按钮插入新表格)时,getModel直接返回null,getMode因此判定为CREATION_MODE; - 当对话框是
tableProperties(例如通过右键菜单打开"表格属性")时,从当前选区中提取表格元素,找到就返回该CKEDITOR.dom.element,getMode因此判定为EDITING_MODE;找不到(例如选中区域内其实没有表格)则返回null,回落为CREATION_MODE。
与之配合的是对话框onShow中的既有逻辑(同样位于 plugins/table/dialogs/table.js):如果通过选区/元素路径找到了目标表格,就调用this.setupContent( table )回填属性,并禁用"行数 / 列数"输入框(txtRows、txtCols),因为编辑已有表格时不允许再改行列数;否则启用这两个输入框,让用户指定新建表格的行列。而onOk中则通过this._.selectedElement || makeElement( 'table' )决定是"原地更新已有表格"还是"按txtRows/txtCols生成新表格并插入"(生成逻辑见 plugins/table/dialogs/table.js 的onOk部分,包含按行列循环append( makeElement( 'tr' ) )、append( makeElement( 'td' ) )并cell.appendBogus()的完整建表流程)。
从调用链看,编辑模式通常经由以下入口触发(见 plugins/table/plugin.js):
editor.addCommand( 'tableProperties', new CKEDITOR.dialogCommand( 'tableProperties', createDef() ) );- 右键菜单项
table(label 为lang.menu,对应"表格属性"),其command指向tableProperties,group: 'table'、order: 5; - 双击表格事件:
element.is( 'table' )时强制evt.data.dialog = 'tableProperties'。
四、手动测试:editingmode.md 逐步复现
editingmode.md 是一个 bender 手动测试(manual test)说明,配套页面为 editingmode.html,其中定义了一个含 5 行 4 列表格的编辑器实例,并通过辅助脚本把对话框状态实时输出到页面上方:
dialogTools.initDialogEditingModeTest( CKEDITOR.replace( 'editor' ) );辅助脚本位于 tests/plugins/dialog/manual/_helpers/tools.js,它在每个编辑器的dialogShow事件上挂接监听,展示三项信息:
Dialog name:对话框名称(table或tableProperties);Dialog is in ... mode:通过dialog.getMode( editor )与CKEDITOR.dialog.EDITING_MODE比较后显示editing或creation;Currently editing:通过dialog.getModel( editor )得到的目标对象——元素则输出其getOuterHtml(),widget 输出Widget (name),文档输出document,空值输出null。
手动测试共分三个阶段,预期结果如下(与测试文件中的Expected完全一致):
场景一:焦点位于首个段落时点击表格按钮
- 将焦点置于编辑器第一个段落;
- 点击工具栏"表格"按钮(触发
table对话框); - 观察编辑器上方的状态栏。
预期输出:
Dialog name: table in editor editor. Dialog is in creation mode. Currently editing: null场景二:选区位于第一个表格单元格内时点击表格按钮
- 取消对话框;
- 将选区置于第一个表格单元格内;
- 再次点击"表格"按钮;
- 观察状态栏。
预期输出(与场景一相同):
Dialog name: table in editor editor. Dialog is in creation mode. Currently editing: null这说明:即使光标已经位于表格内部,通过工具栏按钮打开的仍然是table对话框,模型为null,处于创建模式——插入新表格的语义与光标位置无关。
场景三:通过右键菜单打开"表格属性"
- 取消对话框;
- 将选区置于第一个表格单元格内;
- 打开右键菜单,点击
Table properties(表格属性)选项; - 观察状态栏。
预期输出:
Dialog name: tableProperties in editor editor. Dialog is in editing mode. Currently editing: [element HTML]此时对话框名称变为tableProperties,模式变为editing,模型为选中的表格元素(其getOuterHtml()会显示实际 HTML)。这正是getModel中range._getTableElement( { table: 1 } )生效的结果。
测试文件中还附带一条提示:在现代浏览器中,expected结果里你会看到"真实"的 HTML,而不是[element HTML]这样的占位字符串——这是手动测试文档的书写惯例。
五、自动化测试:同一行为的代码级验证
同一行为在自动化测试 tests/plugins/table/table.js 中得到了直接验证,与手动测试互为印证:
// (#2423) 'test create table': function() { ... bot.dialog( 'tableProperties', function( dialog ) { // Check defaults. assert.areSame( '3', dialog.getValueOf( 'info', 'txtRows' ) ); assert.areSame( '2', dialog.getValueOf( 'info', 'txtCols' ) ); // Table width is set either to 100% or 500px depending on the editable size. assert.areSame( isSmallViewport ? '100%' : '500px', dialog.getValueOf( 'info', 'txtWidth' ) ); // (#2423) assert.isNull( dialog.getModel( bot.editor ) ); assert.areEqual( CKEDITOR.dialog.CREATION_MODE, dialog.getMode( bot.editor ) ); ... } ); }, // (#2423) 'test model for existing table': function() { var editor = this.editors.editor, bot = this.editorBots.editor; bot.setData( '<table><tr><td>1</td></tr><tr><td>2</td></tr></table>', function() { var table = editor.editable().findOne( 'table' ); editor.getSelection().selectElement( table ); bot.dialog( 'tableProperties', function( dialog ) { assert.areEqual( table, dialog.getModel( editor ) ); assert.areEqual( CKEDITOR.dialog.EDITING_MODE, dialog.getMode( editor ) ); dialog.hide(); } ); } ); }, // (#2423) 'test model for table dialog': function() { var bot = this.editorBots.editor; bot.dialog( 'table', function( dialog ) { assert.isNull( dialog.getModel( bot.editor ) ); assert.areEqual( CKEDITOR.dialog.CREATION_MODE, dialog.getMode( bot.editor ) ); dialog.hide(); } ); }三个测试用例分别覆盖了:无目标表格时的创建模式、选中已有表格后的编辑模式、以及table对话框恒为创建模式。测试中还顺带验证了表格对话框的默认值(3 行、2 列、宽度按编辑区尺寸取100%或500px),可作为配置默认值的参考。
六、模式机制的可复用价值
从源码结构看,该机制并非表格插件专属。模式 API 的默认判定(plugins/dialog/plugin.js 中的getMode)对任何"创建/编辑两用"对话框都通用,仓库中div、image、iframe、link、docprops等插件及其测试(例如 tests/plugins/div/div.js、tests/plugins/link/dialog.js)同样引用了EDITING_MODE/CREATION_MODE常量;widget 类插件(如image2、placeholder)则在getModel中返回CKEDITOR.plugins.widget实例,配合 widget 的对话框集成(可参考 tests/plugins/widget/integration/dialog/model.js)实现同样的模式区分。
如果要在自己的插件对话框中应用这套机制,建议按如下步骤组织(均可对照表格插件实现验证):
- 在对话框定义中实现
getModel( editor ):创建场景返回null,编辑场景返回选中的目标CKEDITOR.dom.element(或 widget 实例); - 让
getMode沿用默认实现,由getModel自动推导模式; - 在
onShow中依据模型是否存在决定字段的可用状态与初始值回填(参考表格对话框对txtRows/txtCols的处理); - 在
onOk中依据this._.selectedElement(或等价标记)决定"更新已有对象"还是"新建并插入"。
七、如何运行验证
该手动测试属于 bender 测试体系,仓库根目录提供了 bender.js、bender.ci.js 与 bender-runner.config.json。运行手动测试的常规方式是在本地启动 bender 服务器后,通过浏览器访问测试页面并手动执行 editingmode.md 中的步骤;自动化用例则可通过bender run tests/plugins/table之类的方式批量执行(具体命令以项目测试文档为准)。
需要注意:该特性于 4.13.0 引入,仅适用于启用dialog插件体系且包含table插件的构建(手动测试页面声明依赖wysiwygarea, table, toolbar, contextmenu四个插件,见 editingmode.md 的@bender-ckeditor-plugins元数据);同时 plugins/table/plugin.js 中对右键菜单、双击事件的处理依赖contextmenu插件是否加载,这是复现"编辑模式"入口的前置条件。
- 前端
- UI组件
【免费下载链接】ckeditor4
The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.
相关推荐
CKEditor 4 Forms 插件对话框的创建模式与编辑模式:从 editingmode 手动测试深入 dialog 模型机制
CKEditor 4 Forms 插件对话框的创建模式与编辑模式:从 editingmode 手动测试深入 dialog 模型机制 本篇技术指南以 CKEdit
前端UI组件CKEditor 4 图片对话框的创建模式与编辑模式:从手动测试用例到源码实现
CKEditor 4 图片对话框的创建模式与编辑模式:从手动测试用例到源码实现 本文以 CKEditor 4 仓库中 tests/plugins/image/m
前端UI组件CKEditor 4 列表样式对话框编辑模式(Dialog Editing Mode)实战指南:基于 liststyle 插件的手动测试解析
CKEditor 4 列表样式对话框编辑模式(Dialog Editing Mode)实战指南:基于 liststyle 插件的手动测试解析 本文以 tests
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考