diff --git a/packages/slate-react/src/components/content.js b/packages/slate-react/src/components/content.js index 5ff5f13bc..1c8f20b02 100644 --- a/packages/slate-react/src/components/content.js +++ b/packages/slate-react/src/components/content.js @@ -105,6 +105,7 @@ class Content extends React.Component { nodeRef: React.createRef(), nodeRefs: {}, contentKey: 0, + nativeSelection: {}, // Native selection object stored to check if `onNativeSelectionChange` has triggered yet } /** @@ -237,7 +238,35 @@ class Content extends React.Component { // If the Slate selection is unset, but the DOM selection has a range // selected in the editor, we need to remove the range. - if (selection.isUnset && rangeCount && this.isInEditor(anchorNode)) { + // However we should _not_ remove the range if the selection as + // reported by `getSelection` is not equal to `this.tmp.nativeSelection` + // as this suggests `onNativeSelectionChange` has not triggered yet (which can occur in Firefox) + // See: https://github.com/ianstormtaylor/slate/pull/2995 + + const propsToCompare = [ + 'anchorNode', + 'anchorOffset', + 'focusNode', + 'focusOffset', + 'isCollapsed', + 'rangeCount', + 'type', + ] + + let selectionsEqual = true + + for (const prop of propsToCompare) { + if (this.tmp.nativeSelection[prop] !== native[prop]) { + selectionsEqual = false + } + } + + if ( + selection.isUnset && + rangeCount && + this.isInEditor(anchorNode) && + selectionsEqual + ) { removeAllRanges(native) updated = true } @@ -288,39 +317,43 @@ class Content extends React.Component { // Otherwise, set the `isUpdatingSelection` flag and update the selection. updated = true this.tmp.isUpdatingSelection = true + removeAllRanges(native) - if (!IS_FIREFOX) { - removeAllRanges(native) - - // COMPAT: IE 11 does not support `setBaseAndExtent`. (2018/11/07) - if (native.setBaseAndExtent) { - // COMPAT: Since the DOM range has no concept of backwards/forwards - // we need to check and do the right thing here. - if (isBackward) { - native.setBaseAndExtent( - range.endContainer, - range.endOffset, - range.startContainer, - range.startOffset - ) - } else { - native.setBaseAndExtent( - range.startContainer, - range.startOffset, - range.endContainer, - range.endOffset - ) - } + // COMPAT: IE 11 does not support `setBaseAndExtent`. (2018/11/07) + if (native.setBaseAndExtent) { + // COMPAT: Since the DOM range has no concept of backwards/forwards + // we need to check and do the right thing here. + if (isBackward) { + native.setBaseAndExtent( + range.endContainer, + range.endOffset, + range.startContainer, + range.startOffset + ) } else { - native.addRange(range) + native.setBaseAndExtent( + range.startContainer, + range.startOffset, + range.endContainer, + range.endOffset + ) } + } else { + native.addRange(range) } // Scroll to the selection, in case it's out of view. scrollToSelection(native) - // Then unset the `isUpdatingSelection` flag after a delay. + // Then unset the `isUpdatingSelection` flag after a delay, to ensure that + // it is still set when selection-related events from updating it fire. setTimeout(() => { + // COMPAT: In Firefox, it's not enough to create a range, you also need + // to focus the contenteditable element too. (2016/11/16) + if (IS_FIREFOX && this.ref.current) { + this.ref.current.focus() + } + this.tmp.isUpdatingSelection = false debug.update('updateSelection:setTimeout', { @@ -355,6 +388,12 @@ class Content extends React.Component { let el try { + // COMPAT: In Firefox, sometimes the node can be comment which doesn't + // have .closest and it crashes. + if (target.nodeType === 8) { + return false + } + // COMPAT: Text nodes don't have `isContentEditable` property. So, when // `target` is a text node use its parent node for check. el = target.nodeType === 3 ? target.parentNode : target @@ -481,12 +520,24 @@ class Content extends React.Component { const window = getWindow(event.target) const { activeElement } = window.document + const native = window.getSelection() + debug.update('onNativeSelectionChange', { - anchorOffset: window.getSelection().anchorOffset, + anchorOffset: native.anchorOffset, }) if (activeElement !== this.ref.current) return + this.tmp.nativeSelection = { + anchorNode: native.anchorNode, + anchorOffset: native.anchorOffset, + focusNode: native.focusNode, + focusOffset: native.focusOffset, + isCollapsed: native.isCollapsed, + rangeCount: native.rangeCount, + type: native.type, + } + this.props.onEvent('onSelect', event) }, 100)