1
0
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:
Laufey Rut Guðmundsdóttir
2022-11-17 16:17:10 +00:00
committed by GitHub
parent 6efe3d9a22
commit 06942c6d7e
4 changed files with 103 additions and 61 deletions

View File

@@ -0,0 +1,5 @@
---
'slate-react': minor
---
Make it possible to copy/paste void elements

View File

@@ -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')
})
}) })

View File

@@ -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.
*/ */

View File

@@ -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.
*/ */