Struggling to replace the contents of a node

I have a custom node that is like a label for a paragraph for a custom app we’re writing. We’d like each paragraph to contain this label, or to have a hidden node exempting the paragraph from containing it.

const paragraphCategory: NodeSpec = {
  inline: true,
  // atom: true,
  attr: {
    unmarked: {
      default: null,
    },
  },
  group: 'inline',
  toDOM(node) {
    console.log('toDom')
    const text = `(${node.content.firstChild?.text}) `
    return [
      'paragraph-category',
      {
        class: 'paragraph-category cursor-pointer',
      },
      text,
    ]
  },
  parseDOM: [
    {
      tag: 'paragraph-category',
    },
  ],
}

I want to be able to default the value to UNSET and then toggle between some values when it’s clicked. We don’t want free-text.

I’m trying to write a plugin to set it, but we are struggling to find the exact position (start and finish) of the node, and to then change the value without destroying the node. I can get the size of the node, but am struggling to find the beginning of the node when it’s clicked on. And, when I replace the contents, it seems to not always update the editor. I’m attempting to dispatch a setSelection, followed by dispatch replaceSelectionWith, but it doesn’t seem to work the way I expect.

Can you help me with:

  • Find the exact start and end of a node when it’s clicked on
  • Replace content programmatically

Thanks

Firstly, if you’re storing the text as a child node, you’ll want to just include a 0 to mark its position in the output, and not read it from the child node and emit it, the way you’re doing.

If you’re using a handleClickOn function, that will pass you the node’s position. This is the position before the node, so if you want to range of it’s content that’s pos + 1 to pos + 1 + node.content.size.

To replace content, dispatch a transaction on which you called the replace or replaceWith method.

Thanks for getting back to me. I think I got it working:

  toDOM(node) {
    return [
      'paragraph-category',
      {
        class: 'paragraph-category cursor-pointer',
      },
      ['span', { contenteditable: false }, '('],
      ['span', 0],
      ['span', { contenteditable: false }, ') '],
    ]
  },

The click even is:

  view.dispatch(
    view.state.tr
      // Select the currently clicked Node
      .setSelection(NodeSelection.create(view.state.doc, pos))
      // Replace selection with a new Category
      .replaceSelectionWith(
        createCategoryNode({
          schema: view.state.schema,
          text: val,
        }),
        false,
      ),
  )

How does that look to you?

My next issue is that we’re trying to write some e2e tests with Cypress. When I simulate click events on this node, for some reason, the paragaph-category node disappears, and gets replaced by the text content. Not sure if this is Cypress weirdness, or Prosemirror weirdness. Wondering if Playwright will do the same thing.