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)
|
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) {
|
function startEdit(item) {
|
||||||
editingId.value = item.id
|
editingId.value = item.id
|
||||||
editName.value = item.name
|
editName.value = item.name
|
||||||
@@ -381,6 +393,55 @@ function findTarget(anchors, pointerY) {
|
|||||||
return { targetKey, insertAfterItemId }
|
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) {
|
function setDropTarget(key) {
|
||||||
document.querySelectorAll('.group-header').forEach((h) => h.classList.remove('drop-target'))
|
document.querySelectorAll('.group-header').forEach((h) => h.classList.remove('drop-target'))
|
||||||
const headerEl = key != null ? document.querySelector(`[data-group-header="${key}"]`) : null
|
const headerEl = key != null ? document.querySelector(`[data-group-header="${key}"]`) : null
|
||||||
@@ -400,6 +461,8 @@ function onHandleDown(e, item) {
|
|||||||
item,
|
item,
|
||||||
el: li,
|
el: li,
|
||||||
startY: e.clientY,
|
startY: e.clientY,
|
||||||
|
startScrollY: window.scrollY,
|
||||||
|
lastClientY: e.clientY,
|
||||||
height,
|
height,
|
||||||
anchors,
|
anchors,
|
||||||
originalKey,
|
originalKey,
|
||||||
@@ -412,6 +475,10 @@ function onHandleDown(e, item) {
|
|||||||
li.style.position = 'relative'
|
li.style.position = 'relative'
|
||||||
li.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.2)'
|
li.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.2)'
|
||||||
e.currentTarget.setPointerCapture(e.pointerId)
|
e.currentTarget.setPointerCapture(e.pointerId)
|
||||||
|
startAutoScroll(
|
||||||
|
() => dragReorder?.lastClientY,
|
||||||
|
() => updateHandleDrag(dragReorder.lastClientY),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shifts the sibling items around the current drop slot to visually open a
|
// Shifts the sibling items around the current drop slot to visually open a
|
||||||
@@ -458,20 +525,34 @@ function updateGap() {
|
|||||||
dragReorder.shiftedEls = nextShifted
|
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
|
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)`
|
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.targetKey = targetKey
|
||||||
dragReorder.insertAfterItemId = insertAfterItemId
|
dragReorder.insertAfterItemId = insertAfterItemId
|
||||||
setDropTarget(targetKey)
|
setDropTarget(targetKey)
|
||||||
updateGap()
|
updateGap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onHandleMove(e) {
|
||||||
|
if (!dragReorder) return
|
||||||
|
updateHandleDrag(e.clientY)
|
||||||
|
}
|
||||||
|
|
||||||
function onHandleEnd() {
|
function onHandleEnd() {
|
||||||
if (!dragReorder) return
|
if (!dragReorder) return
|
||||||
|
stopAutoScroll()
|
||||||
const { el, item, targetKey, insertAfterItemId, shiftedEls } = dragReorder
|
const { el, item, targetKey, insertAfterItemId, shiftedEls } = dragReorder
|
||||||
el.style.transform = ''
|
el.style.transform = ''
|
||||||
el.style.transition = ''
|
el.style.transition = ''
|
||||||
@@ -533,17 +614,32 @@ function onRecurHandleDown(e, index) {
|
|||||||
const li = e.currentTarget.closest('li')
|
const li = e.currentTarget.closest('li')
|
||||||
const els = Array.from(li.parentElement.children)
|
const els = Array.from(li.parentElement.children)
|
||||||
const rects = els.map((el) => el.getBoundingClientRect())
|
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.zIndex = 5
|
||||||
els[index].style.position = 'relative'
|
els[index].style.position = 'relative'
|
||||||
els[index].style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.2)'
|
els[index].style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.2)'
|
||||||
e.currentTarget.setPointerCapture(e.pointerId)
|
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
|
if (!recurDrag) return
|
||||||
const { index, rects, els, height, startY } = recurDrag
|
recurDrag.lastClientY = clientY
|
||||||
const dy = e.clientY - startY
|
const { index, rects, els, height, startY, startScrollY } = recurDrag
|
||||||
|
const dy = clientY - startY + (window.scrollY - startScrollY)
|
||||||
els[index].style.transform = `translateY(${dy}px)`
|
els[index].style.transform = `translateY(${dy}px)`
|
||||||
|
|
||||||
const draggedCenter = rects[index].top + height / 2 + dy
|
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() {
|
function onRecurHandleEnd() {
|
||||||
if (!recurDrag) return
|
if (!recurDrag) return
|
||||||
|
stopAutoScroll()
|
||||||
const { index, target, els } = recurDrag
|
const { index, target, els } = recurDrag
|
||||||
els.forEach((el) => {
|
els.forEach((el) => {
|
||||||
el.style.transform = ''
|
el.style.transform = ''
|
||||||
@@ -673,7 +775,14 @@ onMounted(init)
|
|||||||
<span class="triangle" :class="{ open: !collapsed.has(groupKey(group)) }"></span>
|
<span class="triangle" :class="{ open: !collapsed.has(groupKey(group)) }"></span>
|
||||||
<span class="dot" :style="{ background: group.sublist ? group.sublist.color : 'transparent' }"></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="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>
|
</div>
|
||||||
|
|
||||||
<ul v-show="!sublists.length || !collapsed.has(groupKey(group))">
|
<ul v-show="!sublists.length || !collapsed.has(groupKey(group))">
|
||||||
@@ -1036,10 +1145,17 @@ ul {
|
|||||||
.group-name {
|
.group-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
.count {
|
.clear-btn {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.clear-btn:hover {
|
||||||
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
.item {
|
.item {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
Reference in New Issue
Block a user