앱 수정
This commit is contained in:
@@ -37,9 +37,14 @@ import StochPairNodeSettings from './StochPairNodeSettings';
|
|||||||
import PriceExtremePairNodeSettings from './PriceExtremePairNodeSettings';
|
import PriceExtremePairNodeSettings from './PriceExtremePairNodeSettings';
|
||||||
import {
|
import {
|
||||||
isPaletteHtmlDrag,
|
isPaletteHtmlDrag,
|
||||||
|
isPaletteHtmlDragActive,
|
||||||
readPaletteHtmlDragData,
|
readPaletteHtmlDragData,
|
||||||
} from '../../utils/paletteDragSession';
|
} from '../../utils/paletteDragSession';
|
||||||
|
|
||||||
|
function isPaletteDropDrag(e: React.DragEvent): boolean {
|
||||||
|
return isPaletteHtmlDragActive() || isPaletteHtmlDrag(e);
|
||||||
|
}
|
||||||
|
|
||||||
const LOGIC_COLORS: Record<string, string> = {
|
const LOGIC_COLORS: Record<string, string> = {
|
||||||
AND: '#00aaff',
|
AND: '#00aaff',
|
||||||
OR: '#00d4ff',
|
OR: '#00d4ff',
|
||||||
@@ -130,13 +135,13 @@ function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) {
|
|||||||
const { screenToFlowPosition } = useReactFlow();
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
|
|
||||||
const onDragEnter = useCallback((e: React.DragEvent) => {
|
const onDragEnter = useCallback((e: React.DragEvent) => {
|
||||||
if (!isPaletteHtmlDrag(e)) return;
|
if (!isPaletteDropDrag(e)) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onDragOver = useCallback((e: React.DragEvent) => {
|
const onDragOver = useCallback((e: React.DragEvent) => {
|
||||||
if (!isPaletteHtmlDrag(e)) return;
|
if (!isPaletteDropDrag(e)) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.dataTransfer.dropEffect = 'copy';
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
@@ -146,8 +151,10 @@ function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) {
|
|||||||
|
|
||||||
const onDragLeave = useCallback((e: React.DragEvent) => {
|
const onDragLeave = useCallback((e: React.DragEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
// WebKit HTML5 drag — relatedTarget 이 null 인 spurious dragleave 무시
|
||||||
const rel = e.relatedTarget as Node | null;
|
const rel = e.relatedTarget as Node | null;
|
||||||
if (rel && e.currentTarget.contains(rel)) return;
|
if (!rel) return;
|
||||||
|
if (e.currentTarget.contains(rel)) return;
|
||||||
d.onDragLeaveTarget?.(id);
|
d.onDragLeaveTarget?.(id);
|
||||||
}, [d, id]);
|
}, [d, id]);
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ import {
|
|||||||
spawnPositionNearAnchor,
|
spawnPositionNearAnchor,
|
||||||
subtreeToFlatOrphans,
|
subtreeToFlatOrphans,
|
||||||
type EdgeHandleBinding,
|
type EdgeHandleBinding,
|
||||||
|
type StrategyFlowNodeData,
|
||||||
} from '../../utils/strategyFlowLayout';
|
} from '../../utils/strategyFlowLayout';
|
||||||
import {
|
import {
|
||||||
createStartNodeId,
|
createStartNodeId,
|
||||||
@@ -906,10 +907,16 @@ function StrategyEditorCanvasInner({
|
|||||||
const syncNodesFromLayout = () => setNodes(layoutNodes.map(n => {
|
const syncNodesFromLayout = () => setNodes(layoutNodes.map(n => {
|
||||||
const prev = nodesRef.current.find(p => p.id === n.id);
|
const prev = nodesRef.current.find(p => p.id === n.id);
|
||||||
const savedPos = layoutSeed.positions[n.id];
|
const savedPos = layoutSeed.positions[n.id];
|
||||||
|
const dragOver = (prev?.data as StrategyFlowNodeData | undefined)?.dragOver;
|
||||||
|
const activeSourceSide = (prev?.data as StrategyFlowNodeData | undefined)?.activeSourceSide;
|
||||||
return {
|
return {
|
||||||
...n,
|
...n,
|
||||||
position: savedPos ? { x: savedPos.x, y: savedPos.y } : n.position,
|
position: savedPos ? { x: savedPos.x, y: savedPos.y } : n.position,
|
||||||
selected: prev?.selected ?? false,
|
selected: prev?.selected ?? false,
|
||||||
|
data: {
|
||||||
|
...n.data,
|
||||||
|
...(dragOver ? { dragOver, activeSourceSide } : {}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -933,10 +940,15 @@ function StrategyEditorCanvasInner({
|
|||||||
const fresh = layoutNodes.find(ln => ln.id === n.id);
|
const fresh = layoutNodes.find(ln => ln.id === n.id);
|
||||||
if (!fresh) return n;
|
if (!fresh) return n;
|
||||||
const savedPos = layoutSeed.positions[n.id];
|
const savedPos = layoutSeed.positions[n.id];
|
||||||
|
const dragOver = (n.data as StrategyFlowNodeData).dragOver;
|
||||||
|
const activeSourceSide = (n.data as StrategyFlowNodeData).activeSourceSide;
|
||||||
return {
|
return {
|
||||||
...n,
|
...n,
|
||||||
position: savedPos ? { x: savedPos.x, y: savedPos.y } : fresh.position,
|
position: savedPos ? { x: savedPos.x, y: savedPos.y } : fresh.position,
|
||||||
data: fresh.data,
|
data: {
|
||||||
|
...fresh.data,
|
||||||
|
...(dragOver ? { dragOver, activeSourceSide } : {}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
setEdges(mergedEdges);
|
setEdges(mergedEdges);
|
||||||
@@ -1226,7 +1238,10 @@ function StrategyEditorCanvasInner({
|
|||||||
)
|
)
|
||||||
), [root, orphans]);
|
), [root, orphans]);
|
||||||
|
|
||||||
const isPaletteDrag = (e: React.DragEvent) => isPaletteHtmlDrag(e);
|
const isPaletteDrag = useCallback(
|
||||||
|
(e: React.DragEvent) => isPaletteHtmlDragActive() || isPaletteHtmlDrag(e),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const applyPaletteDropAt = useCallback((
|
const applyPaletteDropAt = useCallback((
|
||||||
data: { type: string; value: string; label: string; composite?: boolean },
|
data: { type: string; value: string; label: string; composite?: boolean },
|
||||||
@@ -1291,11 +1306,8 @@ function StrategyEditorCanvasInner({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { flowPos, anchorId } = resolveDropFromClientRef.current(e.clientX, e.clientY);
|
const { flowPos, anchorId } = resolveDropFromClientRef.current(e.clientX, e.clientY);
|
||||||
if (anchorId) {
|
if (anchorId) {
|
||||||
const sideKey = `${anchorId}:${Math.round(flowPos.x / 8)}:${Math.round(flowPos.y / 8)}`;
|
|
||||||
if (sideKey === lastPreviewKeyRef.current) return;
|
|
||||||
lastPreviewKeyRef.current = sideKey;
|
|
||||||
lastPreviewAnchorRef.current = anchorId;
|
|
||||||
updatePreviewRef.current(anchorId, flowPos);
|
updatePreviewRef.current(anchorId, flowPos);
|
||||||
|
lastPreviewAnchorRef.current = anchorId;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (lastPreviewAnchorRef.current != null) {
|
if (lastPreviewAnchorRef.current != null) {
|
||||||
@@ -1311,9 +1323,11 @@ function StrategyEditorCanvasInner({
|
|||||||
markPaletteHtmlDragEnd();
|
markPaletteHtmlDragEnd();
|
||||||
};
|
};
|
||||||
document.addEventListener('dragover', onDocDragOver, { passive: false });
|
document.addEventListener('dragover', onDocDragOver, { passive: false });
|
||||||
|
document.addEventListener('drag', onDocDragOver);
|
||||||
document.addEventListener('dragend', onDocDragEnd);
|
document.addEventListener('dragend', onDocDragEnd);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('dragover', onDocDragOver);
|
document.removeEventListener('dragover', onDocDragOver);
|
||||||
|
document.removeEventListener('drag', onDocDragOver);
|
||||||
document.removeEventListener('dragend', onDocDragEnd);
|
document.removeEventListener('dragend', onDocDragEnd);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -1368,14 +1382,13 @@ function StrategyEditorCanvasInner({
|
|||||||
const { flowPos, anchorId } = resolveDropFromClient(e.clientX, e.clientY);
|
const { flowPos, anchorId } = resolveDropFromClient(e.clientX, e.clientY);
|
||||||
if (anchorId) {
|
if (anchorId) {
|
||||||
updateDropPreview(anchorId, flowPos);
|
updateDropPreview(anchorId, flowPos);
|
||||||
} else {
|
|
||||||
clearDropPreview();
|
|
||||||
}
|
}
|
||||||
}, [resolveDropFromClient, updateDropPreview, clearDropPreview]);
|
}, [resolveDropFromClient, updateDropPreview, isPaletteDrag]);
|
||||||
|
|
||||||
const onPaneDragLeave = useCallback((e: React.DragEvent) => {
|
const onPaneDragLeave = useCallback((e: React.DragEvent) => {
|
||||||
const rel = e.relatedTarget as Element | null;
|
const rel = e.relatedTarget as Element | null;
|
||||||
if (rel && e.currentTarget.contains(rel)) return;
|
if (!rel) return;
|
||||||
|
if (e.currentTarget.contains(rel)) return;
|
||||||
clearDropPreview();
|
clearDropPreview();
|
||||||
}, [clearDropPreview]);
|
}, [clearDropPreview]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ReactFlowInstance } from '@xyflow/react';
|
import type { ReactFlowInstance } from '@xyflow/react';
|
||||||
|
import { getLastHtmlPaletteDragClientXY, isPaletteHtmlDragActive } from './paletteDragSession';
|
||||||
|
|
||||||
type FlowProjector = Pick<ReactFlowInstance, 'screenToFlowPosition' | 'getViewport'>;
|
type FlowProjector = Pick<ReactFlowInstance, 'screenToFlowPosition' | 'getViewport'>;
|
||||||
|
|
||||||
@@ -89,8 +90,27 @@ export function isOverStrategyBuilder(clientX: number, clientY: number): boolean
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** DOM hit-test — overlay 아래 react-flow 노드 탐색 */
|
/** DOM hit-test — react-flow 노드 id (Safari HTML5 drag: elementsFromPoint 불가 → rect 우선) */
|
||||||
export function findFlowNodeIdAtClientPoint(clientX: number, clientY: number): string | null {
|
export function findFlowNodeIdAtClientPoint(clientX: number, clientY: number): string | null {
|
||||||
|
const pad = 10;
|
||||||
|
const nodeEls = document.querySelectorAll<HTMLElement>('.react-flow__node');
|
||||||
|
for (const nodeEl of nodeEls) {
|
||||||
|
const rect = nodeEl.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
clientX >= rect.left - pad && clientX <= rect.right + pad
|
||||||
|
&& clientY >= rect.top - pad && clientY <= rect.bottom + pad
|
||||||
|
) {
|
||||||
|
const id = nodeEl.getAttribute('data-id')
|
||||||
|
?? nodeEl.dataset.id
|
||||||
|
?? (nodeEl.id.startsWith('react-flow__node-')
|
||||||
|
? nodeEl.id.slice('react-flow__node-'.length)
|
||||||
|
: null)
|
||||||
|
?? nodeEl.querySelector<HTMLElement>('[data-nodeid]')?.getAttribute('data-nodeid')
|
||||||
|
?? null;
|
||||||
|
if (id) return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -98,7 +118,9 @@ export function findFlowNodeIdAtClientPoint(clientX: number, clientY: number): s
|
|||||||
?? nodeEl.dataset.id
|
?? nodeEl.dataset.id
|
||||||
?? (nodeEl.id.startsWith('react-flow__node-')
|
?? (nodeEl.id.startsWith('react-flow__node-')
|
||||||
? nodeEl.id.slice('react-flow__node-'.length)
|
? nodeEl.id.slice('react-flow__node-'.length)
|
||||||
: null);
|
: null)
|
||||||
|
?? nodeEl.querySelector<HTMLElement>('[data-nodeid]')?.getAttribute('data-nodeid')
|
||||||
|
?? 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;
|
||||||
@@ -115,6 +137,14 @@ export interface PaletteDropResolution {
|
|||||||
anchorId: string | null;
|
anchorId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** WKWebView HTML5 drag — dragover client 좌표가 0,0 으로 오는 경우 보정 */
|
||||||
|
export function resolvePaletteDragClientPoint(clientX: number, clientY: number): { x: number; y: number } {
|
||||||
|
if (clientX !== 0 || clientY !== 0) return { x: clientX, y: clientY };
|
||||||
|
if (!isPaletteHtmlDragActive()) return { x: clientX, y: clientY };
|
||||||
|
const last = getLastHtmlPaletteDragClientXY();
|
||||||
|
return last ?? { x: clientX, y: clientY };
|
||||||
|
}
|
||||||
|
|
||||||
/** client 좌표 → flow 위치 + 연결 대상 노드 (DOM 우선, flow hit 보조) */
|
/** client 좌표 → flow 위치 + 연결 대상 노드 (DOM 우선, flow hit 보조) */
|
||||||
export function resolvePaletteDropClientPoint(
|
export function resolvePaletteDropClientPoint(
|
||||||
clientX: number,
|
clientX: number,
|
||||||
@@ -128,9 +158,10 @@ export function resolvePaletteDropClientPoint(
|
|||||||
) => { id: string } | null,
|
) => { id: string } | null,
|
||||||
isConnectTarget: (nodeId: string) => boolean,
|
isConnectTarget: (nodeId: string) => boolean,
|
||||||
): PaletteDropResolution {
|
): PaletteDropResolution {
|
||||||
const flowPos = projectScreenToFlowPosition(clientX, clientY, rf, containerEl);
|
const { x: px, y: py } = resolvePaletteDragClientPoint(clientX, clientY);
|
||||||
|
const flowPos = projectScreenToFlowPosition(px, py, rf, containerEl);
|
||||||
|
|
||||||
const domId = findFlowNodeIdAtClientPoint(clientX, clientY);
|
const domId = findFlowNodeIdAtClientPoint(px, py);
|
||||||
if (domId && nodes.some(n => n.id === domId) && isConnectTarget(domId)) {
|
if (domId && nodes.some(n => n.id === domId) && isConnectTarget(domId)) {
|
||||||
return { flowPos, anchorId: domId };
|
return { flowPos, anchorId: domId };
|
||||||
}
|
}
|
||||||
@@ -140,12 +171,13 @@ export function resolvePaletteDropClientPoint(
|
|||||||
const escaped = typeof CSS !== 'undefined' && CSS.escape
|
const escaped = typeof CSS !== 'undefined' && CSS.escape
|
||||||
? CSS.escape(node.id)
|
? CSS.escape(node.id)
|
||||||
: node.id.replace(/"/g, '\\"');
|
: node.id.replace(/"/g, '\\"');
|
||||||
const el = document.querySelector(`.react-flow__node[data-id="${escaped}"]`);
|
const el = document.querySelector(`.react-flow__node[data-id="${escaped}"]`)
|
||||||
|
?? document.getElementById(`react-flow__node-${node.id}`);
|
||||||
if (!el) continue;
|
if (!el) continue;
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
if (
|
if (
|
||||||
clientX >= rect.left && clientX <= rect.right
|
px >= rect.left && px <= rect.right
|
||||||
&& clientY >= rect.top && clientY <= rect.bottom
|
&& py >= rect.top && py <= rect.bottom
|
||||||
) {
|
) {
|
||||||
return { flowPos, anchorId: node.id };
|
return { flowPos, anchorId: node.id };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ let pendingMove: { x: number; y: number } | null = null;
|
|||||||
let unbindGesture: (() => void) | null = null;
|
let unbindGesture: (() => void) | null = null;
|
||||||
let lastDragClientXY: { x: number; y: number } | null = null;
|
let lastDragClientXY: { x: number; y: number } | null = null;
|
||||||
let htmlPaletteDragActive = false;
|
let htmlPaletteDragActive = false;
|
||||||
|
let lastHtmlDragClientXY: { x: number; y: number } | null = null;
|
||||||
|
|
||||||
function installHtmlPaletteDragGlobals() {
|
function installHtmlPaletteDragGlobals() {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
@@ -64,12 +65,23 @@ function installHtmlPaletteDragGlobals() {
|
|||||||
|
|
||||||
window.addEventListener('dragover', (e) => {
|
window.addEventListener('dragover', (e) => {
|
||||||
if (!htmlPaletteDragActive) return;
|
if (!htmlPaletteDragActive) return;
|
||||||
|
if (e.clientX !== 0 || e.clientY !== 0) {
|
||||||
|
lastHtmlDragClientXY = { x: e.clientX, y: e.clientY };
|
||||||
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
|
window.addEventListener('drag', (e) => {
|
||||||
|
if (!htmlPaletteDragActive) return;
|
||||||
|
if (e.clientX !== 0 || e.clientY !== 0) {
|
||||||
|
lastHtmlDragClientXY = { x: e.clientX, y: e.clientY };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('dragend', () => {
|
window.addEventListener('dragend', () => {
|
||||||
htmlPaletteDragActive = false;
|
htmlPaletteDragActive = false;
|
||||||
|
lastHtmlDragClientXY = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,10 +340,16 @@ export function handlePaletteTouchStart(
|
|||||||
|
|
||||||
export function markPaletteHtmlDragStart(): void {
|
export function markPaletteHtmlDragStart(): void {
|
||||||
htmlPaletteDragActive = true;
|
htmlPaletteDragActive = true;
|
||||||
|
lastHtmlDragClientXY = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markPaletteHtmlDragEnd(): void {
|
export function markPaletteHtmlDragEnd(): void {
|
||||||
htmlPaletteDragActive = false;
|
htmlPaletteDragActive = false;
|
||||||
|
lastHtmlDragClientXY = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLastHtmlPaletteDragClientXY(): { x: number; y: number } | null {
|
||||||
|
return lastHtmlDragClientXY;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPaletteHtmlDragActive(): boolean {
|
export function isPaletteHtmlDragActive(): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user