// fixtures/strip-reorder.mjs — reordering slides from the strip, three
// ways to one op (move_slide): HTML5 drag and drop of an item, Alt+Up
// / Alt+Down on a focused item's link, and the item menu's Move up /
// Move down (the Slide section's buttons are the same commands). The
// drop target is the item under the pointer, before or after it by
// which half was crossed, and said with a data-drop attribute the
// stylesheet draws as a line.
//
// The pure part is dropTarget: given the item ids in order, the
// dragged id, the item dropped on and the half, the `after` a
// move_slide wants (null for first) — or undefined when the drop
// changes nothing.
/** The `after` for dropping `id` before/after `on`: null puts it first; undefined means no move. */
export function dropTarget (ids, id, on, half) {
if (id === on) return undefined
const rest = ids.filter(x => x !== id)
const i = rest.indexOf(on)
if (i < 0) return undefined
const at = half === 'before' ? i : i + 1
const after = at === 0 ? null : rest[at - 1]
const was = ids.indexOf(id)
const now = rest.slice(0, at).length
if (was === now) return undefined
return after
}
/** move_slide from a drop, or null. */
export function dropOp (ids, id, on, half) {
const after = dropTarget(ids, id, on, half)
if (after === undefined) return null
return after === null ? { op: 'move_slide', id } : { op: 'move_slide', id, after }
}
/**
* Wires drag and drop and the keyboard onto the strip's
.
* getIDs() is the slide order; onMove(op) applies a move_slide. The
* item is what is dragged (draggable="true" in the markup); the link
* inside it is what takes the keys.
*/
export function mountReorder ({ root, getIDs, onMove, onNudge }) {
let dragging = null
const itemOf = el => el && el.closest('[slides-strip-item][data-slide-id]')
function clearMarks () {
for (const li of root.querySelectorAll('[data-drop]')) delete li.dataset.drop
}
root.addEventListener('dragstart', e => {
const li = itemOf(e.target)
if (!li) return
dragging = li.dataset.slideId
li.dataset.dragging = 'true'
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', dragging)
})
root.addEventListener('dragover', e => {
const li = itemOf(e.target)
if (!li || !dragging) return
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
const r = li.getBoundingClientRect()
const vertical = getComputedStyle(root).flexDirection === 'column'
const before = vertical ? e.clientY < r.top + r.height / 2 : e.clientX < r.left + r.width / 2
clearMarks()
li.dataset.drop = before ? 'before' : 'after'
})
root.addEventListener('dragleave', e => {
if (!root.contains(e.relatedTarget)) clearMarks()
})
root.addEventListener('drop', e => {
const li = itemOf(e.target)
if (!li || !dragging) return
e.preventDefault()
const op = dropOp(getIDs(), dragging, li.dataset.slideId, li.dataset.drop || 'after')
clearMarks()
if (op) onMove(op)
})
root.addEventListener('dragend', () => {
for (const li of root.querySelectorAll('[data-dragging]')) delete li.dataset.dragging
clearMarks()
dragging = null
})
root.addEventListener('keydown', e => {
if (!e.altKey || (e.key !== 'ArrowUp' && e.key !== 'ArrowDown')) return
const li = itemOf(e.target)
if (!li) return
e.preventDefault()
onNudge(li.dataset.slideId, e.key === 'ArrowUp' ? 'up' : 'down')
})
}