Die Kachel zeigte auf die Folge und wollte zur Serie
„Nächste Folge" führte ins Leere, und es lag weder am Scan noch am Auffrischen: die Kachel trug die Folgenkennung und öffnete damit eine Serienseite — die es unter dieser Kennung nie gibt. Heraus kam ausgerechnet dort „Diese Serie gibt es nicht mehr", wo man gerade weiterschauen wollte. Der Server kannte die Serie an beiden Stellen und schickte sie nur nicht mit. Wohin die Kachel führt und was sie misst, ist jetzt getrennt. Beides auf dieselbe Kennung zu legen wäre der nächste Fehler gewesen: „Als gesehen markieren" hätte die ganze Serie abgehakt statt der einen Folge, die man zur Hälfte gesehen hat. Dazu Streichen wie im Dateimanager: gedrückt halten und darüberfahren wählt aus, rückwärts über Gewähltes nimmt wieder weg. Ein kurzer Klick bleibt, was er war. Abgetastet wird die Strecke zwischen zwei Zeigerereignissen, nicht nur deren Endpunkte — gemessen: ein Zug über vier Kacheln erwischte sonst die erste und die letzte. Plakate lassen sich nicht mehr durch die Gegend schleppen. „Abwählen" ist ein Kreuz geworden; als vierter Knopf neben drei Aktionen sah es aus, als täte es etwas mit den Titeln. In der kleinen Leiste lag das Vorschaubild über dem Fortschrittsstrich und verdeckte ihn rechts. Und ein Klick genau darauf tat nichts, obwohl das die Stelle ist, auf die man zeigt — jetzt holt auch das Bild den Film zurück. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7df9fa6c01
commit
c46063637f
@@ -239,6 +239,15 @@ export function mediathek(
|
||||
const k = ziel.closest<HTMLElement>('.kachel');
|
||||
if (!k || !gitter.contains(k)) return;
|
||||
|
||||
// **Nach einem Streichen kommt noch ein Klick.** Der Browser schickt ihn
|
||||
// auf die Kachel, auf der die Maus losgelassen wurde — und der würde
|
||||
// genau das wieder umschalten, was gerade gewählt wurde.
|
||||
if (geradeGestrichen) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Das Kästchen wählt immer. Die Kachel selbst nur dann, wenn schon
|
||||
// etwas gewählt ist — sonst bliebe die Mediathek unbenutzbar.
|
||||
if (!ziel.closest('.wahlmarke') && gewaehlt.size === 0) return;
|
||||
@@ -248,6 +257,166 @@ export function mediathek(
|
||||
umschalten(k);
|
||||
}, true);
|
||||
|
||||
/* ---------- Streichen wie im Dateimanager ----------
|
||||
*
|
||||
* **Gedrückt halten und darüberfahren wählt aus.**
|
||||
*
|
||||
* Ein kurzer Klick bleibt, was er war: die Detailseite. Erst eine
|
||||
* Bewegung über ein paar Bildpunkte hinaus macht daraus ein Streichen —
|
||||
* dieselbe Unterscheidung, die jeder Dateimanager trifft, und deshalb
|
||||
* muss sie niemand lernen.
|
||||
*
|
||||
* **Was die erste Kachel wird, werden alle.** Fängt man auf einer
|
||||
* ungewählten an, wählt der ganze Zug aus; fängt man auf einer gewählten
|
||||
* an, nimmt er weg. Jede einzeln umzuschalten hiesse, dass ein Zug über
|
||||
* eine gemischte Reihe sie nur vertauscht — und das will niemand.
|
||||
*/
|
||||
|
||||
/** Wie weit die Maus muss, bevor aus dem Klick ein Streichen wird. */
|
||||
const SCHWELLE = 6;
|
||||
|
||||
/** Wie nah an den Rand, bevor die Seite mitwandert. */
|
||||
const RANDBEREICH = 72;
|
||||
|
||||
/** In welchen Abständen die Strecke zwischen zwei Ereignissen abgetastet wird.
|
||||
*
|
||||
* **Kleiner als die schmalste Kachel.** Bei 150 Bildpunkten Breite liesse
|
||||
* sich mit 48 keine überspringen — 24 ist der halbe Weg dahin und kostet
|
||||
* nichts, weil `elementFromPoint` nur nachschlägt. */
|
||||
const ABTASTUNG = 24;
|
||||
|
||||
let geradeGestrichen = false;
|
||||
let zug: {
|
||||
anschalten: boolean;
|
||||
losX: number; losY: number;
|
||||
losKachel: HTMLElement;
|
||||
begonnen: boolean;
|
||||
zeiger: number;
|
||||
/** Wo der Zeiger zuletzt war — für die Strecke dazwischen. */
|
||||
letztX: number; letztY: number;
|
||||
} | null = null;
|
||||
let randtakt = 0;
|
||||
|
||||
gitter.addEventListener('pointerdown', e => {
|
||||
// **Jede neue Berührung fängt sauber an.**
|
||||
//
|
||||
// Die Sperre hier zurückzusetzen und nicht erst im Klick ist der
|
||||
// Unterschied zwischen „funktioniert" und „funktioniert meistens":
|
||||
// ging der Klick nach einem Streichen woandershin — auf das Kreuz der
|
||||
// Auswahlleiste etwa —, blieb sie stehen, und der nächste Klick auf
|
||||
// eine Kachel öffnete nichts mehr. Genau so gemessen.
|
||||
geradeGestrichen = false;
|
||||
|
||||
// **Nur die Maus.** Auf einem Berührungsbildschirm wäre jedes Wischen
|
||||
// zum Blättern ein Streichen über die Kacheln — die Liste liesse sich
|
||||
// nicht mehr scrollen.
|
||||
if (e.pointerType !== 'mouse' || e.button !== 0) return;
|
||||
|
||||
const k = (e.target as HTMLElement).closest<HTMLElement>('.kachel');
|
||||
if (!k || !gitter.contains(k) || !k.dataset.id) return;
|
||||
|
||||
zug = {
|
||||
anschalten: !gewaehlt.has(k.dataset.id),
|
||||
losX: e.clientX, losY: e.clientY,
|
||||
losKachel: k,
|
||||
begonnen: false,
|
||||
zeiger: e.pointerId,
|
||||
letztX: e.clientX, letztY: e.clientY,
|
||||
};
|
||||
|
||||
window.addEventListener('pointermove', beiZug);
|
||||
window.addEventListener('pointerup', zugEnde);
|
||||
window.addEventListener('pointercancel', zugEnde);
|
||||
});
|
||||
|
||||
function beiZug(e: PointerEvent): void {
|
||||
if (!zug || e.pointerId !== zug.zeiger) return;
|
||||
|
||||
if (!zug.begonnen) {
|
||||
if (Math.abs(e.clientX - zug.losX) < SCHWELLE
|
||||
&& Math.abs(e.clientY - zug.losY) < SCHWELLE) return;
|
||||
|
||||
zug.begonnen = true;
|
||||
gitter.classList.add('zieht');
|
||||
// Ohne das markiert der Browser nebenher die Beschriftungen blau.
|
||||
document.body.classList.add('keine-auswahl');
|
||||
setzen(zug.losKachel, zug.anschalten);
|
||||
}
|
||||
|
||||
// **Die Strecke zählt, nicht nur der Punkt.**
|
||||
//
|
||||
// Ein Zeigerereignis kommt nicht für jeden Bildpunkt: wer schnell
|
||||
// streicht, springt mit einem Sprung über zwei Kacheln hinweg, und die
|
||||
// blieben ungewählt. Gemessen beim Prüfen: ein Zug über vier Kacheln
|
||||
// erwischte die erste und die letzte. Deshalb wird die Strecke seit dem
|
||||
// letzten Mal abgeschritten.
|
||||
//
|
||||
// Nicht `e.target`: der Zeiger ist gedrückt, und damit gehen alle
|
||||
// Ereignisse an die Kachel, auf der es angefangen hat.
|
||||
const weitX = e.clientX - zug.letztX;
|
||||
const weitY = e.clientY - zug.letztY;
|
||||
const strecke = Math.hypot(weitX, weitY);
|
||||
const schritte = Math.max(1, Math.ceil(strecke / ABTASTUNG));
|
||||
|
||||
for (let i = 1; i <= schritte; i++) {
|
||||
const x = zug.letztX + (weitX * i) / schritte;
|
||||
const y = zug.letztY + (weitY * i) / schritte;
|
||||
const k = document.elementFromPoint(x, y)?.closest<HTMLElement>('.kachel');
|
||||
if (k && gitter.contains(k)) setzen(k, zug.anschalten);
|
||||
}
|
||||
|
||||
zug.letztX = e.clientX;
|
||||
zug.letztY = e.clientY;
|
||||
randlauf(e.clientY);
|
||||
}
|
||||
|
||||
function zugEnde(e: PointerEvent): void {
|
||||
if (zug && e.pointerId !== zug.zeiger) return;
|
||||
|
||||
window.removeEventListener('pointermove', beiZug);
|
||||
window.removeEventListener('pointerup', zugEnde);
|
||||
window.removeEventListener('pointercancel', zugEnde);
|
||||
window.cancelAnimationFrame(randtakt);
|
||||
randtakt = 0;
|
||||
|
||||
gitter.classList.remove('zieht');
|
||||
document.body.classList.remove('keine-auswahl');
|
||||
geradeGestrichen = zug?.begonnen === true;
|
||||
zug = null;
|
||||
}
|
||||
|
||||
/** Setzt eine Kachel auf einen Wert — ohne hin und her zu schalten. */
|
||||
function setzen(k: HTMLElement, an: boolean): void {
|
||||
const id = k.dataset.id;
|
||||
if (!id || gewaehlt.has(id) === an) return;
|
||||
|
||||
if (an) gewaehlt.add(id); else gewaehlt.delete(id);
|
||||
k.classList.toggle('gewaehlt', an);
|
||||
k.querySelector('.wahlmarke')?.setAttribute('aria-checked', String(an));
|
||||
nachAuswahl();
|
||||
}
|
||||
|
||||
/** Streicht man an den Rand, wandert die Seite mit.
|
||||
*
|
||||
* **Sonst endet jede Auswahl am unteren Bildrand.** Bei 447 Titeln sind
|
||||
* das zwei Reihen von vierzig. */
|
||||
function randlauf(y: number): void {
|
||||
window.cancelAnimationFrame(randtakt);
|
||||
if (!zug?.begonnen) return;
|
||||
|
||||
const oben = y - RANDBEREICH;
|
||||
const unten = y - (window.innerHeight - RANDBEREICH);
|
||||
const schritt = oben < 0 ? Math.max(-24, oben / 3)
|
||||
: unten > 0 ? Math.min(24, unten / 3)
|
||||
: 0;
|
||||
if (schritt === 0) return;
|
||||
|
||||
randtakt = window.requestAnimationFrame(() => {
|
||||
window.scrollBy(0, schritt);
|
||||
randlauf(y);
|
||||
});
|
||||
}
|
||||
|
||||
// Dieselbe Geste für die Tastatur.
|
||||
gitter.addEventListener('keydown', e => {
|
||||
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||
@@ -312,12 +481,19 @@ export function mediathek(
|
||||
}));
|
||||
}
|
||||
|
||||
// **Abwählen bleibt immer anklickbar** — es ist der Weg heraus.
|
||||
const wegKnopf = knopf('Abwählen', 'btn');
|
||||
// **Ein Kreuz, kein Wort.**
|
||||
//
|
||||
// „Abwählen" stand neben drei Knöpfen, die etwas tun — und sah damit aus
|
||||
// wie ein vierter davon. Das Kreuz sagt, was es ist: die Auswahl
|
||||
// schliessen. Es steht abgesetzt am Rand, nicht in der Reihe.
|
||||
const wegKnopf = document.createElement('button');
|
||||
wegKnopf.className = 'auswahl-zu';
|
||||
wegKnopf.innerHTML = icons.schliessen;
|
||||
wegKnopf.title = 'Auswahl schliessen';
|
||||
wegKnopf.setAttribute('aria-label', 'Auswahl schliessen');
|
||||
wegKnopf.onclick = abwaehlen;
|
||||
knoepfe.appendChild(wegKnopf);
|
||||
|
||||
b.append(zahl, knoepfe);
|
||||
b.append(zahl, knoepfe, wegKnopf);
|
||||
|
||||
return {
|
||||
element: b,
|
||||
|
||||
Reference in New Issue
Block a user