From 06942c6d7e4b8418a467f022750b010491dbdbe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laufey=20Rut=20Gu=C3=B0mundsd=C3=B3ttir?= Date: Thu, 17 Nov 2022 16:17:10 +0000 Subject: [PATCH] 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 --- .changeset/seven-waves-rhyme.md | 5 ++ .../integration/examples/editable-voids.ts | 8 +- .../slate-react/src/components/editable.tsx | 83 +++++-------------- .../slate-react/src/plugin/react-editor.ts | 68 +++++++++++++++ 4 files changed, 103 insertions(+), 61 deletions(-) create mode 100644 .changeset/seven-waves-rhyme.md diff --git a/.changeset/seven-waves-rhyme.md b/.changeset/seven-waves-rhyme.md new file mode 100644 index 000000000..5159db86d --- /dev/null +++ b/.changeset/seven-waves-rhyme.md @@ -0,0 +1,5 @@ +--- +'slate-react': minor +--- + +Make it possible to copy/paste void elements diff --git a/cypress/integration/examples/editable-voids.ts b/cypress/integration/examples/editable-voids.ts index 2367d1d6a..d33de6fbf 100644 --- a/cypress/integration/examples/editable-voids.ts +++ b/cypress/integration/examples/editable-voids.ts @@ -1,7 +1,8 @@ describe('editable voids', () => { + const input = 'input[type="text"]' const elements = [ { tag: 'h4', count: 3 }, - { tag: 'input[type="text"]', count: 1 }, + { tag: input, count: 1 }, { tag: 'input[type="radio"]', count: 2 }, ] @@ -25,4 +26,9 @@ describe('editable voids', () => { 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') + }) }) diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 3ad9d961c..e4f202fd2 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -206,12 +206,12 @@ export const Editable = (props: EditableProps) => { const { anchorNode, focusNode } = domSelection const anchorNodeSelectable = - hasEditableTarget(editor, anchorNode) || - isTargetInsideNonReadonlyVoid(editor, anchorNode) + ReactEditor.hasEditableTarget(editor, anchorNode) || + ReactEditor.isTargetInsideNonReadonlyVoid(editor, anchorNode) const focusNodeSelectable = - hasEditableTarget(editor, focusNode) || - isTargetInsideNonReadonlyVoid(editor, focusNode) + ReactEditor.hasEditableTarget(editor, focusNode) || + ReactEditor.isTargetInsideNonReadonlyVoid(editor, focusNode) if (anchorNodeSelectable && focusNodeSelectable) { const range = ReactEditor.toSlateRange(editor, domSelection, { @@ -434,7 +434,7 @@ export const Editable = (props: EditableProps) => { if ( !readOnly && - hasEditableTarget(editor, event.target) && + ReactEditor.hasEditableTarget(editor, event.target) && !isDOMEventHandled(event, propsOnDOMBeforeInput) ) { // 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 && !readOnly && !isEventHandled(event, attributes.onBeforeInput) && - hasEditableTarget(editor, event.target) + ReactEditor.hasSelectableTarget(editor, event.target) ) { event.preventDefault() if (!ReactEditor.isComposing(editor)) { @@ -892,7 +892,7 @@ export const Editable = (props: EditableProps) => { if ( readOnly || state.isUpdatingSelection || - !hasEditableTarget(editor, event.target) || + !ReactEditor.hasSelectableTarget(editor, event.target) || isEventHandled(event, attributes.onBlur) ) { return @@ -956,7 +956,7 @@ export const Editable = (props: EditableProps) => { onClick={useCallback( (event: React.MouseEvent) => { if ( - hasTarget(editor, event.target) && + ReactEditor.hasTarget(editor, event.target) && !isEventHandled(event, attributes.onClick) && isDOMNode(event.target) ) { @@ -1013,7 +1013,7 @@ export const Editable = (props: EditableProps) => { )} onCompositionEnd={useCallback( (event: React.CompositionEvent) => { - if (hasEditableTarget(editor, event.target)) { + if (ReactEditor.hasSelectableTarget(editor, event.target)) { if (ReactEditor.isComposing(editor)) { setIsComposing(false) IS_COMPOSING.set(editor, false) @@ -1067,7 +1067,7 @@ export const Editable = (props: EditableProps) => { onCompositionUpdate={useCallback( (event: React.CompositionEvent) => { if ( - hasEditableTarget(editor, event.target) && + ReactEditor.hasSelectableTarget(editor, event.target) && !isEventHandled(event, attributes.onCompositionUpdate) ) { if (!ReactEditor.isComposing(editor)) { @@ -1080,7 +1080,7 @@ export const Editable = (props: EditableProps) => { )} onCompositionStart={useCallback( (event: React.CompositionEvent) => { - if (hasEditableTarget(editor, event.target)) { + if (ReactEditor.hasSelectableTarget(editor, event.target)) { androidInputManager?.handleCompositionStart(event) if ( @@ -1120,7 +1120,7 @@ export const Editable = (props: EditableProps) => { onCopy={useCallback( (event: React.ClipboardEvent) => { if ( - hasEditableTarget(editor, event.target) && + ReactEditor.hasSelectableTarget(editor, event.target) && !isEventHandled(event, attributes.onCopy) ) { event.preventDefault() @@ -1137,7 +1137,7 @@ export const Editable = (props: EditableProps) => { (event: React.ClipboardEvent) => { if ( !readOnly && - hasEditableTarget(editor, event.target) && + ReactEditor.hasSelectableTarget(editor, event.target) && !isEventHandled(event, attributes.onCut) ) { event.preventDefault() @@ -1165,7 +1165,7 @@ export const Editable = (props: EditableProps) => { onDragOver={useCallback( (event: React.DragEvent) => { if ( - hasTarget(editor, event.target) && + ReactEditor.hasTarget(editor, event.target) && !isEventHandled(event, attributes.onDragOver) ) { // Only when the target is void, call `preventDefault` to signal @@ -1184,7 +1184,7 @@ export const Editable = (props: EditableProps) => { (event: React.DragEvent) => { if ( !readOnly && - hasTarget(editor, event.target) && + ReactEditor.hasTarget(editor, event.target) && !isEventHandled(event, attributes.onDragStart) ) { const node = ReactEditor.toSlateNode(editor, event.target) @@ -1215,7 +1215,7 @@ export const Editable = (props: EditableProps) => { (event: React.DragEvent) => { if ( !readOnly && - hasTarget(editor, event.target) && + ReactEditor.hasTarget(editor, event.target) && !isEventHandled(event, attributes.onDrop) ) { event.preventDefault() @@ -1260,7 +1260,7 @@ export const Editable = (props: EditableProps) => { !readOnly && state.isDraggingInternally && attributes.onDragEnd && - hasTarget(editor, event.target) + ReactEditor.hasTarget(editor, event.target) ) { attributes.onDragEnd(event) } @@ -1277,7 +1277,7 @@ export const Editable = (props: EditableProps) => { if ( !readOnly && !state.isUpdatingSelection && - hasEditableTarget(editor, event.target) && + ReactEditor.hasSelectableTarget(editor, event.target) && !isEventHandled(event, attributes.onFocus) ) { const el = ReactEditor.toDOMNode(editor, editor) @@ -1299,7 +1299,10 @@ export const Editable = (props: EditableProps) => { )} onKeyDown={useCallback( (event: React.KeyboardEvent) => { - if (!readOnly && hasEditableTarget(editor, event.target)) { + if ( + !readOnly && + ReactEditor.hasEditableTarget(editor, event.target) + ) { androidInputManager?.handleKeyDown(event) const { nativeEvent } = event @@ -1573,7 +1576,7 @@ export const Editable = (props: EditableProps) => { (event: React.ClipboardEvent) => { if ( !readOnly && - hasEditableTarget(editor, event.target) && + ReactEditor.hasSelectableTarget(editor, event.target) && !isEventHandled(event, attributes.onPaste) ) { // 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. */ diff --git a/packages/slate-react/src/plugin/react-editor.ts b/packages/slate-react/src/plugin/react-editor.ts index 5a74ad2fb..e015aefc9 100644 --- a/packages/slate-react/src/plugin/react-editor.ts +++ b/packages/slate-react/src/plugin/react-editor.ts @@ -33,6 +33,7 @@ import { DOMStaticRange, isDOMElement, isDOMSelection, + isDOMNode, normalizeDOMPoint, hasShadowRoot, DOMText, @@ -52,6 +53,22 @@ export interface ReactEditor extends BaseEditor { originEvent?: 'drag' | 'copy' | 'cut' ) => void 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 @@ -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. */