Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-drag-handle-rtl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/extension-drag-handle': patch
---

Fixed drag handle ghost image for RTL and mixed-direction content: the ghost wrapper now uses the dragged block’s computed `direction` (via `domAtPos`), and the drag image hotspot uses the cursor position relative to the ghost `wrapper` so the preview aligns with the pointer in both LTR and RTL.
28 changes: 28 additions & 0 deletions packages/extension-drag-handle/PULL_REQUEST_BODY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!-- Copy the sections below into your GitHub PR description when opening/updating the MR. -->

## Changes Overview

Fix drag ghost (`setDragImage`) for RTL editors and mixed-direction blocks: the temporary wrapper now mirrors the **dragged block’s** text direction, and the drag hotspot is aligned to the **pointer** using the wrapper’s bounds (not a fixed corner).

## Implementation Approach

1. **Direction on the ghost wrapper**
Resolve the DOM node at the drag range start (`view.domAtPos`), normalize text nodes to their `parentElement`, then read `getComputedStyle(...).direction` (falling back to `view.dom`). Set `wrapper`’s `dir` so the clone matches the block (including RTL page + LTR paragraph cases).

2. **Drag image hotspot**
`DataTransfer.setDragImage` uses the off-screen `wrapper` as the image. After cloning and `append`, measure `wrapper.getBoundingClientRect()` and set the hotspot x to `clientX - wrapperRect.left`, clamped to `[0, wrapperRect.width]`, so the ghost tracks the cursor in both LTR and RTL.

## Testing Done

- Manual: drag handle in an RTL-configured editor; mixed Arabic/Latin paragraphs where block `direction` differs from `view.dom`.
- Confirmed ghost text direction and pointer alignment during drag.

## Verification Steps

1. Set editor / root to `dir="rtl"` (or CSS `direction: rtl`).
2. Drag a block via the drag handle; confirm the ghost renders with correct direction and stays under/near the cursor.
3. Optional: `dir="auto"` or LTR block inside RTL editor — ghost should follow the **block** direction.

## Additional Notes

- Supersedes an earlier approach that only toggled `dir` on the editor root and used `offsetWidth` as the hotspot, which mis-handled mixed-direction content and mis-aligned the ghost.
15 changes: 14 additions & 1 deletion packages/extension-drag-handle/src/helpers/dragHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ export function dragHandler(

const { tr } = view.state
const wrapper = document.createElement('div')

const domNode = view.domAtPos(ranges[0].$from.pos).node
const draggedDom: Element | null =
domNode.nodeType === Node.TEXT_NODE
? (domNode as Text).parentElement
: domNode instanceof Element
? domNode
: null
const contentDir = draggedDom ? getComputedStyle(draggedDom).direction : getComputedStyle(view.dom).direction
wrapper.setAttribute('dir', contentDir || 'ltr')

const from = ranges[0].$from.pos
const to = ranges[ranges.length - 1].$to.pos

Expand Down Expand Up @@ -122,7 +133,9 @@ export function dragHandler(
document.body.append(wrapper)

event.dataTransfer.clearData()
event.dataTransfer.setDragImage(wrapper, 0, 0)
const wrapperRect = wrapper.getBoundingClientRect()
const dragImageX = event.clientX - wrapperRect.left
event.dataTransfer.setDragImage(wrapper, Math.max(0, Math.min(dragImageX, wrapperRect.width)), 0)

// Tell ProseMirror the dragged content.
// Pass the NodeSelection as `node` so ProseMirror's drop handler can use it
Expand Down