DOMException in Chrome

I have prosemirror setup with collaboration but have encountered an exception raised in Chrome that doesn’t occur in Firefox:

setSelection (prosemirror-view/src/viewdesc.js:366):
   Uncaught (in promise) DOMException: Failed to execute 'extend' on 'Selection': This Selection object doesn't have any Ranges.

It’s triggered when I have two collaborators editing the same document and one of them deletes all of the text in the document. If the other browser is Chrome the exception is raised. If the other browser is Firefox the exception is not.

I found a similar bug in draft-js: https://github.com/facebook/draft-js/issues/1188

Which they tracked to this change in Chrome: https://bugs.chromium.org/p/chromium/issues/detail?id=690240

It looks like the spec requires an exception when calling .extend() with an empty range. I assume Firefox doesn’t implement the spec correctly.

ProseMirror doesn’t (intentionally) call extend on a possibly-empty selection. The only place where it calls that is directly after calling addRange (or, since 1.14.4, collapse), which should ensure that there’s a range in the selection.

Can you set up a situation that simulates this case in a deterministic way (using a programmatic change instead of collaborative editing)?

I am getting this same exception in chrome in some cases. For me specifically it is in prosemirror-view/src/viewdesc.js:361 where I don’t see any checks to make sure domSel isn’t an empty selection.

// ...
let domSel = root.getSelection()

if (!force &&
    isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset) &&
    isEquivalentPosition(headDOM.node, headDOM.offset, domSel.focusNode, domSel.focusOffset))
  return

// Selection.extend can be used to create an 'inverted' selection
// (one where the focus is before the anchor), but not all
// browsers support it yet.
if (domSel.extend || anchor == head) {
  domSel.collapse(anchorDOM.node, anchorDOM.offset)
  if (anchor != head) domSel.extend(headDOM.node, headDOM.offset)
} else {
// ...

Am I missing something?

Directly after calling Selection.collapse, the selection shouldn’t normally be empty. Though I guess it could be if the editor isn’t actually in the DOM. Is anything like that going on when the exception happens?

Stepping through it, that is exactly what I am seeing. After calling collapse with a valid node/offset (it would throw if the offset wasn’t valid) the selection is empty. The editor is in the DOM. It seems like a chrome bug, but I have been unsuccessful in creating a minimal reproduction. I am not sure where to go from here. The non-extend code path works great. I can monkey patch setSelection to get it working, but I hate doing that. I am not sure I have any more time to spend figuring out how to isolate the chrome issue though.

If you wanted to accept a PR where the extend is wrapped in a try/catch and falls back to manually creating the range I would happily submit it. But I know that would be kind of a lazy band aid solution.

For reference, here is my working monkey patch:

const setSelection = editorView.docView.constructor.prototype.setSelection;
editorView.docView.setSelection = function(anchor, head, root, force) {
  const extend = Selection.prototype.extend;
  Selection.prototype.extend = undefined;
  try {
    setSelection.call(editorView.docView, anchor, head, root, force);
  } finally {
    Selection.prototype.extend = extend;
  }
};

I hate it when that happens, but yeah, does look like that. I’d be okay with a pull request that falls back to the old-style code when the collapse path throws.

PR submitted. https://github.com/ProseMirror/prosemirror-view/pull/71