Description
Hi, we’re building an editor on top of ProseMirror and we have several selectable nodes which don’t contain any editable content but do contain some non-editable text. We want this text to also be selectable so that the user can copy it without having to copy the whole node.
For example, consider a node which has an image with a caption - we want the user to be able to click any part of it to select the whole node. After that, the user should be able to select just the caption and copy it while the node remains selected in the editor. Take a look at the gifs below to get a better idea of what we’re aiming for:
Default behaviour:
Expected behaviour:
Implementation
We’re trying to implement the described behaviour for any node which:
- Is selectable, i.e. has
selectable: true
in its spec - Doesn’t contain content, i.e. has
content: ""
in its spec - Has a node view, created in a plugin
- Renders some DOM element which contains only plain text
The default behaviour doesn’t let you set the DOM selection to a piece of text within such a node. However, using a combination of nodeView.selectNode
, nodeView.stopEvent
, and nodeView.ignoreMutation
, we’ve managed to get things mostly working, but we’re still encountering some oddities, and aren’t sure if we’re doing things the “correct” way.
We’ve tried 2 approaches - one in which the focus/document.activeElement
remains on the editor when the node is selected, and one where it moves to the root DOM element of the node view. I’ve created a minimal sandbox for each approach, which include imageWithText
node as an example to show the issues we’re running into:
The main issue we’re encountering here is that the DOM selection highlights the entire node view, while we only want to show an outline to indicate that the node is selected. We have 2 ways in which we can fix this.
The first is to just check whether the DOM selection contains the node and apply a class name to it, in order to hide the selection via CSS. However, it seems like ProseMirror sets the DOM selection after nodeView.selectNode
is called, so we have to use a selectionchange
event listener inside the node view to do this.
The second is to move the DOM selection ourselves, to an empty one inside the node. Since it’s empty, copying will return false
in nodeView.stopEvent
, and so the editor will handle the event and copy the whole block anyway. Because the DOM selection is collapsed, there is no selection that we need to hide. However, we again have to do this in a selectionchange
event listener as ProseMirror sets the DOM selection after nodeView.selectNode
is called.
In both of these cases, it would be easier & cleaner if we could prevent ProseMirror from setting the DOM selection and handle it ourselves inside nodeView.selectNode
. We also tried setting tabIndex
to -1 on the node view root element as suggested in this post, though this didn’t seem to fix the issue.
It’s expected that keyboard navigation and other editor key handling isn’t working here since I wanted to keep this sandbox minimal. However, there are two issues that we’re running into with this approach.
First, is that cut
/copy
events are not always captured by nodeView.stopEvent
. If you have the editor focused and the selection in one of the paragraphs, then click the image and hit Cmd+C, nothing happens. However, if you click the text in the imageWithText
node and hit Cmd+C, a copy
event is captured by nodeView.stopEvent
, which you can see logged to the console. After clicking the text and then clicking the image, copying also works fine. So it doesn’t work if you click the image without having clicked the text right before.
Second, is that sometimes when the imageWithText
node is selected, clicking on one of the paragraphs focuses the editor but doesn’t reliably move the selection. While this is flaky, you should be able to reproduce this by first selecting the imageWithText
node, then clicking a paragraph somewhere to the right of its text content. The selection changes and then immediately changes again. If you clicked on the first paragraph, the selection moves to the imageWithText
node and then the paragraph, while the opposite happens when clicking the second paragraph instead.
So overall, it would be great to get some feedback on anything we might be doing wrong with these two approaches, or if there’s anything else wrong with our implementations of them.