1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-16 20:24:01 +02:00

Fix missing callback dependencies in slate-react <Editable /> (#3192)

* Fix missing callback dependencies

* Fix editable formatting
This commit is contained in:
Caleb Delnay
2019-12-01 19:19:39 -05:00
committed by Ian Storm Taylor
parent 0dd3d17cdb
commit 52e20da69a

View File

@@ -445,77 +445,83 @@ export const Editable = (
editor.exec({ type: 'insert_text', text })
}
}, [])}
onBlur={useCallback((event: React.FocusEvent<HTMLDivElement>) => {
if (
readOnly ||
state.isUpdatingSelection ||
!hasEditableTarget(editor, event.target) ||
isEventHandled(event, attributes.onBlur)
) {
return
}
// COMPAT: If the current `activeElement` is still the previous
// one, this is due to the window being blurred when the tab
// itself becomes unfocused, so we want to abort early to allow to
// editor to stay focused when the tab becomes focused again.
if (state.latestElement === window.document.activeElement) {
return
}
const { relatedTarget } = event
const el = ReactEditor.toDOMNode(editor, editor)
// COMPAT: The event should be ignored if the focus is returning
// to the editor from an embedded editable element (eg. an <input>
// element inside a void node).
if (relatedTarget === el) {
return
}
// COMPAT: The event should be ignored if the focus is moving from
// the editor to inside a void node's spacer element.
if (
isDOMElement(relatedTarget) &&
relatedTarget.hasAttribute('data-slate-spacer')
) {
return
}
// COMPAT: The event should be ignored if the focus is moving to a
// non- editable section of an element that isn't a void node (eg.
// a list item of the check list example).
if (
relatedTarget != null &&
isDOMNode(relatedTarget) &&
ReactEditor.hasDOMNode(editor, relatedTarget)
) {
const node = ReactEditor.toSlateNode(editor, relatedTarget)
if (Element.isElement(node) && !editor.isVoid(node)) {
onBlur={useCallback(
(event: React.FocusEvent<HTMLDivElement>) => {
if (
readOnly ||
state.isUpdatingSelection ||
!hasEditableTarget(editor, event.target) ||
isEventHandled(event, attributes.onBlur)
) {
return
}
}
IS_FOCUSED.delete(editor)
}, [])}
onClick={useCallback((event: React.MouseEvent<HTMLDivElement>) => {
if (
!readOnly &&
hasTarget(editor, event.target) &&
!isEventHandled(event, attributes.onClick) &&
isDOMNode(event.target)
) {
const node = ReactEditor.toSlateNode(editor, event.target)
const path = ReactEditor.findPath(editor, node)
const start = Editor.start(editor, path)
if (Editor.match(editor, start, 'void')) {
const range = Editor.range(editor, start)
Editor.select(editor, range)
// COMPAT: If the current `activeElement` is still the previous
// one, this is due to the window being blurred when the tab
// itself becomes unfocused, so we want to abort early to allow to
// editor to stay focused when the tab becomes focused again.
if (state.latestElement === window.document.activeElement) {
return
}
}
}, [])}
const { relatedTarget } = event
const el = ReactEditor.toDOMNode(editor, editor)
// COMPAT: The event should be ignored if the focus is returning
// to the editor from an embedded editable element (eg. an <input>
// element inside a void node).
if (relatedTarget === el) {
return
}
// COMPAT: The event should be ignored if the focus is moving from
// the editor to inside a void node's spacer element.
if (
isDOMElement(relatedTarget) &&
relatedTarget.hasAttribute('data-slate-spacer')
) {
return
}
// COMPAT: The event should be ignored if the focus is moving to a
// non- editable section of an element that isn't a void node (eg.
// a list item of the check list example).
if (
relatedTarget != null &&
isDOMNode(relatedTarget) &&
ReactEditor.hasDOMNode(editor, relatedTarget)
) {
const node = ReactEditor.toSlateNode(editor, relatedTarget)
if (Element.isElement(node) && !editor.isVoid(node)) {
return
}
}
IS_FOCUSED.delete(editor)
},
[attributes.onBlur]
)}
onClick={useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
if (
!readOnly &&
hasTarget(editor, event.target) &&
!isEventHandled(event, attributes.onClick) &&
isDOMNode(event.target)
) {
const node = ReactEditor.toSlateNode(editor, event.target)
const path = ReactEditor.findPath(editor, node)
const start = Editor.start(editor, path)
if (Editor.match(editor, start, 'void')) {
const range = Editor.range(editor, start)
Editor.select(editor, range)
}
}
},
[attributes.onClick]
)}
onCompositionEnd={useCallback(
(event: React.CompositionEvent<HTMLDivElement>) => {
if (
@@ -533,7 +539,7 @@ export const Editable = (
}
}
},
[]
[attributes.onCompositionEnd]
)}
onCompositionStart={useCallback(
(event: React.CompositionEvent<HTMLDivElement>) => {
@@ -544,318 +550,342 @@ export const Editable = (
state.isComposing = true
}
},
[]
[attributes.onCompositionStart]
)}
onCopy={useCallback((event: React.ClipboardEvent<HTMLDivElement>) => {
if (
hasEditableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onCopy)
) {
event.preventDefault()
setFragmentData(event.clipboardData, editor)
}
}, [])}
onCut={useCallback((event: React.ClipboardEvent<HTMLDivElement>) => {
if (
!readOnly &&
hasEditableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onCut)
) {
event.preventDefault()
setFragmentData(event.clipboardData, editor)
const { selection } = editor
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
}
}
}, [])}
onDragOver={useCallback((event: React.DragEvent<HTMLDivElement>) => {
if (
hasTarget(editor, event.target) &&
!isEventHandled(event, attributes.onDragOver)
) {
// Only when the target is void, call `preventDefault` to signal
// that drops are allowed. Editable content is droppable by
// default, and calling `preventDefault` hides the cursor.
const node = ReactEditor.toSlateNode(editor, event.target)
if (Element.isElement(node) && editor.isVoid(node)) {
event.preventDefault()
}
}
}, [])}
onDragStart={useCallback((event: React.DragEvent<HTMLDivElement>) => {
if (
hasTarget(editor, event.target) &&
!isEventHandled(event, attributes.onDragStart)
) {
const node = ReactEditor.toSlateNode(editor, event.target)
const path = ReactEditor.findPath(editor, node)
const voidMatch = Editor.match(editor, path, 'void')
// If starting a drag on a void node, make sure it is selected
// so that it shows up in the selection's fragment.
if (voidMatch) {
const range = Editor.range(editor, path)
Editor.select(editor, range)
}
setFragmentData(event.dataTransfer, editor)
}
}, [])}
onDrop={useCallback((event: React.DragEvent<HTMLDivElement>) => {
if (
hasTarget(editor, event.target) &&
!readOnly &&
!isEventHandled(event, attributes.onDrop)
) {
// COMPAT: Firefox doesn't fire `beforeinput` events at all, and
// Chromium browsers don't properly fire them for files being
// dropped into a `contenteditable`. (2019/11/26)
// https://bugs.chromium.org/p/chromium/issues/detail?id=1028668
onCopy={useCallback(
(event: React.ClipboardEvent<HTMLDivElement>) => {
if (
IS_FIREFOX ||
(!IS_SAFARI && event.dataTransfer.files.length > 0)
hasEditableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onCopy)
) {
event.preventDefault()
const range = ReactEditor.findEventRange(editor, event)
const data = event.dataTransfer
Editor.select(editor, range)
editor.exec({ type: 'insert_data', data })
setFragmentData(event.clipboardData, editor)
}
}
}, [])}
onFocus={useCallback((event: React.FocusEvent<HTMLDivElement>) => {
if (
!readOnly &&
!state.isUpdatingSelection &&
hasEditableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onFocus)
) {
const el = ReactEditor.toDOMNode(editor, editor)
state.latestElement = window.document.activeElement
// COMPAT: If the editor has nested editable elements, the focus
// can go to them. In Firefox, this must be prevented because it
// results in issues with keyboard navigation. (2017/03/30)
if (IS_FIREFOX && event.target !== el) {
el.focus()
return
}
IS_FOCUSED.set(editor, true)
}
}, [])}
onKeyDown={event => {
if (
!readOnly &&
hasEditableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onKeyDown)
) {
const { nativeEvent } = event
const { selection } = editor
// COMPAT: Since we prevent the default behavior on
// `beforeinput` events, the browser doesn't think there's ever
// any history stack to undo or redo, so we have to manage these
// hotkeys ourselves. (2019/11/06)
if (Hotkeys.isRedo(nativeEvent)) {
},
[attributes.onCopy]
)}
onCut={useCallback(
(event: React.ClipboardEvent<HTMLDivElement>) => {
if (
!readOnly &&
hasEditableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onCut)
) {
event.preventDefault()
editor.exec({ type: 'redo' })
return
setFragmentData(event.clipboardData, editor)
const { selection } = editor
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
}
}
},
[attributes.onCut]
)}
onDragOver={useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
if (
hasTarget(editor, event.target) &&
!isEventHandled(event, attributes.onDragOver)
) {
// Only when the target is void, call `preventDefault` to signal
// that drops are allowed. Editable content is droppable by
// default, and calling `preventDefault` hides the cursor.
const node = ReactEditor.toSlateNode(editor, event.target)
if (Hotkeys.isUndo(nativeEvent)) {
event.preventDefault()
editor.exec({ type: 'undo' })
return
if (Element.isElement(node) && editor.isVoid(node)) {
event.preventDefault()
}
}
},
[attributes.onDragOver]
)}
onDragStart={useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
if (
hasTarget(editor, event.target) &&
!isEventHandled(event, attributes.onDragStart)
) {
const node = ReactEditor.toSlateNode(editor, event.target)
const path = ReactEditor.findPath(editor, node)
const voidMatch = Editor.match(editor, path, 'void')
// COMPAT: Certain browsers don't handle the selection updates
// properly. In Chrome, the selection isn't properly extended.
// And in Firefox, the selection isn't properly collapsed.
// (2017/10/17)
if (Hotkeys.isMoveLineBackward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'line', reverse: true })
return
}
if (Hotkeys.isMoveLineForward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'line' })
return
}
if (Hotkeys.isExtendLineBackward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, {
unit: 'line',
edge: 'focus',
reverse: true,
})
return
}
if (Hotkeys.isExtendLineForward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'line', edge: 'focus' })
return
}
// COMPAT: If a void node is selected, or a zero-width text node
// adjacent to an inline is selected, we need to handle these
// hotkeys manually because browsers won't be able to skip over
// the void node with the zero-width space not being an empty
// string.
if (Hotkeys.isMoveBackward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isCollapsed(selection)) {
Editor.move(editor, { reverse: true })
} else {
Editor.collapse(editor, { edge: 'start' })
// If starting a drag on a void node, make sure it is selected
// so that it shows up in the selection's fragment.
if (voidMatch) {
const range = Editor.range(editor, path)
Editor.select(editor, range)
}
return
setFragmentData(event.dataTransfer, editor)
}
if (Hotkeys.isMoveForward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isCollapsed(selection)) {
Editor.move(editor)
} else {
Editor.collapse(editor, { edge: 'end' })
}
return
}
if (Hotkeys.isMoveWordBackward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'word', reverse: true })
return
}
if (Hotkeys.isMoveWordForward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'word' })
return
}
// COMPAT: Firefox doesn't support the `beforeinput` event, so we
// fall back to guessing at the input intention for hotkeys.
// COMPAT: In iOS, some of these hotkeys are handled in the
if (IS_FIREFOX) {
// We don't have a core behavior for these, but they change the
// DOM if we don't prevent them, so we have to.
},
[attributes.onDragStart]
)}
onDrop={useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
if (
hasTarget(editor, event.target) &&
!readOnly &&
!isEventHandled(event, attributes.onDrop)
) {
// COMPAT: Firefox doesn't fire `beforeinput` events at all, and
// Chromium browsers don't properly fire them for files being
// dropped into a `contenteditable`. (2019/11/26)
// https://bugs.chromium.org/p/chromium/issues/detail?id=1028668
if (
Hotkeys.isBold(nativeEvent) ||
Hotkeys.isItalic(nativeEvent) ||
Hotkeys.isTransposeCharacter(nativeEvent)
IS_FIREFOX ||
(!IS_SAFARI && event.dataTransfer.files.length > 0)
) {
event.preventDefault()
return
}
if (Hotkeys.isSplitBlock(nativeEvent)) {
event.preventDefault()
editor.exec({ type: 'insert_break' })
return
}
if (Hotkeys.isDeleteBackward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_backward', unit: 'character' })
}
return
}
if (Hotkeys.isDeleteForward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_forward', unit: 'character' })
}
return
}
if (Hotkeys.isDeleteLineBackward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_backward', unit: 'line' })
}
return
}
if (Hotkeys.isDeleteLineForward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_forward', unit: 'line' })
}
return
}
if (Hotkeys.isDeleteWordBackward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_backward', unit: 'word' })
}
return
}
if (Hotkeys.isDeleteWordForward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_forward', unit: 'word' })
}
return
const range = ReactEditor.findEventRange(editor, event)
const data = event.dataTransfer
Editor.select(editor, range)
editor.exec({ type: 'insert_data', data })
}
}
}
}}
onPaste={useCallback((event: React.ClipboardEvent<HTMLDivElement>) => {
// COMPAT: Firefox doesn't support the `beforeinput` event, so we
// fall back to React's `onPaste` here instead.
if (
IS_FIREFOX &&
!readOnly &&
hasEditableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onPaste)
) {
event.preventDefault()
editor.exec({
type: 'insert_data',
data: event.clipboardData,
})
}
}, [])}
},
[attributes.onDrop]
)}
onFocus={useCallback(
(event: React.FocusEvent<HTMLDivElement>) => {
if (
!readOnly &&
!state.isUpdatingSelection &&
hasEditableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onFocus)
) {
const el = ReactEditor.toDOMNode(editor, editor)
state.latestElement = window.document.activeElement
// COMPAT: If the editor has nested editable elements, the focus
// can go to them. In Firefox, this must be prevented because it
// results in issues with keyboard navigation. (2017/03/30)
if (IS_FIREFOX && event.target !== el) {
el.focus()
return
}
IS_FOCUSED.set(editor, true)
}
},
[attributes.onFocus]
)}
onKeyDown={useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (
!readOnly &&
hasEditableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onKeyDown)
) {
const { nativeEvent } = event
const { selection } = editor
// COMPAT: Since we prevent the default behavior on
// `beforeinput` events, the browser doesn't think there's ever
// any history stack to undo or redo, so we have to manage these
// hotkeys ourselves. (2019/11/06)
if (Hotkeys.isRedo(nativeEvent)) {
event.preventDefault()
editor.exec({ type: 'redo' })
return
}
if (Hotkeys.isUndo(nativeEvent)) {
event.preventDefault()
editor.exec({ type: 'undo' })
return
}
// COMPAT: Certain browsers don't handle the selection updates
// properly. In Chrome, the selection isn't properly extended.
// And in Firefox, the selection isn't properly collapsed.
// (2017/10/17)
if (Hotkeys.isMoveLineBackward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'line', reverse: true })
return
}
if (Hotkeys.isMoveLineForward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'line' })
return
}
if (Hotkeys.isExtendLineBackward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, {
unit: 'line',
edge: 'focus',
reverse: true,
})
return
}
if (Hotkeys.isExtendLineForward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'line', edge: 'focus' })
return
}
// COMPAT: If a void node is selected, or a zero-width text node
// adjacent to an inline is selected, we need to handle these
// hotkeys manually because browsers won't be able to skip over
// the void node with the zero-width space not being an empty
// string.
if (Hotkeys.isMoveBackward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isCollapsed(selection)) {
Editor.move(editor, { reverse: true })
} else {
Editor.collapse(editor, { edge: 'start' })
}
return
}
if (Hotkeys.isMoveForward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isCollapsed(selection)) {
Editor.move(editor)
} else {
Editor.collapse(editor, { edge: 'end' })
}
return
}
if (Hotkeys.isMoveWordBackward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'word', reverse: true })
return
}
if (Hotkeys.isMoveWordForward(nativeEvent)) {
event.preventDefault()
Editor.move(editor, { unit: 'word' })
return
}
// COMPAT: Firefox doesn't support the `beforeinput` event, so we
// fall back to guessing at the input intention for hotkeys.
// COMPAT: In iOS, some of these hotkeys are handled in the
if (IS_FIREFOX) {
// We don't have a core behavior for these, but they change the
// DOM if we don't prevent them, so we have to.
if (
Hotkeys.isBold(nativeEvent) ||
Hotkeys.isItalic(nativeEvent) ||
Hotkeys.isTransposeCharacter(nativeEvent)
) {
event.preventDefault()
return
}
if (Hotkeys.isSplitBlock(nativeEvent)) {
event.preventDefault()
editor.exec({ type: 'insert_break' })
return
}
if (Hotkeys.isDeleteBackward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_backward', unit: 'character' })
}
return
}
if (Hotkeys.isDeleteForward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_forward', unit: 'character' })
}
return
}
if (Hotkeys.isDeleteLineBackward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_backward', unit: 'line' })
}
return
}
if (Hotkeys.isDeleteLineForward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_forward', unit: 'line' })
}
return
}
if (Hotkeys.isDeleteWordBackward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_backward', unit: 'word' })
}
return
}
if (Hotkeys.isDeleteWordForward(nativeEvent)) {
event.preventDefault()
if (selection && Range.isExpanded(selection)) {
editor.exec({ type: 'delete_fragment' })
} else {
editor.exec({ type: 'delete_forward', unit: 'word' })
}
return
}
}
}
},
[attributes.onKeyDown]
)}
onPaste={useCallback(
(event: React.ClipboardEvent<HTMLDivElement>) => {
// COMPAT: Firefox doesn't support the `beforeinput` event, so we
// fall back to React's `onPaste` here instead.
if (
IS_FIREFOX &&
!readOnly &&
hasEditableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onPaste)
) {
event.preventDefault()
editor.exec({
type: 'insert_data',
data: event.clipboardData,
})
}
},
[attributes.onPaste]
)}
>
<Children
decorate={decorate}