diff --git a/frontend/src/components/strategyEditor/PaletteDragOverlay.tsx b/frontend/src/components/strategyEditor/PaletteDragOverlay.tsx index 7f99fa5..110a534 100644 --- a/frontend/src/components/strategyEditor/PaletteDragOverlay.tsx +++ b/frontend/src/components/strategyEditor/PaletteDragOverlay.tsx @@ -68,12 +68,6 @@ export default function PaletteDragOverlay() { const s = sessionRef.current; if (!s || e.pointerId !== s.pointerId) return; e.preventDefault(); - e.stopPropagation(); - const label = labelRef.current; - if (label) { - label.style.left = `${e.clientX}px`; - label.style.top = `${e.clientY}px`; - } notifyPaletteDragMove(e.clientX, e.clientY); }, []); @@ -81,7 +75,6 @@ export default function PaletteDragOverlay() { const s = sessionRef.current; if (!s || e.pointerId !== s.pointerId) return; e.preventDefault(); - e.stopPropagation(); notifyPaletteDragEnd(e.clientX, e.clientY); }, []); @@ -89,7 +82,6 @@ export default function PaletteDragOverlay() { const s = sessionRef.current; if (!s || e.pointerId !== s.pointerId) return; e.preventDefault(); - e.stopPropagation(); notifyPaletteDragCancel(e.clientX, e.clientY); }, []); diff --git a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx index 167c8d1..ab3da77 100644 --- a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx +++ b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx @@ -32,8 +32,6 @@ import { import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage'; import { isPaletteHtmlDrag, - completePaletteDragAt, - getActivePaletteDrag, needsPointerPaletteDrag, readPaletteHtmlDragData, setPaletteDragDropHandler, @@ -1282,6 +1280,7 @@ function StrategyEditorCanvasInner({ const resolveDropFromClientRef = useRef(resolveDropFromClient); resolveDropFromClientRef.current = resolveDropFromClient; const lastPreviewAnchorRef = useRef(null); + const lastPreviewKeyRef = useRef(null); useEffect(() => { if (!needsPointerPaletteDrag()) return; @@ -1290,13 +1289,18 @@ function StrategyEditorCanvasInner({ if (!isOverStrategyBuilder(ev.x, ev.y)) { if (lastPreviewAnchorRef.current != null) { lastPreviewAnchorRef.current = null; + lastPreviewKeyRef.current = null; clearPreviewRef.current(); } return; } const { flowPos, anchorId } = resolveDropFromClientRef.current(ev.x, ev.y); - if (anchorId === lastPreviewAnchorRef.current) return; + const previewKey = anchorId + ? `${anchorId}:${Math.round(flowPos.x)}:${Math.round(flowPos.y)}` + : ''; + if (previewKey === lastPreviewKeyRef.current) return; lastPreviewAnchorRef.current = anchorId; + lastPreviewKeyRef.current = previewKey; if (anchorId) { updatePreviewRef.current(anchorId, flowPos); } else { @@ -1306,6 +1310,7 @@ function StrategyEditorCanvasInner({ } if (ev.phase === 'end' || ev.phase === 'cancel') { lastPreviewAnchorRef.current = null; + lastPreviewKeyRef.current = null; } if (ev.phase === 'cancel') { clearPreviewRef.current(); @@ -1351,21 +1356,11 @@ function StrategyEditorCanvasInner({ } }, [selectedNodeId, handleDelete, deleteSelectedNodes, handleDisconnectEdge]); - const handleCanvasPointerUp = useCallback((e: React.PointerEvent) => { - if (!needsPointerPaletteDrag()) return; - if (e.button !== 0) return; - if (!getActivePaletteDrag()) return; - e.preventDefault(); - e.stopPropagation(); - completePaletteDragAt(e.clientX, e.clientY); - }, []); - return (
void) | null = null; let pendingPointerListeners: (() => void) | null = null; +let lastDragClientXY: { x: number; y: number } | null = null; function hasCoarsePointer(): boolean { if (typeof window === 'undefined') return false; @@ -136,6 +137,7 @@ function cleanupDrag() { activePointerId = null; activeTouchId = null; pendingMove = null; + lastDragClientXY = null; if (moveRafId != null) { cancelAnimationFrame(moveRafId); moveRafId = null; @@ -149,17 +151,20 @@ function cleanupDrag() { function finishDrag(phase: 'end' | 'cancel', xy: { x: number; y: number }) { if (!activePayload) return; const payload = activePayload; - cleanupDrag(); + const dropXY = lastDragClientXY ?? xy; + if (phase === 'end') { suppressClickUntil = Date.now() + 400; - dispatchEnd(xy, payload); + dispatchEnd(dropXY, payload); } else { - emit({ phase: 'cancel', x: xy.x, y: xy.y, payload }); + emit({ phase: 'cancel', x: dropXY.x, y: dropXY.y, payload }); } + cleanupDrag(); } export function notifyPaletteDragMove(clientX: number, clientY: number) { if (!activePayload) return; + lastDragClientXY = { x: clientX, y: clientY }; pendingMove = { x: clientX, y: clientY }; if (moveRafId != null) return; moveRafId = requestAnimationFrame(() => { @@ -212,7 +217,7 @@ function bindActiveDragWindowListeners(pointerId: number) { if (!eventMatchesActiveInput(ev)) return; finishDrag('end', xy); }, - { preventTouchScroll: true }, + { preventTouchScroll: true, useCapture: true }, ); } @@ -244,6 +249,7 @@ function beginDrag( activePayload = payload; activePointerId = pointerId; activeTouchId = touchId; + lastDragClientXY = { x: xy.x, y: xy.y }; document.body.classList.add('se-palette-drag-active'); bindActiveDragWindowListeners(pointerId); diff --git a/frontend/src/utils/pointerDrag.ts b/frontend/src/utils/pointerDrag.ts index 020aba6..566e4e6 100644 --- a/frontend/src/utils/pointerDrag.ts +++ b/frontend/src/utils/pointerDrag.ts @@ -40,6 +40,8 @@ export function clientXYFromNative(e: Event): ClientXY | null { export interface BindWindowDragOptions { /** 터치 드래그 중 페이지 스크롤 방지 (기본 true) */ preventTouchScroll?: boolean; + /** capture 단계에서 수신 — overlay stopPropagation 우회 */ + useCapture?: boolean; } /** @@ -51,9 +53,16 @@ export function bindWindowDrag( onEnd: (xy: ClientXY, ev: Event) => void, options: BindWindowDragOptions = {}, ): () => void { - const { preventTouchScroll = true } = options; + const { preventTouchScroll = true, useCapture = false } = options; let lastXY: ClientXY = { x: 0, y: 0 }; + const moveOpts: AddEventListenerOptions | boolean = useCapture + ? { capture: true, passive: !preventTouchScroll } + : { passive: !preventTouchScroll }; + const endOpts: AddEventListenerOptions | boolean = useCapture + ? { capture: true } + : false; + const move = (ev: Event) => { const xy = clientXYFromNative(ev); if (!xy) return; @@ -69,24 +78,24 @@ export function bindWindowDrag( onEnd(xy, ev); }; - window.addEventListener('pointermove', move); - window.addEventListener('pointerup', end); - window.addEventListener('pointercancel', end); - window.addEventListener('mousemove', move); - window.addEventListener('mouseup', end); - window.addEventListener('touchmove', move, { passive: !preventTouchScroll }); - window.addEventListener('touchend', end); - window.addEventListener('touchcancel', end); + window.addEventListener('pointermove', move, moveOpts); + window.addEventListener('pointerup', end, endOpts); + window.addEventListener('pointercancel', end, endOpts); + window.addEventListener('mousemove', move, moveOpts); + window.addEventListener('mouseup', end, endOpts); + window.addEventListener('touchmove', move, moveOpts); + window.addEventListener('touchend', end, endOpts); + window.addEventListener('touchcancel', end, endOpts); return () => { - window.removeEventListener('pointermove', move); - window.removeEventListener('pointerup', end); - window.removeEventListener('pointercancel', end); - window.removeEventListener('mousemove', move); - window.removeEventListener('mouseup', end); - window.removeEventListener('touchmove', move); - window.removeEventListener('touchend', end); - window.removeEventListener('touchcancel', end); + window.removeEventListener('pointermove', move, moveOpts); + window.removeEventListener('pointerup', end, endOpts); + window.removeEventListener('pointercancel', end, endOpts); + window.removeEventListener('mousemove', move, moveOpts); + window.removeEventListener('mouseup', end, endOpts); + window.removeEventListener('touchmove', move, moveOpts); + window.removeEventListener('touchend', end, endOpts); + window.removeEventListener('touchcancel', end, endOpts); }; }