import type { Cell } from "@commontools/runner"; /** * State information for an active drag operation. */ export interface DragState { /** The Cell being dragged */ cell: Cell; /** Optional type identifier for filtering drop zones */ type?: string; /** The source element that initiated the drag */ sourceElement: HTMLElement; /** The preview element being shown during drag */ preview: HTMLElement; /** Optional cleanup function to call when drag ends */ previewCleanup?: () => void; /** Current pointer X position (updated during drag) */ pointerX: number; /** Current pointer Y position (updated during drag) */ pointerY: number; } /** * Callback function invoked when drag state changes. * Receives the new drag state, or null when drag ends. */ export type DragListener = (state: DragState | null) => void; // Module-level singleton state let currentDrag: DragState | null = null; const listeners: Set = new Set(); /** * Begin a drag operation with the given state. * Notifies all subscribers of the new drag state. * * @param state - The drag state to set */ export function startDrag(state: DragState): void { currentDrag = state; notifyListeners(state); } /** * End the current drag operation. * First notifies listeners with the final state (so drop zones can emit drop events), * then cleans up the preview element and notifies with null. */ export function endDrag(): void { if (!currentDrag) { return; } // Store reference before clearing const finalState = currentDrag; // Notify listeners that drag is ending (with isEnding flag) // Drop zones use this to emit ct-drop if pointer is over them notifyListenersOfEnd(finalState); // Call cleanup function if provided if (finalState.previewCleanup) { finalState.previewCleanup(); } // Remove preview element from DOM if (finalState.preview.parentNode) { finalState.preview.parentNode.removeChild(finalState.preview); } // Clear state currentDrag = null; // Notify all subscribers that drag has ended notifyListeners(null); } /** * Callbacks for when drag is ending (before cleanup). * Used by drop zones to emit drop events. */ type DragEndListener = (state: DragState) => void; const endListeners: Set = new Set(); /** * Subscribe to drag end events. * Called with the final drag state BEFORE it's cleared. * Use this to emit drop events if the pointer is over your drop zone. * * @param listener - Callback invoked when drag ends * @returns Unsubscribe function */ export function subscribeToEndDrag(listener: DragEndListener): () => void { endListeners.add(listener); return () => { endListeners.delete(listener); }; } /** * Internal helper to notify end listeners. */ function notifyListenersOfEnd(state: DragState): void { endListeners.forEach((listener) => { try { listener(state); } catch (error) { console.error("[drag-state] Error in drag end listener:", error); } }); } /** * Get the current drag state. * * @returns The current drag state, or null if no drag is active */ export function getCurrentDrag(): DragState | null { return currentDrag; } /** * Check if a drag operation is currently active. * * @returns true if a drag is active, false otherwise */ export function isDragging(): boolean { return currentDrag !== null; } /** * Update the current pointer position during drag. * This is called by drag-source on pointermove to keep drop zones informed. * * @param x - Current pointer X position * @param y - Current pointer Y position */ export function updateDragPointer(x: number, y: number): void { if (!currentDrag) { return; } currentDrag.pointerX = x; currentDrag.pointerY = y; // Notify listeners of position update notifyListeners(currentDrag); } /** * Subscribe to drag state changes. * The listener will be called immediately with the current state, * and then on every state change. * * @param listener - Callback function to invoke on state changes * @returns Unsubscribe function to remove the listener */ export function subscribeToDrag(listener: DragListener): () => void { listeners.add(listener); // Call immediately with current state listener(currentDrag); // Return unsubscribe function return () => { listeners.delete(listener); }; } /** * Internal helper to notify all listeners of state change. */ function notifyListeners(state: DragState | null): void { listeners.forEach((listener) => { try { listener(state); } catch (error) { console.error("[drag-state] Error in drag listener:", error); } }); }