Question about nodeviews with interactive buttons, cursor navigation (mouse)

Consider the following (set of components):

This is an editor that has a couple of custom node types (with node views) wrapped into each other. The ordering is as follows.

A. a container node with 
B. 1+ container nodes inside, each having
C. 3..5 custom nodes

Every A - has a button to create a new B container. Every B container has a button to remove itself from an A container

There is also a master button [KPI container] to create a new A container and upon removal of the last B container inside of an A container - that container itself is removed.

All of this is now working okay. But there are issues - especially with respect to custor navigation/selection (and gapcursor).

What I’m seeing is that placement of a gapcursor is non-deterministic, sometimes it will show up (it should show-up before, between and after A containers), sometimes it simply does not. Another thing I’ve noticed is that cursor navigation between containers behaves very unpredicatble. It seems to wander along relatively randomly, sometimes even stopping. Additionally, I’m noticing that whenever I change the cursor position from a ‘paragraph’ to an A, B or one of the C nodes, with the mouse - the selection does not change at all, and the system reports that paragraph is still selected.

From the looks of it these components sometimes seems detached from the actual editor state. Question, is what I’m trying to do possible with ProseMirror - what are common caveats with it?

I guess what I am struggling with is

  • when to use a NodeView and
  • when to use a (Node) Decoration

for interactivity as is shown above. Did I miss something in the manual? Seems like you can achieve the same kind of behavior with both approaches - but surely one of them is preferred given a certain use case. Right @marijn :grinning: ?

Done the same exercise, this time with decorations.

I’m seeing some benefits to this approach, in that things are separated a bit more clearly.

Downside for me is that I find it hard to position the decorations correctly. In the above example I have a ‘+’ sign to add a new KPI to the container in context. No problem there - I can simply place it at the node’s position that I have found in my deco process.

The ‘-’ signs are a bit more difficult. I’m determining all the information in one go, with an array that looks like:

  interface Result {
    containerPosition: number;
    containerID: number;
    kpiResult: {
      kpiPosition: number;
      kpiID: number;
    }[];
  }

Then with some number juggling I can put the ‘-’ right next to the KPI in context. However, because the NodeView is still there for the structural styling of containers (divs, or in this case a definition list (DL) for every KPI) and the buttons are specified in the decorations - getting everything properly lined up (and responsive) is hard (for me).

Here is an example on the HTML:

<div contenteditable="true" translate="no" tabindex="0" class="ProseMirror">
  <button contenteditable="false" class="ProseMirror-widget">+</button>
  <div class="kpi-container">
    <button contenteditable="false" class="ProseMirror-widget">-</button>
    <div class="kpi-flex">
      <dl class="kpi">
        <div>
          <dt>Text Above 1</dt>
          <dd>een-1-x</dd>
        </div>
        <div>
          <dt>Text Above 2</dt>
          <dd>een-2-x</dd>
        </div>
        <div>
          <dt>Value</dt>
          <dd>Value 3_2</dd>
        </div>
      </dl>
    </div>
    <button contenteditable="false" class="ProseMirror-widget">-</button>
    <div class="kpi-flex">
      <dl class="kpi">
        <div>
          <dt>Text Above 1</dt>
          <dd>twee-1-xx</dd>
        </div>
        <div>
          <dt>Text Above 2</dt>
          <dd>twee-2-xx</dd>
        </div>
        <div>
          <dt>Value</dt>
          <dd>Value 3_2</dd>
        </div>
        <div>
          <dt>Previous Prefix</dt>
          <dd>PP 4_2</dd>
        </div>
      </dl>
    </div>
  </div>
  <button contenteditable="false" class="ProseMirror-widget">+</button>
  <div class="kpi-container">
    <button contenteditable="false" class="ProseMirror-widget">-</button>
    <div class="kpi-flex">
      <dl class="kpi">
        <div>
          <dt>Text Above 1</dt>
          <dd>drie-1-xxx</dd>
        </div>
        <div>
          <dt>Text Above 2</dt>
          <dd>drie-2-xxx</dd>
        </div>
        <div>
          <dt>Value</dt>
          <dd>Value 3_2</dd>
        </div>
        <div>
          <dt>Previous Prefix</dt>
          <dd>PP 4_2</dd>
        </div>
        <div>
          <dt>Previous Value</dt>
          <dd>PV 5_2</dd>
        </div>
      </dl>
    </div>
  </div>
</div>

Effectively I’d want to put the:

    <button contenteditable="false" class="ProseMirror-widget">-</button>

As the first child element in the:

<div class="kpi-flex">

But that div is created in my (tiptap) nodeview like so:

addNodeView() {
    return () => {
      const dom = document.createElement('div');
      dom.classList.add('kpi-flex');

      const dl = document.createElement('dl');
      dl.classList.add('kpi');

      dom.append(dl);

      return {
        dom,
        contentDOM: dl,
      };
    };
  },

Whether a gap cursor is appropriate at a given document position is a straightforward and pure function of the document and position, so this would suggest there is either some disconnect between what’s visible in the DOM and what ProseMirror thinks your document is like, or you are talking about something else.

I’m not sure what it is that you’re trying to do, in regard to cursor navigation and user interaction. But for scenarios like this, where content isn’t a vertical arrangement of blocks, browser cursor navigation is definitely going to be terrible, and ProseMirror won’t do much to make it better, so you can expect to need custom key handlers implementing your desired cursor motion.