Selection in node view

I am unable to get the simple behavior where the cursor can seamlessly enter and exit a node view, I expected this to be the default behavior when contentDOM is defined in a node view, however by default it is seen as an atom, unless you explicitly click on some of the text within the node view.

I tried to get this behavior by checking from which side the cursor entered and then setting the selection to either the first (left) or last (right) character within the node view, but I cannot seem to get selection to work within a node view.

If I throw an exception within selectNode, it does work the way I want, namely DOM marked not contentEditable are considered atoms, the rest can have editable text. That probably is the default behavior of contentEditable. This is the node view I am testing:

class TagView {
    private dom: HTMLElement;
    private contentDOM: HTMLElement;
    constructor(
            private node: ProseNode,
            private view: EditorView,
            private getPos: () => number,
            private decorations: Decoration[]) {
        console.log("TagView")
        const tag = document.createElement("span");
        tag.classList.add("tag");
        const before = document.createElement("span");
        before.classList.add("before");
        before.appendChild(document.createTextNode("{{"));
        before.contentEditable = "false";
        const after = document.createElement("span");
        after.classList.add("after");
        after.appendChild(document.createTextNode("}}"));
        after.contentEditable = "false";
        const contents = document.createElement("span");
        contents.classList.add("contents");
        tag.appendChild(before);
        tag.appendChild(contents);
        tag.appendChild(after);
        this.dom = tag;
        this.contentDOM = contents;
    }
    setSelection() {
        console.log("set selection");
    }
    selectNode() {
        console.log("select node");
        this.view.dispatch(this.view.state.tr.setSelection(TextSelection.create(this.view.state.doc, this.getPos())));
    }
    deselectNode() {
        console.log("deselect node");
    }
}

I thought maybe I misunderstood how selections worked, but when I tried them outside the node view, all my tests matched my expectations:

window.addEventListener("keydown", e => {
    if (e.ctrlKey && e.keyCode === "D".charCodeAt(0)) {
        e.preventDefault();
        e.stopImmediatePropagation();
        console.log("pressed ctrl+d");
        const view = editorView;
        const state = view.state;
        const selection = TextSelection.create(state.doc, 3, 4)
        // const selection = Selection.near(state.doc.resolve(4))
        const selectTr = state.tr.setSelection(selection)
        view.focus()
        view.dispatch(selectTr)
        // view.updateState(state.apply(selectTr))
    }
});

Am I misunderstanding something about node views? I know I could workaround the issue by replicating what is done in the footnote example, but this would cost more resources and should not be necessary for simple cases.

That might be a bug. Does the behavior improve if you add && node.isAtom to this line?

Yes, adding && node.isAtom solves the immediate problem since it no longer is considered an atom and allows me to enter it from either side. However it seems there is still an open bug, something I failed to mention, namely that if you exit the node view from the right, it will skip the adjacent character. So if I start with t{{es|}}t and go right, it will result in t{{es}}t|, rather than the expected t{{es}}|t.

Also, it seems I somehow have broken ProseMirror after updating to 0.23.0, because any change I make is automatically corrected back to the original initial state, but I remember seeing the right exit cursor location bug before the update as well, so it should be unrelated to this. I will check the release notes and my code to figure why ProseMirror is now no longer working properly.

ProseMirror was broken due to yarn not upgrading the sub dependencies, so some packages were still on 0.22 while other were using 0.23, causing it to break on new features and such.

Now that it is fixed, I can confirm that the mentioned cursor bug is still present.

Added a patch to that effect here.

This is the reason why the recommendation for inline nodes with content is ‘use a node view, and add your own code to make the cursor behave appropriately on the boundaries’ — there is no obvious way the library should handle this (in many cases, it moving across the boundaries like this is the desired behavior), so I’m not even going to try. You might be able to kludge around this particular issue by putting zero-width spaces at the sides of your node view.

Thanks for the patch!

Ah right, now that you mention zero-width spaces I remember it being mentioned in Discussion: Inline nodes with content. Thanks for the tip and explanation!

The zero-width space does create a boundary, but as described in the linked topic you get the behavior where the same cursor position is used to represent two different actual positions, which is not what I was aiming for. I experimented a bit myself and found that if I add an empty span on both sides, they also function as boundaries, but without the downsides of introducing additional characters. It is an ugly workaround, but it seems to work just fine, so I will be using that for now.

1 Like

After some sleep and checking again, I noticed that on the right side the empty span is filled once I use this and write there, and if I put contenteditiable="false" on them, you also get, the same visual cursor position represents two, behavior. So I guess I have to live with that behavior for now, or maybe I should just try and correct the cursor position itself instead.