Replacing content with textNode results to visually empty paragraph

I will try to be short:

  1. I have a custom inline node which can be a part of a paragraph and it can hold a text
export const noteGenerationSchema = new Schema({
  nodes: {
    doc: {
      content: 'paragraph+'
    },
    text: {
      group: 'inline'
    },
    paragraph: {
      ...baseSchema.spec.nodes.get('paragraph'),
      content: 'inline*',
      group: 'block',
      parseDOM: [{ tag: 'p' }],
      toDOM() {
        return ['p', 0];
      }
    },
    addedContent: {
      inline: true, // It's an inline node
      group: 'inline', // Can be used inside inline content like text
      content: 'text*', // It can have text inside it
      toDOM() {
        return ['span', { class: 'added-content highlight' }, 0];
      },
      parseDOM: [
        {
          tag: 'span.added-content'
        }
      ]
    }
  },
});

  1. I have a nodeView constructor for addedContent which applies clickEvent on it and tries to replace this node with simple text node
export class WithCustomEventsNodeViewBuilder implements NodeView {
  public dom: HTMLElement;
  public contentDOM: HTMLElement | undefined;

  public actualNode: Node;
  public actualPos: number | undefined;

  constructor(
    public node: Node,
    public view: EditorView,
    public getPos: () => number | undefined,
  ) {
    this.actualNode = node;

      const { dom, contentDOM } = DOMSerializer.renderSpec(document, node.type.spec.toDOM!(node), null);

      this.dom = dom as HTMLElement;
      this.contentDOM = contentDOM;

    this.dom.addEventListener('click', () => {
      const props = {
          dom: this.dom,
          node: this.node,
          actualPosition: this.getPos() || 0,
          view: this.view
        }

        //get raw text of a clicked node
        const nodeText = props.node.textContent;
        //create a text node
        const textNode = props.view.state.schema.text(nodeText)
        //get actual size of a clicked node
        const actualSize = this.node.nodeSize;

        const tr = this.view.state.tr;

        tr.replaceRangeWith(props.actualPosition, (props.actualPosition) + actualSize, textNode);
        
        this.view.dispatch(tr);
    });
  }

  public update(node: Node): boolean {
    this.actualNode = node;

    return true;
  }
}
  1. It works fine in cases when `addedContent’ is surrounded by text node or when it’s located in the end of a paragraph
  2. However it doesn’t work fine when `addedContent’ node is. a) In the beginning of a paragraph. b) occupies entire paragraph. c) in the end but goes right after another ‘addedContent’ which reaches the left edge of a paragraph

My observation show next:

  • Line get’s empty but when I try to get a state I see that it is in a correct format: Paragraph contains just a text with a content
  • On the html paragraph contains a span with added-content class
  • Trying to delete(from,to) then tr.insert(from, textNode) or any sorts of method didn’t result to success
  • Just delete(from, to) works as expected in all cases
  • If I put a 0ms timer and then do tr.insert(from, textNode) it will work fine and no issues will be there

Please check out screenshots and stackblits example

Pic 1. Before actions

Pic 2. After clicking, State in the console:

The issue may be that your nodeview update method doesn’t check the type of the node it gets. You’ll want to return false when it’s not an addedContent node.

I do not think this is an issue for this example. This method is not called at all unless I type something in the editor. Please try opening my link and just click on purple text

Well, it is. Try it.

Oh… how come… Sorry after performing this check and console logging I clearly see that this method is indeed called and the type.name is ‘text’.

I tried to read about this method but didn’t figured out how is it possible for it to be called with a different type.name

From your experience can you describe why is that happening? Why it doesn’t when it’s in the middle of a paragraph and why it’s so when it’s in the beginning?

Thank you very much for the help!

It’s possible to write node views that can represent several types of nodes, so when another node takes the place where a node view currently is, it gets a chance to ‘become’ that node. Thus, the first thing you should do in a node view update method is confirm that you are able to handle the node that’s being passed in.

Thanks I’ve got the idea. Editor tries to put some other’s node content into any existing one, I guess for optimization purposes. And if my custom node is not intended to handle that content I should say no.

Again, thank you very much for helping me with this issue!