From c14e1fbc77c51f7928ba8ab089c76f3e3438fb97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claud=C3=A9ric=20Demers?= Date: Wed, 5 May 2021 19:26:43 -0400 Subject: [PATCH] Fix duplicated content and other bugs related to drag and drop handling (#4238) * Fix drag and drop logic * Add changeset --- .changeset/drag-and-drop.md | 5 ++ .../slate-react/src/components/editable.tsx | 72 ++++++++++++++----- 2 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 .changeset/drag-and-drop.md diff --git a/.changeset/drag-and-drop.md b/.changeset/drag-and-drop.md new file mode 100644 index 000000000..407dd3866 --- /dev/null +++ b/.changeset/drag-and-drop.md @@ -0,0 +1,5 @@ +--- +'slate-react': patch +--- + +Fix duplicated content and other bugs related to drag and drop handling diff --git a/packages/slate-react/src/components/editable.tsx b/packages/slate-react/src/components/editable.tsx index 75d129a6c..f2a8a22fa 100644 --- a/packages/slate-react/src/components/editable.tsx +++ b/packages/slate-react/src/components/editable.tsx @@ -130,6 +130,7 @@ export const Editable = (props: EditableProps) => { const state = useMemo( () => ({ isComposing: false, + isDraggingInternally: false, isUpdatingSelection: false, latestElement: null as DOMElement | null, }), @@ -423,7 +424,12 @@ export const Editable = (props: EditableProps) => { // while a selection is being dragged. const onDOMSelectionChange = useCallback( throttle(() => { - if (!readOnly && !state.isComposing && !state.isUpdatingSelection) { + if ( + !readOnly && + !state.isComposing && + !state.isUpdatingSelection && + !state.isDraggingInternally + ) { const root = ReactEditor.findDocumentOrShadowRoot(editor) const { activeElement } = root const el = ReactEditor.toDOMNode(editor, editor) @@ -748,7 +754,9 @@ export const Editable = (props: EditableProps) => { ) { const node = ReactEditor.toSlateNode(editor, event.target) const path = ReactEditor.findPath(editor, node) - const voidMatch = Editor.void(editor, { at: path }) + const voidMatch = + Editor.isVoid(editor, node) || + Editor.void(editor, { at: path, voids: true }) // If starting a drag on a void node, make sure it is selected // so that it shows up in the selection's fragment. @@ -757,6 +765,8 @@ export const Editable = (props: EditableProps) => { Transforms.select(editor, range) } + state.isDraggingInternally = true + ReactEditor.setFragmentData(editor, event.dataTransfer) } }, @@ -765,28 +775,58 @@ export const Editable = (props: EditableProps) => { onDrop={useCallback( (event: React.DragEvent) => { if ( - hasTarget(editor, event.target) && !readOnly && + hasTarget(editor, event.target) && !isEventHandled(event, attributes.onDrop) ) { - // COMPAT: Certain browsers don'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 ( - !HAS_BEFORE_INPUT_SUPPORT || - (!IS_SAFARI && event.dataTransfer.files.length > 0) - ) { - event.preventDefault() - const range = ReactEditor.findEventRange(editor, event) - const data = event.dataTransfer - Transforms.select(editor, range) - ReactEditor.insertData(editor, data) + event.preventDefault() + + // Keep a reference to the dragged range before updating selection + const draggedRange = editor.selection + + // Find the range where the drop happened + const range = ReactEditor.findEventRange(editor, event) + const data = event.dataTransfer + + Transforms.select(editor, range) + + if (state.isDraggingInternally) { + if (draggedRange) { + Transforms.delete(editor, { + at: draggedRange, + }) + } + + state.isDraggingInternally = false + } + + ReactEditor.insertData(editor, data) + + // When dragging from another source into the editor, it's possible + // that the current editor does not have focus. + if (!ReactEditor.isFocused(editor)) { + ReactEditor.focus(editor) } } }, [readOnly, attributes.onDrop] )} + onDragEnd={useCallback( + (event: React.DragEvent) => { + // When dropping on a different droppable element than the current editor, + // `onDrop` is not called. So we need to clean up in `onDragEnd` instead. + // Note: `onDragEnd` is only called when `onDrop` is not called + if ( + !readOnly && + state.isDraggingInternally && + hasTarget(editor, event.target) && + !isEventHandled(event, attributes.onDragEnd) + ) { + state.isDraggingInternally = false + } + }, + [readOnly, attributes.onDragEnd] + )} onFocus={useCallback( (event: React.FocusEvent) => { if (