feat: replace drag-and-drop with pointer events for item reordering
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m10s

This commit is contained in:
2026-05-23 23:28:11 -04:00
parent 229b6b6f7a
commit 0b3d1d5ed0

View File

@@ -28,13 +28,8 @@
class="item-row" class="item-row"
:data-idx="idx" :data-idx="idx"
:class="{ 'drag-over': dragOverIdx === idx, dragging: draggingIdx === idx }" :class="{ 'drag-over': dragOverIdx === idx, dragging: draggingIdx === idx }"
draggable="true"
@dragstart="(e) => onDragStart(e, idx)"
@dragover.prevent="(e) => onDragOver(e, idx)"
@drop.prevent="onDrop(idx)"
@dragend="onDragEnd"
> >
<span class="drag-handle" title="Drag to reorder" @touchstart="(e) => onTouchStart(e, idx)"></span> <span class="drag-handle" title="Drag to reorder" @pointerdown.prevent="(e) => onPointerDown(e, idx)"></span>
<div class="item-left"> <div class="item-left">
<img <img
v-if="item.image_url" v-if="item.image_url"
@@ -334,44 +329,13 @@ function cancelEditItem() {
newItemLocalFile.value = null newItemLocalFile.value = null
} }
function onDragStart(e: DragEvent, idx: number) { function onPointerDown(e: PointerEvent, idx: number) {
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'
cancelEditItem()
draggingIdx.value = idx
}
function onDragOver(e: DragEvent, idx: number) {
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'
if (draggingIdx.value === null || draggingIdx.value === idx) return
dragOverIdx.value = idx
}
function onDrop(idx: number) {
if (draggingIdx.value === null || draggingIdx.value === idx) {
draggingIdx.value = null
dragOverIdx.value = null
return
}
const moved = items.value.splice(draggingIdx.value, 1)[0]
items.value.splice(idx, 0, moved)
normalizeItemOrder()
draggingIdx.value = null
dragOverIdx.value = null
}
function onDragEnd() {
draggingIdx.value = null
dragOverIdx.value = null
}
function onTouchStart(e: TouchEvent, idx: number) {
draggingIdx.value = idx draggingIdx.value = idx
cancelEditItem() cancelEditItem()
const handleTouchMove = (moveEvt: TouchEvent) => { const handlePointerMove = (moveEvt: PointerEvent) => {
moveEvt.preventDefault() moveEvt.preventDefault()
const touch = moveEvt.touches[0] const el = document.elementFromPoint(moveEvt.clientX, moveEvt.clientY) as Element | null
const el = document.elementFromPoint(touch.clientX, touch.clientY) as Element | null
if (!el) return if (!el) return
const row = el.closest('[data-idx]') as HTMLElement | null const row = el.closest('[data-idx]') as HTMLElement | null
if (!row) return if (!row) return
@@ -381,9 +345,9 @@ function onTouchStart(e: TouchEvent, idx: number) {
} }
} }
const handleTouchEnd = (endEvt: TouchEvent) => { const cleanup = (upEvt?: PointerEvent) => {
const touch = endEvt.changedTouches[0] if (upEvt) {
const el = document.elementFromPoint(touch.clientX, touch.clientY) as Element | null const el = document.elementFromPoint(upEvt.clientX, upEvt.clientY) as Element | null
const fromIdx = draggingIdx.value const fromIdx = draggingIdx.value
if (el && fromIdx !== null) { if (el && fromIdx !== null) {
const row = el.closest('[data-idx]') as HTMLElement | null const row = el.closest('[data-idx]') as HTMLElement | null
@@ -396,14 +360,20 @@ function onTouchStart(e: TouchEvent, idx: number) {
} }
} }
} }
}
draggingIdx.value = null draggingIdx.value = null
dragOverIdx.value = null dragOverIdx.value = null
document.removeEventListener('touchmove', handleTouchMove) document.removeEventListener('pointermove', handlePointerMove)
document.removeEventListener('touchend', handleTouchEnd) document.removeEventListener('pointerup', handlePointerUp)
document.removeEventListener('pointercancel', handlePointerCancel)
} }
document.addEventListener('touchmove', handleTouchMove, { passive: false }) const handlePointerUp = (upEvt: PointerEvent) => cleanup(upEvt)
document.addEventListener('touchend', handleTouchEnd) const handlePointerCancel = () => cleanup()
document.addEventListener('pointermove', handlePointerMove, { passive: false })
document.addEventListener('pointerup', handlePointerUp)
document.addEventListener('pointercancel', handlePointerCancel)
} }
async function uploadImage(file: File): Promise<string> { async function uploadImage(file: File): Promise<string> {
@@ -575,8 +545,9 @@ function handleCancel() {
cursor: grab; cursor: grab;
color: var(--form-label, #aaa); color: var(--form-label, #aaa);
font-size: 1.15rem; font-size: 1.15rem;
padding-right: 0.45rem; padding: 0.4rem 0.45rem 0.4rem 0;
user-select: none; user-select: none;
touch-action: none;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -584,13 +555,6 @@ function handleCancel() {
cursor: grabbing; cursor: grabbing;
} }
@media (hover: none) {
.drag-handle {
padding: 0.5rem 0.6rem;
font-size: 1.4rem;
}
}
.item-left { .item-left {
display: flex; display: flex;
align-items: center; align-items: center;