// Zeilen per Anfasser umsortieren, ohne Fremdbibliothek. // // Umgesetzt mit Pointer Events statt der HTML5-Drag-and-Drop-API: Letztere // funktioniert auf Touchgeräten praktisch nicht, und die App wird // überwiegend am Telefon bedient. // // Verfahren: Das gezogene Element folgt dem Finger per transform. Sobald // es den Mittelpunkt eines Nachbarn überschreitet, wird es im DOM davor // oder dahinter einsortiert und der Bezugspunkt zurückgesetzt - dadurch // springt nichts. "use strict"; const DRAG_THRESHOLD = 4; // Pixel, bevor aus einem Tippen ein Ziehen wird /** * @param {HTMLElement} container Elternknoten der sortierbaren Zeilen * @param {object} options * @param {string} options.handleSelector Auswahl für den Anfasser * @param {string} options.itemSelector Auswahl für eine Zeile * @param {(ids: string[]) => void} options.onReorder neue Reihenfolge */ export function makeSortable(container, { handleSelector, itemSelector, onReorder }) { let dragging = null; let startY = 0; let offset = 0; let moved = false; let originalOrder = []; const rows = () => [...container.querySelectorAll(itemSelector)]; const idsOf = () => rows().map((r) => r.dataset.id); function begin(ev) { const handle = ev.target.closest(handleSelector); if (!handle || !container.contains(handle)) return; if (ev.button !== undefined && ev.button !== 0) return; dragging = handle.closest(itemSelector); if (!dragging) return; startY = ev.clientY; offset = 0; moved = false; originalOrder = idsOf(); handle.setPointerCapture(ev.pointerId); ev.preventDefault(); } function move(ev) { if (!dragging) return; offset = ev.clientY - startY; if (!moved && Math.abs(offset) < DRAG_THRESHOLD) return; if (!moved) { moved = true; dragging.classList.add("dragging"); container.classList.add("sorting"); } dragging.style.transform = `translateY(${offset}px)`; const box = dragging.getBoundingClientRect(); const middle = box.top + box.height / 2; const previous = dragging.previousElementSibling; const next = dragging.nextElementSibling; if (previous && middle < previous.getBoundingClientRect().top + previous.offsetHeight / 2) { container.insertBefore(dragging, previous); reanchor(ev); } else if (next && middle > next.getBoundingClientRect().top + next.offsetHeight / 2) { container.insertBefore(next, dragging); reanchor(ev); } } /** Nach einem Umsortieren sitzt das Element an neuer Stelle. Ohne das * Zurücksetzen des Bezugspunkts würde es um seine eigene Höhe springen. */ function reanchor(ev) { startY = ev.clientY; offset = 0; dragging.style.transform = ""; } function end() { if (!dragging) return; dragging.style.transform = ""; dragging.classList.remove("dragging"); container.classList.remove("sorting"); const wasMoved = moved; dragging = null; moved = false; if (!wasMoved) return; const order = idsOf(); if (order.join() !== originalOrder.join()) onReorder(order); } container.addEventListener("pointerdown", begin); container.addEventListener("pointermove", move); container.addEventListener("pointerup", end); container.addEventListener("pointercancel", end); // Bedienbar auch ohne Zeigegerät: Anfasser fokussieren, dann Pfeiltasten. container.addEventListener("keydown", (ev) => { const handle = ev.target.closest(handleSelector); if (!handle) return; if (ev.key !== "ArrowUp" && ev.key !== "ArrowDown") return; const row = handle.closest(itemSelector); const before = idsOf(); if (ev.key === "ArrowUp" && row.previousElementSibling) { container.insertBefore(row, row.previousElementSibling); } else if (ev.key === "ArrowDown" && row.nextElementSibling) { container.insertBefore(row.nextElementSibling, row); } else { return; } ev.preventDefault(); handle.focus(); const after = idsOf(); if (after.join() !== before.join()) onReorder(after); }); }