• 前端
  • UI组件

【免费下载链接】ckeditor4

The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.

项目地址: https://gitcode.com/gh_mirrors/ck/ckeditor4
点击查看 免费下载

本文围绕 CKEditor 4 仓库中 preservestyle.md 这份手动测试用例展开,完整还原它对 Font family(字体)与 Font size(字号)下拉框的"重复应用不丢失、(Default) 一键重置"验收场景,并结合 font 插件源码 与 自动化测试 深入讲解其背后的命令执行链、样式匹配(checkElementMatch)判定与折叠选区切分逻辑,帮助读者理解这一行为从 4.8.0(issue #584)起的设计动机与实现细节。

1. 背景:#584 带来的交互行为变更

这份手动测试文件位于 bender 手动测试目录 tests/plugins/font/manual/ 下,文件头部的元数据声明了它的归属:

@bender-tags: 4.8.0, feature, 584
@bender-ui: collapsed
@bender-ckeditor-plugins: wysiwygarea, toolbar, font, elementspath, sourcearea

即该场景对应 CKEditor 4.8.0 中修复的 issue #584,需要启用 wysiwygarea、toolbar、font、elementspath、sourcearea 插件。CHANGES.md 的 CKEditor 4.8 章节对此有明确记载:

#584: [Font size and Family] and [Format] drop-downs are not toggleable anymore. Default option to reset styles added.

换言之,4.8.0 之前,点击一个已经处于激活状态的下拉项会把样式"切换"掉(toggleable);4.8.0 之后,重复点击同一项不再撤销样式,而是保持原状,同时下拉列表顶部新增了 (Default) 选项作为唯一的重置入口。preservestyle.md 正是针对这一行为变更编写的验收脚本。配套的验证页面是 preservestyle.html,其中仅有一个 textarea 和 CKEDITOR.replace( 'editor' ) 初始化调用,属于默认配置的编辑器实例。

2. 手动测试场景全文(Font size 与 Font family 均需执行)

原文档要求:"Note: Perform below scenario for both Font size and Font family drop-downs.",即下述步骤要对字号下拉框和字体下拉框分别完整执行一遍。原文给出的操作步骤如下:

  1. 选中一段文字(Select some text)。
  2. 修改 Font size(例如选 24)。
  3. 重新选中这段文字。
    • 预期:Font size 下拉框应回显上一步选择的值。
  4. 再次选择相同的字号(例如还是 24)。
    • 预期:选中文本应保持其字号不变(preserve its font size)。
  5. 选择 (Default) 选项以重置 Font size。
    • 预期:字号恢复默认,承载字号样式的 <span> 元素应消失。

原文档的 Expected 结论为:

Choosen font size/name will remain the same, after click on its corresponding item in the drop-down menu. The item will remain marked as active. Selected text preserves chosen style after reapplying it multiple times.

即:点击下拉菜单中已对应当前样式的条目后,所选字号/字体名保持不变,条目继续保持"激活"标记,且多次重复应用后选区仍保留所选样式。

3. 插件实现:两个 RichCombo 与六个配置项

plugins/font/plugin.js 在 init 阶段通过 addCombo() 注册两个下拉框,二者共用同一套代码,仅参数不同:

参数Font(字体)FontSize(字号)
comboNameFontFontSize
commandNamefontfontSize
styleVariablefamilysize
entriesconfig.font_namesconfig.fontSize_sizes
defaultLabelconfig.font_defaultLabelconfig.fontSize_defaultLabel
styleDefinitionconfig.font_styleconfig.fontSize_style
order(工具栏排序)3040

插件依赖 richcombo(requires: 'richcombo')。相关的六个配置项均在 plugin.js 底部声明默认值:

  • config.font_names:字体列表,分号分隔,支持 显示名/实际CSS值 语法。默认值:

    CKEDITOR.config.font_names = 'Arial/Arial, Helvetica, sans-serif;' +
        'Comic Sans MS/Comic Sans MS, cursive;' +
        'Courier New/Courier New, Courier, monospace;' +
        'Georgia/Georgia, serif;' +
        'Lucida Sans Unicode/Lucida Sans Unicode, Lucida Grande, sans-serif;' +
        'Tahoma/Tahoma, Geneva, sans-serif;' +
        'Times New Roman/Times New Roman, Times, serif;' +
        'Trebuchet MS/Trebuchet MS, Helvetica, sans-serif;' +
        'Verdana/Verdana, Geneva, sans-serif';
    
  • config.font_defaultLabel:当光标处没有任何字体匹配时显示在字体下拉框中的文本,默认为空字符串 ''。

  • config.font_style:应用字体时使用的样式定义,默认:

    CKEDITOR.config.font_style = {
        element: 'span',
        styles: { 'font-family': '#(family)' },
        overrides: [ { element: 'font', attributes: { 'face': null } } ]
    };
    
  • config.fontSize_sizes:字号列表,同样支持 显示名/CSS值 语法(如 'Bigger Font/14px'),支持 12px、2.3em、130%、larger、x-small 等任意 CSS 尺寸。默认值覆盖 8/8px 到 72/72px 共 15 档,其中就包含手动测试场景里用到的 24/24px:

    CKEDITOR.config.fontSize_sizes =
        '8/8px;9/9px;10/10px;11/11px;12/12px;14/14px;16/16px;18/18px;20/20px;22/22px;24/24px;26/26px;28/28px;36/36px;48/48px;72/72px';
    
  • config.fontSize_defaultLabel:字号下拉框在无匹配时的显示文本,默认为 ''。

  • config.fontSize_style:应用字号的样式定义,默认:

    CKEDITOR.config.fontSize_style = {
        element: 'span',
        styles: { 'font-size': '#(size)' },
        overrides: [ { element: 'font', attributes: { 'size': null } } ]
    };
    

overrides 中的 face/size 属性覆盖,以及 addCombo 中注册的 contentTransformations,共同保证旧式 <font face="..." size="..."> 元素能被自动转换为 <span style="...">(size 属性会映射到 sizes 数组:x-small … 48px,其中索引 6 的 '48px' 注释为 "Closest value to what size="7" might mean")。

4. 命令执行链:为什么重复点击同项会"保持"而非"切换"

手动测试第 4 步(再次选择 24)的正确性取决于 exec 的逻辑。addCombo 中通过 editor.addCommand( definition.commandName, {...} ) 注册的命令,其 exec 流程如下(plugin.js):

  1. 从 data.newStyle / data.oldStyle 取出目标样式与旧样式;newStyle === undefined 即视为"移除"(对应点击 (Default))。

  2. 若 oldStyle 与 newStyle 均为空,直接返回,什么都不做。

  3. 若存在 oldStyle 且当前选区是折叠选区(collapsed range),先调用 splitElementOnCollapsedRange() 把元素切开(见第 6 节),因为直接 removeStyle 会删掉整个元素而不是只作用于光标位置。

  4. 若是移除操作:editor.removeStyle( oldStyle )。

  5. 若是应用操作:仅当 oldStyle 与 newStyle 不相等时才先 editor.removeStyle( oldStyle ),随后总是 editor.applyStyle( newStyle ):

    if ( isRemove ) {
        editor.removeStyle( oldStyle );
    } else {
        if ( oldStyle && !isEqualStyle( oldStyle, newStyle ) ) {
            editor.removeStyle( oldStyle );
        }
        editor.applyStyle( newStyle );
    }
    

这里就是"#584 不再 toggleable"的落点:旧逻辑下再次点击同项会走向 removeStyle 分支;新逻辑下 isEqualStyle( oldStyle, newStyle ) 返回 true,跳过了移除步骤,只再做一次 applyStyle,而 CKEditor 的样式系统对已存在的同一样式会合并而非嵌套包裹,因此 <span style="font-size:24px"> 原样保留。isEqualStyle 通过比较两个样式的 attributes(CKEDITOR.tools.objectCompare)与 CKEDITOR.style.getStyleText() 序列化结果来判定"同一样式"。

(Default) 选项则走移除分支:combo 的 defaultValue 被固定为 'cke-default'(plugin.js),而 StyleData._.data 中不存在该键,故 stylesData.getStyle( 'cke-default' ) 返回 undefined,触发 isRemove = true,最终 removeStyle 把承载样式的 <span> 从 DOM 中移除——正对应手动测试第 5 步"<span> 元素应消失"的预期。自动化测试 font.js 中的 #584 test remove font size from text 用 <p><span style="font-size:12px">[foo]</span></p> 点击默认值后断言结果为 <p>foo@</p>,与手动场景第 5 步一一对应。

onClickHandler(plugin.js)是 UI 与命令之间的桥梁:

function onClickHandler( newValue ) {
    var oldValue = this.getValue();
    editor.focus();
    editor.fire( 'saveSnapshot' );
    editor.execCommand( definition.commandName, {
        newStyle: stylesData.getStyle( newValue ),
        oldStyle: stylesData.getStyle( oldValue )
    } );
    editor.fire( 'saveSnapshot' );
}

注意 oldValue 取自 this.getValue()——即 combo 当前回显的值,而不是"光标处 DOM 上的实际样式"。这也解释了为什么手动测试第 3 步(重新选中文字后下拉框回显之前选的值)必须先发生:combo 的显示值本身就是 oldStyle 的来源。此外前后各有一次 saveSnapshot,保证 undo 可整体回退这次操作。

5. 下拉框回显机制:selectionChange 与 checkElementMatch

手动测试第 3 步"Font size 下拉框显示上一步选中的值"以及 Expected 中的"item will remain marked as active",由 combo 的 onRender 中的 selectionChange 监听实现(plugin.js):

editor.on( 'selectionChange', function( ev ) {
    var currentValue = this.getValue(),
        elementPath = ev.data.path,
        value = stylesData.getMatchingValue( editor, elementPath );

    if ( value ) {
        if ( value != currentValue ) {
            this.setValue( value );
        }
        return;
    }
    // If no styles match, just empty it.
    this.setValue( '', definition.defaultLabel );
}, this );

其调用链为:选区变化 → 取当前 elementPath → StyleData.getMatchingValue() 遍历 path 上的元素,对每个元素调用 findMatchingStyleName()(plugin.js),后者用 CKEDITOR.tools.array.find 在全部样式名中寻找第一个满足 style.checkElementMatch( element, true, editor ) 的名字(fullMatch = true 要求严格匹配)。core/style.js 的 checkElementMatch 会比对元素名与样式定义的 element(均为 span),并用 compareCssText 逐条比对 style 属性中的 CSS 声明(如 font-size:24px)。匹配成功则 setValue( value ) 让对应条目高亮;没有任何样式匹配时回落到 defaultLabel(即空字符串或自定义的 font_defaultLabel 文本)。

另外,命令还注册了 refresh 钩子(plugin.js):当 defaultContentStyle.checkApplicable( path, editor, editor.activeFilter ) 为假时把命令置为 CKEDITOR.TRISTATE_DISABLED,例如样式被过滤或在表格等不允许的位置时,整个 combo 会被禁用。

6. 折叠选区下的元素切分:splitElementOnCollapsedRange

重复应用样式时最棘手的场景是光标处于样式元素中间(折叠选区)。若直接 removeStyle,会把整个 <span> 连同未选中的文本一起删掉。splitElementOnCollapsedRange 为此专门实现了三分支切分:

  1. 空元素(光标同时位于匹配元素的起止边界,如 <span style="font-size:48px"><em>[]</em></span>):用 createBookmark 保位,逐个 insertBefore 子节点后移除该元素,结果为 <em>[]</em>,不丢失内部其他空行内元素;
  2. 光标恰在元素边界上:把选区移出该元素(POSITION_BEFORE_START 或 POSITION_AFTER_END),再由 cloneSubtreeIntoRange()(plugin.js)递归克隆元素路径上的中间层,使后续输入继承原有嵌套结构;
  3. 光标在元素中间:range.splitElement( matching ) 将 <span> 一分为二,再把 startContainer 与匹配元素之间的路径元素克隆进新位置。

配套的自动化测试在 font.js 中覆盖了这些分支,例如:

  • test apply font size over another font size (collapsed selection):x<span style="font-size:48px">f{}oo</span>x 应用 24 后得到 x<span ...48px>f</span><span ...24px>bar</span><span ...48px>oo</span>x;
  • test apply font size over another font size (deeply nested collapsed selection):嵌套 <em><u> 场景下同样保留内层结构(对应 ticket 12690)。

7. 自动化测试对手动场景的直接印证

tests/plugins/font/font.js 顶部有一条关键注释,直接记录了 #584 的行为边界(font.js):

// Click again to exit the style.
// Since 4.8.0, 2nd click on the same menu item does not unselect it.
// It is required to click on the '(Default)' option to reset style (#584).

与 preservestyle.md 各步骤直接对应的断言测试包括:

命令层面的等价验证见 fontcommand.js,其中 test should do nothing when the same font style is applied 与 test should do nothing when the same fontSize style is applied 以 oldStyle === newStyle 的方式直接调用 editor.execCommand,断言 DOM 不变,进一步佐证了第 4 节中 isEqualStyle 分支的效果。

8. 如何执行该手动测试及相关场景

preservestyle 属于 bender 框架下的手动测试:每个场景由一对 .html + .md 文件构成(页面提供编辑器,md 提供步骤与预期),可结合仓库根目录的 bender.js 与 bender.ci.js 配置,通过 bender 的手动测试界面运行,md 文件头部声明的 @bender-ckeditor-plugins 会控制该页面加载的插件集。同一 manual/ 目录下还有多个相关验收场景,可对照阅读:

9. 小结

preservestyle.md 虽只描述了 5 步手动操作,但其验收点精确对应 font 插件实现中的三处关键逻辑:exec 中基于 isEqualStyle 的"同样式跳过移除"分支、combo onRender 中基于 checkElementMatch 的选区回显与高亮,以及 (Default)(cke-default)触发 removeStyle 的重置路径。对于在 CKEditor 4(当前仓库版本 4.25.1-lts,见 package.json)上定制字体/字号下拉框的开发者,理解这条链路后可以安全地覆写 font_names、fontSize_sizes 与两个 *_style 配置项——它们的解析全部发生在 StyleData 构造函数 中(分号切分条目、斜杠切分显示名与 CSS 值、#(variable) 模板填充),行为不会因配置变化而偏离上述保持/重置语义。

  • 前端
  • UI组件

【免费下载链接】ckeditor4

The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.

项目地址: https://gitcode.com/gh_mirrors/ck/ckeditor4
点击查看 免费下载
Logo

邀请您加入社区

更多推荐