Custom Element (Web Component) won't accept keyboard input when inserted by a CKEditor 5 plugin

364 views Asked by At

I'm in the initial stages of developing a plugin that will allow the user to insert placeholder elements into HTML content that will be processed server-side and used to incorporate some simple logic into a generated PDF document. To this end, I'm attempting to insert a custom element that I've defined using the web components API.

class NSLoop extends HTMLElement {
    constructor() {
        super();
    }

    get source() {
        return this.getAttribute('source');
    }

    get as() {
        return this.getAttribute('as');
    }
}

window.customElements.define('ns-loop', NSLoop);

The contents of loopediting.js:

import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
import Widget from "@ckeditor/ckeditor5-widget/src/widget";
import {viewToModelPositionOutsideModelElement} from "@ckeditor/ckeditor5-widget/src/utils";
import LoopCommand from "./loopcommand";

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

    constructor(editor) {
        super(editor);
    }

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

        this.editor.commands.add('loop', new LoopCommand(this.editor));

        this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));
    }

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

        schema.register('loop', {
            isBlock: false,
            isLimit: false,
            isObject: false,
            isInline: false,
            isSelectable: false,
            isContent: false,
            allowWhere: '$block',
            allowAttributes: ['for', 'as'],
        });

        schema.extend( '$text', {
            allowIn: 'loop'
        } );

        schema.extend( '$block', {
            allowIn: 'loop'
        } );
    }

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

        conversion.for('upcast').elementToElement({
            view:  {
                name:    'ns-loop',
            },
            model: (viewElement, {write: modelWriter}) => {
                const source = viewElement.getAttribute('for');
                const as     = viewElement.getAttribute('as');

                return modelWriter.createElement('loop', {source: source, as: as});
            }
        });

        conversion.for('editingDowncast').elementToElement({
            model: 'loop',
            view:  (modelItem, {writer: viewWriter}) => {
                const widgetElement = createLoopView(modelItem, viewWriter);
                return widgetElement;
            }
        });

        function createLoopView(modelItem, viewWriter) {
            const source = modelItem.getAttribute('source');
            const as     = modelItem.getAttribute('as');
            const loopElement = viewWriter.createContainerElement('ns-loop', {'for': source, 'as': as});

            return loopElement;
        }
    }
}

This code works, in the sense that an <ns-loop> element is successfully inserted into the editor content; however, I am not able to edit this element's content. Any keyboard input is inserted into a <p> before the <ns-loop> element, and any text selection disappears once the mouse stops moving. Additionally, it is only possible to place the cursor at the beginning of the element.

If I simply swap out 'ns-loop' as the tag name for 'div' or 'p', I am able to type within the element without issue, so I suspect that I am missing something in the schema definition to make CKEditor aware that this element is "allowed" to be typed in, however I have no idea what I may have missed -- as far as I'm aware, that's what I should be achieving with the schema.extend() calls.

I have tried innumerable variations of allowedIn, allowedWhere, inheritAllFrom, isBlock, isLimit, etc within the schema definition, with no apparent change in behaviour.

Can anyone provide any insight?

Edit: Some additional information I just noticed - when the cursor is within the <ns-loop> element, the Heading/Paragraph dropdown menu is empty. That may be relevant.

Edit 2: Aaand I found the culprit staring me in the face.

this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));

I'm new to the CKE5 plugin space, and was using other plugins as a reference point, and I guess I copied that code from another plugin. Removing that code solves the problem.

1

There are 1 answers

0
MarkW On

As noted in the second edit, the culprit was the code,

this.editor.editing.mapper.on('viewToModelPosition', viewToModelPositionOutsideModelElement(this.editor.model, viewElement => viewElement.is('element', 'ns-loop')));

which I apparently copied from another plugin I was using for reference. Removing this code has solved the immediate problem.

I'll accept this answer and close the question once the 2-day timer is up.