Make the upcoming-queue drag reorder work on touch devices
Build and push server image / build-and-push (push) Successful in 34s

The native HTML5 Drag-and-Drop API (draggable/dragstart/dragover/drop)
is mouse-only by spec and never fires on phones/tablets, so reordering
was unusable on mobile. Replaced it with the Pointer Events API
(pointerdown/pointermove/pointerup), which unifies mouse, touch, and
pen into one code path, plus touch-action: none on the cards so
touching one to drag it doesn't get hijacked by the browser's default
scroll gesture. Same visual behavior as before (dim the dragged card,
outline the drop target).
This commit is contained in:
2026-07-19 12:59:53 -04:00
parent 3868d357ff
commit aef8a10029
+67 -19
View File
@@ -27,7 +27,14 @@
.thumb { width: 160px; max-width: 100%; border-radius: 4px; display: block; }
.photo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px; margin-top: 8px; }
.photo-card { position: relative; cursor: grab; border-radius: 6px; overflow: hidden; aspect-ratio: 1; background: #f3f4f6; border: 1px solid #e5e7eb; }
.photo-card {
position: relative; cursor: grab; border-radius: 6px; overflow: hidden; aspect-ratio: 1;
background: #f3f4f6; border: 1px solid #e5e7eb;
/* Touching a card is what starts a drag -- without this the browser
treats that touch as the start of a page scroll instead, and
pointermove events for the drag never arrive on mobile. */
touch-action: none;
}
.photo-card:active { cursor: grabbing; }
.photo-card.dragging { opacity: 0.35; }
.photo-card.drag-over { outline: 3px solid #2563eb; outline-offset: -3px; }
@@ -158,7 +165,25 @@
});
let upcomingItems = [];
let dragFromIndex = null;
// Pointer Events (not the native HTML5 Drag-and-Drop API) so the same
// code drives mouse, touch, and pen -- native drag-and-drop is
// mouse-only by spec and never fires at all on phones/tablets.
let dragState = null; // { pointerId, fromIndex, toIndex, moved }
const DRAG_START_THRESHOLD_PX = 6; // ignore tiny jitter from an imprecise tap
function clearDragOverStyling() {
document.querySelectorAll('.photo-card.drag-over').forEach((el) => el.classList.remove('drag-over'));
}
function endDrag(card) {
card.classList.remove('dragging');
clearDragOverStyling();
if (dragState && dragState.moved && dragState.toIndex !== undefined && dragState.toIndex !== dragState.fromIndex) {
moveItem(dragState.fromIndex, dragState.toIndex);
}
dragState = null;
}
function renderUpcoming(items) {
upcomingItems = items;
@@ -168,7 +193,7 @@
items.forEach((item, i) => {
const card = document.createElement('div');
card.className = 'photo-card';
card.draggable = true;
card.dataset.index = String(i);
const img = document.createElement('img');
img.src = item.thumbnail_url;
@@ -190,25 +215,48 @@
});
card.appendChild(nextBtn);
card.addEventListener('dragstart', (e) => {
dragFromIndex = i;
card.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
card.addEventListener('pointerdown', (e) => {
if (e.target.closest('.show-next')) {
return; // let the button's own click handler run, don't start a drag
}
if (e.pointerType === 'mouse' && e.button !== 0) {
return; // left button only
}
dragState = { pointerId: e.pointerId, fromIndex: i, toIndex: undefined, moved: false, startX: e.clientX, startY: e.clientY };
card.setPointerCapture(e.pointerId);
});
card.addEventListener('dragend', () => {
card.classList.remove('dragging');
dragFromIndex = null;
card.addEventListener('pointermove', (e) => {
if (!dragState || dragState.pointerId !== e.pointerId) {
return;
}
if (!dragState.moved) {
const dx = e.clientX - dragState.startX;
const dy = e.clientY - dragState.startY;
if (Math.hypot(dx, dy) < DRAG_START_THRESHOLD_PX) {
return;
}
dragState.moved = true;
card.classList.add('dragging');
}
const overCard = document.elementFromPoint(e.clientX, e.clientY)?.closest('.photo-card');
clearDragOverStyling();
if (overCard && overCard !== card) {
overCard.classList.add('drag-over');
dragState.toIndex = Number(overCard.dataset.index);
} else {
dragState.toIndex = undefined;
}
});
card.addEventListener('dragover', (e) => {
e.preventDefault();
card.classList.add('drag-over');
card.addEventListener('pointerup', (e) => {
if (dragState && dragState.pointerId === e.pointerId) {
endDrag(card);
}
});
card.addEventListener('dragleave', () => card.classList.remove('drag-over'));
card.addEventListener('drop', (e) => {
e.preventDefault();
card.classList.remove('drag-over');
if (dragFromIndex !== null && dragFromIndex !== i) {
moveItem(dragFromIndex, i);
card.addEventListener('pointercancel', (e) => {
if (dragState && dragState.pointerId === e.pointerId) {
endDrag(card);
}
});