1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-29 18:09:49 +02:00

fix focusing across multiple editors (#2396)

This commit is contained in:
Ian Storm Taylor
2018-11-07 16:45:01 -08:00
committed by GitHub
parent 990d37b694
commit e493905f29
3 changed files with 166 additions and 84 deletions

View File

@@ -146,103 +146,120 @@ class Content extends React.Component {
const { isBackward } = selection const { isBackward } = selection
const window = getWindow(this.element) const window = getWindow(this.element)
const native = window.getSelection() const native = window.getSelection()
const { activeElement } = window.document
// .getSelection() can return null in some cases // COMPAT: In Firefox, there's a but where `getSelection` can return `null`.
// https://bugzilla.mozilla.org/show_bug.cgi?id=827585 // https://bugzilla.mozilla.org/show_bug.cgi?id=827585 (2018/11/07)
if (!native) return if (!native) {
return
}
const { rangeCount, anchorNode } = native const { rangeCount, anchorNode } = native
let updated = false
// If both selections are blurred, do nothing. // If the Slate selection is blurred, but the DOM's active element is still
if (!rangeCount && selection.isBlurred) return // the editor, we need to blur it.
if (selection.isBlurred && activeElement === this.element) {
// If the selection has been blurred, but is still inside the editor in the
// DOM, blur it manually.
if (selection.isBlurred) {
if (!this.isInEditor(anchorNode)) return
removeAllRanges(native)
this.element.blur() this.element.blur()
debug('updateSelection', { selection, native }) updated = true
return
} }
// If the selection isn't set, do nothing. // If the Slate selection is unset, but the DOM selection has a range
if (selection.isUnset) return // selected in the editor, we need to remove the range.
if (selection.isUnset && rangeCount && this.isInEditor(anchorNode)) {
removeAllRanges(native)
updated = true
}
// If the Slate selection is focused, but the DOM's active element is not
// the editor, we need to focus it.
if (selection.isFocused && activeElement !== this.element) {
this.element.focus()
updated = true
}
// Otherwise, figure out which DOM nodes should be selected... // Otherwise, figure out which DOM nodes should be selected...
const current = !!rangeCount && native.getRangeAt(0) if (selection.isFocused && selection.isSet) {
const range = findDOMRange(selection, window) const current = !!rangeCount && native.getRangeAt(0)
const range = findDOMRange(selection, window)
if (!range) { if (!range) {
warning( warning(
false, false,
'Unable to find a native DOM range from the current selection.' 'Unable to find a native DOM range from the current selection.'
) )
return
}
const { startContainer, startOffset, endContainer, endOffset } = range
// If the new range matches the current selection, there is nothing to fix.
// COMPAT: The native `Range` object always has it's "start" first and "end"
// last in the DOM. It has no concept of "backwards/forwards", so we have
// to check both orientations here. (2017/10/31)
if (current) {
if (
(startContainer == current.startContainer &&
startOffset == current.startOffset &&
endContainer == current.endContainer &&
endOffset == current.endOffset) ||
(startContainer == current.endContainer &&
startOffset == current.endOffset &&
endContainer == current.startContainer &&
endOffset == current.startOffset)
) {
return return
} }
}
// Otherwise, set the `isUpdatingSelection` flag and update the selection. const { startContainer, startOffset, endContainer, endOffset } = range
this.tmp.isUpdatingSelection = true
removeAllRanges(native)
// COMPAT: IE 11 does not support Selection.setBaseAndExtent // If the new range matches the current selection, there is nothing to fix.
if (native.setBaseAndExtent) { // COMPAT: The native `Range` object always has it's "start" first and "end"
// COMPAT: Since the DOM range has no concept of backwards/forwards // last in the DOM. It has no concept of "backwards/forwards", so we have
// we need to check and do the right thing here. // to check both orientations here. (2017/10/31)
if (isBackward) { if (current) {
native.setBaseAndExtent( if (
range.endContainer, (startContainer == current.startContainer &&
range.endOffset, startOffset == current.startOffset &&
range.startContainer, endContainer == current.endContainer &&
range.startOffset endOffset == current.endOffset) ||
) (startContainer == current.endContainer &&
} else { startOffset == current.endOffset &&
native.setBaseAndExtent( endContainer == current.startContainer &&
range.startContainer, endOffset == current.startOffset)
range.startOffset, ) {
range.endContainer, return
range.endOffset }
)
} }
} else {
// COMPAT: IE 11 does not support Selection.extend, fallback to addRange // Otherwise, set the `isUpdatingSelection` flag and update the selection.
native.addRange(range) updated = true
this.tmp.isUpdatingSelection = true
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
)
}
} 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, 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.element) {
this.element.focus()
}
this.tmp.isUpdatingSelection = false
})
} }
// Scroll to the selection, in case it's out of view. if (updated) {
scrollToSelection(native) debug('updateSelection', { selection, native, activeElement })
}
// Then unset the `isUpdatingSelection` flag after a delay.
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.element) this.element.focus()
this.tmp.isUpdatingSelection = false
})
debug('updateSelection', { selection, native })
} }
/** /**
@@ -345,9 +362,11 @@ class Content extends React.Component {
handler == 'onDragStart' || handler == 'onDragStart' ||
handler == 'onDrop' handler == 'onDrop'
) { ) {
const { target } = event const closest = event.target.closest('[data-slate-editor]')
const targetEditorNode = target.closest('[data-slate-editor]')
if (targetEditorNode !== this.element) return if (closest !== this.element) {
return
}
} }
// Some events require being in editable in the editor, so if the event // Some events require being in editable in the editor, so if the event
@@ -366,7 +385,9 @@ class Content extends React.Component {
handler == 'onPaste' || handler == 'onPaste' ||
handler == 'onSelect' handler == 'onSelect'
) { ) {
if (!this.isInEditor(event.target)) return if (!this.isInEditor(event.target)) {
return
}
} }
this.props.onEvent(handler, event) this.props.onEvent(handler, event)

View File

@@ -24,6 +24,8 @@ const EVENT_HANDLERS = [
'onFocus', 'onFocus',
'onKeyDown', 'onKeyDown',
'onKeyUp', 'onKeyUp',
'onMouseDown',
'onMouseUp',
'onPaste', 'onPaste',
'onSelect', 'onSelect',
] ]

View File

@@ -31,6 +31,7 @@ const debug = Debug('slate:after')
function AfterPlugin(options = {}) { function AfterPlugin(options = {}) {
let isDraggingInternally = null let isDraggingInternally = null
let isMouseDown = false
/** /**
* On before input. * On before input.
@@ -375,6 +376,30 @@ function AfterPlugin(options = {}) {
next() next()
} }
/**
* On focus.
*
* @param {Event} event
* @param {Editor} editor
* @param {Function} next
*/
function onFocus(event, editor, next) {
debug('onFocus', { event })
// COMPAT: If the focus event is a mouse-based one, it will be shortly
// followed by a `selectionchange`, so we need to deselect here to prevent
// the old selection from being set by the `updateSelection` of `<Content>`,
// preventing the `selectionchange` from firing. (2018/11/07)
if (isMouseDown) {
editor.deselect().focus()
} else {
editor.focus()
}
next()
}
/** /**
* On input. * On input.
* *
@@ -580,6 +605,34 @@ function AfterPlugin(options = {}) {
next() next()
} }
/**
* On mouse down.
*
* @param {Event} event
* @param {Editor} editor
* @param {Function} next
*/
function onMouseDown(event, editor, next) {
debug('onMouseDown', { event })
isMouseDown = true
next()
}
/**
* On mouse up.
*
* @param {Event} event
* @param {Editor} editor
* @param {Function} next
*/
function onMouseUp(event, editor, next) {
debug('onMouseUp', { event })
isMouseDown = false
next()
}
/** /**
* On paste. * On paste.
* *
@@ -638,7 +691,10 @@ function AfterPlugin(options = {}) {
// Otherwise, determine the Slate selection from the native one. // Otherwise, determine the Slate selection from the native one.
let range = findRange(native, editor) let range = findRange(native, editor)
if (!range) return
if (!range) {
return
}
const { anchor, focus } = range const { anchor, focus } = range
const anchorText = document.getNode(anchor.key) const anchorText = document.getNode(anchor.key)
@@ -715,8 +771,11 @@ function AfterPlugin(options = {}) {
onDragEnd, onDragEnd,
onDragStart, onDragStart,
onDrop, onDrop,
onFocus,
onInput, onInput,
onKeyDown, onKeyDown,
onMouseDown,
onMouseUp,
onPaste, onPaste,
onSelect, onSelect,
} }