Can a MarkView know when selection is inside their node?

I’m building some complex custom NodeViews that have controls and nested Content DOMs. To determine whether the selection is inside the node in a NodeView, i.e. the user is currently typing in that NodeViews Content DOM I am using the getPos function and checking if the selection is between getPos() and getPos() + node.nodeSize:

            const { getPos, node } = reactNodeView.nodeViewProps;
			const cursorFrom = editorState.selection.from;
			const cursorTo = editorState.selection.to;
			const nodeFrom = getPos();
			const nodeTo = nodeFrom + node.nodeSize;
			return (
				(cursorFrom >= nodeFrom && cursorFrom <= nodeFrom) ||
				(cursorTo <= nodeTo && cursorTo >= nodeFrom)
			);

I’m trying to build a MarkView to render a floating panel with controls to edit the props on the mark. The floating panel should appear if the selection is currently inside the Mark. Without the getPos function I don’t know how I would tell if the selection is inside the mark?

I’ve tried using a NodeView and making the mark an inline Node but that causes issues with cursor positioning (the edges of the node are valid cursor positions) and the backspace key (hitting backspace just outside the node causes the node to be deleted rather than moving into the node and deleting the last character of the nodes content)

Any advice on how to proceed?

You didn’t really ask a very clear question, but the problem may revolve around the fact that a node view doesn’t really get to see arbitrary updates to the document, so there’s no good point to run the code you pasted. It might be easier to drive this panel from a plugin, which can test whether the selection is inside a node of the relevant type, and adds or removes the panel (either overlaid on the editor or via a widget decoration) as appropriate.

I would also like to know why there is no getPos function for MarkViews. Is this a technical limitation or just a missing feature?

2 Likes

Marks aren’t ‘objects’, in that they are metadata applied to groups of other nodes, but their actual start and end isn’t really meaningful, and they may be split or merged at arbitrary points in time. As such, making them behave like stateful objects in the view is tricky and doesn’t seem like a good conceptual fit to begin with.

My specific problem is that when using a MarkView, I don’t have any clue at which position it is rendered in the document (because of a missing getPos). This makes it impossible to update its attrs.

Okay I found a basic way to calculate a mark view range. I can get the start position with posAtDOM and the dom element. After that I have to get the full mark range within the parent node.

getMarkPos() {
    const pos = this.view.posAtDOM(this.dom)
    const resolvedPos = this.view.state.doc.resolve(pos)
    const range = getMarkRange(resolvedPos, this.node.type)
    return range
}
getMarkRange($pos = null, type = null) {

  if (!$pos || !type) {
    return false
  }

  const start = $pos.parent.childAfter($pos.parentOffset)

  if (!start.node) {
    return false
  }

  const link = start.node.marks.find(mark => mark.type === type)
  if (!link) {
    return false
  }

  let startIndex = $pos.index()
  let startPos = $pos.start() + start.offset
  let endIndex = startIndex + 1
  let endPos = startPos + start.node.nodeSize

  while (startIndex > 0 && link.isInSet($pos.parent.child(startIndex - 1).marks)) {
    startIndex -= 1
    startPos -= $pos.parent.child(startIndex).nodeSize
  }

  while (endIndex < $pos.parent.childCount && link.isInSet($pos.parent.child(endIndex).marks)) {
    endPos += $pos.parent.child(endIndex).nodeSize
    endIndex += 1
  }

  return { from: startPos, to: endPos }

}

@marijn yep, understand now. I’ll go with building a plugin that displays the overlay if the selection is inside a link mark.

Could you explain what the purpose of a MarkView is then? Is it only to vary how the mark is rendered?

Yes, it’s just a hook that you can use to change the rendering in the editor.