/** * Shared drag-and-drop coordination for track items. * * Uses the HTML5 Drag and Drop API with a custom MIME type so that * drag sources and drop targets across different shadow roots can * communicate. A global custom event ("yj-drag-active") is * dispatched on `document` so that non-participating components * (sidebar, queue button) can react to the drag lifecycle. */ /** MIME type used in dataTransfer for in-app track drags. */ export const DRAG_MIME = 'application/x-yj-tracks'; /** Sources that can originate a drag. */ export type DragSource = | 'track-list' | 'cover-grid' | 'queue' | 'playlist'; /** Serialized payload stored in dataTransfer. */ export interface DragPayload { filePaths: string[]; source: DragSource; sourcePlaylistId?: number; } // ===================================================================== // Active drag source tracking // ===================================================================== let activeDragSource: DragSource | null = null; let activeDragPlaylistId: number | undefined; /** Return the source of the in-progress drag, or null. */ export function getActiveDragSource(): DragSource | null { return activeDragSource; } /** Return the playlist ID of the in-progress drag, if any. */ export function getActiveDragPlaylistId(): | number | undefined { return activeDragPlaylistId; } // ===================================================================== // Global drag-active event // ===================================================================== export interface DragActiveDetail { active: boolean; } /** * Notify the entire document that a track drag has started or ended. * Non-participating components listen for this to show/hide drop * affordances (e.g. sidebar hover-to-navigate, queue button glow). */ export function emitDragActive(active: boolean): void { if (!active) { activeDragSource = null; activeDragPlaylistId = undefined; } document.dispatchEvent( new CustomEvent( 'yj-drag-active', { bubbles: true, composed: true, detail: { active }, }, ), ); } // ===================================================================== // Helpers for drag sources // ===================================================================== /** * Populate a DragEvent's dataTransfer with the standard payload. * Returns false if dataTransfer is unavailable. */ export function setDragPayload( e: DragEvent, payload: DragPayload, ): boolean { if (!e.dataTransfer) return false; activeDragSource = payload.source; activeDragPlaylistId = payload.sourcePlaylistId; e.dataTransfer.effectAllowed = 'copyMove'; e.dataTransfer.setData( DRAG_MIME, JSON.stringify(payload), ); return true; } // ===================================================================== // Helpers for drop targets // ===================================================================== /** Check whether a dragover event carries our custom MIME type. */ export function hasTrackPayload(e: DragEvent): boolean { return ( e.dataTransfer?.types.includes(DRAG_MIME) ?? false ); } /** * Extract the DragPayload from a drop event. * Returns null if the data is missing or malformed. */ export function getDragPayload( e: DragEvent, ): DragPayload | null { const raw = e.dataTransfer?.getData(DRAG_MIME); if (!raw) return null; try { const parsed: unknown = JSON.parse(raw); if ( typeof parsed === 'object' && parsed !== null && 'filePaths' in parsed && Array.isArray( (parsed as DragPayload).filePaths, ) ) { return parsed as DragPayload; } return null; } catch { return null; } }