diff --git a/frontend/src/components/strategyEditor/FlowNodes.tsx b/frontend/src/components/strategyEditor/FlowNodes.tsx index cd3712f..dd731f0 100644 --- a/frontend/src/components/strategyEditor/FlowNodes.tsx +++ b/frontend/src/components/strategyEditor/FlowNodes.tsx @@ -129,6 +129,12 @@ function MultiSideHandles({ function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) { const { screenToFlowPosition } = useReactFlow(); + const onDragEnter = useCallback((e: React.DragEvent) => { + if (!isPaletteHtmlDrag(e)) return; + e.preventDefault(); + e.stopPropagation(); + }, []); + const onDragOver = useCallback((e: React.DragEvent) => { if (!isPaletteHtmlDrag(e)) return; e.preventDefault(); @@ -154,13 +160,13 @@ function usePaletteDropHandlers(id: string, d: StrategyFlowNodeData) { d.onDropTarget?.(id, payload, flowPos); }, [d, id, screenToFlowPosition]); - return { onDragOver, onDragLeave, onDrop }; + return { onDragEnter, onDragOver, onDragLeave, onDrop }; } export const StartNode = memo(function StartNode({ id, data }: NodeProps) { const d = data as StrategyFlowNodeData; const isSell = d.signalTab === 'sell'; - const { onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d); + const { onDragEnter, onDragOver, onDragLeave, onDrop } = usePaletteDropHandlers(id, d); const candleTypes = d.candleTypes?.length ? d.candleTypes : getStartCandleTypes({ candleType: d.candleType ?? '1m' }); @@ -171,6 +177,7 @@ export const StartNode = memo(function StartNode({ id, data }: NodeProps) { return (
{ + markPaletteHtmlDragEnd(); + }; + const onPointerDown = (e: React.PointerEvent) => { handlePalettePointerDown(e, buildPayload(), composite ? `${label} + ${label}` : label); }; @@ -92,6 +97,7 @@ export default function PaletteChip({ draggable={!needsPointerPaletteDrag()} className={`se-palette-card ${color ? `se-palette-card--${color}` : 'se-palette-card--ind'}${composite ? ' se-palette-card--composite' : ''}${selected ? ' se-palette-card--selected' : ''}`} onDragStart={onDragStart} + onDragEnd={onDragEnd} onPointerDown={onPointerDown} onMouseDown={onMouseDown} onTouchStart={onTouchStart} diff --git a/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx b/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx index 979cf9d..3a51b9f 100644 --- a/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx +++ b/frontend/src/components/strategyEditor/SidewaysFilterPaletteTab.tsx @@ -4,6 +4,7 @@ import type { DefType } from '../../utils/strategyEditorShared'; import { handlePalettePointerDown, handlePaletteTouchStart, + markPaletteHtmlDragEnd, needsPointerPaletteDrag, writePaletteHtmlDragData, type PaletteDragPayload, @@ -85,6 +86,11 @@ function SidewaysFilterChip({ writePaletteHtmlDragData(e, payload as PaletteDragPayload); }; + const onDragEnd = () => { + hideHint(); + markPaletteHtmlDragEnd(); + }; + const onPointerDown = (e: React.PointerEvent) => { hideHint(); const payload: PaletteDragPayload = { @@ -123,6 +129,7 @@ function SidewaysFilterChip({ draggable={!needsPointerPaletteDrag()} className={`se-palette-card se-palette-card--range se-palette-card--range-${item.mode}${selected ? ' se-palette-card--selected' : ''}`} onDragStart={onDragStart} + onDragEnd={onDragEnd} onPointerDown={onPointerDown} onTouchStart={onTouchStart} onClick={handleClick} diff --git a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx index d60721c..e6d156e 100644 --- a/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx +++ b/frontend/src/components/strategyEditor/StrategyEditorCanvas.tsx @@ -32,6 +32,8 @@ import { import { buildSidewaysFilterNode } from '../../utils/sidewaysFilterPaletteStorage'; import { isPaletteHtmlDrag, + isPaletteHtmlDragActive, + markPaletteHtmlDragEnd, needsPointerPaletteDrag, readPaletteHtmlDragData, setPaletteDragDropHandler, @@ -1282,6 +1284,40 @@ function StrategyEditorCanvasInner({ const lastPreviewAnchorRef = useRef(null); const lastPreviewKeyRef = useRef(null); + useEffect(() => { + if (needsPointerPaletteDrag()) return; + const onDocDragOver = (e: DragEvent) => { + if (!isPaletteHtmlDragActive()) return; + e.preventDefault(); + const { flowPos, anchorId } = resolveDropFromClientRef.current(e.clientX, e.clientY); + 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); + return; + } + if (lastPreviewAnchorRef.current != null) { + lastPreviewAnchorRef.current = null; + lastPreviewKeyRef.current = null; + clearPreviewRef.current(); + } + }; + const onDocDragEnd = () => { + lastPreviewAnchorRef.current = null; + lastPreviewKeyRef.current = null; + clearPreviewRef.current(); + markPaletteHtmlDragEnd(); + }; + document.addEventListener('dragover', onDocDragOver, { passive: false }); + document.addEventListener('dragend', onDocDragEnd); + return () => { + document.removeEventListener('dragover', onDocDragOver); + document.removeEventListener('dragend', onDocDragEnd); + }; + }, []); + useEffect(() => { if (!needsPointerPaletteDrag()) return; return subscribePaletteDrag((ev: PaletteDragEvent) => { @@ -1320,6 +1356,11 @@ function StrategyEditorCanvasInner({ }); }, []); + const onPaneDragEnter = useCallback((e: React.DragEvent) => { + if (!isPaletteDrag(e)) return; + e.preventDefault(); + }, [isPaletteDrag]); + const onPaneDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); if (!isPaletteDrag(e)) return; @@ -1378,6 +1419,7 @@ function StrategyEditorCanvasInner({ isValidConnection={isValidConnection} onReconnect={onReconnect} onDrop={onPaneDrop} + onDragEnter={onPaneDragEnter} onDragOver={onPaneDragOver} onDragLeave={onPaneDragLeave} connectionMode={ConnectionMode.Loose} diff --git a/frontend/src/utils/paletteDragSession.ts b/frontend/src/utils/paletteDragSession.ts index f18690e..7ffb51d 100644 --- a/frontend/src/utils/paletteDragSession.ts +++ b/frontend/src/utils/paletteDragSession.ts @@ -54,6 +54,26 @@ let moveRafId: number | null = null; let pendingMove: { x: number; y: number } | null = null; let unbindGesture: (() => void) | null = null; let lastDragClientXY: { x: number; y: number } | null = null; +let htmlPaletteDragActive = false; + +function installHtmlPaletteDragGlobals() { + if (typeof window === 'undefined') return; + const w = window as Window & { __gcPaletteHtmlDragGlobals?: boolean }; + if (w.__gcPaletteHtmlDragGlobals) return; + w.__gcPaletteHtmlDragGlobals = true; + + window.addEventListener('dragover', (e) => { + if (!htmlPaletteDragActive) return; + e.preventDefault(); + if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; + }, { passive: false }); + + window.addEventListener('dragend', () => { + htmlPaletteDragActive = false; + }); +} + +installHtmlPaletteDragGlobals(); function hasCoarsePointer(): boolean { if (typeof window === 'undefined') return false; @@ -306,7 +326,20 @@ export function handlePaletteTouchStart( startPaletteDragGesture(touch.clientX, touch.clientY, touch.identifier, payload); } +export function markPaletteHtmlDragStart(): void { + htmlPaletteDragActive = true; +} + +export function markPaletteHtmlDragEnd(): void { + htmlPaletteDragActive = false; +} + +export function isPaletteHtmlDragActive(): boolean { + return htmlPaletteDragActive; +} + export function writePaletteHtmlDragData(e: React.DragEvent, payload: PaletteDragPayload): void { + markPaletteHtmlDragStart(); const json = JSON.stringify(payload); e.dataTransfer.setData('application/json', json); e.dataTransfer.setData('text/plain', json); @@ -314,13 +347,16 @@ export function writePaletteHtmlDragData(e: React.DragEvent, payload: PaletteDra } export function isPaletteHtmlDrag(e: React.DragEvent): boolean { - return e.dataTransfer.types.includes('application/json') - || e.dataTransfer.types.includes('text/plain'); + if (htmlPaletteDragActive) return true; + const types = e.dataTransfer?.types; + if (!types) return false; + return Array.from(types).includes('application/json') + || Array.from(types).includes('text/plain'); } export function readPaletteHtmlDragData(e: React.DragEvent): PaletteDragPayload | null { - const raw = e.dataTransfer.getData('application/json') - || e.dataTransfer.getData('text/plain'); + const raw = e.dataTransfer.getData('text/plain') + || e.dataTransfer.getData('application/json'); if (!raw) return null; try { return JSON.parse(raw) as PaletteDragPayload;