Positions, Resolved positions and dom elements

I have the following problem: I have a node mathPanel. I want to find a dom of that node to display bubble menu below it. I tried to use both Tiptap and my own method as follows:

   /* My nodeWithPos */
          let nodeWithPos = findNode(editor, 'mathPanel');
          let element = editor.view.nodeDOM(nodeWithPos?.pos!) as HTMLElement;

          /* Tiptap nodeWithPos */
          const tiptapNodeWithPos = editor.$node('mathPanel');
          let tiptapElement = tiptapNodeWithPos?.element!;

There is only mathPanel node in my doc and my method returns pos=0 while Tiptap ones returns pos: 1, and in that case, my method wins because element corresponds to the mathPanel dom, while Tiptap returns dom of mathPanelName element (first child of mathPanel node). I tried state.doc.nodeAt and again my method returns the problem node.

Here is my method:

/**
Searches for a node within the current selection. If direction is not null, it looks backward and/or forward too.
 */
export function findNode(
  editor: any,
  nodeType: string,
  direction: -1 | 1 | 'both' | null = null
): NodeWithPos | null {
  let view = editor.view;
  const { state } = view;
  const { selection } = state;

  /* support for CellSelections */
  const { ranges } = selection;
  const from = Math.min(...ranges.map((range) => range.$from.pos));
  const to = Math.max(...ranges.map((range) => range.$to.pos));

  let foundNode: Node | null = null;
  let foundPos: number | null = null;

  editor.view.state.doc.nodesBetween(from, to, (node, pos) => {
    if (foundNode) return false;
    if (node.type.name === nodeType) {
      foundPos = pos;
      foundNode = node;
    }
  });

  if (!foundNode && (direction === 'both' || direction === 1)) {
    editor.view.state.doc.nodesBetween(
      to,
      state.doc.content.size,
      (node, pos) => {
        if (foundNode) return false;
        if (node.type.name === nodeType) {
          foundPos = pos;
          foundNode = node;
        }
      }
    );
  }

  if (!foundNode && (direction === 'both' || direction === -1)) {
    editor.view.state.doc.nodesBetween(0, from, (node, pos) => {
      if (foundNode) return false;
      if (node.type.name === nodeType) {
        foundPos = pos;
        foundNode = node;
      }
    });
  }

  if (foundNode) {
    return { node: foundNode, pos: foundPos! };
  }

  return null;
}

And here is Tiptap code:

I don’t understand what drives that difference. I think that my results make more sense, i.e. the node pos indeed corresponds to the dom and node at that position.

Your code looks correct. I didn’t look too closely into that TipTap code (the ridiculous inefficiency of it made me uncomfortable), but I’d avoid it and go with the simple solution you have.

I’d be curious to know your high level thoughts on their nodePos implementation and what in particular is so inefficient. Seems like a good teachable moment for the rest of us if you can find a couple minutes to spare to explain your thoughts.

Thanks again for the wonderful library @marijn.

I’m not familiar enough with the interface to produce a nuanced critique here, but when I saw that firstChild is implemented as this.children[0] and children is a getter that computes a full new array of child objects every time it is read, I made a face.

1 Like