import { memo, useMemo, useRef, type RefObject } from "react"; import { type DomEditSelection } from "./domEditing"; import { resolveDomEditGroupOverlayRect, toOverlayRect } from "./domEditOverlayGeometry"; import { type BlockedMoveState, type FocusableDomEditOverlay, type GestureState, type GroupGestureState, focusDomEditOverlayElement, } from "./domEditOverlayGestures"; import { useDomEditOverlayRects } from "./useDomEditOverlayRects"; import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures"; // Re-exports for external consumers — preserving existing import paths. export { filterNestedDomEditGroupItems, resolveDomEditCoordinateScale, resolveDomEditGroupOverlayRect, } from "./domEditOverlayGeometry"; export { focusDomEditOverlayElement, hasDomEditRotationChanged, resolveDomEditResizeGesture, resolveDomEditRotationGesture, } from "./domEditOverlayGestures"; export interface DomEditGroupPathOffsetCommit { selection: DomEditSelection; next: { x: number; y: number }; } interface DomEditOverlayProps { iframeRef: RefObject; activeCompositionPath: string | null; selection: DomEditSelection | null; groupSelections?: DomEditSelection[]; hoverSelection: DomEditSelection | null; allowCanvasMovement?: boolean; onCanvasMouseDown: ( event: React.MouseEvent, options?: { preferClipAncestor?: boolean }, ) => void; onCanvasPointerMove: ( event: React.PointerEvent, options?: { preferClipAncestor?: boolean }, ) => DomEditSelection | null; onCanvasPointerLeave: () => void; onSelectionChange: ( selection: DomEditSelection, options?: { revealPanel?: boolean; additive?: boolean }, ) => void; onBlockedMove: (selection: DomEditSelection) => void; onManualDragStart?: () => void; onPathOffsetCommit: ( selection: DomEditSelection, next: { x: number; y: number }, ) => Promise | void; onGroupPathOffsetCommit: (updates: DomEditGroupPathOffsetCommit[]) => Promise | void; onBoxSizeCommit: ( selection: DomEditSelection, next: { width: number; height: number }, ) => Promise | void; onRotationCommit: (selection: DomEditSelection, next: { angle: number }) => Promise | void; } export const DomEditOverlay = memo(function DomEditOverlay({ iframeRef, activeCompositionPath, selection, groupSelections = [], hoverSelection, allowCanvasMovement = true, onCanvasMouseDown, onCanvasPointerMove, onCanvasPointerLeave, onSelectionChange, onBlockedMove, onManualDragStart, onPathOffsetCommit, onGroupPathOffsetCommit, onBoxSizeCommit, onRotationCommit, }: DomEditOverlayProps) { const overlayRef = useRef(null); const boxRef = useRef(null); const gestureRef = useRef(null); const groupGestureRef = useRef(null); const blockedMoveRef = useRef(null); const suppressNextBoxClickRef = useRef(false); const suppressNextBoxMouseDownRef = useRef(false); const suppressNextOverlayMouseDownRef = useRef(false); const rafPausedRef = useRef(false); const selectionRef = useRef(selection); selectionRef.current = selection; const activeCompositionPathRef = useRef(activeCompositionPath); activeCompositionPathRef.current = activeCompositionPath; const groupSelectionsRef = useRef(groupSelections); groupSelectionsRef.current = groupSelections; const hoverSelectionRef = useRef(hoverSelection); hoverSelectionRef.current = hoverSelection; const onPathOffsetCommitRef = useRef(onPathOffsetCommit); onPathOffsetCommitRef.current = onPathOffsetCommit; const onGroupPathOffsetCommitRef = useRef(onGroupPathOffsetCommit); onGroupPathOffsetCommitRef.current = onGroupPathOffsetCommit; const onBoxSizeCommitRef = useRef(onBoxSizeCommit); onBoxSizeCommitRef.current = onBoxSizeCommit; const onRotationCommitRef = useRef(onRotationCommit); onRotationCommitRef.current = onRotationCommit; const onBlockedMoveRef = useRef(onBlockedMove); onBlockedMoveRef.current = onBlockedMove; const onManualDragStartRef = useRef(onManualDragStart); onManualDragStartRef.current = onManualDragStart; const onCanvasPointerMoveRef = useRef(onCanvasPointerMove); onCanvasPointerMoveRef.current = onCanvasPointerMove; const onCanvasPointerLeaveRef = useRef(onCanvasPointerLeave); onCanvasPointerLeaveRef.current = onCanvasPointerLeave; const onSelectionChangeRef = useRef(onSelectionChange); onSelectionChangeRef.current = onSelectionChange; const { overlayRect, overlayRectRef, setOverlayRect, hoverRect, groupOverlayItems, groupOverlayItemsRef, setGroupOverlayItems, } = useDomEditOverlayRects({ iframeRef, overlayRef, selectionRef, activeCompositionPathRef, groupSelectionsRef, hoverSelectionRef, rafPausedRef, }); const gestures = createDomEditOverlayGestureHandlers({ overlayRef, boxRef, selectionRef, overlayRectRef, groupOverlayItemsRef, gestureRef, groupGestureRef, blockedMoveRef, rafPausedRef, suppressNextBoxClickRef, setOverlayRect, setGroupOverlayItems, onBlockedMoveRef, onManualDragStartRef, onPathOffsetCommitRef, onGroupPathOffsetCommitRef, onBoxSizeCommitRef, onRotationCommitRef, onCanvasPointerMoveRef, onCanvasMouseDown, }); const selectionKey = useMemo(() => { if (!selection) return "none"; return `${selection.sourceFile}:${selection.id ?? selection.selector ?? selection.label}:${selection.selectorIndex ?? 0}`; }, [selection]); const groupBounds = useMemo( () => resolveDomEditGroupOverlayRect(groupOverlayItems.map((item) => item.rect)), [groupOverlayItems], ); const hasGroupSelection = groupSelections.length > 1; const groupCanMove = hasGroupSelection && groupOverlayItems.length > 1 && groupOverlayItems.every((item) => item.selection.capabilities.canApplyManualOffset); const handleOverlayMouseDown = (event: React.MouseEvent) => { if (suppressNextOverlayMouseDownRef.current) { suppressNextOverlayMouseDownRef.current = false; suppressNextBoxMouseDownRef.current = false; suppressNextBoxClickRef.current = false; event.preventDefault(); event.stopPropagation(); return; } const target = event.target as HTMLElement | null; if (target?.closest('[data-dom-edit-selection-box="true"]')) return; onCanvasMouseDown(event, { preferClipAncestor: false }); if (event.shiftKey) { suppressNextBoxMouseDownRef.current = true; suppressNextBoxClickRef.current = true; } }; const handleOverlayPointerDown = (event: React.PointerEvent) => { if (!allowCanvasMovement || event.button !== 0) return; if (event.shiftKey) { const candidate = onCanvasPointerMoveRef.current(event, { preferClipAncestor: false }) ?? hoverSelectionRef.current; if (!candidate) return; event.preventDefault(); event.stopPropagation(); suppressNextOverlayMouseDownRef.current = true; suppressNextBoxMouseDownRef.current = true; suppressNextBoxClickRef.current = true; onSelectionChangeRef.current(candidate, { additive: true }); return; } const target = event.target as HTMLElement | null; if (target?.closest('[data-dom-edit-selection-box="true"]')) return; const candidate = onCanvasPointerMoveRef.current(event, { preferClipAncestor: false }) ?? hoverSelectionRef.current; if (!candidate?.capabilities.canApplyManualOffset) return; const overlayEl = overlayRef.current; const iframe = iframeRef.current; const candidateRect = overlayEl && iframe ? toOverlayRect(overlayEl, iframe, candidate.element) : null; if (!candidateRect) return; suppressNextOverlayMouseDownRef.current = true; selectionRef.current = candidate; overlayRectRef.current = candidateRect; setOverlayRect(candidateRect); const didStartGesture = gestures.startGesture("drag", event, { selection: candidate, rect: candidateRect, }); if (!didStartGesture) { suppressNextOverlayMouseDownRef.current = false; return; } onSelectionChangeRef.current(candidate); }; const handleBoxClick = (event: React.MouseEvent) => { if (gestureRef.current || groupGestureRef.current) return; if (suppressNextBoxClickRef.current) { suppressNextBoxClickRef.current = false; event.stopPropagation(); return; } onCanvasMouseDown(event, { preferClipAncestor: false }); }; const suppressBoxMouseDown = (e: React.MouseEvent) => { if (!suppressNextBoxMouseDownRef.current) return; suppressNextBoxMouseDownRef.current = false; e.preventDefault(); e.stopPropagation(); }; return (
focusDomEditOverlayElement(event.currentTarget as FocusableDomEditOverlay) } onPointerDown={handleOverlayPointerDown} onMouseDown={handleOverlayMouseDown} onPointerMove={gestures.onPointerMove} onPointerLeave={() => onCanvasPointerLeaveRef.current()} onPointerUp={gestures.onPointerUp} onPointerCancel={() => gestures.clearPointerState(selectionRef)} > {hoverSelection && hoverRect && (