Skip to content

Commit

Permalink
Fix Copy/pasting void elements is not working (#5121)
Browse files Browse the repository at this point in the history
* Create new function hasSelectableTarget and use it instead of hasEditableTarget. Fixes Copy/pasting void elements is not working #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
  • Loading branch information
laufeyrut committed Nov 17, 2022
1 parent 6efe3d9 commit 06942c6
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 61 deletions.
5 changes: 5 additions & 0 deletions .changeset/seven-waves-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'slate-react': minor
---

Make it possible to copy/paste void elements
8 changes: 7 additions & 1 deletion cypress/integration/examples/editable-voids.ts
Original file line number Diff line number Diff line change
@@ -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 },
]

Expand All @@ -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')
})
})
83 changes: 23 additions & 60 deletions packages/slate-react/src/components/editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -956,7 +956,7 @@ export const Editable = (props: EditableProps) => {
onClick={useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
if (
hasTarget(editor, event.target) &&
ReactEditor.hasTarget(editor, event.target) &&
!isEventHandled(event, attributes.onClick) &&
isDOMNode(event.target)
) {
Expand Down Expand Up @@ -1013,7 +1013,7 @@ export const Editable = (props: EditableProps) => {
)}
onCompositionEnd={useCallback(
(event: React.CompositionEvent<HTMLDivElement>) => {
if (hasEditableTarget(editor, event.target)) {
if (ReactEditor.hasSelectableTarget(editor, event.target)) {
if (ReactEditor.isComposing(editor)) {
setIsComposing(false)
IS_COMPOSING.set(editor, false)
Expand Down Expand Up @@ -1067,7 +1067,7 @@ export const Editable = (props: EditableProps) => {
onCompositionUpdate={useCallback(
(event: React.CompositionEvent<HTMLDivElement>) => {
if (
hasEditableTarget(editor, event.target) &&
ReactEditor.hasSelectableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onCompositionUpdate)
) {
if (!ReactEditor.isComposing(editor)) {
Expand All @@ -1080,7 +1080,7 @@ export const Editable = (props: EditableProps) => {
)}
onCompositionStart={useCallback(
(event: React.CompositionEvent<HTMLDivElement>) => {
if (hasEditableTarget(editor, event.target)) {
if (ReactEditor.hasSelectableTarget(editor, event.target)) {
androidInputManager?.handleCompositionStart(event)

if (
Expand Down Expand Up @@ -1120,7 +1120,7 @@ export const Editable = (props: EditableProps) => {
onCopy={useCallback(
(event: React.ClipboardEvent<HTMLDivElement>) => {
if (
hasEditableTarget(editor, event.target) &&
ReactEditor.hasSelectableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onCopy)
) {
event.preventDefault()
Expand All @@ -1137,7 +1137,7 @@ export const Editable = (props: EditableProps) => {
(event: React.ClipboardEvent<HTMLDivElement>) => {
if (
!readOnly &&
hasEditableTarget(editor, event.target) &&
ReactEditor.hasSelectableTarget(editor, event.target) &&
!isEventHandled(event, attributes.onCut)
) {
event.preventDefault()
Expand Down Expand Up @@ -1165,7 +1165,7 @@ export const Editable = (props: EditableProps) => {
onDragOver={useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
if (
hasTarget(editor, event.target) &&
ReactEditor.hasTarget(editor, event.target) &&
!isEventHandled(event, attributes.onDragOver)
) {
// Only when the target is void, call `preventDefault` to signal
Expand All @@ -1184,7 +1184,7 @@ export const Editable = (props: EditableProps) => {
(event: React.DragEvent<HTMLDivElement>) => {
if (
!readOnly &&
hasTarget(editor, event.target) &&
ReactEditor.hasTarget(editor, event.target) &&
!isEventHandled(event, attributes.onDragStart)
) {
const node = ReactEditor.toSlateNode(editor, event.target)
Expand Down Expand Up @@ -1215,7 +1215,7 @@ export const Editable = (props: EditableProps) => {
(event: React.DragEvent<HTMLDivElement>) => {
if (
!readOnly &&
hasTarget(editor, event.target) &&
ReactEditor.hasTarget(editor, event.target) &&
!isEventHandled(event, attributes.onDrop)
) {
event.preventDefault()
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
Expand All @@ -1299,7 +1299,10 @@ export const Editable = (props: EditableProps) => {
)}
onKeyDown={useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!readOnly && hasEditableTarget(editor, event.target)) {
if (
!readOnly &&
ReactEditor.hasEditableTarget(editor, event.target)
) {
androidInputManager?.handleKeyDown(event)

const { nativeEvent } = event
Expand Down Expand Up @@ -1573,7 +1576,7 @@ export const Editable = (props: EditableProps) => {
(event: React.ClipboardEvent<HTMLDivElement>) => {
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
Expand Down Expand Up @@ -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.
*/
Expand Down
68 changes: 68 additions & 0 deletions packages/slate-react/src/plugin/react-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
DOMStaticRange,
isDOMElement,
isDOMSelection,
isDOMNode,
normalizeDOMPoint,
hasShadowRoot,
DOMText,
Expand All @@ -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
Expand Down Expand Up @@ -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.
*/
Expand Down

0 comments on commit 06942c6

Please sign in to comment.