Skip to content

Commit da2c37a

Browse files
fix(editor): improve block drag-and-drop hit targeting (#100)
Increase the dead zone from 4px to 16px so the drag handle appears more readily when hovering near a block. Add a nearest-block fallback so the cursor always resolves to a target during drag operations, even when positioned in the gap between blocks. Extract element lookup into getTopLevelBlockElements for reuse across the two search passes. Closes #99 Co-authored-by: Ona <no-reply@ona.com>
1 parent 24956f8 commit da2c37a

1 file changed

Lines changed: 67 additions & 18 deletions

File tree

src/components/editor/draggable-block-plugin.tsx

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,44 +22,83 @@ const DROP_INDICATOR_CLASSNAME = "memo-drop-indicator";
2222
// Position drag handle within the anchor's left padding (anchor has pl-8 = 32px)
2323
const HANDLE_LEFT_OFFSET = 0;
2424
// Vertical dead zone — mouse must be within this distance of a block to show handle
25-
const HANDLE_DEAD_ZONE = 4;
26-
27-
function getBlockElement(
28-
anchorElem: HTMLElement,
29-
editor: LexicalEditor,
30-
event: MouseEvent
31-
): HTMLElement | null {
32-
const editorBounds = anchorElem.getBoundingClientRect();
33-
const y = event.clientY;
34-
35-
// Walk through top-level block elements in the editor
36-
let blockElem: HTMLElement | null = null;
37-
let minDistance = Infinity;
25+
const HANDLE_DEAD_ZONE = 16;
3826

27+
function getTopLevelBlockElements(
28+
anchorElem: HTMLElement
29+
): HTMLCollectionOf<Element> | NodeListOf<Element> | undefined {
3930
const children = anchorElem.querySelectorAll(
4031
"[data-lexical-editor] > *"
4132
);
33+
if (children.length > 0) return children;
4234

43-
// If no direct children found, try the contentEditable's children
4435
const contentEditable = anchorElem.querySelector(
4536
'[contenteditable="true"]'
4637
);
47-
const elements = children.length > 0 ? children : contentEditable?.children;
38+
return contentEditable?.children;
39+
}
4840

41+
function getBlockElement(
42+
anchorElem: HTMLElement,
43+
editor: LexicalEditor,
44+
event: MouseEvent,
45+
/** When true (during drag), always return the nearest block regardless of dead zone */
46+
useUnboundedSearch = false
47+
): HTMLElement | null {
48+
const editorBounds = anchorElem.getBoundingClientRect();
49+
const y = event.clientY;
50+
51+
const elements = getTopLevelBlockElements(anchorElem);
4952
if (!elements) return null;
5053

54+
// First pass: find the closest block within the dead zone
55+
let blockElem: HTMLElement | null = null;
56+
let minDistance = Infinity;
57+
5158
for (let i = 0; i < elements.length; i++) {
5259
const elem = elements[i] as HTMLElement;
5360
const rect = elem.getBoundingClientRect();
5461
const centerY = rect.top + rect.height / 2;
5562
const distance = Math.abs(y - centerY);
5663

57-
if (distance < minDistance && y >= rect.top - HANDLE_DEAD_ZONE && y <= rect.bottom + HANDLE_DEAD_ZONE) {
64+
if (
65+
distance < minDistance &&
66+
y >= rect.top - HANDLE_DEAD_ZONE &&
67+
y <= rect.bottom + HANDLE_DEAD_ZONE
68+
) {
5869
minDistance = distance;
5970
blockElem = elem;
6071
}
6172
}
6273

74+
// Fallback: if nothing matched within the dead zone (cursor is in a gap
75+
// between blocks or beyond the last block), find the absolute nearest block.
76+
// Always used during drag; for hover, only when cursor is within the editor
77+
// vertical bounds.
78+
if (!blockElem) {
79+
const withinEditorY =
80+
y >= editorBounds.top && y <= editorBounds.bottom;
81+
82+
if (useUnboundedSearch || withinEditorY) {
83+
let fallbackDistance = Infinity;
84+
for (let i = 0; i < elements.length; i++) {
85+
const elem = elements[i] as HTMLElement;
86+
const rect = elem.getBoundingClientRect();
87+
// Distance to the nearest edge of the element
88+
const dist =
89+
y < rect.top
90+
? rect.top - y
91+
: y > rect.bottom
92+
? y - rect.bottom
93+
: 0;
94+
if (dist < fallbackDistance) {
95+
fallbackDistance = dist;
96+
blockElem = elem;
97+
}
98+
}
99+
}
100+
}
101+
63102
// Verify the mouse is within the editor bounds horizontally
64103
if (
65104
blockElem &&
@@ -245,7 +284,12 @@ export function DraggableBlockPlugin({
245284

246285
event.preventDefault();
247286

248-
const blockElem = getBlockElement(anchorElem, editor, event);
287+
const blockElem = getBlockElement(
288+
anchorElem,
289+
editor,
290+
event,
291+
true
292+
);
249293
if (!blockElem || !dropIndicatorRef.current) return true;
250294

251295
const blockRect = blockElem.getBoundingClientRect();
@@ -275,7 +319,12 @@ export function DraggableBlockPlugin({
275319
const nodeKey = event.dataTransfer.getData(DRAG_DATA_FORMAT);
276320
if (!nodeKey) return true;
277321

278-
const blockElem = getBlockElement(anchorElem, editor, event);
322+
const blockElem = getBlockElement(
323+
anchorElem,
324+
editor,
325+
event,
326+
true
327+
);
279328
if (!blockElem) return true;
280329

281330
const blockRect = blockElem.getBoundingClientRect();

0 commit comments

Comments
 (0)