Skip to content

Commit 2de72b1

Browse files
authored
Update code to use draggable nodes in vscode (#426)
1 parent a391bd1 commit 2de72b1

File tree

2 files changed

+90
-16
lines changed

2 files changed

+90
-16
lines changed

packages/lexical/src/plugins/DraggableBlockPlugin.tsx

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,54 @@ function isOnMenu(element: HTMLElement): boolean {
175175
return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`);
176176
}
177177

178+
/**
179+
* Helper function to find the scrolling container and get scroll offsets.
180+
* Checks if the element itself has scrollable overflow, then checks parent.
181+
*/
182+
function getScrollingContainer(elem: HTMLElement): {
183+
element: HTMLElement;
184+
scrollTop: number;
185+
scrollLeft: number;
186+
} {
187+
// Check if the element itself is scrollable
188+
const style = window.getComputedStyle(elem);
189+
const hasScrollY = style.overflowY === 'auto' || style.overflowY === 'scroll';
190+
const hasScrollX = style.overflowX === 'auto' || style.overflowX === 'scroll';
191+
192+
if (hasScrollY || hasScrollX) {
193+
return {
194+
element: elem,
195+
scrollTop: elem.scrollTop || 0,
196+
scrollLeft: elem.scrollLeft || 0,
197+
};
198+
}
199+
200+
// Check parent element
201+
const parent = elem.parentElement;
202+
if (parent) {
203+
const parentStyle = window.getComputedStyle(parent);
204+
const parentHasScrollY =
205+
parentStyle.overflowY === 'auto' || parentStyle.overflowY === 'scroll';
206+
const parentHasScrollX =
207+
parentStyle.overflowX === 'auto' || parentStyle.overflowX === 'scroll';
208+
209+
if (parentHasScrollY || parentHasScrollX) {
210+
return {
211+
element: parent,
212+
scrollTop: parent.scrollTop || 0,
213+
scrollLeft: parent.scrollLeft || 0,
214+
};
215+
}
216+
}
217+
218+
// No scrolling container found
219+
return {
220+
element: elem,
221+
scrollTop: 0,
222+
scrollLeft: 0,
223+
};
224+
}
225+
178226
function setMenuPosition(
179227
targetElem: HTMLElement | null,
180228
floatingElem: HTMLElement,
@@ -191,11 +239,12 @@ function setMenuPosition(
191239
const floatingElemRect = floatingElem.getBoundingClientRect();
192240
const anchorElementRect = anchorElem.getBoundingClientRect();
193241

194-
const top =
195-
targetRect.top +
196-
(parseInt(targetStyle.lineHeight, 10) - floatingElemRect.height) / 2 -
197-
anchorElementRect.top;
242+
const { scrollTop } = getScrollingContainer(anchorElem);
198243

244+
// Calculate position with scroll compensation
245+
const lineHeight = parseInt(targetStyle.lineHeight, 10);
246+
const topOffset = (lineHeight - floatingElemRect.height) / 2;
247+
const top = targetRect.top + topOffset - anchorElementRect.top + scrollTop;
199248
const left = SPACE;
200249

201250
floatingElem.style.opacity = '1';
@@ -228,14 +277,17 @@ function setTargetLine(
228277
const { top: anchorTop, width: anchorWidth } =
229278
anchorElem.getBoundingClientRect();
230279
const { marginTop, marginBottom } = getCollapsedMargins(targetBlockElem);
280+
281+
const { scrollTop } = getScrollingContainer(anchorElem);
282+
231283
let lineTop = targetBlockElemTop;
232284
if (mouseY >= targetBlockElemTop) {
233285
lineTop += targetBlockElemHeight + marginBottom / 2;
234286
} else {
235287
lineTop -= marginTop / 2;
236288
}
237289

238-
const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT;
290+
const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT + scrollTop;
239291
const left = TEXT_BOX_HORIZONTAL_PADDING - SPACE;
240292

241293
targetLineElem.style.transform = `translate(${left}px, ${top}px)`;
@@ -257,14 +309,17 @@ function useDraggableBlockMenu(
257309
anchorElem: HTMLElement,
258310
isEditable: boolean,
259311
): JSX.Element {
260-
const scrollerElem = anchorElem.parentElement;
261-
262312
const menuRef = useRef<HTMLDivElement>(null);
263313
const targetLineRef = useRef<HTMLDivElement>(null);
264314
const isDraggingBlockRef = useRef<boolean>(false);
265315
const [draggableBlockElem, setDraggableBlockElem] =
266316
useState<HTMLElement | null>(null);
267317

318+
// Get theme classes from editor config
319+
const theme = editor._config.theme;
320+
const menuThemeClass = theme.draggableBlockMenu || '';
321+
const targetLineThemeClass = theme.draggableBlockTargetLine || '';
322+
268323
useEffect(() => {
269324
function onMouseMove(event: MouseEvent) {
270325
const target = event.target;
@@ -273,6 +328,11 @@ function useDraggableBlockMenu(
273328
return;
274329
}
275330

331+
// Performance optimization: Early exit if mouse is outside editor area
332+
if (!anchorElem.contains(target)) {
333+
return;
334+
}
335+
276336
if (isOnMenu(target)) {
277337
return;
278338
}
@@ -286,14 +346,17 @@ function useDraggableBlockMenu(
286346
setDraggableBlockElem(null);
287347
}
288348

289-
scrollerElem?.addEventListener('mousemove', onMouseMove);
290-
scrollerElem?.addEventListener('mouseleave', onMouseLeave);
349+
// BUGFIX: Attach to document instead of scrollerElem to ensure mouse events
350+
// work even after scrolling. The scrollerElem approach fails when content scrolls
351+
// because the mouse event coordinates become misaligned with element positions.
352+
document.addEventListener('mousemove', onMouseMove);
353+
document.addEventListener('mouseleave', onMouseLeave);
291354

292355
return () => {
293-
scrollerElem?.removeEventListener('mousemove', onMouseMove);
294-
scrollerElem?.removeEventListener('mouseleave', onMouseLeave);
356+
document.removeEventListener('mousemove', onMouseMove);
357+
document.removeEventListener('mouseleave', onMouseLeave);
295358
};
296-
}, [scrollerElem, anchorElem, editor]);
359+
}, [anchorElem, editor]);
297360

298361
useEffect(() => {
299362
if (menuRef.current) {
@@ -412,15 +475,18 @@ function useDraggableBlockMenu(
412475
return createPortal(
413476
<>
414477
<div
415-
className="icon draggable-block-menu"
478+
className={`icon draggable-block-menu ${menuThemeClass}`}
416479
ref={menuRef}
417480
draggable
418481
onDragStart={onDragStart}
419482
onDragEnd={onDragEnd}
420483
>
421484
<div className={isEditable ? 'icon' : ''} />
422485
</div>
423-
<div className="draggable-block-target-line" ref={targetLineRef} />
486+
<div
487+
className={`draggable-block-target-line ${targetLineThemeClass}`}
488+
ref={targetLineRef}
489+
/>
424490
</>,
425491
anchorElem,
426492
);

packages/lexical/style/lexical/DraggableBlockPlugin.css

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,24 @@
1818
.draggable-block-menu .icon {
1919
width: 16px;
2020
height: 16px;
21-
opacity: 0.3;
21+
opacity: 0.4;
2222
background-image: url(./icons/draggable-block-menu.svg);
23+
background-size: contain;
24+
background-position: center;
25+
background-repeat: no-repeat;
2326
}
2427

2528
.draggable-block-menu:active {
2629
cursor: grabbing;
2730
}
2831

2932
.draggable-block-menu:hover {
30-
background-color: #efefef;
33+
background-color: rgba(128, 128, 128, 0.1);
34+
opacity: 1;
35+
}
36+
37+
.draggable-block-menu:hover .icon {
38+
opacity: 0.8;
3139
}
3240

3341
.draggable-block-target-line {

0 commit comments

Comments
 (0)