전략편집기 수정
This commit is contained in:
@@ -68,12 +68,6 @@ export default function PaletteDragOverlay() {
|
|||||||
const s = sessionRef.current;
|
const s = sessionRef.current;
|
||||||
if (!s || e.pointerId !== s.pointerId) return;
|
if (!s || e.pointerId !== s.pointerId) return;
|
||||||
e.preventDefault();
|
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);
|
notifyPaletteDragMove(e.clientX, e.clientY);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -81,7 +75,6 @@ export default function PaletteDragOverlay() {
|
|||||||
const s = sessionRef.current;
|
const s = sessionRef.current;
|
||||||
if (!s || e.pointerId !== s.pointerId) return;
|
if (!s || e.pointerId !== s.pointerId) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
|
||||||
notifyPaletteDragEnd(e.clientX, e.clientY);
|
notifyPaletteDragEnd(e.clientX, e.clientY);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -89,7 +82,6 @@ export default function PaletteDragOverlay() {
|
|||||||
const s = sessionRef.current;
|
const s = sessionRef.current;
|
||||||
if (!s || e.pointerId !== s.pointerId) return;
|
if (!s || e.pointerId !== s.pointerId) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
|
||||||
notifyPaletteDragCancel(e.clientX, e.clientY);
|
notifyPaletteDragCancel(e.clientX, e.clientY);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ import {
|
|||||||
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
|
import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage';
|
||||||
import {
|
import {
|
||||||
isPaletteHtmlDrag,
|
isPaletteHtmlDrag,
|
||||||
completePaletteDragAt,
|
|
||||||
getActivePaletteDrag,
|
|
||||||
needsPointerPaletteDrag,
|
needsPointerPaletteDrag,
|
||||||
readPaletteHtmlDragData,
|
readPaletteHtmlDragData,
|
||||||
setPaletteDragDropHandler,
|
setPaletteDragDropHandler,
|
||||||
@@ -1282,6 +1280,7 @@ function StrategyEditorCanvasInner({
|
|||||||
const resolveDropFromClientRef = useRef(resolveDropFromClient);
|
const resolveDropFromClientRef = useRef(resolveDropFromClient);
|
||||||
resolveDropFromClientRef.current = resolveDropFromClient;
|
resolveDropFromClientRef.current = resolveDropFromClient;
|
||||||
const lastPreviewAnchorRef = useRef<string | null>(null);
|
const lastPreviewAnchorRef = useRef<string | null>(null);
|
||||||
|
const lastPreviewKeyRef = useRef<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!needsPointerPaletteDrag()) return;
|
if (!needsPointerPaletteDrag()) return;
|
||||||
@@ -1290,13 +1289,18 @@ function StrategyEditorCanvasInner({
|
|||||||
if (!isOverStrategyBuilder(ev.x, ev.y)) {
|
if (!isOverStrategyBuilder(ev.x, ev.y)) {
|
||||||
if (lastPreviewAnchorRef.current != null) {
|
if (lastPreviewAnchorRef.current != null) {
|
||||||
lastPreviewAnchorRef.current = null;
|
lastPreviewAnchorRef.current = null;
|
||||||
|
lastPreviewKeyRef.current = null;
|
||||||
clearPreviewRef.current();
|
clearPreviewRef.current();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { flowPos, anchorId } = resolveDropFromClientRef.current(ev.x, ev.y);
|
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;
|
lastPreviewAnchorRef.current = anchorId;
|
||||||
|
lastPreviewKeyRef.current = previewKey;
|
||||||
if (anchorId) {
|
if (anchorId) {
|
||||||
updatePreviewRef.current(anchorId, flowPos);
|
updatePreviewRef.current(anchorId, flowPos);
|
||||||
} else {
|
} else {
|
||||||
@@ -1306,6 +1310,7 @@ function StrategyEditorCanvasInner({
|
|||||||
}
|
}
|
||||||
if (ev.phase === 'end' || ev.phase === 'cancel') {
|
if (ev.phase === 'end' || ev.phase === 'cancel') {
|
||||||
lastPreviewAnchorRef.current = null;
|
lastPreviewAnchorRef.current = null;
|
||||||
|
lastPreviewKeyRef.current = null;
|
||||||
}
|
}
|
||||||
if (ev.phase === 'cancel') {
|
if (ev.phase === 'cancel') {
|
||||||
clearPreviewRef.current();
|
clearPreviewRef.current();
|
||||||
@@ -1351,21 +1356,11 @@ function StrategyEditorCanvasInner({
|
|||||||
}
|
}
|
||||||
}, [selectedNodeId, handleDelete, deleteSelectedNodes, handleDisconnectEdge]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={canvasWrapRef}
|
ref={canvasWrapRef}
|
||||||
className={`se-canvas-wrap se-canvas-wrap--${signalTab} se-canvas-wrap--${interactionMode}`}
|
className={`se-canvas-wrap se-canvas-wrap--${signalTab} se-canvas-wrap--${interactionMode}`}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onPointerUpCapture={handleCanvasPointerUp}
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
|
|||||||
@@ -94,7 +94,11 @@ export function findFlowNodeIdAtClientPoint(clientX: number, clientY: number): s
|
|||||||
for (const el of elementsUnderDragPoint(clientX, clientY)) {
|
for (const el of elementsUnderDragPoint(clientX, clientY)) {
|
||||||
const nodeEl = el.closest('.react-flow__node') as HTMLElement | null;
|
const nodeEl = el.closest('.react-flow__node') as HTMLElement | null;
|
||||||
if (nodeEl) {
|
if (nodeEl) {
|
||||||
const id = nodeEl.getAttribute('data-id');
|
const id = nodeEl.getAttribute('data-id')
|
||||||
|
?? nodeEl.dataset.id
|
||||||
|
?? (nodeEl.id.startsWith('react-flow__node-')
|
||||||
|
? nodeEl.id.slice('react-flow__node-'.length)
|
||||||
|
: null);
|
||||||
if (id) return id;
|
if (id) return id;
|
||||||
}
|
}
|
||||||
const handleEl = el.closest('[data-nodeid]') as HTMLElement | null;
|
const handleEl = el.closest('[data-nodeid]') as HTMLElement | null;
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ let moveRafId: number | null = null;
|
|||||||
let pendingMove: { x: number; y: number } | null = null;
|
let pendingMove: { x: number; y: number } | null = null;
|
||||||
let unbindActiveDrag: (() => void) | null = null;
|
let unbindActiveDrag: (() => void) | null = null;
|
||||||
let pendingPointerListeners: (() => void) | null = null;
|
let pendingPointerListeners: (() => void) | null = null;
|
||||||
|
let lastDragClientXY: { x: number; y: number } | null = null;
|
||||||
|
|
||||||
function hasCoarsePointer(): boolean {
|
function hasCoarsePointer(): boolean {
|
||||||
if (typeof window === 'undefined') return false;
|
if (typeof window === 'undefined') return false;
|
||||||
@@ -136,6 +137,7 @@ function cleanupDrag() {
|
|||||||
activePointerId = null;
|
activePointerId = null;
|
||||||
activeTouchId = null;
|
activeTouchId = null;
|
||||||
pendingMove = null;
|
pendingMove = null;
|
||||||
|
lastDragClientXY = null;
|
||||||
if (moveRafId != null) {
|
if (moveRafId != null) {
|
||||||
cancelAnimationFrame(moveRafId);
|
cancelAnimationFrame(moveRafId);
|
||||||
moveRafId = null;
|
moveRafId = null;
|
||||||
@@ -149,17 +151,20 @@ function cleanupDrag() {
|
|||||||
function finishDrag(phase: 'end' | 'cancel', xy: { x: number; y: number }) {
|
function finishDrag(phase: 'end' | 'cancel', xy: { x: number; y: number }) {
|
||||||
if (!activePayload) return;
|
if (!activePayload) return;
|
||||||
const payload = activePayload;
|
const payload = activePayload;
|
||||||
cleanupDrag();
|
const dropXY = lastDragClientXY ?? xy;
|
||||||
|
|
||||||
if (phase === 'end') {
|
if (phase === 'end') {
|
||||||
suppressClickUntil = Date.now() + 400;
|
suppressClickUntil = Date.now() + 400;
|
||||||
dispatchEnd(xy, payload);
|
dispatchEnd(dropXY, payload);
|
||||||
} else {
|
} 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) {
|
export function notifyPaletteDragMove(clientX: number, clientY: number) {
|
||||||
if (!activePayload) return;
|
if (!activePayload) return;
|
||||||
|
lastDragClientXY = { x: clientX, y: clientY };
|
||||||
pendingMove = { x: clientX, y: clientY };
|
pendingMove = { x: clientX, y: clientY };
|
||||||
if (moveRafId != null) return;
|
if (moveRafId != null) return;
|
||||||
moveRafId = requestAnimationFrame(() => {
|
moveRafId = requestAnimationFrame(() => {
|
||||||
@@ -212,7 +217,7 @@ function bindActiveDragWindowListeners(pointerId: number) {
|
|||||||
if (!eventMatchesActiveInput(ev)) return;
|
if (!eventMatchesActiveInput(ev)) return;
|
||||||
finishDrag('end', xy);
|
finishDrag('end', xy);
|
||||||
},
|
},
|
||||||
{ preventTouchScroll: true },
|
{ preventTouchScroll: true, useCapture: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +249,7 @@ function beginDrag(
|
|||||||
activePayload = payload;
|
activePayload = payload;
|
||||||
activePointerId = pointerId;
|
activePointerId = pointerId;
|
||||||
activeTouchId = touchId;
|
activeTouchId = touchId;
|
||||||
|
lastDragClientXY = { x: xy.x, y: xy.y };
|
||||||
document.body.classList.add('se-palette-drag-active');
|
document.body.classList.add('se-palette-drag-active');
|
||||||
|
|
||||||
bindActiveDragWindowListeners(pointerId);
|
bindActiveDragWindowListeners(pointerId);
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export function clientXYFromNative(e: Event): ClientXY | null {
|
|||||||
export interface BindWindowDragOptions {
|
export interface BindWindowDragOptions {
|
||||||
/** 터치 드래그 중 페이지 스크롤 방지 (기본 true) */
|
/** 터치 드래그 중 페이지 스크롤 방지 (기본 true) */
|
||||||
preventTouchScroll?: boolean;
|
preventTouchScroll?: boolean;
|
||||||
|
/** capture 단계에서 수신 — overlay stopPropagation 우회 */
|
||||||
|
useCapture?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,9 +53,16 @@ export function bindWindowDrag(
|
|||||||
onEnd: (xy: ClientXY, ev: Event) => void,
|
onEnd: (xy: ClientXY, ev: Event) => void,
|
||||||
options: BindWindowDragOptions = {},
|
options: BindWindowDragOptions = {},
|
||||||
): () => void {
|
): () => void {
|
||||||
const { preventTouchScroll = true } = options;
|
const { preventTouchScroll = true, useCapture = false } = options;
|
||||||
let lastXY: ClientXY = { x: 0, y: 0 };
|
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 move = (ev: Event) => {
|
||||||
const xy = clientXYFromNative(ev);
|
const xy = clientXYFromNative(ev);
|
||||||
if (!xy) return;
|
if (!xy) return;
|
||||||
@@ -69,24 +78,24 @@ export function bindWindowDrag(
|
|||||||
onEnd(xy, ev);
|
onEnd(xy, ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('pointermove', move);
|
window.addEventListener('pointermove', move, moveOpts);
|
||||||
window.addEventListener('pointerup', end);
|
window.addEventListener('pointerup', end, endOpts);
|
||||||
window.addEventListener('pointercancel', end);
|
window.addEventListener('pointercancel', end, endOpts);
|
||||||
window.addEventListener('mousemove', move);
|
window.addEventListener('mousemove', move, moveOpts);
|
||||||
window.addEventListener('mouseup', end);
|
window.addEventListener('mouseup', end, endOpts);
|
||||||
window.addEventListener('touchmove', move, { passive: !preventTouchScroll });
|
window.addEventListener('touchmove', move, moveOpts);
|
||||||
window.addEventListener('touchend', end);
|
window.addEventListener('touchend', end, endOpts);
|
||||||
window.addEventListener('touchcancel', end);
|
window.addEventListener('touchcancel', end, endOpts);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('pointermove', move);
|
window.removeEventListener('pointermove', move, moveOpts);
|
||||||
window.removeEventListener('pointerup', end);
|
window.removeEventListener('pointerup', end, endOpts);
|
||||||
window.removeEventListener('pointercancel', end);
|
window.removeEventListener('pointercancel', end, endOpts);
|
||||||
window.removeEventListener('mousemove', move);
|
window.removeEventListener('mousemove', move, moveOpts);
|
||||||
window.removeEventListener('mouseup', end);
|
window.removeEventListener('mouseup', end, endOpts);
|
||||||
window.removeEventListener('touchmove', move);
|
window.removeEventListener('touchmove', move, moveOpts);
|
||||||
window.removeEventListener('touchend', end);
|
window.removeEventListener('touchend', end, endOpts);
|
||||||
window.removeEventListener('touchcancel', end);
|
window.removeEventListener('touchcancel', end, endOpts);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user