Selection and node view

I’m observing some strange behavior with selection and NodeView, and I’m trying to figure out the best way for them to work together.

My understanding is that (on chrome at least), selection is done by observing selectionchange events.

It seems that for my NodeView with the following spec, ProseMirror sets contenteditable=false on the top-level node.

export const mqSpec : NodeSpec = {
  inline: true,
  group: "inline",
  content: "text*",
  selectable: true,
  toDOM: () => {
    return ["span", {"mathquill": true}, 0]
  },
  parseDOM: [{tag: "span[mathquill]"}]
}

This ends up interacting awkwardly with the surrounding contenteditable=true prosemirror DOM:

  • when a selection starts inside the contenteditable dom, the browser does not emit selectionchange events while the mouse moves over the contents of the contentedibale=false NodeView.

  • when the selection starts on the border of the contentedibale=false NodeView, we do emit events. However the cursor is simply dragged around after the mouse rather than causing a selection to happen.

The other thing I am noticing is that the setSelection method I defined on my nodeView is never triggered… is this something that only makes sense when you have contentDOM defined? I can’t seem to figure out how to actually get that to work.

My current plan is to do the following:

  1. listen for mouse events on the top-level NodeView node, and modify the selection in the EditorView accordingly. I’m hoping this will let me select the entire node (and style it as such).

  2. whenever the state updates, see if the selection encompasses the given NodeView, and if so, style it accordingly.

  3. figure out what’s going wrong with the selectionchange (on the border of contenteditable) events and figure out some way to filter them.

A nodeview without contentDOM will act like an uneditable ‘island’ in the document. Any interaction with its content will have to be handled by you, and, as you noticed, you can’t have a selection that has one end in editable content and one in uneditable content. setSelection will only be called when the whole selection is inside the node view, so you can use it to position the cursor or selection inside the node view when the outer editor’s selection is set there (the codemirror demo does this).

So if you’re trying to wire in some kind of existing math editor, editing in that will have to happen in ‘isolation’ from editing in the outer document. You have to notice when the user selects or edits something inside of your node view, and forward those changes to the outer editor. Likewise, your update and setSelection methods are used to transfer state from the outer editor to the inner one.

2 Likes

I have this working already. And, I’m happy with selections that start inside the math node to be constrained to just the math node. My current issue is just in getting selection in the surrounding PM to work. I want the math node to be treated as all selected or none selected. A good example is how the dinosaur nodes behave in the dino demo, or how an image or a drawing is selectable in google docs.

In particular, I want to accomplish the following:

  • style the math node as highlighted when it’s part of the PM selection
  • add the math node to the PM selection as soon as the user drags the mouse over the node (do not require the user to drag past the node to select the other side)
  • make the selection behave the same when it starts adjacent to the node (inside PM).

These are UX fixes, but ones that I feel are necessary to get the editing experience feeling right.

1 Like

Hum. Did some more digging on this and it seems like this is something that’s partially buggy browser behavior, and partially tickled by the MathQuill editor.

Chrome at least seems to not handle selections anchored at the border of contenteditable true/false nodes well. You can play around with that in this jsbin

Try selecting something starting next to one of the contenteditable=false elements - you’ll see the same behavior as shown in the gif above (the cursor being dragged behind the mouse rather than creating a selection). I also am occasionally seeing the cursor shifting up out of line when selecting immediately after the dinosaur image. I’m going to report this to Chrome and hopefully it can get addressed…

The other bit is the MathQuill project itself. It has a wrapping element that defines user-select: none, and different browsers handle that differently. Chrome doesn’t highlight it in the selection at all, while Firefox does. Either way it’s not relevant to PM since the dinosaur demo shows that elements typically can be highlighted!

For posterity, here’s a summary of my discussion with one the MathQuill authors:

  • user-select:none is there to avoid random characters being copied into the clipboard.
  • even though it’s there, it really is quite buggy… Some browsers still show a visual selection across user-select:none elements, while others don’t. Chrome still includes the content of user-select:none when you copy, even when they visually don’t indicate the selection.

here are some related bugs:

https://bugs.chromium.org/p/chromium/issues/detail?id=147490 https://bugs.webkit.org/show_bug.cgi?id=80159 https://bugzilla.mozilla.org/show_bug.cgi?id=739396

I checked the contenteditable=false issues in chrome canary and it seems to be fixed, so hopefully that will get resolved shortly.

I filed a bug about the cursor jumping out of line next to tall contenteditable=false elements here: https://bugs.chromium.org/p/chromium/issues/detail?id=701985

Indeed, browser behavior in contentEditable-related corner cases is universally terrible and inconsistent. This is a big damper on innovative techniques (and the reason some editor do everything without contentEditable, accessibility be damned).

One followup note on this-

I ended up ‘faking’ selection on my custom nodeView, for the time being. That is, after every transaction, I check whether my node is part of the PM selection, and if so apply a “selected” class on top of it.

I ended up listening to the state changes via a plugin that I wrote to hook into the PM update cycle, but I wonder if it would be worthwhile to add a convenience method to a NodeView for such cases - something that would be called whenever the EditorView renders a new state.

2 Likes

I think a plugin is a reasonable way to handle that. Node views are explicitly ‘local’ to their node, and allowing them to be wired into the global state could cause all kinds of confusion on when and how they update themselves.

Where does the code for applying a “selected” class live? Is the node view hooked up with a plugin?

We’re going to need to follow the same pattern here with our node views.

Yep! I have a plugin that hooks into the update cycle using a statefield, and provides a way to add callbacks. I use the same one to persist the state.

Will document one more attempt I made at using the browser-native selection, rather than faking selection appearance via JS…

There’s a user-select: none span. I floated a 1px x 1px transparent image above that, stretched to match the size of the node. Images behave in a desirable way with selection - the whole rectangle gets highlighted when they are part of the selection.

This works great on chrome! Unfortunately, it doesn’t work on safari or firefox - the noselect span gets selected twice, and shift-arrowing updates the selection in a bizzare order. I suspect this is related to the issues those browsers have with the user-select:none tag in general (still visually showing the selection, etc…) as documented in some of the bugs above.

I also didn’t check how this jives with the PM selection logic. I suspect you’d still have to be careful with blocking selection events with preventDefault, and update the PM selection yourself

2 Likes