You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CKEditor自定义占位符插件表格内span的data属性重载丢失问题

问题根因

表格单元格内的占位符属性被剥离,是两层默认规则未覆盖自定义元素导致的,和span标签本身无关:

  • CKEditor5的Table插件会将每个<td>、<th>初始化为独立的嵌套可编辑实例,这些实例默认不会继承根编辑器注册的自定义元素schema、内容转换规则,重载初始化时会自动清理不在自身白名单内的类名、自定义属性,仅保留标签结构与内部文本。你测试发现<i>标签能正常保留属性,只是因为默认白名单对这类行内元素预先开放了属性权限,不是i标签有特殊保护逻辑。
  • Neos对表格这类嵌套内容块额外配置了服务端内容清洗规则,默认仅放行span标签本身,自定义data属性、非内置类名会在内容入库前被过滤。
解决方案

全程不需要替换span标签,按以下三步做最小范围配置即可:

1. 给嵌套表格编辑器同步注册占位符规则

你之前给根编辑器写的占位符schema、upcast/downcast转换逻辑,需要同步注册到所有td/th的嵌套编辑器实例上,在你的自定义插件init方法中加入以下逻辑:

editor.on('instanceReady', () => {
  const nestedEditableEls = editor.ui.getEditableElement()?.querySelectorAll('.ck-editor__nested-editable');
  nestedEditableEls?.forEach(el => {
    const nestedEditor = el.ckeditorInstance;
    if (!nestedEditor) return;

    // 以下注册逻辑和你在根编辑器写的占位符规则完全一致
    nestedEditor.model.schema.register('fooPlaceholder', {
      inheritAllFrom: '$inlineObject',
      allowAttributes: ['placeholderIdentifier', 'placeholderValue', 'nodeIdentifier']
    });

    // 配置upcast:将带对应属性的span转换为模型元素
    nestedEditor.conversion.for('upcast').elementToElement({
      view: {
        name: 'span',
        classes: 'foo-placeholder'
      },
      model: (viewEl, { writer }) => {
        return writer.createElement('fooPlaceholder', {
          placeholderIdentifier: viewEl.getAttribute('data-placeholder-identifier'),
          placeholderValue: viewEl.getAttribute('data-placeholder-value'),
          nodeIdentifier: viewEl.getAttribute('data-node-identifier')
        });
      }
    });

    // 配置editingDowncast:将模型元素渲染为带widget属性的span
    nestedEditor.conversion.for('editingDowncast').elementToElement({
      model: 'fooPlaceholder',
      view: (modelEl, { writer }) => {
        const placeholderSpan = writer.createContainerElement('span', {
          class: 'foo-placeholder ck-widget',
          contenteditable: 'false',
          'data-placeholder-identifier': modelEl.getAttribute('placeholderIdentifier'),
          'data-placeholder-value': modelEl.getAttribute('placeholderValue'),
          'data-node-identifier': modelEl.getAttribute('nodeIdentifier')
        });
        return toWidget(placeholderSpan, writer, { hasSelectionHandler: true });
      }
    });

    // 配置dataDowncast:保存时输出正确的带属性span结构
    nestedEditor.conversion.for('dataDowncast').elementToElement({
      model: 'fooPlaceholder',
      view: (modelEl, { writer }) => {
        return writer.createContainerElement('span', {
          class: 'foo-placeholder',
          'data-placeholder-identifier': modelEl.getAttribute('placeholderIdentifier'),
          'data-placeholder-value': modelEl.getAttribute('placeholderValue'),
          'data-node-identifier': modelEl.getAttribute('nodeIdentifier')
        });
      }
    });
  });
});

2. 配置Neos表格白名单放行自定义属性

找到项目内的表格节点配置Yaml(一般在NodeTypes/Content/Table.yaml或全局CKEditor预设配置中),给内容处理规则加放行配置,避免服务端清洗时剥离属性:

'Neos.NodeTypes:Table':
  properties:
    content:
      ui:
        inline:
          editorOptions:
            allowedHtml:
              span:
                attributes:
                  class: true
                  contenteditable: true
                  'data-placeholder-identifier': true
                  'data-placeholder-value': true
                  'data-node-identifier': true

3. 关闭表格对widget元素的自动拆分逻辑

在插件初始化时加一行配置,禁止表格编辑逻辑对识别为widget的占位符元素做拆分、额外包裹处理:

editor.plugins.get('Table').editingDowncastDispatcher.on('insert:tableCell', (evt, data, conversionApi) => {
  conversionApi.writer.setCustomProperty('allowNestingWidgets', true, data.item);
}, { priority: 'high' });
验证方式

改完后清除Neos应用缓存、浏览器硬刷新:

  • 在任意表格单元格内插入占位符,保存发布
  • 重载页面后检查单元格内元素,占位符span的类名、所有自定义data属性会完整保留,功能恢复正常

注意:不要全局关闭CKEditor的内容过滤、也不要修改Table插件核心源码,以上配置为最小范围放行规则,不会引入额外XSS安全风险。

内容的提问来源于stack exchange,提问作者Markus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 13:36:26