Files
goldenChart/frontend/src/components/strategyEditor/PaletteDragOverlay.tsx
T

131 lines
3.7 KiB
TypeScript

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import {
bindPaletteDragOverlayElement,
notifyPaletteDragCancel,
notifyPaletteDragEnd,
notifyPaletteDragMove,
registerPaletteDragOverlay,
subscribePaletteDrag,
type PaletteDragOverlaySession,
} from '../../utils/paletteDragSession';
import { clientXYFromReact } from '../../utils/pointerDrag';
/**
* pointer/touch 드래그 overlay (sessionRef 로 stale closure 방지, window touch 백업과 연동)
*/
export default function PaletteDragOverlay() {
const [session, setSession] = useState<PaletteDragOverlaySession | null>(null);
const sessionRef = useRef<PaletteDragOverlaySession | null>(null);
const labelRef = useRef<HTMLDivElement | null>(null);
sessionRef.current = session;
useEffect(() => {
registerPaletteDragOverlay({
mount: next => {
sessionRef.current = next;
setSession(next);
},
unmount: () => {
sessionRef.current = null;
setSession(null);
bindPaletteDragOverlayElement(null);
},
bindElement: () => { /* ref callback 에서 처리 */ },
});
return () => registerPaletteDragOverlay(null);
}, []);
useEffect(() => {
return subscribePaletteDrag(ev => {
if (ev.phase !== 'move' && ev.phase !== 'start') return;
const label = labelRef.current;
if (!label) return;
label.style.left = `${ev.x}px`;
label.style.top = `${ev.y}px`;
});
}, []);
const overlayRefCallback = useCallback((el: HTMLDivElement | null) => {
bindPaletteDragOverlayElement(el);
const s = sessionRef.current;
if (el && s) {
const label = labelRef.current;
if (label) {
label.style.left = `${s.x}px`;
label.style.top = `${s.y}px`;
}
}
}, []);
const onPointerMove = useCallback((e: React.PointerEvent) => {
const s = sessionRef.current;
if (!s || e.pointerId !== s.pointerId) return;
e.preventDefault();
notifyPaletteDragMove(e.clientX, e.clientY);
}, []);
const onPointerUp = useCallback((e: React.PointerEvent) => {
const s = sessionRef.current;
if (!s || e.pointerId !== s.pointerId) return;
e.preventDefault();
notifyPaletteDragEnd(e.clientX, e.clientY);
}, []);
const onPointerCancel = useCallback((e: React.PointerEvent) => {
const s = sessionRef.current;
if (!s || e.pointerId !== s.pointerId) return;
e.preventDefault();
notifyPaletteDragCancel(e.clientX, e.clientY);
}, []);
const onTouchMove = useCallback((e: React.TouchEvent) => {
const s = sessionRef.current;
if (!s || e.touches.length !== 1) return;
e.preventDefault();
const { x, y } = clientXYFromReact(e);
notifyPaletteDragMove(x, y);
}, []);
const onTouchEnd = useCallback((e: React.TouchEvent) => {
const s = sessionRef.current;
if (!s) return;
const { x, y } = clientXYFromReact(e);
e.preventDefault();
notifyPaletteDragEnd(x, y);
}, []);
const onTouchCancel = useCallback((e: React.TouchEvent) => {
const s = sessionRef.current;
if (!s) return;
const { x, y } = clientXYFromReact(e);
e.preventDefault();
notifyPaletteDragCancel(x, y);
}, []);
if (!session) return null;
return createPortal(
<div
ref={overlayRefCallback}
className="se-palette-drag-overlay"
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerCancel}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
onTouchCancel={onTouchCancel}
>
<div
ref={labelRef}
className="se-palette-drag-overlay-label"
style={{ left: session.x, top: session.y }}
>
{session.payload.label}
</div>
</div>,
document.body,
);
}