add
This commit is contained in:
+125
-9
@@ -276,6 +276,18 @@ async function removeItem(item) {
|
||||
items.value = items.value.filter((i) => i.id !== item.id)
|
||||
}
|
||||
|
||||
async function clearSublistItems(group) {
|
||||
if (!group.items.length) return
|
||||
const name = group.sublist ? group.sublist.name : 'this section'
|
||||
if (!window.confirm(`Clear all items in "${name}"?`)) return
|
||||
|
||||
const ids = new Set(group.items.map((i) => i.id))
|
||||
await Promise.all(
|
||||
[...ids].map((id) => fetch(`/api/lists/${listId.value}/items/${id}`, { method: 'DELETE' })),
|
||||
)
|
||||
items.value = items.value.filter((i) => !ids.has(i.id))
|
||||
}
|
||||
|
||||
function startEdit(item) {
|
||||
editingId.value = item.id
|
||||
editName.value = item.name
|
||||
@@ -381,6 +393,55 @@ function findTarget(anchors, pointerY) {
|
||||
return { targetKey, insertAfterItemId }
|
||||
}
|
||||
|
||||
// Long lists are taller than the viewport, and .handle has touch-action:
|
||||
// none (so a real drag doesn't also trigger native scrolling) — without
|
||||
// this, there'd be no way to drag an item to a part of the list that's
|
||||
// scrolled out of view. While dragging near the top/bottom edge of the
|
||||
// screen, we scroll the page ourselves and keep re-running the drag's
|
||||
// move logic against the same last-known pointer position, since the
|
||||
// pointer itself isn't moving.
|
||||
const AUTO_SCROLL_EDGE = 70
|
||||
const AUTO_SCROLL_MAX_SPEED = 14
|
||||
|
||||
function autoScrollSpeed(clientY) {
|
||||
if (clientY < AUTO_SCROLL_EDGE) {
|
||||
return -AUTO_SCROLL_MAX_SPEED * (1 - Math.max(0, clientY) / AUTO_SCROLL_EDGE)
|
||||
}
|
||||
const fromBottom = window.innerHeight - clientY
|
||||
if (fromBottom < AUTO_SCROLL_EDGE) {
|
||||
return AUTO_SCROLL_MAX_SPEED * (1 - Math.max(0, fromBottom) / AUTO_SCROLL_EDGE)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
let autoScrollRAF = null
|
||||
let autoScrollDrag = null // { getClientY, update }
|
||||
|
||||
function autoScrollTick() {
|
||||
if (!autoScrollDrag) {
|
||||
autoScrollRAF = null
|
||||
return
|
||||
}
|
||||
const clientY = autoScrollDrag.getClientY()
|
||||
const speed = clientY == null ? 0 : autoScrollSpeed(clientY)
|
||||
if (speed !== 0) {
|
||||
window.scrollBy(0, speed)
|
||||
autoScrollDrag.update()
|
||||
}
|
||||
autoScrollRAF = requestAnimationFrame(autoScrollTick)
|
||||
}
|
||||
|
||||
function startAutoScroll(getClientY, update) {
|
||||
autoScrollDrag = { getClientY, update }
|
||||
if (!autoScrollRAF) autoScrollRAF = requestAnimationFrame(autoScrollTick)
|
||||
}
|
||||
|
||||
function stopAutoScroll() {
|
||||
autoScrollDrag = null
|
||||
if (autoScrollRAF) cancelAnimationFrame(autoScrollRAF)
|
||||
autoScrollRAF = null
|
||||
}
|
||||
|
||||
function setDropTarget(key) {
|
||||
document.querySelectorAll('.group-header').forEach((h) => h.classList.remove('drop-target'))
|
||||
const headerEl = key != null ? document.querySelector(`[data-group-header="${key}"]`) : null
|
||||
@@ -400,6 +461,8 @@ function onHandleDown(e, item) {
|
||||
item,
|
||||
el: li,
|
||||
startY: e.clientY,
|
||||
startScrollY: window.scrollY,
|
||||
lastClientY: e.clientY,
|
||||
height,
|
||||
anchors,
|
||||
originalKey,
|
||||
@@ -412,6 +475,10 @@ function onHandleDown(e, item) {
|
||||
li.style.position = 'relative'
|
||||
li.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.2)'
|
||||
e.currentTarget.setPointerCapture(e.pointerId)
|
||||
startAutoScroll(
|
||||
() => dragReorder?.lastClientY,
|
||||
() => updateHandleDrag(dragReorder.lastClientY),
|
||||
)
|
||||
}
|
||||
|
||||
// Shifts the sibling items around the current drop slot to visually open a
|
||||
@@ -458,20 +525,34 @@ function updateGap() {
|
||||
dragReorder.shiftedEls = nextShifted
|
||||
}
|
||||
|
||||
function onHandleMove(e) {
|
||||
// Shared by real pointermove events and the auto-scroll loop (which re-runs
|
||||
// this against the same last-known pointer position while the page scrolls
|
||||
// itself). scrollDelta folds the scroll-since-drag-start into both the
|
||||
// dragged element's offset and the pointer position used against the
|
||||
// anchors' rects, which were captured once at drag start and never
|
||||
// re-measured — see buildAnchors' comment.
|
||||
function updateHandleDrag(clientY) {
|
||||
if (!dragReorder) return
|
||||
const dy = e.clientY - dragReorder.startY
|
||||
dragReorder.lastClientY = clientY
|
||||
const scrollDelta = window.scrollY - dragReorder.startScrollY
|
||||
const dy = clientY - dragReorder.startY + scrollDelta
|
||||
dragReorder.el.style.transform = `translateY(${dy}px)`
|
||||
|
||||
const { targetKey, insertAfterItemId } = findTarget(dragReorder.anchors, e.clientY)
|
||||
const { targetKey, insertAfterItemId } = findTarget(dragReorder.anchors, clientY + scrollDelta)
|
||||
dragReorder.targetKey = targetKey
|
||||
dragReorder.insertAfterItemId = insertAfterItemId
|
||||
setDropTarget(targetKey)
|
||||
updateGap()
|
||||
}
|
||||
|
||||
function onHandleMove(e) {
|
||||
if (!dragReorder) return
|
||||
updateHandleDrag(e.clientY)
|
||||
}
|
||||
|
||||
function onHandleEnd() {
|
||||
if (!dragReorder) return
|
||||
stopAutoScroll()
|
||||
const { el, item, targetKey, insertAfterItemId, shiftedEls } = dragReorder
|
||||
el.style.transform = ''
|
||||
el.style.transition = ''
|
||||
@@ -533,17 +614,32 @@ function onRecurHandleDown(e, index) {
|
||||
const li = e.currentTarget.closest('li')
|
||||
const els = Array.from(li.parentElement.children)
|
||||
const rects = els.map((el) => el.getBoundingClientRect())
|
||||
recurDrag = { index, target: index, startY: e.clientY, rects, els, height: rects[index].height }
|
||||
recurDrag = {
|
||||
index,
|
||||
target: index,
|
||||
startY: e.clientY,
|
||||
startScrollY: window.scrollY,
|
||||
lastClientY: e.clientY,
|
||||
rects,
|
||||
els,
|
||||
height: rects[index].height,
|
||||
}
|
||||
els[index].style.zIndex = 5
|
||||
els[index].style.position = 'relative'
|
||||
els[index].style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.2)'
|
||||
e.currentTarget.setPointerCapture(e.pointerId)
|
||||
startAutoScroll(
|
||||
() => recurDrag?.lastClientY,
|
||||
() => updateRecurDrag(recurDrag.lastClientY),
|
||||
)
|
||||
}
|
||||
|
||||
function onRecurHandleMove(e) {
|
||||
// See updateHandleDrag above for why scrollDelta is folded in here too.
|
||||
function updateRecurDrag(clientY) {
|
||||
if (!recurDrag) return
|
||||
const { index, rects, els, height, startY } = recurDrag
|
||||
const dy = e.clientY - startY
|
||||
recurDrag.lastClientY = clientY
|
||||
const { index, rects, els, height, startY, startScrollY } = recurDrag
|
||||
const dy = clientY - startY + (window.scrollY - startScrollY)
|
||||
els[index].style.transform = `translateY(${dy}px)`
|
||||
|
||||
const draggedCenter = rects[index].top + height / 2 + dy
|
||||
@@ -566,8 +662,14 @@ function onRecurHandleMove(e) {
|
||||
})
|
||||
}
|
||||
|
||||
function onRecurHandleMove(e) {
|
||||
if (!recurDrag) return
|
||||
updateRecurDrag(e.clientY)
|
||||
}
|
||||
|
||||
function onRecurHandleEnd() {
|
||||
if (!recurDrag) return
|
||||
stopAutoScroll()
|
||||
const { index, target, els } = recurDrag
|
||||
els.forEach((el) => {
|
||||
el.style.transform = ''
|
||||
@@ -673,7 +775,14 @@ onMounted(init)
|
||||
<span class="triangle" :class="{ open: !collapsed.has(groupKey(group)) }"></span>
|
||||
<span class="dot" :style="{ background: group.sublist ? group.sublist.color : 'transparent' }"></span>
|
||||
<span class="group-name">{{ group.sublist ? group.sublist.name : 'No sublist' }}</span>
|
||||
<span class="count">{{ group.items.length }}</span>
|
||||
<button
|
||||
v-if="group.items.length"
|
||||
type="button"
|
||||
class="clear-btn"
|
||||
@click.stop="clearSublistItems(group)"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul v-show="!sublists.length || !collapsed.has(groupKey(group))">
|
||||
@@ -1036,10 +1145,17 @@ ul {
|
||||
.group-name {
|
||||
flex: 1;
|
||||
}
|
||||
.count {
|
||||
.clear-btn {
|
||||
font-weight: 400;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.2rem 0.4rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.clear-btn:hover {
|
||||
color: var(--danger);
|
||||
}
|
||||
.item {
|
||||
position: relative;
|
||||
|
||||
Reference in New Issue
Block a user