Problem Statement
In the new Block Editor (TipTap v3, libs/new-block-editor), when a Block Editor / Story Block field contains two or more embedded contentlets, dragging a contentlet to reorder it creates a duplicate of the dragged contentlet. The reorder itself does not remove the original — the dragged node is copied to the new position while the source node remains, so the field ends up with one extra contentlet.
Impact: All sites using the new Block Editor with multiple embedded contentlets. The duplicate is persisted and rendered on the published page, so it is customer-visible on live content.
No data loss: nothing is removed from the field; deleting the duplicate and re-saving/re-publishing restores the correct content.
Old Block Editor is unaffected (libs/block-editor) — reproduced clean there. This confirms the regression is isolated to the new editor's drag path.
The embedded contentlet is a ProseMirror atom node rendered through an Angular NodeView:
core-web/libs/new-block-editor/src/lib/editor/extensions/nodes/contentlet/contentlet.extension.ts
return Node.create({
name: DOT_CONTENTLET_NODE_NAME, // 'dotContent'
group: 'block',
atom: true,
draggable: true,
...
addNodeView(): NodeViewRenderer {
return AngularNodeViewRenderer(DotContentletNodeViewComponent, { injector });
}
});
Reordering is driven by TipTap's @tiptap/extension-drag-handle, configured in:
core-web/libs/new-block-editor/src/lib/editor/extensions/block-gutter.extension.ts
return DragHandle.configure({
computePositionConfig: GUTTER_COMPUTE_POSITION_CONFIG,
onNodeChange: (raw) => { ... }
});
The node move (copy slice to target + delete source range) is delegated to TipTap/ProseMirror core; there is no custom move/drop command in this file. The symptom — one extra copy created on the first drag only — is consistent with the drop being treated as a copy rather than a move (source range not deleted) for an atom node that is backed by a framework NodeView. Atom + AngularNodeViewRenderer is a known problem surface for ProseMirror drag "move" semantics: the NodeView DOM/mutation handling can prevent the source deletion half of the move transaction from applying.
A secondary suspect is the gutter's custom drag-image handler createFixDragImageOffsetHandler in the same file, which attaches a one-shot bubble-phase dragstart listener on view.dom.parentElement gated by an isDragHandleDrag flag. It only swaps the drag bitmap (shouldn't create a node on its own), but its first-drag/one-shot timing lines up with the "only the first drag duplicates" observation and should be ruled in or out during the fix.
Verified against latest main (HEAD 2026-08-08): block-gutter.extension.ts and the contentlet node are unchanged from the last release and no commit addresses drag-reorder duplication — the bug is present on current code, not already fixed.
Suggested Investigation / Fix
Log view.dragging and inspect the drop transaction on the first reorder — confirm whether the source range delete is missing (move being downgraded to copy).
If confirmed, ensure the drag-handle move for atom contentlet nodes issues a proper move (delete source + insert), e.g. via a custom drop/moveNode handling in the contentlet extension or the gutter, rather than relying on default slice behavior for the NodeView-backed atom.
Temporarily disable createFixDragImageOffsetHandler and re-test to isolate whether the drag-image fix contributes to the first-drag timing.
Steps to Reproduce
- Create (or open) a content type that has a Block Editor / Story Block field. (Reported using content type Blog)
- Create a contentlet of that type. In the Block Editor field, add 2 or more embedded contentlets (any content types).
- Save & Publish, then close the editor.
- Re-open the same contentlet so the Block Editor loads the saved contentlets.
- Drag one of the lower contentlets upward in the order (e.g. bottom → top) using the block drag handle (the six-dot grip).
Current Behavior
The dragged contentlet is duplicated — it appears both at the drop position and at (or near) its original position.
The duplicate is saved and renders on the live page after save/publish.
Nuance: the duplication happens only on the first drag within an editor session. Subsequent drags in the same open editor reorder correctly without duplicating.
Expected Behavior
Dragging a contentlet to a new position moves it — the node is removed from its original position and inserted at the drop position. The total number of contentlets is unchanged. No duplicate is created on the first drag or any subsequent drag.
Analysis
Acceptance Criteria
- Dragging an embedded contentlet to a new position in the new Block Editor moves it: the field's contentlet count is unchanged and no duplicate node is created — on the first drag and on all subsequent drags within the same editor session.
- Verified for a Block Editor field containing 2+ embedded contentlets, dragging both upward (bottom→top) and downward.
- After save & publish, the rendered page reflects the reordered contentlets with no duplicate.
- No regression to reordering of non-contentlet blocks (paragraphs, images, grids, tables) or to the "+" add-block gutter control.
- Covered by an automated test (unit/e2e) that reorders an embedded contentlet via the drag handle and asserts the document contains exactly one instance of the moved node in the new position.
dotCMS Version
Current Release (dotEvergreen)
Severity
Medium - Some functionality impacted
Links
https://helpdesk.dotcms.com/a/tickets/38739
Problem Statement
In the new Block Editor (TipTap v3, libs/new-block-editor), when a Block Editor / Story Block field contains two or more embedded contentlets, dragging a contentlet to reorder it creates a duplicate of the dragged contentlet. The reorder itself does not remove the original — the dragged node is copied to the new position while the source node remains, so the field ends up with one extra contentlet.
Impact: All sites using the new Block Editor with multiple embedded contentlets. The duplicate is persisted and rendered on the published page, so it is customer-visible on live content.
No data loss: nothing is removed from the field; deleting the duplicate and re-saving/re-publishing restores the correct content.
Old Block Editor is unaffected (libs/block-editor) — reproduced clean there. This confirms the regression is isolated to the new editor's drag path.
The embedded contentlet is a ProseMirror atom node rendered through an Angular NodeView:
core-web/libs/new-block-editor/src/lib/editor/extensions/nodes/contentlet/contentlet.extension.ts
Reordering is driven by TipTap's @tiptap/extension-drag-handle, configured in:
core-web/libs/new-block-editor/src/lib/editor/extensions/block-gutter.extension.ts
The node move (copy slice to target + delete source range) is delegated to TipTap/ProseMirror core; there is no custom move/drop command in this file. The symptom — one extra copy created on the first drag only — is consistent with the drop being treated as a copy rather than a move (source range not deleted) for an atom node that is backed by a framework NodeView. Atom + AngularNodeViewRenderer is a known problem surface for ProseMirror drag "move" semantics: the NodeView DOM/mutation handling can prevent the source deletion half of the move transaction from applying.
A secondary suspect is the gutter's custom drag-image handler createFixDragImageOffsetHandler in the same file, which attaches a one-shot bubble-phase dragstart listener on view.dom.parentElement gated by an isDragHandleDrag flag. It only swaps the drag bitmap (shouldn't create a node on its own), but its first-drag/one-shot timing lines up with the "only the first drag duplicates" observation and should be ruled in or out during the fix.
Verified against latest main (HEAD 2026-08-08): block-gutter.extension.ts and the contentlet node are unchanged from the last release and no commit addresses drag-reorder duplication — the bug is present on current code, not already fixed.
Suggested Investigation / Fix
Log view.dragging and inspect the drop transaction on the first reorder — confirm whether the source range delete is missing (move being downgraded to copy).
If confirmed, ensure the drag-handle move for atom contentlet nodes issues a proper move (delete source + insert), e.g. via a custom drop/moveNode handling in the contentlet extension or the gutter, rather than relying on default slice behavior for the NodeView-backed atom.
Temporarily disable createFixDragImageOffsetHandler and re-test to isolate whether the drag-image fix contributes to the first-drag timing.
Steps to Reproduce
Current Behavior
The dragged contentlet is duplicated — it appears both at the drop position and at (or near) its original position.
The duplicate is saved and renders on the live page after save/publish.
Nuance: the duplication happens only on the first drag within an editor session. Subsequent drags in the same open editor reorder correctly without duplicating.
Expected Behavior
Dragging a contentlet to a new position moves it — the node is removed from its original position and inserted at the drop position. The total number of contentlets is unchanged. No duplicate is created on the first drag or any subsequent drag.
Analysis
Acceptance Criteria
dotCMS Version
Current Release (dotEvergreen)
Severity
Medium - Some functionality impacted
Links
https://helpdesk.dotcms.com/a/tickets/38739