Make the upcoming-queue drag reorder work on touch devices
Build and push server image / build-and-push (push) Successful in 34s
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:
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user