Skip to content

Commit afc699c

Browse files
committed
fix: allow reordering checked list items
1 parent f3880ce commit afc699c

1 file changed

Lines changed: 107 additions & 28 deletions

File tree

src/views/ChecklistDetail.vue

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -83,19 +83,29 @@
8383
</ul>
8484
<template v-if="checkedItems.length > 0">
8585
<h3 class="pantry-detail__section-title">{{ strings.doneTitle }}</h3>
86-
<ul class="pantry-detail__items pantry-detail__items--done">
87-
<ChecklistItemRow
88-
v-for="item in checkedItems"
89-
:key="item.id"
90-
:item="item"
91-
:category="categoryFor(item.categoryId)"
92-
:house-id="houseIdNum"
93-
@toggle="handleToggle"
94-
@view="openView"
95-
@edit="startEdit"
96-
@remove="handleRemove"
97-
@preview="openPreview"
98-
/>
86+
<ul ref="checkedListRef" class="pantry-detail__items pantry-detail__items--done">
87+
<template v-for="gi in checkedGridItems" :key="gi.key">
88+
<li
89+
v-if="gi.type === 'placeholder'"
90+
class="pantry-detail__placeholder"
91+
@dragover.prevent
92+
@drop.prevent.stop="onPlaceholderDrop"
93+
/>
94+
<ChecklistItemRow
95+
v-else
96+
:item="gi.item"
97+
:category="categoryFor(gi.item.categoryId)"
98+
:house-id="houseIdNum"
99+
:reorder-enabled="isCustomSort"
100+
@toggle="handleToggle"
101+
@view="openView"
102+
@edit="startEdit"
103+
@remove="handleRemove"
104+
@preview="openPreview"
105+
@drag-start="onItemDragStart"
106+
@reorder-over="onReorderOver"
107+
/>
108+
</template>
99109
</ul>
100110
</template>
101111
</template>
@@ -251,20 +261,33 @@ const isCustomSort = computed(() => currentSort.value === 'custom')
251261
const uncheckedItems = computed(() => sortWithinPartition(items.value.filter((i) => !i.done)))
252262
const checkedItems = computed(() => sortWithinPartition(items.value.filter((i) => i.done)))
253263
254-
// ----- Drag/drop reorder (unchecked partition only, custom sort) -----
264+
// ----- Drag/drop reorder (custom sort, per partition) -----
255265
256266
type ListGridItem =
257267
| { type: 'item'; key: string; item: ChecklistItem }
258268
| { type: 'placeholder'; key: string }
259269
270+
type Partition = 'unchecked' | 'checked'
271+
260272
const draggingItemId = ref<number | null>(null)
273+
const draggingPartition = ref<Partition | null>(null)
261274
const dropIndex = ref<number | null>(null)
262275
const uncheckedListRef = ref<HTMLElement | null>(null)
276+
const checkedListRef = ref<HTMLElement | null>(null)
263277
264-
const uncheckedGridItems = computed<ListGridItem[]>(() => {
265-
const source = uncheckedItems.value
278+
function partitionItems(p: Partition): ChecklistItem[] {
279+
return p === 'unchecked' ? uncheckedItems.value : checkedItems.value
280+
}
281+
282+
function buildGridItems(p: Partition): ListGridItem[] {
283+
const source = partitionItems(p)
266284
const dragId = draggingItemId.value
267-
if (!isCustomSort.value || dragId === null || dropIndex.value === null) {
285+
if (
286+
!isCustomSort.value ||
287+
dragId === null ||
288+
dropIndex.value === null ||
289+
draggingPartition.value !== p
290+
) {
268291
return source.map((i) => ({ type: 'item' as const, key: 'i-' + i.id, item: i }))
269292
}
270293
const without = source.filter((i) => i.id !== dragId)
@@ -276,18 +299,33 @@ const uncheckedGridItems = computed<ListGridItem[]>(() => {
276299
const clamped = Math.min(dropIndex.value, items.length)
277300
items.splice(clamped, 0, { type: 'placeholder', key: 'drop-placeholder' })
278301
return items
279-
})
302+
}
303+
304+
const uncheckedGridItems = computed<ListGridItem[]>(() => buildGridItems('unchecked'))
305+
const checkedGridItems = computed<ListGridItem[]>(() => buildGridItems('checked'))
306+
307+
function findPartitionOf(itemId: number): Partition | null {
308+
if (uncheckedItems.value.some((i) => i.id === itemId)) return 'unchecked'
309+
if (checkedItems.value.some((i) => i.id === itemId)) return 'checked'
310+
return null
311+
}
280312
281313
function onItemDragStart(itemId: number) {
282314
draggingItemId.value = itemId
315+
draggingPartition.value = findPartitionOf(itemId)
283316
dropIndex.value = null
284317
}
285318
286319
function computeItemDropIndex(hoveredItemId: number, clientY: number, target: HTMLElement | null) {
287320
const dragId = draggingItemId.value
288321
if (!dragId || dragId === hoveredItemId) return
289322
290-
const without = uncheckedItems.value.filter((i) => i.id !== dragId)
323+
const partition = draggingPartition.value
324+
if (!partition) return
325+
326+
// Only allow reordering within the same partition.
327+
const source = partitionItems(partition)
328+
const without = source.filter((i) => i.id !== dragId)
291329
const idx = without.findIndex((i) => i.id === hoveredItemId)
292330
if (idx === -1) return
293331
@@ -311,12 +349,17 @@ function onPlaceholderDrop() {
311349
async function commitReorder() {
312350
const dragId = draggingItemId.value
313351
const idx = dropIndex.value
352+
const partition = draggingPartition.value
314353
draggingItemId.value = null
354+
draggingPartition.value = null
315355
dropIndex.value = null
316356
317-
if (dragId === null || idx === null) return
357+
if (dragId === null || idx === null || !partition) return
318358
319-
const source = uncheckedItems.value
359+
// Reorder within the dragged partition, then merge with the other partition
360+
// (preserving its relative order) so the API receives a complete sort order
361+
// for all items in the list.
362+
const source = partitionItems(partition)
320363
const dragged = source.find((i) => i.id === dragId)
321364
if (!dragged) return
322365
@@ -325,28 +368,45 @@ async function commitReorder() {
325368
const reordered = [...without]
326369
reordered.splice(clamped, 0, dragged)
327370
328-
const entries = reordered.map((i, idx) => ({ id: i.id, sortOrder: idx }))
371+
const otherPartition: Partition = partition === 'unchecked' ? 'checked' : 'unchecked'
372+
const other = partitionItems(otherPartition)
373+
374+
// Unchecked items always come first in the sortOrder sequence.
375+
const finalOrder = partition === 'unchecked' ? [...reordered, ...other] : [...other, ...reordered]
376+
377+
const entries = finalOrder.map((i, n) => ({ id: i.id, sortOrder: n }))
329378
await reorderItems(entries)
330379
}
331380
332-
// Capture-phase listeners
381+
// Capture-phase listeners — attached to both partition lists.
333382
function onDropCapture() {
334383
commitReorder()
335384
}
336385
function onDragEndCapture() {
337386
draggingItemId.value = null
387+
draggingPartition.value = null
338388
dropIndex.value = null
339389
}
390+
function bindDragListeners(el: HTMLElement | null) {
391+
if (!el) return
392+
el.addEventListener('drop', onDropCapture, true)
393+
el.addEventListener('dragend', onDragEndCapture, true)
394+
}
395+
function unbindDragListeners(el: HTMLElement | null) {
396+
if (!el) return
397+
el.removeEventListener('drop', onDropCapture, true)
398+
el.removeEventListener('dragend', onDragEndCapture, true)
399+
}
340400
onMounted(() => {
341-
uncheckedListRef.value?.addEventListener('drop', onDropCapture, true)
342-
uncheckedListRef.value?.addEventListener('dragend', onDragEndCapture, true)
401+
bindDragListeners(uncheckedListRef.value)
402+
bindDragListeners(checkedListRef.value)
343403
})
344404
onBeforeUnmount(() => {
345-
uncheckedListRef.value?.removeEventListener('drop', onDropCapture, true)
346-
uncheckedListRef.value?.removeEventListener('dragend', onDragEndCapture, true)
405+
unbindDragListeners(uncheckedListRef.value)
406+
unbindDragListeners(checkedListRef.value)
347407
})
348408
349-
// Touch reorder
409+
// Touch reorder — one composable instance per partition list.
350410
useTouchReorder(
351411
uncheckedListRef,
352412
{
@@ -358,6 +418,25 @@ useTouchReorder(
358418
onDrop: commitReorder,
359419
onCancel() {
360420
draggingItemId.value = null
421+
draggingPartition.value = null
422+
dropIndex.value = null
423+
},
424+
},
425+
isCustomSort,
426+
)
427+
428+
useTouchReorder(
429+
checkedListRef,
430+
{
431+
onDragStart: onItemDragStart,
432+
onReorderOver(hoveredId, _clientX, clientY) {
433+
const el = checkedListRef.value?.querySelector<HTMLElement>(`[data-drag-id="${hoveredId}"]`)
434+
computeItemDropIndex(hoveredId, clientY, el)
435+
},
436+
onDrop: commitReorder,
437+
onCancel() {
438+
draggingItemId.value = null
439+
draggingPartition.value = null
361440
dropIndex.value = null
362441
},
363442
},

0 commit comments

Comments
 (0)