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

CKEditor5自定义占位符插件修改ViewElement innerText致编辑器异常

CKEditor5 自定义占位符插件外部DOM操作兼容问题

场景说明

  • 开发目标:为Neos CMS开发适配的CKEditor5自定义占位符插件,Neos本身搭载了经过自定义视图改造的CKEditor5实现
  • 插件基础逻辑:支持配置存储「标识符、标签」键值对的数据源,编辑器下拉菜单自动加载所有配置项,用户选中对应项后即可插入占位符元素,最终输出为携带若干data属性的span标签
  • 初始方案问题:最初设计是生成仅携带data属性的空span元素,通过class和data属性标识元素后,由独立JS脚本注入实时业务数据;实际开发中发现该方案存在适配问题:外部JS直接修改编辑器内的span元素DOM时,CKEditor无法识别这类变更,会触发运行异常
  • 当前验证状态:在downCasting阶段给占位符元素写入固定innerText、且后续不对元素内容做替换时,插件运行完全正常;预期实现效果为空span支持动态注入实时数据,无需在转换阶段写死占位文本

Neos后台占位符元素示例

现有核心代码

当前插件尚未开发完成,动态数据注入为核心待实现特性,核心逻辑代码如下:

import {Plugin, toWidget, viewToModelPositionOutsideModelElement, Widget,} from "ckeditor5-exports";

import PlaceholderCommand from "./placeHolderCommand";

export default class PlaceholderEditing extends Plugin {
    static get requires() {
        return [Widget];
    }

    init() {
        this._defineSchema();
        this._defineConverters();

        this.editor.commands.add(
            "placeholder",
            new PlaceholderCommand(this.editor)
        );

        this.editor.editing.mapper.on(
            "viewToModelPosition",
            viewToModelPositionOutsideModelElement(this.editor.model, (viewElement) =>
                viewElement.hasClass("internezzo-placeholder")
            )
        );

        this.editor.config.define("placeholderProps", {
            types: ["name", "node", "nodePath"],
        });

        this.editor.config.define("placeholderBrackets", {
            open: "[",
            close: "]",
        });
    }

    _defineSchema() {
        const schema = this.editor.model.schema;

        schema.register("placeholder", {
            allowWhere: "$text",
            isInline: true,
            isObject: true,
            allowAttributes: [
                "name",
                "node",
                "nodePath",
                "data-placeholder-identifier",
                "data-node-identifier",
                "data-node-path",
            ],
        });
    }

    _defineConverters() {
        const conversion = this.editor.conversion;
        const config = this.editor.config;

        conversion.for("upcast").elementToElement({
            view: {
                name: "span",
                classes: ["foobar-placeholder"],
            },
            model: (viewElement, writer) => {
                const name = viewElement.getAttribute('data-placeholder-identifier');
                const node = viewElement.getAttribute('data-node-identifier');
                const nodePath = viewElement.getAttribute('data-node-path');
                const modelWriter = writer.writer || writer;
                return modelWriter.createElement("placeholder", {name, node, nodePath, editable: false});
            },
        });

        conversion.for("editingDowncast").elementToElement({
            model: "placeholder",
            view: (modelItem, writer) => {
                const viewWriter = writer.writer || writer;
                const widgetElement = createPlaceholderView(modelItem, viewWriter);
                return toWidget(widgetElement, viewWriter);
            },
        });

        conversion.for("dataDowncast").elementToElement({
            model: "placeholder",
            view: (modelItem, writer) => {
                const viewWriter = writer.writer || writer;

                return createPlaceholderView(modelItem, viewWriter);
            },
        });

        // Helper method for downcast converters.
        function createPlaceholderView(modelItem, viewWriter) {
            const name = modelItem.getAttribute("name");
            const node = modelItem.getAttribute("node");
            const nodePath = modelItem.getAttribute("nodePath");

            const placeholderView = viewWriter.createContainerElement("span", {
                class: "foobar-placeholder",
                "data-placeholder-identifier": name,
                "data-node-identifier": node,
                "data-node-path": nodePath,
            });

            // Would be nice to remove that and have just empty spans that get dynamic data
            let innerText = config.get("placeholderBrackets.open") + name;
            innerText += config.get("placeholderBrackets.close");
            viewWriter.insert(
                viewWriter.createPositionAt(placeholderView, 0),
                viewWriter.createText(innerText)
            );

            return placeholderView;
        }
    }
}

补充信息与核心疑问

站点前端使用的独立JS脚本会查找所有class为foobar-placeholder的span元素,向其中写入实时业务数据,该逻辑在普通前端页面运行完全正常,但在搭载CKEditor的CMS后台编辑页执行时,会因DOM被外部修改导致编辑器运行异常。

核心疑问:是否可以直接操作DOM层对应的CKEditor ViewElement,同时保证编辑器正常运行?

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 23:30:52