Manipulate innerText of a CKEditor ViewElement

395 Views Asked by At

I am creating a little custom plugin for the CKEditor5 for the #neoscms. Neos is using the #ckeditor5 but with a custom view.

The plugin is more or less a placeholder plugin. The user can configure a data-source with a key value store for items (identifier and labels). The dropdown in the CKEditor is filled with the items and when the user selects an item from the dropdown, it creates a placeholder element that should end in a span element with some data-attributes.

The main idea was to have an empty element and just data-attributes to identify the element and being able to assign live data. But it turns out that the live data thing is tricky. When I manipulate the span with an extra JS snippet on the Website, the CKEditor cannot handle this.

Is it possible to manipulate a view element in the DOM and still have a working Editor? The Plugin works fine if I just add inner Text in the downCasting and don't replace something. But the live data would be nice.

Neos Backend with a element

Maybe that code gives an idea of the package. It is not ready yet as this is more or less the main feature ;)

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;
        }
    }
}

So, the extra JS snippet that is used by the website is searching for spans with the class foobar-placeholder and writes a value with live data into the span. That works in the frontend, of course, but the backend of the CMS that uses CKEditor has issues with the changing data.

1

There are 1 best solutions below

0
On

I could not find a solution with docs of CKEditor, and maybe I misuse the API somehow, but I now found a working solution for me.

My website snippet is now communicating with the Plugin via Broadcast messages. And then I search for placeholder elements and check if I need to change an attribute.

const broadcastChannel = new BroadcastChannel('placeholder:changeData');
broadcastChannel.postMessage({identifier: name, value});

And in the plugin

// Receive new values for placeholder via broadcast
        const broadcastChannel = new BroadcastChannel('placeholder:changeData');
        broadcastChannel.onmessage = (message) => {
            const identifier = get('data.identifier', message);
            const newValue = get('data.value', message);
            this.editor.model.change( writer => {
                if (identifier) {
                    this._replaceAttribute(writer, identifier, newValue);
                }
            });
        };

Only downside now is that I need to reload the page, but already read that this is maybe cause by my element down casting and I change attributes.