131 lines
3.7 KiB
TypeScript
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,
|
|
);
|
|
}
|