fix: prevent duplication of work package chips during drag and drop#129
fix: prevent duplication of work package chips during drag and drop#129ihordubas99 wants to merge 5 commits intodevfrom
Conversation
judithroth
left a comment
There was a problem hiding this comment.
Ok... maybe I need to stop working for today. Testing this was though somehow.
So, first I tried your implementation.
Then I saw that you're using TipTap again to solve this and my thoughts were like "Did the BlockNote devs really not solve this probem?!" So I went to their docs and they did.
Then I went ahead and tried that and it works. Then I wanted to verify that by switching back. Switching back to the dev branch of op-blocknote-extensions did not change it though (dragging still worked without creating duplicates or links). Switching back to the implementation of version 0.0.25 did though and the wrong behaviour was back.
So, long story short: I think I maybe need to test this again tomorrow.
Can you maybe also test if drag and drop is still broken on the dev branch without these changes here (to confirm)?
| return useCallback(() => { | ||
| if (!editor) return; | ||
|
|
||
| const view = editor._tiptapEditor?.view; |
There was a problem hiding this comment.
Thanks for the pointer, that was exactly the API I needed. Switched the inline chip to meta.draggable: true and dropped the TipTap usage from the drag path entirely.
One behavioural caveat worth flagging though. There are three drag scenarios for an inline chip:
- Drag the chip alone → chip moves.
- Select text + chip + text, drag by the text → whole selection moves.
- Select text + chip + text, drag by the chip → only the chip moves; the surrounding text stays in place.
Case 3 is a regression vs. 0.0.25 - in the old TipTap-based fix it would move the whole selection. The cause is in HTML5 drag-and-drop itself: when a drag starts on an element with draggable="true", the browser performs a native element drag of just that element and ignores the active text selection. The old workaround handled it by overriding ProseMirror's view.dragging with a slice of the selection, and there's no public BlockNote API for that as far as I can tell.
I added a small dragstart handler on the chip that clears any active selection before the drag, so the result is at least clean - no duplicates, no half-moved selections.
For the block chip, drag now works both through the ⠿ side menu (as before) and directly on the block itself. Direct drag is wired up via SideMenuExtension.blockDragStart, which is the same mechanism the ⠿ uses internally, so both entry points behave identically.
This PR is scoped to drag-and-drop only, so I left the remaining _tiptapEditor usage in BlockWorkPackageComponent (the selectionUpdate listener) untouched. I found a public BlockNote API for that too - will tackle it in a separate PR to keep this one focused.
Let me know if case 3 is a blocker - if so, the cleanest fallback would be keeping the old TipTap fix just for that path with a comment explaining why. Otherwise I'd lean toward shipping this as-is.
2419c10 to
c86a21d
Compare
Ticket
https://community.openproject.org/projects/communicator-stream/work_packages/74540
What are you trying to accomplish?
Fix a duplication bug that occurs when dragging and dropping Work Package chips (both Inline and Block) inside the BlockNote editor.
Previously, if a user selected text along with a chip and initiated the drag specifically by grabbing the chip (
contentEditable={false}), ProseMirror would treat the drop as a new HTML insertion (copy) rather than a structural move, leaving the original content behind.Screenshots
Screencast.from.2026-05-07.14-05-22.webm
What approach did you choose and why?
The root cause is in how the browser handles native
dragstartevents on non-editable elements. When a user drags a chip, the browser takes over the drag operation, causing ProseMirror to lose the context of the internal text selection. Because of this, ProseMirror defaults to a copy operation upon drop, inserting the HTML without removing the original source.The fix uses public BlockNote APIs for both inline and block chips, avoiding any direct access to the underlying TipTap/ProseMirror internals.
Inline chip
meta.draggable: truetoopenProjectWorkPackageInlineSpecanddata-drag-handleto the rendered chip element. This is the official BlockNote way to mark inline content as draggable.dragstarthandler that clears any active selection before the drag. Without it, dragging a chip while a wider selection is active produces inconsistent results (text-only drags, duplicates, half-moved selections). Clearing the selection ensures a clean chip-only drag every time.InlineWorkPackageChipInEditorwrapper, keepingInlineWorkPackageChipfree of any BlockNote context dependency so it stays independently testable and renderable outside of the editor. The drag handler is passed down as an optionalonDragStartprop.Block chip
SideMenuExtension.blockDragStart, the same mechanism the ⠿ side menu uses internally. Both entry points (the side menu and direct drag on the block) now behave identically.editor.extensions(which is aMapat runtime) instead of viauseExtension(SideMenuExtension, ...). The class-identity check inuseExtensionthrows "Extension not found" when@blocknote/coreis duplicated in the host app'snode_modules— looking up by registered key sidesteps this entirely.Drag behaviour summary
The third case is a known HTML5 drag-and-drop quirk: when a drag starts on an element with
draggable="true", the browser performs a native element drag of that element and ignores the active text selection. There's no public BlockNote API to override this for inline content, so we accept the limitation in favour of keeping the codebase free of TipTap internals.Alternatives considered
useDragSelectionhook reaching intoeditor._tiptapEditor.viewto override ProseMirror'sview.draggingwith a slice of the full selection plusmove: true. This was the original fix and it did handle the third drag scenario above correctly, but it relies on private TipTap APIs that the project is moving away from.handleDroportransformPastedHTMLon the ProseMirror view directly. Discarded because it creates a leaky abstraction and risks breaking standard drag-and-drop behaviour across the entire editor.Merge checklist