basic drag-and-drop, fixed end of scan behavior
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user