mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
playlists(#37): drag-and-drop reorder in the Playlists management tab
Videos within a playlist can now be dragged to reorder (desktop), on top of the ↑/↓ buttons kept for touch/accessibility. HTML5 DnD delegated on the list: dragstart records the row's playlist + index, dragover highlights the drop target (accent inset bar) and allows the drop, drop reorders via reorderPlaylistItems and publishes optimistically. Drag is scoped to a single playlist (can't cross playlists); dragging row dims to 0.5; grab/grabbing cursor. Lint + build clean; profile-modal-controller tests 26/26. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
791296266a
commit
87e31d9853
2 files changed
+110
No files matched your search
@@ -3137,6 +3137,19 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.playlist-video:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.playlist-video--dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.playlist-video--drop-target {
|
||||
box-shadow: inset 0 var(--size-2px) 0 0 var(--color-accent);
|
||||
}
|
||||
|
||||
.playlist-video__title {
|
||||
|
||||
@@ -79,6 +79,8 @@ export class ProfilePlaylistsController {
|
||||
this.playlists = [];
|
||||
this.expanded = new Set();
|
||||
this.loading = false;
|
||||
this._drag = null;
|
||||
this._dropTarget = null;
|
||||
this._onChanged = () => {
|
||||
void this.refresh();
|
||||
};
|
||||
@@ -102,6 +104,16 @@ export class ProfilePlaylistsController {
|
||||
this.listEl.addEventListener("click", (event) =>
|
||||
this.handleListClick(event),
|
||||
);
|
||||
// Desktop drag-to-reorder for videos within a playlist. The ↑/↓ buttons
|
||||
// stay as the accessible / touch-friendly path.
|
||||
this.listEl.addEventListener("dragstart", (event) =>
|
||||
this.handleDragStart(event),
|
||||
);
|
||||
this.listEl.addEventListener("dragover", (event) =>
|
||||
this.handleDragOver(event),
|
||||
);
|
||||
this.listEl.addEventListener("drop", (event) => this.handleDrop(event));
|
||||
this.listEl.addEventListener("dragend", () => this.clearDrag());
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("bitvid:playlists-changed", this._onChanged);
|
||||
@@ -268,6 +280,9 @@ export class ProfilePlaylistsController {
|
||||
}
|
||||
playlist.items.forEach((item, index) => {
|
||||
const row = el("li", "playlist-video");
|
||||
row.draggable = true;
|
||||
row.dataset.plId = playlist.id;
|
||||
row.dataset.plIndex = String(index);
|
||||
const info = videoMap.get(item.value);
|
||||
const dtag = item.value.split(":")[2] || item.value;
|
||||
const label = info?.title || dtag;
|
||||
@@ -338,6 +353,88 @@ export class ProfilePlaylistsController {
|
||||
}
|
||||
}
|
||||
|
||||
handleDragStart(event) {
|
||||
const row = event.target?.closest?.(".playlist-video");
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
this._drag = {
|
||||
playlistId: row.dataset.plId,
|
||||
fromIndex: Number(row.dataset.plIndex),
|
||||
};
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
try {
|
||||
event.dataTransfer.setData("text/plain", String(this._drag.fromIndex));
|
||||
} catch (error) {
|
||||
// some browsers require setData(); ignore failures
|
||||
}
|
||||
}
|
||||
row.classList.add("playlist-video--dragging");
|
||||
}
|
||||
|
||||
handleDragOver(event) {
|
||||
if (!this._drag) {
|
||||
return;
|
||||
}
|
||||
const row = event.target?.closest?.(".playlist-video");
|
||||
if (!row || row.dataset.plId !== this._drag.playlistId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
if (this._dropTarget && this._dropTarget !== row) {
|
||||
this._dropTarget.classList.remove("playlist-video--drop-target");
|
||||
}
|
||||
if (Number(row.dataset.plIndex) !== this._drag.fromIndex) {
|
||||
row.classList.add("playlist-video--drop-target");
|
||||
this._dropTarget = row;
|
||||
}
|
||||
}
|
||||
|
||||
handleDrop(event) {
|
||||
const drag = this._drag;
|
||||
const row = event.target?.closest?.(".playlist-video");
|
||||
this.clearDrag();
|
||||
if (!drag || !row || row.dataset.plId !== drag.playlistId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const toIndex = Number(row.dataset.plIndex);
|
||||
if (
|
||||
!Number.isInteger(drag.fromIndex) ||
|
||||
!Number.isInteger(toIndex) ||
|
||||
drag.fromIndex === toIndex
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const playlist = this.playlists.find((p) => p.id === drag.playlistId);
|
||||
if (!playlist) {
|
||||
return;
|
||||
}
|
||||
const items = reorderPlaylistItems(playlist.items, drag.fromIndex, toIndex);
|
||||
void this.publishUpdate({ ...playlist, items }, null);
|
||||
}
|
||||
|
||||
clearDrag() {
|
||||
this._drag = null;
|
||||
this._dropTarget = null;
|
||||
if (this.listEl) {
|
||||
this.listEl
|
||||
.querySelectorAll(
|
||||
".playlist-video--dragging, .playlist-video--drop-target",
|
||||
)
|
||||
.forEach((node) => {
|
||||
node.classList.remove(
|
||||
"playlist-video--dragging",
|
||||
"playlist-video--drop-target",
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggleExpand(id) {
|
||||
if (this.expanded.has(id)) {
|
||||
this.expanded.delete(id);
|
||||
|
||||
Reference in new issue
Block a user