diff --git a/.changeset/two-tomatoes-deliver.md b/.changeset/two-tomatoes-deliver.md new file mode 100644 index 0000000000..80ff3f3efc --- /dev/null +++ b/.changeset/two-tomatoes-deliver.md @@ -0,0 +1,5 @@ +--- +'slate-react': minor +--- + +Fix invalid usage of the selection API in firefox diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 7eb1b4f72c..093b703304 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -305,12 +305,33 @@ export const Editable = (props: EditableProps) => { return } + // Get anchorNode and focusNode + const focusNode = domSelection.focusNode + let anchorNode + + // COMPAT: In firefox the normal seletion way does not work + // (https://github.com/ianstormtaylor/slate/pull/5486#issue-1820720223) + if (IS_FIREFOX && domSelection.rangeCount > 1) { + const firstRange = domSelection.getRangeAt(0) + const lastRange = domSelection.getRangeAt(domSelection.rangeCount - 1) + + // Right to left + if (firstRange.startContainer === focusNode) { + anchorNode = lastRange.endContainer + } else { + // Left to right + anchorNode = firstRange.startContainer + } + } else { + anchorNode = domSelection.anchorNode + } + // verify that the dom selection is in the editor const editorElement = EDITOR_TO_ELEMENT.get(editor)! let hasDomSelectionInEditor = false if ( - editorElement.contains(domSelection.anchorNode) && - editorElement.contains(domSelection.focusNode) + editorElement.contains(anchorNode) && + editorElement.contains(focusNode) ) { hasDomSelectionInEditor = true } @@ -336,7 +357,6 @@ export const Editable = (props: EditableProps) => { } // Ensure selection is inside the mark placeholder - const { anchorNode } = domSelection if ( anchorNode?.parentElement?.hasAttribute( 'data-slate-mark-placeholder' @@ -391,19 +411,16 @@ export const Editable = (props: EditableProps) => { return newDomRange } - const newDomRange = setDomSelection() + // In firefox if there is more then 1 range and we call setDomSelection we remove the ability to select more cells in a table + if (domSelection.rangeCount <= 1) { + setDomSelection() + } + const ensureSelection = androidInputManagerRef.current?.isFlushing() === 'action' if (!IS_ANDROID || !ensureSelection) { 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 (newDomRange && IS_FIREFOX) { - const el = ReactEditor.toDOMNode(editor, editor) - el.focus() - } - state.isUpdatingSelection = false }) return diff --git a/packages/slate-react/src/plugin/react-editor.ts b/packages/slate-react/src/plugin/react-editor.ts index 15281ab649..f2e56ba300 100644 --- a/packages/slate-react/src/plugin/react-editor.ts +++ b/packages/slate-react/src/plugin/react-editor.ts @@ -829,15 +829,37 @@ export const ReactEditor: ReactEditorInterface = { if (el) { if (isDOMSelection(domRange)) { - anchorNode = domRange.anchorNode - anchorOffset = domRange.anchorOffset - focusNode = domRange.focusNode - focusOffset = domRange.focusOffset + // COMPAT: In firefox the normal seletion way does not work + // (https://github.com/ianstormtaylor/slate/pull/5486#issue-1820720223) + if (IS_FIREFOX && domRange.rangeCount > 1) { + focusNode = domRange.focusNode // Focus node works fine + const firstRange = domRange.getRangeAt(0) + const lastRange = domRange.getRangeAt(domRange.rangeCount - 1) + + // Right to left + if (firstRange.startContainer === focusNode) { + anchorNode = lastRange.endContainer + anchorOffset = lastRange.endOffset + focusOffset = firstRange.startOffset + } else { + // Left to right + anchorNode = firstRange.startContainer + anchorOffset = firstRange.endOffset + focusOffset = lastRange.startOffset + } + } else { + anchorNode = domRange.anchorNode + anchorOffset = domRange.anchorOffset + focusNode = domRange.focusNode + focusOffset = domRange.focusOffset + } + // COMPAT: There's a bug in chrome that always returns `true` for // `isCollapsed` for a Selection that comes from a ShadowRoot. // (2020/08/08) // https://bugs.chromium.org/p/chromium/issues/detail?id=447523 - if (IS_CHROME && hasShadowRoot(anchorNode)) { + // IsCollapsed might not work in firefox, but this will + if ((IS_CHROME && hasShadowRoot(anchorNode)) || IS_FIREFOX) { isCollapsed = domRange.anchorNode === domRange.focusNode && domRange.anchorOffset === domRange.focusOffset @@ -876,15 +898,19 @@ export const ReactEditor: ReactEditorInterface = { focusOffset = anchorNode.textContent?.length || 0 } - let anchor = ReactEditor.toSlatePoint(editor, [anchorNode, anchorOffset], { - exactMatch, - suppressThrow, - }) + const anchor = ReactEditor.toSlatePoint( + editor, + [anchorNode, anchorOffset], + { + exactMatch, + suppressThrow, + } + ) if (!anchor) { return null as T extends true ? Range | null : Range } - let focus = isCollapsed + const focus = isCollapsed ? anchor : ReactEditor.toSlatePoint(editor, [focusNode, focusOffset], { exactMatch, @@ -894,46 +920,6 @@ export const ReactEditor: ReactEditorInterface = { return null as T extends true ? Range | null : Range } - /** - * suppose we have this document: - * - * { type: 'paragraph', - * children: [ - * { text: 'foo ' }, - * { text: 'bar' }, - * { text: ' baz' } - * ] - * } - * - * a double click on "bar" on chrome will create this range: - * - * anchor -> [0,1] offset 0 - * focus -> [0,1] offset 3 - * - * while on firefox will create this range: - * - * anchor -> [0,0] offset 4 - * focus -> [0,2] offset 0 - * - * let's try to fix it... - */ - - if (IS_FIREFOX && !isCollapsed && anchorNode !== focusNode) { - const isEnd = Editor.isEnd(editor, anchor!, anchor.path) - const isStart = Editor.isStart(editor, focus!, focus.path) - - if (isEnd) { - const after = Editor.after(editor, anchor as Point) - // Editor.after() might return undefined - anchor = (after || anchor!) as T extends true ? Point | null : Point - } - - if (isStart) { - const before = Editor.before(editor, focus as Point) - focus = (before || focus!) as T extends true ? Point | null : Point - } - } - let range: Range = { anchor: anchor as Point, focus: focus as Point } // if the selection is a hanging range that ends in a void // and the DOM focus is an Element