Skip to content

Commit 298da8e

Browse files
lucbrinkmanclaude
andcommitted
Fix node dragging bug when scrolling during drag
When dragging a node and scrolling with the mouse wheel (normal or shift+scroll), the node would stay at its original position instead of following the cursor. This was caused by using delta-based positioning from a fixed reference point that didn't account for scroll offset changes. Solution: Use absolute canvas coordinate conversion (screenToCanvasCoords) on every mouse move, similar to how arrow connector dragging works. This function always reads the current scroll position, so nodes correctly track the cursor even when the viewport moves. Changes: - Added screenToCanvasCoords function to Flowchart component - Pass screenToCanvasCoords to Node components as prop - Updated Node drag logic to store offset from mouse to node center - Use screenToCanvasCoords in handleMouseDown, handleGlobalMouseMove, and handleGlobalMouseUp to calculate positions using current scroll state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 22b933e commit 298da8e

File tree

2 files changed

+76
-47
lines changed

2 files changed

+76
-47
lines changed

components/Flowchart.tsx

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,34 @@ export default function Flowchart({
562562
}
563563
}, [isDragging]);
564564

565+
// Screen to canvas coordinate conversion - used by nodes and edges for dragging
566+
const screenToCanvasCoords = useCallback(
567+
(screenX: number, screenY: number) => {
568+
if (!containerRef.current || !scrollContainerRef.current)
569+
return { x: 0, y: 0 };
570+
const scrollContainer = scrollContainerRef.current;
571+
const scrollRect = scrollContainer.getBoundingClientRect();
572+
const zoomFactor = zoom / 100;
573+
574+
// Convert screen coords to position within scrollable area, then to canvas coords
575+
const canvasX =
576+
(scrollContainer.scrollLeft +
577+
screenX -
578+
scrollRect.left -
579+
CANVAS_PADDING) /
580+
zoomFactor;
581+
const canvasY =
582+
(scrollContainer.scrollTop +
583+
screenY -
584+
scrollRect.top -
585+
CANVAS_PADDING) /
586+
zoomFactor;
587+
588+
return { x: canvasX, y: canvasY };
589+
},
590+
[zoom]
591+
);
592+
565593
return (
566594
<div
567595
ref={scrollContainerRefCallback}
@@ -839,31 +867,18 @@ export default function Flowchart({
839867
: undefined
840868
}
841869
showAddArrows={shouldShowAddArrows}
870+
screenToCanvasCoords={screenToCanvasCoords}
842871
onAddArrow={
843872
shouldShowAddArrows
844873
? (direction, mousePos) => {
845874
const bounds = nodeBounds.get(node.id);
846875
// Convert screen coordinates to canvas coordinates if mousePos provided
847876
let canvasPos: { x: number; y: number } | undefined;
848-
if (mousePos && scrollContainerRef.current) {
849-
const scrollContainer = scrollContainerRef.current;
850-
const scrollRect =
851-
scrollContainer.getBoundingClientRect();
852-
const zoomFactor = zoom / 100;
853-
canvasPos = {
854-
x:
855-
(scrollContainer.scrollLeft +
856-
mousePos.clientX -
857-
scrollRect.left -
858-
CANVAS_PADDING) /
859-
zoomFactor,
860-
y:
861-
(scrollContainer.scrollTop +
862-
mousePos.clientY -
863-
scrollRect.top -
864-
CANVAS_PADDING) /
865-
zoomFactor,
866-
};
877+
if (mousePos) {
878+
canvasPos = screenToCanvasCoords(
879+
mousePos.clientX,
880+
mousePos.clientY
881+
);
867882
}
868883
onAddArrow(
869884
node.id,

components/Node.tsx

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ interface NodeProps {
6060
onSliderChange?: (value: number) => void;
6161
onSliderChangeComplete?: () => void;
6262
showAddArrows?: boolean;
63+
screenToCanvasCoords: (
64+
screenX: number,
65+
screenY: number
66+
) => { x: number; y: number };
6367
onAddArrow?: (
6468
direction: "top" | "bottom" | "left" | "right",
6569
mousePos?: { clientX: number; clientY: number }
@@ -98,6 +102,7 @@ const Node = forwardRef<HTMLDivElement, NodeProps>(
98102
onSliderChange,
99103
onSliderChangeComplete,
100104
showAddArrows,
105+
screenToCanvasCoords,
101106
onAddArrow,
102107
},
103108
ref
@@ -108,8 +113,10 @@ const Node = forwardRef<HTMLDivElement, NodeProps>(
108113
const [isDragging, setIsDragging] = useState(false);
109114
const [shiftHeld, setShiftHeld] = useState(false); // Track if Shift is held during drag
110115
const dragStartRef = useRef<{
111-
mouseX: number;
112-
mouseY: number;
116+
// Offset from mouse position to node center in canvas coordinates
117+
nodeOffsetX: number;
118+
nodeOffsetY: number;
119+
// Original node position for calculating deltas
113120
nodeX: number;
114121
nodeY: number;
115122
} | null>(null);
@@ -339,9 +346,14 @@ const Node = forwardRef<HTMLDivElement, NodeProps>(
339346
onDragStateChange(true, false, e.clientX, e.clientY);
340347
didDragRef.current = false;
341348
lastUpdateTimeRef.current = Date.now(); // Reset timer for throttling
349+
350+
// Convert mouse position to canvas coordinates
351+
const canvasPos = screenToCanvasCoords(e.clientX, e.clientY);
352+
353+
// Store offset from mouse to node center in canvas coordinates
342354
dragStartRef.current = {
343-
mouseX: e.clientX,
344-
mouseY: e.clientY,
355+
nodeOffsetX: x - canvasPos.x,
356+
nodeOffsetY: y - canvasPos.y,
345357
nodeX: x,
346358
nodeY: y,
347359
};
@@ -372,24 +384,22 @@ const Node = forwardRef<HTMLDivElement, NodeProps>(
372384
}
373385
onDragStateChange(true, e.shiftKey, e.clientX, e.clientY);
374386

375-
// Calculate raw delta in screen space
376-
const rawDeltaX = e.clientX - dragStartRef.current.mouseX;
377-
const rawDeltaY = e.clientY - dragStartRef.current.mouseY;
387+
// Convert current mouse position to canvas coordinates
388+
const canvasPos = screenToCanvasCoords(e.clientX, e.clientY);
389+
390+
// Calculate new node position by adding the stored offset
391+
let newX = canvasPos.x + dragStartRef.current.nodeOffsetX;
392+
let newY = canvasPos.y + dragStartRef.current.nodeOffsetY;
393+
394+
// Calculate raw delta to check if we've moved enough to count as dragging
395+
const rawDeltaX = newX - dragStartRef.current.nodeX;
396+
const rawDeltaY = newY - dragStartRef.current.nodeY;
378397

379398
// Mark that we've dragged (moved more than a few pixels)
380399
if (Math.abs(rawDeltaX) > 3 || Math.abs(rawDeltaY) > 3) {
381400
didDragRef.current = true;
382401
}
383402

384-
// Convert to canvas coordinates
385-
const zoomFactor = zoom / 100;
386-
const canvasDeltaX = rawDeltaX / zoomFactor;
387-
const canvasDeltaY = rawDeltaY / zoomFactor;
388-
389-
// Calculate new position in canvas coordinates
390-
let newX = dragStartRef.current.nodeX + canvasDeltaX;
391-
let newY = dragStartRef.current.nodeY + canvasDeltaY;
392-
393403
// Apply snap to grid unless Shift is held
394404
// Simple, predictable: what you see during drag = final position
395405
if (!e.shiftKey) {
@@ -412,9 +422,7 @@ const Node = forwardRef<HTMLDivElement, NodeProps>(
412422
lastUpdateTimeRef.current = now;
413423

414424
// Trigger bounds recalculation so arrows update (use snapped position)
415-
const snappedCanvasDeltaX = newX - dragStartRef.current.nodeX;
416-
const snappedCanvasDeltaY = newY - dragStartRef.current.nodeY;
417-
onDragMove(node.index, snappedCanvasDeltaX, snappedCanvasDeltaY);
425+
onDragMove(node.index, snappedDeltaX, snappedDeltaY);
418426
}
419427
};
420428

@@ -424,15 +432,11 @@ const Node = forwardRef<HTMLDivElement, NodeProps>(
424432
if (!dragStartRef.current) return;
425433

426434
// Calculate final position (same logic as mousemove for consistency)
427-
const rawDeltaX = e.clientX - dragStartRef.current.mouseX;
428-
const rawDeltaY = e.clientY - dragStartRef.current.mouseY;
435+
const canvasPos = screenToCanvasCoords(e.clientX, e.clientY);
429436

430-
const zoomFactor = zoom / 100;
431-
const canvasDeltaX = rawDeltaX / zoomFactor;
432-
const canvasDeltaY = rawDeltaY / zoomFactor;
433-
434-
let newX = dragStartRef.current.nodeX + canvasDeltaX;
435-
let newY = dragStartRef.current.nodeY + canvasDeltaY;
437+
// Calculate new node position by adding the stored offset
438+
let newX = canvasPos.x + dragStartRef.current.nodeOffsetX;
439+
let newY = canvasPos.y + dragStartRef.current.nodeOffsetY;
436440

437441
// Apply snap to grid unless Shift was held
438442
if (!e.shiftKey) {
@@ -463,7 +467,17 @@ const Node = forwardRef<HTMLDivElement, NodeProps>(
463467
window.removeEventListener("mousemove", handleGlobalMouseMove);
464468
window.removeEventListener("mouseup", handleGlobalMouseUp);
465469
};
466-
}, [isDragging, zoom, onDragMove, onDragEnd, node.id, node.index]);
470+
}, [
471+
isDragging,
472+
zoom,
473+
onDragMove,
474+
onDragEnd,
475+
onDragStateChange,
476+
node.id,
477+
node.index,
478+
screenToCanvasCoords,
479+
shiftHeld,
480+
]);
467481

468482
// Format text (replace | with line breaks)
469483
const formattedText = text.replace(/\|/g, "\n");

0 commit comments

Comments
 (0)