type DragEventHandler = (e: DragEvent) => void; type DragListener = (isDragging: boolean) => void; type DropHandler = (files: File[]) => void; interface DragEventHandlers { dragover: DragEventHandler; dragenter: DragEventHandler; dragleave: DragEventHandler; drop: DragEventHandler; } interface DragAreaState { element: HTMLElement | null; isGlobal: boolean; } export class GlobalDragManager { private static instance: GlobalDragManager; // State management private dragState = { counter: 0, isDragging: false, }; // Listener management private listeners = { drag: new Set(), drop: new Set(), }; // Drag area state private dragArea: DragAreaState = { element: null, isGlobal: true, }; // Event handlers private handlers: DragEventHandlers; private constructor() { this.handlers = this.createEventHandlers(); this.initGlobalHandlers(); } static getInstance(): GlobalDragManager { if (!GlobalDragManager.instance) { GlobalDragManager.instance = new GlobalDragManager(); } return GlobalDragManager.instance; } // Base event handling private static handleDragOver(e: DragEvent): void { e.preventDefault(); e.stopPropagation(); } // Event handler creation private createEventHandlers(): DragEventHandlers { return { dragover: GlobalDragManager.handleDragOver, dragenter: this.handleDragEnter.bind(this), dragleave: this.handleDragLeave.bind(this), drop: this.handleDrop.bind(this), }; } private handleDragEnter(e: DragEvent): void { GlobalDragManager.handleDragOver(e); this.dragState.counter += 1; this.updateDraggingState(true); } private handleDragLeave(e: DragEvent): void { GlobalDragManager.handleDragOver(e); this.dragState.counter -= 1; if (this.dragState.counter === 0) { this.updateDraggingState(false); } } private handleDrop(e: DragEvent): void { GlobalDragManager.handleDragOver(e); this.resetDragState(); const files = e.dataTransfer?.files; if (files && files.length > 0) { this.notifyDropHandlers(Array.from(files)); } } // State updates private resetDragState(): void { this.dragState.counter = 0; this.updateDraggingState(false); } private updateDraggingState(isDragging: boolean): void { if (this.dragState.isDragging !== isDragging) { this.dragState.isDragging = isDragging; this.notifyDragListeners(); } } // Event listener management private attachEventHandlers(element: Document | HTMLElement): void { Object.entries(this.handlers).forEach(([event, handler]) => { element.addEventListener(event, handler); }); } private detachEventHandlers(element: Document | HTMLElement): void { Object.entries(this.handlers).forEach(([event, handler]) => { element.removeEventListener(event, handler); }); } // Listener notifications private notifyDragListeners(): void { this.listeners.drag.forEach((listener) => { listener(this.dragState.isDragging); }); } private notifyDropHandlers(files: File[]): void { this.listeners.drop.forEach((handler) => { handler(files); }); } // Initialization private initGlobalHandlers(): void { this.attachEventHandlers(document); } // Public API setDragArea(selector: string): void { const element = document.querySelector(selector); if (!(element instanceof HTMLElement)) { return; } // Clean up existing listeners if (this.dragArea.isGlobal) { this.detachEventHandlers(document); } else if (this.dragArea.element) { this.detachEventHandlers(this.dragArea.element); } // Set new drag area this.dragArea = { element, isGlobal: false, }; this.attachEventHandlers(element); } resetDragArea(): void { if (!this.dragArea.element) { return; } this.detachEventHandlers(this.dragArea.element); this.dragArea = { element: null, isGlobal: false, }; } subscribe(listener: DragListener): () => void { this.listeners.drag.add(listener); return () => this.listeners.drag.delete(listener); } onDrop(handler: DropHandler): () => void { this.listeners.drop.add(handler); return () => this.listeners.drop.delete(handler); } isCurrentlyDragging(): boolean { return this.dragState.isDragging; } }