Skip to content

Commit

Permalink
Fix firefox disconnected selection api usage (#5486)
Browse files Browse the repository at this point in the history
* Fix firefox disconnected selection api usage

* Add changeset

* Fix typo + add link to explanation

* Fix integration tests
  • Loading branch information
WcaleNieWolny committed Jul 26, 2023
1 parent ca77e93 commit 8b548fb
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 61 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-tomatoes-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'slate-react': minor
---

Fix invalid usage of the selection API in firefox
39 changes: 28 additions & 11 deletions packages/slate-react/src/components/editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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'
Expand Down Expand Up @@ -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
Expand Down
86 changes: 36 additions & 50 deletions packages/slate-react/src/plugin/react-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down

0 comments on commit 8b548fb

Please sign in to comment.