How to re-set text selection at the end of a newly inserted text?

In a particular circumstence, i had to insertText in a pararaph node generated by the toDom of a certain custom node:

const nodeStartPos = vm.findNodePosition(doc, oldEditorParagraph);
tr = tr.insertText(textContent, nodeStartPos + 1, nodeStartPos + 1 + oldEditorParagraph.content.size);
const textNode = tr.doc.content.content[0].content.content[0].content.content[0].content.content[0];
const textNodeStartPos = vm.findNodePosition(tr.doc, textNode);
const newSelection = TextSelection.create(tr.doc, textNodeStartPos + textNode.text.length);
tr = tr.setSelection(newSelection);
view.updateState(view.state.apply(tr));

I noticed that after calling tr.insertText(), the previous selection is lost, that’s why i’m trying to reset it with tr.setSelection(newSelection) to get the selection back at the end of the newly inserted text.

I wonder what should be the second parameter value passed to TextSelection.create() (which is the $head parameter). I have been trying many expressions , finally i came with this textNodeStartPos + textNode.text.length which i think the most logical but it’s not working neither.

The problem is that i never get the selection back.

Another better solution is to force the paragraph inside the editorTitleHeader node to have a text node as content initially even when it’s empty. If so, it won’t be needed to insert text, just to update the pre-existing text node text property. I couldn’t do that throught the node’s definition.

Any clue please?

Here are my custom nodes (if needed)

const editorTitleHeader = {
    name: 'editorTitleHeader',
    inlineContent: true,
    content: 'paragraph',
    isTextblock: true,
    attrs: {
      'data-before-content': { default: 'Untitled item' },
    },
    parseDOM: [
      {
        tag: 'editor-title-header[data-before-content]',
        getAttrs(dom) {
          return true;
        },
      },
    ],
    toDOM(node) {
      const inner = document.createElement('editor-title-header');
      inner.classList.add('editorTitleHeaderCls');
      let textContent = '';
      if (
        node.content &&
        node.content.content &&
        node.content.content[0] &&
        node.content.content[0].content &&
        node.content.content[0].content.content &&
        node.content.content[0].content.content[0] &&
        node.content.content[0].content.content[0].text
      ) {
        textContent = node.content.content[0].content.content[0].text;
      }
      inner.textContent = textContent;
      return inner;
    },
  };
  const editorTitle = {
    name: 'editorTitle',
    content: 'editorTitleHeader',
    isTextblock: true,
    attrs: {
      class: { default: 'editorTitleCls' },
    },    
    parseDOM: [
      {
        tag: 'div[class]',
        getAttrs(dom) {
          return dom.getAttribute('class') == 'editorTitleCls';
        },
      },
    ],
    toDOM(node) {
      return ['div', { class: 'editorTitleCls' }, 0];
    },
    persistContent(newState) {
      proseMirrorOnChangeHandler(null, newState, mySchema, props);
    },
  };

I tired to reproduce the selection bug in this codesandbox but it seems that the stopEvent of the editorTitleView class isn’t working in that demo which prevents the ‘input’ listener from being triggered (i dunno why , in my app it all works fine)

I’m not entirely sure what’s going wrong, but I do want point out that an expression like tr.doc.content.content[0].content.content[0].content.content[0].content.content[0] is A) using the non-public Fragment.content property, B) really dodgy and fragile. If you already know the position you need (which I’d guess is nodeStartPos + 1 + textContent.length), why don’t you use that?

As an aside, finding nodes by identity, as I assume your findNodePosition function is doing, isn’t safe—it is perfectly valid for the same node object to appear multiple times in a document.

Actually , i found that it’s not only the previous text selection that is lost but the entire view lose the focus too. I found document.activeElement pointing to document.body after calling tr.insertText() and view.updateState(view.state.apply(tr)); So i tried to call view.focus(); and then reset the selection later and not before the view.updateState() call but i still didn’t get the selection back I tried also with nodeStartPos + 1 + textContent.length as you said but in vain

insertText most definitely does not change the browser focus. Probably you’re triggering this code from some control that’s being clicked and receiving focus.

Yes, you’re right, it’s not insertText which changes the focus. But as view.updateState() does update a node to which is attached a custom node view then the node view gets updated and its whole dom is recreated. That’s how the focus and selection are lost

This is a sequence of calls i found while debugging from the view.updateState() till the new node view instance constructor execution:

view.updateState()
...
ViewTreeUpdater.prototype.addNode()
...
NodeViewDesc.create()
...
new editorTitleView(node, view, getPos) // the node view reinstanciated

Why ? Node instances aren’t they uniques for each of their occurences in the doc?

They are just immutable values representing some document structure. Not, as I mentioned, objects with meaningful identities.

1 Like