Umsortiert wird gezogen, nicht geklickt
Die Pfeile sind weg; eine Mediathek wird in der Seitenleiste an ihren Platz gezogen. Mit Pointer Events statt HTML5-Drag: letzteres kennt kein Telefon, zeichnet ein Geisterbild, das sich nicht gestalten lässt, und liefert während des Ziehens keine brauchbaren Koordinaten. Mit Schwelle von sechs Bildpunkten — sonst verschiebt jeder leicht wackelige Klick die Reihenfolge, und ein Klick ohne Bewegung führt weiterhin zur Mediathek, auch im Bearbeitungsmodus. Die weichenden Zeilen gleiten, die gezogene folgt dem Zeiger ohne Verzögerung; bei „Bewegung reduzieren" gleitet nichts. Die Pfeiltasten bleiben auf der fokussierten Zeile, nur ohne sichtbare Knöpfe. Ziehen ist mit der Tastatur nicht bedienbar, und eine Funktion, die nur mit der Maus erreichbar ist, ist für den, der keine benutzt, gar keine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a22fa9b58e
commit
a8bb98c44c
+108
-31
@@ -147,8 +147,16 @@ function leisteBauen(): void {
|
||||
// die Entscheidung zurück, sie zu trennen.
|
||||
if (mediatheken.length) {
|
||||
gruppe('Mediatheken', istAdmin() && mediatheken.length > 1);
|
||||
|
||||
// **Ein eigener Behälter für die Mediatheken.** Beim Ziehen wird gerechnet,
|
||||
// die wievielte Zeile unter dem Zeiger liegt; stünden Überschriften und
|
||||
// „Start" dazwischen, ginge diese Rechnung schief.
|
||||
const liste = document.createElement('div');
|
||||
liste.className = 'nav-liste';
|
||||
leiste.appendChild(liste);
|
||||
|
||||
mediatheken.forEach((b, i) => eintrag(
|
||||
b.name, `#/m/${b.id}`, b.kind === 'Shows' ? 'serien' : 'filme', b.itemCount, i));
|
||||
b.name, `#/m/${b.id}`, b.kind === 'Shows' ? 'serien' : 'filme', b.itemCount, i, liste));
|
||||
}
|
||||
|
||||
if (istAdmin()) { gruppe('Server'); eintrag('Verwaltung', '#/verwaltung', 'verwaltung'); }
|
||||
@@ -184,7 +192,7 @@ function leisteBauen(): void {
|
||||
|
||||
function eintrag(
|
||||
text: string, ziel: string, bild: keyof typeof icons,
|
||||
anzahl?: number | null, platz?: number,
|
||||
anzahl?: number | null, platz?: number, behaelter?: HTMLElement,
|
||||
): void {
|
||||
const b = document.createElement('button');
|
||||
b.dataset.ziel = ziel;
|
||||
@@ -192,43 +200,112 @@ function leisteBauen(): void {
|
||||
+ `<span class="name">${esc(text)}</span>`
|
||||
+ (anzahl != null && !leisteBearbeiten ? `<span class="zahl">${anzahl}</span>` : '');
|
||||
|
||||
// Im Bearbeitungsmodus führt die Zeile nirgendwohin — sonst wechselt man
|
||||
// beim Zielen auf einen Pfeil versehentlich die Ansicht.
|
||||
if (leisteBearbeiten && platz != null) {
|
||||
b.classList.add('verschiebbar');
|
||||
b.appendChild(pfeil('hoch', platz, platz - 1, platz === 0, text));
|
||||
b.appendChild(pfeil('runter', platz, platz + 1, platz === mediatheken.length - 1, text));
|
||||
if (leisteBearbeiten && platz != null && behaelter) {
|
||||
b.classList.add('ziehbar');
|
||||
|
||||
// Der Griff links statt eines Knopfes rechts: er sagt „hier anfassen",
|
||||
// und er nimmt die Zeile nicht als Ganzes in Beschlag.
|
||||
const griff = document.createElement('span');
|
||||
griff.className = 'nav-griff icon';
|
||||
griff.innerHTML = icons.griff;
|
||||
b.prepend(griff);
|
||||
|
||||
// **Die Tastatur bleibt bedienbar.** Ziehen geht damit nicht, und eine
|
||||
// Funktion, die nur mit der Maus erreichbar ist, ist für den, der keine
|
||||
// benutzt, gar keine. Sichtbare Pfeile braucht es dafür nicht.
|
||||
b.setAttribute('aria-roledescription', 'Verschiebbarer Eintrag');
|
||||
b.title = `„${text}" ziehen — oder mit den Pfeiltasten verschieben`;
|
||||
b.onkeydown = e => {
|
||||
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
|
||||
e.preventDefault();
|
||||
void schieben(platz, platz + (e.key === 'ArrowDown' ? 1 : -1));
|
||||
};
|
||||
|
||||
ziehbarMachen(b, platz, behaelter);
|
||||
} else {
|
||||
b.onclick = () => { location.hash = ziel; };
|
||||
}
|
||||
|
||||
leiste.appendChild(b);
|
||||
(behaelter ?? leiste).appendChild(b);
|
||||
}
|
||||
}
|
||||
|
||||
/** Ein Pfeil zum Schieben.
|
||||
*
|
||||
* **Zwei Knöpfe statt Ziehen und Ablegen.** Ziehen ist schöner, aber mit
|
||||
* der Tastatur unbedienbar und auf dem Telefon ein Glücksspiel.
|
||||
*/
|
||||
function pfeil(
|
||||
richtung: 'hoch' | 'runter', von: number, nach: number, gesperrt: boolean, name: string,
|
||||
): HTMLElement {
|
||||
const p = document.createElement('span');
|
||||
p.className = 'nav-pfeil' + (gesperrt ? ' gesperrt' : '');
|
||||
p.setAttribute('role', 'button');
|
||||
if (!gesperrt) p.setAttribute('tabindex', '0');
|
||||
p.setAttribute('aria-label', `„${name}" nach ${richtung === 'hoch' ? 'oben' : 'unten'}`);
|
||||
p.innerHTML = icons.hoch;
|
||||
if (richtung === 'runter') p.style.transform = 'rotate(180deg)';
|
||||
/** Macht eine Zeile ziehbar.
|
||||
*
|
||||
* **Pointer Events statt HTML5-Drag.** Letzteres kennt kein Telefon, zeichnet
|
||||
* ein Geisterbild, das sich nicht gestalten lässt, und liefert während des
|
||||
* Ziehens keine brauchbaren Koordinaten. Pointer Events bedienen Maus, Finger
|
||||
* und Stift mit demselben Code.
|
||||
*
|
||||
* **Mit Schwelle.** Erst nach sechs Bildpunkten gilt es als Ziehen — sonst
|
||||
* verschiebt jeder leicht wackelige Klick die Reihenfolge.
|
||||
*/
|
||||
function ziehbarMachen(zeile: HTMLElement, platz: number, behaelter: HTMLElement): void {
|
||||
zeile.addEventListener('pointerdown', start);
|
||||
|
||||
const los = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
if (gesperrt) return;
|
||||
void schieben(von, nach);
|
||||
function start(e: PointerEvent): void {
|
||||
// Nur die linke Maustaste; rechts gehört dem Kontextmenü.
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
|
||||
const zeilen = [...behaelter.querySelectorAll<HTMLElement>('.ziehbar')];
|
||||
const hoehe = zeile.getBoundingClientRect().height
|
||||
+ parseFloat(getComputedStyle(behaelter).rowGap || '0');
|
||||
const startY = e.clientY;
|
||||
|
||||
let zieht = false;
|
||||
let ziel = platz;
|
||||
|
||||
zeile.setPointerCapture(e.pointerId);
|
||||
|
||||
const bewegen = (m: PointerEvent) => {
|
||||
const weg = m.clientY - startY;
|
||||
|
||||
if (!zieht) {
|
||||
if (Math.abs(weg) < 6) return;
|
||||
zieht = true;
|
||||
behaelter.classList.add('zieht');
|
||||
zeile.classList.add('wird-gezogen');
|
||||
}
|
||||
|
||||
// Wohin die Zeile fiele, wenn man jetzt losliesse.
|
||||
ziel = Math.max(0, Math.min(zeilen.length - 1, platz + Math.round(weg / hoehe)));
|
||||
|
||||
zeile.style.transform = `translateY(${weg}px)`;
|
||||
|
||||
// Die anderen machen Platz — nach oben oder unten, je nachdem, auf
|
||||
// welcher Seite sie liegen.
|
||||
zeilen.forEach((andere, i) => {
|
||||
if (andere === zeile) return;
|
||||
let versatz = 0;
|
||||
if (platz < ziel && i > platz && i <= ziel) versatz = -hoehe;
|
||||
else if (platz > ziel && i >= ziel && i < platz) versatz = hoehe;
|
||||
andere.style.transform = versatz ? `translateY(${versatz}px)` : '';
|
||||
});
|
||||
};
|
||||
p.onclick = los;
|
||||
p.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); los(e); } };
|
||||
return p;
|
||||
|
||||
const beenden = () => {
|
||||
zeile.removeEventListener('pointermove', bewegen);
|
||||
zeile.removeEventListener('pointerup', beenden);
|
||||
zeile.removeEventListener('pointercancel', beenden);
|
||||
|
||||
behaelter.classList.remove('zieht');
|
||||
zeilen.forEach(z => { z.style.transform = ''; z.classList.remove('wird-gezogen'); });
|
||||
|
||||
// Ohne Ziehen war es ein Klick — und der führt zur Mediathek, auch im
|
||||
// Bearbeitungsmodus. Sonst wäre die Leiste dort unbenutzbar.
|
||||
if (!zieht) {
|
||||
const weg = zeile.dataset.ziel;
|
||||
if (weg) location.hash = weg;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ziel !== platz) void schieben(platz, ziel);
|
||||
};
|
||||
|
||||
zeile.addEventListener('pointermove', bewegen);
|
||||
zeile.addEventListener('pointerup', beenden);
|
||||
zeile.addEventListener('pointercancel', beenden);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user