Erste Produktivversion
This commit is contained in:
130
web/html/js/sortable.js
Normal file
130
web/html/js/sortable.js
Normal file
@@ -0,0 +1,130 @@
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user