basic drag-and-drop, fixed end of scan behavior

This commit is contained in:
2026-02-19 15:25:49 -05:00
parent d9ebd38382
commit 193f65bd98
12 changed files with 1043 additions and 31 deletions
+117
View File
@@ -0,0 +1,117 @@
/**
* 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;
}
// =====================================================================
// 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 {
document.dispatchEvent(
new CustomEvent<DragActiveDetail>(
'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;
e.dataTransfer.effectAllowed = 'copy';
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;
}
}
+34
View File
@@ -0,0 +1,34 @@
/**
* Creates a custom drag image element showing a track count badge.
* The element is appended to the document body (required by the
* setDragImage API) and removed after the drag ends.
*/
export function createDragImage(count: number): HTMLElement {
const el = document.createElement('div');
el.textContent = `${count} track${count !== 1 ? 's' : ''}`;
el.style.cssText = [
'position: fixed',
'top: -1000px',
'left: -1000px',
'padding: 6px 14px',
'border-radius: 6px',
'background: #ffd43b',
'color: #000',
'font-size: 13px',
'font-weight: 600',
'font-family: inherit',
'white-space: nowrap',
'pointer-events: none',
'z-index: 9999',
].join(';');
document.body.appendChild(el);
return el;
}
/** Remove a drag image element created by createDragImage. */
export function removeDragImage(el: HTMLElement): void {
el.remove();
}