mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-13 18:53:59 +02:00
Fix Copy/pasting void elements is not working (#5121)
* Create new function hasSelectableTarget and use it instead of hasEditableTarget. Fixes Copy/pasting void elements is not working https://github.com/ianstormtaylor/slate/issues/4808 * Add changeset * Revert a change that made editable void not editable and add cypress test for editing editable void * Extract methoods into easily overridable with help from @alex-vladut
This commit is contained in:
committed by
GitHub
parent
6efe3d9a22
commit
06942c6d7e
5
.changeset/seven-waves-rhyme.md
Normal file
5
.changeset/seven-waves-rhyme.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'slate-react': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Make it possible to copy/paste void elements
|
@@ -1,7 +1,8 @@
|
|||||||
describe('editable voids', () => {
|
describe('editable voids', () => {
|
||||||
|
const input = 'input[type="text"]'
|
||||||
const elements = [
|
const elements = [
|
||||||
{ tag: 'h4', count: 3 },
|
{ tag: 'h4', count: 3 },
|
||||||
{ tag: 'input[type="text"]', count: 1 },
|
{ tag: input, count: 1 },
|
||||||
{ tag: 'input[type="radio"]', count: 2 },
|
{ tag: 'input[type="radio"]', count: 2 },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -25,4 +26,9 @@ describe('editable voids', () => {
|
|||||||
cy.get(tag).should('have.length', count * 2)
|
cy.get(tag).should('have.length', count * 2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('make sure you can edit editable void', () => {
|
||||||
|
cy.get(input).type('Typing')
|
||||||
|
cy.get(input).should('have.value', 'Typing')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@@ -206,12 +206,12 @@ export const Editable = (props: EditableProps) => {
|
|||||||
const { anchorNode, focusNode } = domSelection
|
const { anchorNode, focusNode } = domSelection
|
||||||
|
|
||||||
const anchorNodeSelectable =
|
const anchorNodeSelectable =
|
||||||
hasEditableTarget(editor, anchorNode) ||
|
ReactEditor.hasEditableTarget(editor, anchorNode) ||
|
||||||
isTargetInsideNonReadonlyVoid(editor, anchorNode)
|
ReactEditor.isTargetInsideNonReadonlyVoid(editor, anchorNode)
|
||||||
|
|
||||||
const focusNodeSelectable =
|
const focusNodeSelectable =
|
||||||
hasEditableTarget(editor, focusNode) ||
|
ReactEditor.hasEditableTarget(editor, focusNode) ||
|
||||||
isTargetInsideNonReadonlyVoid(editor, focusNode)
|
ReactEditor.isTargetInsideNonReadonlyVoid(editor, focusNode)
|
||||||
|
|
||||||
if (anchorNodeSelectable && focusNodeSelectable) {
|
if (anchorNodeSelectable && focusNodeSelectable) {
|
||||||
const range = ReactEditor.toSlateRange(editor, domSelection, {
|
const range = ReactEditor.toSlateRange(editor, domSelection, {
|
||||||
@@ -434,7 +434,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!readOnly &&
|
!readOnly &&
|
||||||
hasEditableTarget(editor, event.target) &&
|
ReactEditor.hasEditableTarget(editor, event.target) &&
|
||||||
!isDOMEventHandled(event, propsOnDOMBeforeInput)
|
!isDOMEventHandled(event, propsOnDOMBeforeInput)
|
||||||
) {
|
) {
|
||||||
// COMPAT: BeforeInput events aren't cancelable on android, so we have to handle them differently using the android input manager.
|
// COMPAT: BeforeInput events aren't cancelable on android, so we have to handle them differently using the android input manager.
|
||||||
@@ -861,7 +861,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
!HAS_BEFORE_INPUT_SUPPORT &&
|
!HAS_BEFORE_INPUT_SUPPORT &&
|
||||||
!readOnly &&
|
!readOnly &&
|
||||||
!isEventHandled(event, attributes.onBeforeInput) &&
|
!isEventHandled(event, attributes.onBeforeInput) &&
|
||||||
hasEditableTarget(editor, event.target)
|
ReactEditor.hasSelectableTarget(editor, event.target)
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!ReactEditor.isComposing(editor)) {
|
if (!ReactEditor.isComposing(editor)) {
|
||||||
@@ -892,7 +892,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
if (
|
if (
|
||||||
readOnly ||
|
readOnly ||
|
||||||
state.isUpdatingSelection ||
|
state.isUpdatingSelection ||
|
||||||
!hasEditableTarget(editor, event.target) ||
|
!ReactEditor.hasSelectableTarget(editor, event.target) ||
|
||||||
isEventHandled(event, attributes.onBlur)
|
isEventHandled(event, attributes.onBlur)
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
@@ -956,7 +956,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
onClick={useCallback(
|
onClick={useCallback(
|
||||||
(event: React.MouseEvent<HTMLDivElement>) => {
|
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (
|
if (
|
||||||
hasTarget(editor, event.target) &&
|
ReactEditor.hasTarget(editor, event.target) &&
|
||||||
!isEventHandled(event, attributes.onClick) &&
|
!isEventHandled(event, attributes.onClick) &&
|
||||||
isDOMNode(event.target)
|
isDOMNode(event.target)
|
||||||
) {
|
) {
|
||||||
@@ -1013,7 +1013,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
)}
|
)}
|
||||||
onCompositionEnd={useCallback(
|
onCompositionEnd={useCallback(
|
||||||
(event: React.CompositionEvent<HTMLDivElement>) => {
|
(event: React.CompositionEvent<HTMLDivElement>) => {
|
||||||
if (hasEditableTarget(editor, event.target)) {
|
if (ReactEditor.hasSelectableTarget(editor, event.target)) {
|
||||||
if (ReactEditor.isComposing(editor)) {
|
if (ReactEditor.isComposing(editor)) {
|
||||||
setIsComposing(false)
|
setIsComposing(false)
|
||||||
IS_COMPOSING.set(editor, false)
|
IS_COMPOSING.set(editor, false)
|
||||||
@@ -1067,7 +1067,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
onCompositionUpdate={useCallback(
|
onCompositionUpdate={useCallback(
|
||||||
(event: React.CompositionEvent<HTMLDivElement>) => {
|
(event: React.CompositionEvent<HTMLDivElement>) => {
|
||||||
if (
|
if (
|
||||||
hasEditableTarget(editor, event.target) &&
|
ReactEditor.hasSelectableTarget(editor, event.target) &&
|
||||||
!isEventHandled(event, attributes.onCompositionUpdate)
|
!isEventHandled(event, attributes.onCompositionUpdate)
|
||||||
) {
|
) {
|
||||||
if (!ReactEditor.isComposing(editor)) {
|
if (!ReactEditor.isComposing(editor)) {
|
||||||
@@ -1080,7 +1080,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
)}
|
)}
|
||||||
onCompositionStart={useCallback(
|
onCompositionStart={useCallback(
|
||||||
(event: React.CompositionEvent<HTMLDivElement>) => {
|
(event: React.CompositionEvent<HTMLDivElement>) => {
|
||||||
if (hasEditableTarget(editor, event.target)) {
|
if (ReactEditor.hasSelectableTarget(editor, event.target)) {
|
||||||
androidInputManager?.handleCompositionStart(event)
|
androidInputManager?.handleCompositionStart(event)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -1120,7 +1120,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
onCopy={useCallback(
|
onCopy={useCallback(
|
||||||
(event: React.ClipboardEvent<HTMLDivElement>) => {
|
(event: React.ClipboardEvent<HTMLDivElement>) => {
|
||||||
if (
|
if (
|
||||||
hasEditableTarget(editor, event.target) &&
|
ReactEditor.hasSelectableTarget(editor, event.target) &&
|
||||||
!isEventHandled(event, attributes.onCopy)
|
!isEventHandled(event, attributes.onCopy)
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -1137,7 +1137,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
(event: React.ClipboardEvent<HTMLDivElement>) => {
|
(event: React.ClipboardEvent<HTMLDivElement>) => {
|
||||||
if (
|
if (
|
||||||
!readOnly &&
|
!readOnly &&
|
||||||
hasEditableTarget(editor, event.target) &&
|
ReactEditor.hasSelectableTarget(editor, event.target) &&
|
||||||
!isEventHandled(event, attributes.onCut)
|
!isEventHandled(event, attributes.onCut)
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -1165,7 +1165,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
onDragOver={useCallback(
|
onDragOver={useCallback(
|
||||||
(event: React.DragEvent<HTMLDivElement>) => {
|
(event: React.DragEvent<HTMLDivElement>) => {
|
||||||
if (
|
if (
|
||||||
hasTarget(editor, event.target) &&
|
ReactEditor.hasTarget(editor, event.target) &&
|
||||||
!isEventHandled(event, attributes.onDragOver)
|
!isEventHandled(event, attributes.onDragOver)
|
||||||
) {
|
) {
|
||||||
// Only when the target is void, call `preventDefault` to signal
|
// Only when the target is void, call `preventDefault` to signal
|
||||||
@@ -1184,7 +1184,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
(event: React.DragEvent<HTMLDivElement>) => {
|
(event: React.DragEvent<HTMLDivElement>) => {
|
||||||
if (
|
if (
|
||||||
!readOnly &&
|
!readOnly &&
|
||||||
hasTarget(editor, event.target) &&
|
ReactEditor.hasTarget(editor, event.target) &&
|
||||||
!isEventHandled(event, attributes.onDragStart)
|
!isEventHandled(event, attributes.onDragStart)
|
||||||
) {
|
) {
|
||||||
const node = ReactEditor.toSlateNode(editor, event.target)
|
const node = ReactEditor.toSlateNode(editor, event.target)
|
||||||
@@ -1215,7 +1215,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
(event: React.DragEvent<HTMLDivElement>) => {
|
(event: React.DragEvent<HTMLDivElement>) => {
|
||||||
if (
|
if (
|
||||||
!readOnly &&
|
!readOnly &&
|
||||||
hasTarget(editor, event.target) &&
|
ReactEditor.hasTarget(editor, event.target) &&
|
||||||
!isEventHandled(event, attributes.onDrop)
|
!isEventHandled(event, attributes.onDrop)
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -1260,7 +1260,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
!readOnly &&
|
!readOnly &&
|
||||||
state.isDraggingInternally &&
|
state.isDraggingInternally &&
|
||||||
attributes.onDragEnd &&
|
attributes.onDragEnd &&
|
||||||
hasTarget(editor, event.target)
|
ReactEditor.hasTarget(editor, event.target)
|
||||||
) {
|
) {
|
||||||
attributes.onDragEnd(event)
|
attributes.onDragEnd(event)
|
||||||
}
|
}
|
||||||
@@ -1277,7 +1277,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
if (
|
if (
|
||||||
!readOnly &&
|
!readOnly &&
|
||||||
!state.isUpdatingSelection &&
|
!state.isUpdatingSelection &&
|
||||||
hasEditableTarget(editor, event.target) &&
|
ReactEditor.hasSelectableTarget(editor, event.target) &&
|
||||||
!isEventHandled(event, attributes.onFocus)
|
!isEventHandled(event, attributes.onFocus)
|
||||||
) {
|
) {
|
||||||
const el = ReactEditor.toDOMNode(editor, editor)
|
const el = ReactEditor.toDOMNode(editor, editor)
|
||||||
@@ -1299,7 +1299,10 @@ export const Editable = (props: EditableProps) => {
|
|||||||
)}
|
)}
|
||||||
onKeyDown={useCallback(
|
onKeyDown={useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (!readOnly && hasEditableTarget(editor, event.target)) {
|
if (
|
||||||
|
!readOnly &&
|
||||||
|
ReactEditor.hasEditableTarget(editor, event.target)
|
||||||
|
) {
|
||||||
androidInputManager?.handleKeyDown(event)
|
androidInputManager?.handleKeyDown(event)
|
||||||
|
|
||||||
const { nativeEvent } = event
|
const { nativeEvent } = event
|
||||||
@@ -1573,7 +1576,7 @@ export const Editable = (props: EditableProps) => {
|
|||||||
(event: React.ClipboardEvent<HTMLDivElement>) => {
|
(event: React.ClipboardEvent<HTMLDivElement>) => {
|
||||||
if (
|
if (
|
||||||
!readOnly &&
|
!readOnly &&
|
||||||
hasEditableTarget(editor, event.target) &&
|
ReactEditor.hasSelectableTarget(editor, event.target) &&
|
||||||
!isEventHandled(event, attributes.onPaste)
|
!isEventHandled(event, attributes.onPaste)
|
||||||
) {
|
) {
|
||||||
// COMPAT: Certain browsers don't support the `beforeinput` event, so we
|
// COMPAT: Certain browsers don't support the `beforeinput` event, so we
|
||||||
@@ -1668,46 +1671,6 @@ const defaultScrollSelectionIntoView = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the target is in the editor.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const hasTarget = (
|
|
||||||
editor: ReactEditor,
|
|
||||||
target: EventTarget | null
|
|
||||||
): target is DOMNode => {
|
|
||||||
return isDOMNode(target) && ReactEditor.hasDOMNode(editor, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the target is editable and in the editor.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const hasEditableTarget = (
|
|
||||||
editor: ReactEditor,
|
|
||||||
target: EventTarget | null
|
|
||||||
): target is DOMNode => {
|
|
||||||
return (
|
|
||||||
isDOMNode(target) &&
|
|
||||||
ReactEditor.hasDOMNode(editor, target, { editable: true })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the target is inside void and in an non-readonly editor.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const isTargetInsideNonReadonlyVoid = (
|
|
||||||
editor: ReactEditor,
|
|
||||||
target: EventTarget | null
|
|
||||||
): boolean => {
|
|
||||||
if (IS_READ_ONLY.get(editor)) return false
|
|
||||||
|
|
||||||
const slateNode =
|
|
||||||
hasTarget(editor, target) && ReactEditor.toSlateNode(editor, target)
|
|
||||||
return Editor.isVoid(editor, slateNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an event is overrided by a handler.
|
* Check if an event is overrided by a handler.
|
||||||
*/
|
*/
|
||||||
|
@@ -33,6 +33,7 @@ import {
|
|||||||
DOMStaticRange,
|
DOMStaticRange,
|
||||||
isDOMElement,
|
isDOMElement,
|
||||||
isDOMSelection,
|
isDOMSelection,
|
||||||
|
isDOMNode,
|
||||||
normalizeDOMPoint,
|
normalizeDOMPoint,
|
||||||
hasShadowRoot,
|
hasShadowRoot,
|
||||||
DOMText,
|
DOMText,
|
||||||
@@ -52,6 +53,22 @@ export interface ReactEditor extends BaseEditor {
|
|||||||
originEvent?: 'drag' | 'copy' | 'cut'
|
originEvent?: 'drag' | 'copy' | 'cut'
|
||||||
) => void
|
) => void
|
||||||
hasRange: (editor: ReactEditor, range: Range) => boolean
|
hasRange: (editor: ReactEditor, range: Range) => boolean
|
||||||
|
hasTarget: (
|
||||||
|
editor: ReactEditor,
|
||||||
|
target: EventTarget | null
|
||||||
|
) => target is DOMNode
|
||||||
|
hasEditableTarget: (
|
||||||
|
editor: ReactEditor,
|
||||||
|
target: EventTarget | null
|
||||||
|
) => target is DOMNode
|
||||||
|
hasSelectableTarget: (
|
||||||
|
editor: ReactEditor,
|
||||||
|
target: EventTarget | null
|
||||||
|
) => boolean
|
||||||
|
isTargetInsideNonReadonlyVoid: (
|
||||||
|
editor: ReactEditor,
|
||||||
|
target: EventTarget | null
|
||||||
|
) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-redeclare
|
// eslint-disable-next-line no-redeclare
|
||||||
@@ -779,6 +796,57 @@ export const ReactEditor = {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the target is in the editor.
|
||||||
|
*/
|
||||||
|
hasTarget(
|
||||||
|
editor: ReactEditor,
|
||||||
|
target: EventTarget | null
|
||||||
|
): target is DOMNode {
|
||||||
|
return isDOMNode(target) && ReactEditor.hasDOMNode(editor, target)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the target is editable and in the editor.
|
||||||
|
*/
|
||||||
|
hasEditableTarget(
|
||||||
|
editor: ReactEditor,
|
||||||
|
target: EventTarget | null
|
||||||
|
): target is DOMNode {
|
||||||
|
return (
|
||||||
|
isDOMNode(target) &&
|
||||||
|
ReactEditor.hasDOMNode(editor, target, { editable: true })
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the target can be selectable
|
||||||
|
*/
|
||||||
|
hasSelectableTarget(
|
||||||
|
editor: ReactEditor,
|
||||||
|
target: EventTarget | null
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
ReactEditor.hasEditableTarget(editor, target) ||
|
||||||
|
ReactEditor.isTargetInsideNonReadonlyVoid(editor, target)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the target is inside void and in an non-readonly editor.
|
||||||
|
*/
|
||||||
|
isTargetInsideNonReadonlyVoid(
|
||||||
|
editor: ReactEditor,
|
||||||
|
target: EventTarget | null
|
||||||
|
): boolean {
|
||||||
|
if (IS_READ_ONLY.get(editor)) return false
|
||||||
|
|
||||||
|
const slateNode =
|
||||||
|
ReactEditor.hasTarget(editor, target) &&
|
||||||
|
ReactEditor.toSlateNode(editor, target)
|
||||||
|
return Editor.isVoid(editor, slateNode)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Experimental and android specific: Flush all pending diffs and cancel composition at the next possible time.
|
* Experimental and android specific: Flush all pending diffs and cancel composition at the next possible time.
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user