Telefonieren ging nur ueber UDP -- und die Seite sprang immer hoch
=== 1. WARUM TELEFONIEREN NICHT GING ===
Filipe: "es klingelt erscheint auch alles bei jedem aber telefonieren
klappt immer noch nicht."
GEMESSEN, BEVOR ETWAS GEAENDERT WURDE -- und alles war gruen:
coturn laeuft aktiv
STUN von aussen antwortet, nennt meine oeffentliche Adresse
echte Zuteilung von aussen 7/7, Testpaket kam an (Port 49183)
Dreier-Anruf im Prueflauf verbindet, alle hoeren alle
Vier Messungen, vier Mal in Ordnung -- und das Telefon ging trotzdem
nicht. Der Grund: Der Prueflauf faehrt BEIDE Seiten auf demselben
Rechner. Dort finden sie sich ueber die direkte Adresse, und die
Vermittlung wird nie gebraucht. Draussen sitzen sie in verschiedenen
Netzen.
DER FUND: Eingetragen war eine einzige Adresse --
turn:159.195.212.167:3478
Ein `turn:` OHNE `?transport=` heisst UDP, und nur UDP. Nachgemessen
hoert coturn aber auf beidem: 3478/UDP offen, 3478/TCP offen (5349/TLS
ist zu). Angeboten wurde nur der halbe Server.
Wer in einem Netz sitzt, das UDP nach draussen sperrt -- Mobilfunk mit
strengem Profil, Gast-WLAN, Firmennetz --, bekam damit KEINEN
Vermittlungsweg. Es klingelt (das laeuft ueber die Website, also ueber
443), und danach passiert nichts. Genau das gemeldete Bild.
Aus einem Eintrag werden jetzt drei Wege:
stun:host:port die eigene Adresse finden, ohne Vermittlung
turn:host:port?transport=udp der schnelle Weg
turn:host:port?transport=tcp der Weg durch fast jede Sperre
ABGELEITET, NICHT EINGETRAGEN: Drei Zeilen von Hand waeren drei
Stellen, an die beim naechsten Serverumzug jemand denken muesste --
die abgeschriebene Liste, die hier schon zweimal teuer war. Ein
Eintrag MIT `?transport=` bleibt unangetastet, ein fremder Dienst mit
eigenem Passwort sowieso.
pruef-turn-wege (neu, 15/0) sichert beides: dass jeder Weg herauskommt
UND dass jeder turn:-Weg Zugangsdaten traegt. Das Zweite ist das
wichtigere -- auffaechern ohne anmelden haette den Fehler nur
verschoben.
=== 2. DIE SEITE SPRANG BEIM ZURUECKGEHEN IMMER HOCH ===
Filipe: "das nervt man muss dan immer wieder runter scrollen bis man
da ist wo man vorher war."
Der Browser versucht es sogar -- scrollRestoration steht ab Werk auf
"auto". Nur: Jede Seite hier kommt fast leer an und holt ihren Inhalt
danach per Abruf. In dem Moment, in dem der Browser die alte Position
wiederherstellen will, ist das Dokument ein paar hundert Pixel hoch.
Er kann nicht auf Zeile 900 springen, die es noch nicht gibt -- und er
versucht es kein zweites Mal.
Jetzt macht es kopf.js selbst (gilt damit auf allen 21 Seiten): Stelle
merken beim Verlassen, beim Oeffnen zurueckholen und jeden Bildaufbau
lang versuchen, bis das Dokument hoch genug ist. Nach drei Sekunden
wird aufgegeben.
Drei Dinge sind Absicht:
- NUR beim Zurueckgehen, nicht bei jedem Oeffnen. Wer eine Kachel
anklickt, will oben anfangen.
- WER SELBST SCROLLT, GEWINNT. Rad, Wisch oder Taste beenden das
Nachspringen sofort.
- Nach einer Stunde vergessen -- auf Zeile 900 zu landen, weil man
gestern dort war, ist keine Hilfe.
=== 3. DIE LISTE IST NACH ROLLE GETRENNT ===
Filipe: "die rechte hand rolle immer zuerst und dan die modis. die
sollen auch schoen getrennt sein und verschieden aussehen also die
rechte hand viel spezieller."
Die Reihenfolge macht der Server ueber ROLLEN_SORTIERUNG -- dieselbe
Konstante wie ueberall sonst im Haus, nicht eine zweite. Eine
Sortierung nach ROLLE ist ausdruecklich keine Rangliste: Sie sagt
nichts darueber, wie gut jemand ist, nur welche Aufgabe er hat.
Deshalb steht ueber jeder Gruppe ein Satz und keine Zahl.
"Spezieller" heisst hier Material, nicht Groesse: eigener Farbton als
Kante und Schimmer, hellere Flaeche, kraeftigerer Name. Beide Karten
sind GLEICH GROSS und tragen dieselben Zeilen -- zwei Sorten Aufgabe,
keine Rangfolge. Im Kontrastmodus traegt eine doppelte Kante die
Unterscheidung, weil dort keine Farbe mehr wirkt.
=== 4. DER ANRUFKASTEN ===
Vorher: vier gleich aussehende Pillen nebeneinander, darunter eine
Geraeteauswahl, die das breiteste Element war. Kein Name, keine
Ordnung, und der einzige Knopf mit Folgen sah aus wie die anderen.
- MAN SAH NICHT, MIT WEM. Der wichtigste Satz eines Telefonats fehlte.
Der Chat kennt den Namen und gibt ihn jetzt mit; wer rangeht, sieht
den Anrufer.
- "MIKRO AN" WAR ZWEIDEUTIG -- "ist an" oder "schalt an"? Wer falsch
raet, sitzt stumm da. Jetzt traegt ein durchgestrichenes Zeichen
den Zustand, das Wort nur die Sache. aria-pressed bleibt die
Wahrheit, auch fuer Vorleseprogramme.
- DIE GERAETEAUSWAHL liegt hinter einem kleinen Knopf, der nur
erscheint, wenn es ueberhaupt etwas zu waehlen gibt.
- AUFLEGEN ist als einziger Knopf farbig und steht immer rechts.
- Ein ruhiger Puls atmet, solange die Verbindung aufbaut, und haelt
an, sobald sie steht. Bei prefers-reduced-motion steht er still.
=== NEBENBEFUND, DEN DIE PRUEFUNG GEFUNDEN HAT ===
pruef-anruf wurde durch die Auffaecherung an vier Stellen rot: Sie
griff `adressen[0]` und erwartete dort Zugangsdaten. Das ist jetzt der
STUN-Weg, und der hat richtigerweise keine. Gesucht wird nicht mehr
nach PLATZ, sondern nach ART -- ein Index ist eine Annahme darueber,
wie die Liste aussieht, und die hat sich gerade geaendert.
Dazu angepasst: Die Pruefung "die Uhr laeuft" verlangte, dass die Uhr
schon beim Klingeln zaehlt. Seit heute frueh beginnt sie beim
Rangehen (sonst standen im Kasten und im Chat zwei verschiedene
Zahlen). Sie prueft jetzt das Gegenteil -- statt sie zu streichen,
denn eine Zeile weniger haette den Fehler beim naechsten Umbau wieder
durchgelassen.
GEMESSEN: pruef-anruf 114/0 (vorher 111 -- drei MEHR, weil die
Auffaecherung mitgeprueft wird), pruef-turn-wege 15/0,
pruef-meldungen 8/0, pruef-css-klassen ALLES IN ORDNUNG,
pruef-rechtetafel 19/0.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+143
-26
@@ -110,20 +110,86 @@
|
||||
k.setAttribute('role', 'dialog');
|
||||
k.setAttribute('aria-label', 'Anruf');
|
||||
k.innerHTML = `
|
||||
<!-- ============================================================
|
||||
NEU GEBAUT AM 19.09.2026.
|
||||
|
||||
Filipe: „das soll viel besser und geiler aussehen bitte."
|
||||
|
||||
Was vorher nicht stimmte, der Reihe nach:
|
||||
|
||||
1. MAN SAH NICHT, MIT WEM. Oben stand „Es klingelt …" und
|
||||
sonst nichts. Der wichtigste Satz eines Telefonats ist,
|
||||
wen man am Apparat hat.
|
||||
|
||||
2. „MIKRO AN" WAR ZWEIDEUTIG. Ein Knopf mit dieser
|
||||
Aufschrift kann heißen „das Mikro IST an" oder „drück
|
||||
hier, um es anzuschalten". Beides ist plausibel, und wer
|
||||
falsch rät, schaltet sich stumm, ohne es zu merken.
|
||||
Jetzt trägt jeder Knopf ein Zeichen für den Zustand UND
|
||||
ein Wort für die Sache — „Mikro" mit durchgestrichenem
|
||||
Zeichen heißt aus, fertig.
|
||||
|
||||
3. DIE GERÄTEAUSWAHL WAR DAS GRÖSSTE ELEMENT im Kasten,
|
||||
obwohl man sie einmal im Jahr braucht. Sie liegt jetzt
|
||||
hinter einem kleinen Knopf.
|
||||
|
||||
4. ES GAB KEINE ORDNUNG. Vier gleich aussehende Pillen
|
||||
nebeneinander, darunter „Auflegen" in derselben Form.
|
||||
Jetzt: die beiden Schalter links, Auflegen rechts und
|
||||
als einziger Knopf mit Farbe.
|
||||
============================================================= -->
|
||||
<div class="anruf__kopf">
|
||||
<span class="anruf__lage" id="anruf-lage">Verbindung …</span>
|
||||
<span class="anruf__puls" id="anruf-puls" aria-hidden="true"></span>
|
||||
<span class="anruf__kopftext">
|
||||
<span class="anruf__wer" id="anruf-wer">Anruf</span>
|
||||
<span class="anruf__lage" id="anruf-lage">Verbindung …</span>
|
||||
</span>
|
||||
<span class="anruf__zeit" id="anruf-zeit"></span>
|
||||
</div>
|
||||
<div class="anruf__leute" id="anruf-leute"></div>
|
||||
<div class="anruf__knoepfe">
|
||||
<button type="button" class="anruf__knopf anruf__knopf--ton" id="anruf-ton"
|
||||
aria-pressed="true">Mikro an</button>
|
||||
<button type="button" class="anruf__knopf anruf__knopf--hoeren" id="anruf-hoeren"
|
||||
aria-pressed="true">Lautsprecher an</button>
|
||||
<button type="button" class="anruf__knopf anruf__knopf--video" id="anruf-video"
|
||||
aria-pressed="false" hidden>Kamera an</button>
|
||||
<button type="button" class="anruf__knopf anruf__knopf--auf" id="anruf-auflegen">Auflegen</button>
|
||||
<button type="button" class="anruf__schalter" id="anruf-ton" aria-pressed="true">
|
||||
<svg class="anruf__zeichen" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 3.5a3 3 0 0 1 3 3v5a3 3 0 0 1-6 0v-5a3 3 0 0 1 3-3z"/>
|
||||
<path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21"/>
|
||||
<path class="anruf__strich" d="M4 4l16 16"/>
|
||||
</svg>
|
||||
<span class="anruf__schalter-text">Mikro</span>
|
||||
</button>
|
||||
<button type="button" class="anruf__schalter" id="anruf-hoeren" aria-pressed="true">
|
||||
<svg class="anruf__zeichen" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4 9.5h3.5L12 5.5v13L7.5 14.5H4z"/>
|
||||
<path d="M16 9a4.5 4.5 0 0 1 0 6M18.5 6.5a8 8 0 0 1 0 11"/>
|
||||
<path class="anruf__strich" d="M4 4l16 16"/>
|
||||
</svg>
|
||||
<span class="anruf__schalter-text">Ton</span>
|
||||
</button>
|
||||
<button type="button" class="anruf__schalter" id="anruf-video"
|
||||
aria-pressed="false" hidden>
|
||||
<svg class="anruf__zeichen" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M3.5 7h11v10h-11z"/><path d="M14.5 11l6-3.5v9l-6-3.5z"/>
|
||||
<path class="anruf__strich" d="M4 4l16 16"/>
|
||||
</svg>
|
||||
<span class="anruf__schalter-text">Kamera</span>
|
||||
</button>
|
||||
<button type="button" class="anruf__mehr" id="anruf-mehr"
|
||||
aria-expanded="false" aria-label="Toneinstellungen">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/>
|
||||
<circle cx="12" cy="19" r="1.6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="anruf__auf" id="anruf-auflegen">
|
||||
<svg class="anruf__zeichen" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M3.2 10.2c5-3.4 12.6-3.4 17.6 0l-2 3.2-4.2-1.1-.4-2.6a13 13 0 0 0-4.4 0l-.4 2.6-4.2 1.1z"/>
|
||||
</svg>
|
||||
<span>Auflegen</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Die Geräteauswahl wird erst gebaut, wenn es mehr als ein
|
||||
Ausgabegerät gibt (siehe ausgabeAnbieten). Sie liegt hier in
|
||||
einer eigenen Zeile, damit sie die Knopfreihe nicht sprengt. -->
|
||||
<div class="anruf__mehrzeile" id="anruf-mehrzeile" hidden></div>
|
||||
<!-- DER WEG ZUR PROBE (19.09.2026).
|
||||
|
||||
Filipe: „geht es nicht auf der app?" -- Nein, und das war
|
||||
@@ -137,6 +203,16 @@
|
||||
findet sie. -->
|
||||
<a class="anruf__probe" id="anruf-probe" href="anruf-probe.html">Klingelt nichts?</a>`;
|
||||
document.body.append(k);
|
||||
/* Die Toneinstellungen liegen hinter einem kleinen Knopf: Man
|
||||
braucht sie einmal im Jahr, und vorher war die Geraeteauswahl
|
||||
das groesste Element im Kasten. */
|
||||
$('anruf-mehr').hidden = true;
|
||||
$('anruf-mehr').addEventListener('click', () => {
|
||||
const z = $('anruf-mehrzeile');
|
||||
const auf = z.hidden;
|
||||
z.hidden = !auf;
|
||||
$('anruf-mehr').setAttribute('aria-expanded', String(auf));
|
||||
});
|
||||
$('anruf-ton').addEventListener('click', tonSchalten);
|
||||
$('anruf-hoeren').addEventListener('click', hoerenSchalten);
|
||||
$('anruf-video').addEventListener('click', videoSchalten);
|
||||
@@ -146,6 +222,25 @@
|
||||
|
||||
const lage = (text) => { const l = $('anruf-lage'); if (l) l.textContent = text; };
|
||||
|
||||
/** Wen habe ich am Apparat? Steht ganz oben und gross.
|
||||
*
|
||||
* DER WICHTIGSTE SATZ EINES TELEFONATS. Vorher stand dort nur „Es
|
||||
* klingelt …" -- man sah nicht, wen man anruft. */
|
||||
const wer = (text) => { const w = $('anruf-wer'); if (w) w.textContent = text; };
|
||||
|
||||
/** Einen Schalter auf an/aus stellen.
|
||||
*
|
||||
* Die Aufschrift bleibt IMMER dieselbe (die Sache), der Zustand
|
||||
* steht in `aria-pressed` und wird vom Zeichen getragen -- ein
|
||||
* durchgestrichenes Mikrofon heisst aus. Fuer Vorleseprogramme
|
||||
* kommt der Zustand ueber aria-label mit, denn ein Strich im Bild
|
||||
* ist nichts, was man hoeren kann. */
|
||||
function schalterSetzen(k, an, sache) {
|
||||
if (!k) return;
|
||||
k.setAttribute('aria-pressed', String(!!an));
|
||||
k.setAttribute('aria-label', `${sache} ${an ? 'an' : 'aus'} – zum Umschalten antippen`);
|
||||
}
|
||||
|
||||
/* ---------- Die Gegenstelle ------------------------------------------- */
|
||||
|
||||
function verbindungFuer(wer) {
|
||||
@@ -177,6 +272,10 @@
|
||||
tonAus();
|
||||
klingelWache(); // niemand-geht-ran-Wecker abbestellen
|
||||
if (anruf && !anruf.beginn) anruf.beginn = Date.now();
|
||||
/* Der Puls hoert auf zu atmen, sobald die Verbindung steht --
|
||||
eine Bewegung, die nie aufhoert, wird nach zwei Minuten zur
|
||||
Belaestigung. */
|
||||
$('anruf')?.setAttribute('data-steht', 'ja');
|
||||
lage('Verbunden');
|
||||
}
|
||||
/* `failed` heisst: Es kommt nichts mehr. Das gehoert gesagt --
|
||||
@@ -284,7 +383,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function starten(raumId, mitVideo) {
|
||||
async function starten(raumId, mitVideo, raumName) {
|
||||
/* WEN RUFE ICH AN? Der Chat weiss es und gibt es mit. Faellt
|
||||
der Name aus (aelterer Aufruf), bleibt es bei „Anruf" --
|
||||
das ist immer noch richtig, nur weniger. */
|
||||
wer(raumName || 'Anruf');
|
||||
if (anruf) return;
|
||||
kastenBauen();
|
||||
$('anruf').hidden = false;
|
||||
@@ -380,6 +483,10 @@
|
||||
$('anruf-video').hidden = !d.video;
|
||||
if (d.video) $('anruf-video').setAttribute('aria-pressed', 'true');
|
||||
klingelnAus();
|
||||
/* Wer angerufen hat, steht im Klingel-Ereignis und wurde dort
|
||||
schon gemerkt -- sonst staende beim Angerufenen „Anruf" und
|
||||
beim Anrufer der Name, also zweierlei fuer dasselbe Gespraech. */
|
||||
wer(letzterAnrufer || 'Anruf');
|
||||
lage('Verbindung …');
|
||||
uhrLaufen();
|
||||
|
||||
@@ -397,6 +504,8 @@
|
||||
|
||||
async function auflegen(melden) {
|
||||
tonAus();
|
||||
klingelWache();
|
||||
$('anruf')?.removeAttribute('data-steht');
|
||||
klingelnAus();
|
||||
if (!anruf) { const k = $('anruf'); if (k) k.hidden = true; return; }
|
||||
const raumId = anruf.raumId;
|
||||
@@ -472,9 +581,12 @@
|
||||
if (!anruf) return;
|
||||
const an = anruf.eigener.getAudioTracks().some((s) => s.enabled);
|
||||
for (const s of anruf.eigener.getAudioTracks()) s.enabled = !an;
|
||||
const k = $('anruf-ton');
|
||||
k.textContent = an ? 'Mikro aus' : 'Mikro an';
|
||||
k.setAttribute('aria-pressed', String(!an));
|
||||
/* DER ZUSTAND STEHT IN aria-pressed, NICHT IM TEXT (19.09.2026).
|
||||
Ein Knopf mit der Aufschrift „Mikro an" kann zweierlei heissen --
|
||||
„ist an" oder „schalt es an". Wer falsch raet, sitzt stumm da.
|
||||
Jetzt sagt das durchgestrichene Zeichen den Zustand, und das
|
||||
Wort sagt nur, worum es geht. */
|
||||
schalterSetzen($('anruf-ton'), !an, 'Mikrofon');
|
||||
}
|
||||
|
||||
/* ---------- DER LAUTSPRECHER (18.09.2026) ------------------------------
|
||||
@@ -529,11 +641,7 @@
|
||||
|
||||
function hoerenSchalten() {
|
||||
hoeren = !hoeren;
|
||||
const k = $('anruf-hoeren');
|
||||
if (k) {
|
||||
k.textContent = hoeren ? 'Lautsprecher an' : 'Lautsprecher aus';
|
||||
k.setAttribute('aria-pressed', String(hoeren));
|
||||
}
|
||||
schalterSetzen($('anruf-hoeren'), hoeren, 'Ton der anderen');
|
||||
hoerenAnwenden();
|
||||
}
|
||||
|
||||
@@ -569,27 +677,36 @@
|
||||
}
|
||||
w.value = ausgabe || geraete[0].deviceId;
|
||||
w.addEventListener('change', () => { ausgabe = w.value; hoerenAnwenden(); });
|
||||
const reihe = kasten.querySelector('.anruf__knoepfe');
|
||||
/* VOR „Auflegen", nicht dahinter: Der Auflegen-Knopf sitzt per
|
||||
`margin-inline-start: auto` ganz rechts. Wer danach anhaengt,
|
||||
schiebt die Auswahl hinter den einzigen Knopf, der immer an
|
||||
derselben Stelle stehen soll. */
|
||||
if (reihe) reihe.insertBefore(w, reihe.querySelector('.anruf__knopf--auf'));
|
||||
/* IN DIE EIGENE ZEILE, NICHT IN DIE KNOPFREIHE (19.09.2026).
|
||||
Dort war sie das breiteste Element des Kastens -- fuer etwas,
|
||||
das man einmal im Jahr braucht. Jetzt liegt sie hinter dem
|
||||
Knopf mit den drei Punkten und ist erst da, wenn man sie sucht.
|
||||
Der Knopf selbst erscheint nur, wenn es hier auch etwas zu
|
||||
waehlen gibt. */
|
||||
const zeile = $('anruf-mehrzeile');
|
||||
if (!zeile) return;
|
||||
zeile.append(el('span', 'anruf__mehrschild', 'Ton geht an'));
|
||||
zeile.append(w);
|
||||
const mehr = $('anruf-mehr');
|
||||
if (mehr) mehr.hidden = false;
|
||||
}
|
||||
|
||||
function videoSchalten() {
|
||||
if (!anruf) return;
|
||||
const an = anruf.eigener.getVideoTracks().some((s) => s.enabled);
|
||||
for (const s of anruf.eigener.getVideoTracks()) s.enabled = !an;
|
||||
const k = $('anruf-video');
|
||||
k.textContent = an ? 'Kamera aus' : 'Kamera an';
|
||||
k.setAttribute('aria-pressed', String(!an));
|
||||
schalterSetzen($('anruf-video'), !an, 'Kamera');
|
||||
}
|
||||
|
||||
/* ---------- Es klingelt ------------------------------------------------ */
|
||||
|
||||
/* Wer zuletzt angerufen hat -- fuer die Kopfzeile des Kastens, wenn
|
||||
man rangeht. */
|
||||
let letzterAnrufer = '';
|
||||
|
||||
function klingelnZeigen(raumId, von, mitVideo) {
|
||||
klingelnAus();
|
||||
letzterAnrufer = von?.name || '';
|
||||
/* UND ES KLINGELT WIRKLICH. Vorher stand hier nur ein Kasten --
|
||||
wer nicht zufaellig hinsah, merkte nichts. */
|
||||
klingelTon();
|
||||
|
||||
@@ -151,7 +151,10 @@
|
||||
+ 'a2 2 0 0 1-2.2 2A16.5 16.5 0 0 1 4.5 5.7 2 2 0 0 1 6.5 3.5z"/></svg>';
|
||||
ton.addEventListener('click', () => {
|
||||
if (!offen) return;
|
||||
window.anrufStarten(offen.id, false);
|
||||
/* DEN NAMEN MITGEBEN (19.09.2026): Der Anrufkasten zeigte
|
||||
vorher nur „Es klingelt …" -- man sah nicht, wen man
|
||||
anruft. Der Name steht hier ohnehin schon. */
|
||||
window.anrufStarten(offen.id, false, offen.name);
|
||||
});
|
||||
video = document.createElement('button');
|
||||
video.type = 'button';
|
||||
@@ -164,7 +167,7 @@
|
||||
+ '<path d="M15 10.5 21 7v10l-6-3.5z"/></svg>';
|
||||
video.addEventListener('click', () => {
|
||||
if (!offen) return;
|
||||
window.anrufStarten(offen.id, true);
|
||||
window.anrufStarten(offen.id, true, offen.name);
|
||||
});
|
||||
ziel.prepend(video);
|
||||
ziel.prepend(ton);
|
||||
|
||||
@@ -1692,3 +1692,174 @@
|
||||
if (e.persisted) location.reload();
|
||||
});
|
||||
})();
|
||||
|
||||
/* =====================================================================
|
||||
DIE SEITE MERKT SICH, WO DU WARST — 19.09.2026
|
||||
|
||||
Filipe: „wenn ich von einer seite auf die andere zurück gehe, swipt
|
||||
die seite immer wieder komplett hoch. das nervt man muss dan immer
|
||||
wieder runter scrollen bis man da ist wo man vorher war."
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM DER BROWSER ES NICHT VON SELBST HINBEKOMMT
|
||||
|
||||
Er versucht es sogar — `history.scrollRestoration` steht ab Werk auf
|
||||
„auto", und bei einer gewöhnlichen Seite klappt das.
|
||||
|
||||
Hier nicht, und der Grund ist die Bauweise des ganzen Hauses: Jede
|
||||
Seite kommt fast leer an und holt ihren Inhalt danach per Abruf
|
||||
(Aufgaben, Termine, Einträge, Karten). In dem Moment, in dem der
|
||||
Browser die alte Position wiederherstellen will, ist das Dokument
|
||||
ein paar hundert Pixel hoch. Er kann nicht auf Zeile 900 springen,
|
||||
die es noch nicht gibt — also bleibt er oben. Eine halbe Sekunde
|
||||
später ist der Inhalt da, aber der Browser versucht es kein zweites
|
||||
Mal.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WIE ES HIER GELÖST IST
|
||||
|
||||
Wir übernehmen die Aufgabe selbst (`manual`) und warten, bis die
|
||||
Seite WIRKLICH hoch genug ist:
|
||||
|
||||
1. Beim Verlassen wird die Position gemerkt — je Adresse einzeln,
|
||||
inklusive Frageteil (`bereich.html?b=treff` ist eine andere
|
||||
Seite als `bereich.html?b=wunsch`).
|
||||
2. Beim Öffnen wird sie zurückgelesen und dann jeden Bildaufbau
|
||||
lang versucht, bis das Dokument die nötige Höhe hat.
|
||||
3. Nach längstens drei Sekunden wird aufgegeben — eine Seite, die
|
||||
ewig nachspringt, ist schlimmer als eine, die oben steht.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DREI DINGE, DIE ABSICHT SIND
|
||||
|
||||
NUR BEIM ZURÜCKGEHEN, NICHT BEI JEDEM ÖFFNEN. Wer eine Kachel
|
||||
anklickt, will oben anfangen. Wer zurückgeht, will dorthin, wo er
|
||||
war. Unterschieden wird über `performance.navigation`: Nur bei
|
||||
`back_forward` wird wiederhergestellt. Ein eigener Merker fängt den
|
||||
Fall ab, dass der Zurück-Knopf der Kopfleiste benutzt wurde.
|
||||
|
||||
WER SELBST SCROLLT, GEWINNT. Sobald der Mensch das Rad dreht oder
|
||||
wischt, hört das Nachspringen sofort auf. Nichts ist ärgerlicher
|
||||
als eine Seite, die einen zurückzieht.
|
||||
|
||||
UND SIE VERGISST NACH EINER STUNDE. `sessionStorage` überlebt den
|
||||
Reiter ohnehin nicht; der Zeitstempel fängt den Fall ab, dass
|
||||
jemand denselben Reiter am nächsten Morgen weiterbenutzt. Auf
|
||||
Zeile 900 zu landen, weil man gestern dort war, ist keine Hilfe.
|
||||
===================================================================== */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/* Nach so vielen Millisekunden wird nicht mehr versucht. Drei
|
||||
Sekunden sind großzügig für einen Abruf und kurz genug, dass es
|
||||
nie nach „die Seite hängt" aussieht. */
|
||||
var FRIST_MS = 3000;
|
||||
/* Nach einer Stunde gilt die gemerkte Stelle nicht mehr. */
|
||||
var ALTER_MS = 3600000;
|
||||
/* Unter dieser Höhe lohnt das Merken nicht -- wer zehn Pixel
|
||||
gescrollt hat, merkt vom Wiederherstellen nichts. */
|
||||
var MINDEST_PX = 80;
|
||||
|
||||
var SCHLUESSEL = 'ws-stelle:' + location.pathname + location.search;
|
||||
|
||||
function merken() {
|
||||
try {
|
||||
var y = window.scrollY || document.documentElement.scrollTop || 0;
|
||||
if (y < MINDEST_PX) { sessionStorage.removeItem(SCHLUESSEL); return; }
|
||||
sessionStorage.setItem(SCHLUESSEL, JSON.stringify({ y: Math.round(y), am: Date.now() }));
|
||||
} catch (e) { /* Kein Speicher -> dann eben ohne. */ }
|
||||
}
|
||||
|
||||
/* MEHRERE ANLÄSSE, NICHT NUR EINER.
|
||||
|
||||
`beforeunload` feuert auf iOS oft gar nicht, `pagehide` dafür
|
||||
zuverlässig; `visibilitychange` fängt den Fall ab, dass jemand die
|
||||
App in den Hintergrund schiebt und das Gerät sie später beendet.
|
||||
Dreimal dasselbe zu schreiben ist hier billiger als ein Fall, in
|
||||
dem nichts gemerkt wird. */
|
||||
window.addEventListener('pagehide', merken);
|
||||
window.addEventListener('beforeunload', merken);
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'hidden') merken();
|
||||
});
|
||||
/* Und der Zurück-Knopf der Kopfleiste: Er ruft `history.back()`, und
|
||||
dabei ist `pagehide` je nach Browser zu spät dran. */
|
||||
document.addEventListener('click', function (e) {
|
||||
var a = e.target && e.target.closest
|
||||
&& e.target.closest('a[href], .zurueck-knopf, .zurueck');
|
||||
if (a) merken();
|
||||
}, true);
|
||||
|
||||
/* ---- Und zurück ------------------------------------------------- */
|
||||
|
||||
function kamVonZurueck() {
|
||||
try {
|
||||
var e = performance.getEntriesByType('navigation')[0];
|
||||
if (e && e.type) return e.type === 'back_forward';
|
||||
/* Ältere Browser: die alte Zählweise (2 = TYPE_BACK_FORWARD). */
|
||||
return performance.navigation && performance.navigation.type === 2;
|
||||
} catch (f) { return false; }
|
||||
}
|
||||
|
||||
function wiederherstellen() {
|
||||
var roh;
|
||||
try { roh = sessionStorage.getItem(SCHLUESSEL); } catch (e) { return; }
|
||||
if (!roh) return;
|
||||
var stand;
|
||||
try { stand = JSON.parse(roh); } catch (e) { return; }
|
||||
if (!stand || typeof stand.y !== 'number') return;
|
||||
if (Date.now() - (stand.am || 0) > ALTER_MS) {
|
||||
try { sessionStorage.removeItem(SCHLUESSEL); } catch (f) { /* egal */ }
|
||||
return;
|
||||
}
|
||||
|
||||
var bis = Date.now() + FRIST_MS;
|
||||
var abgebrochen = false;
|
||||
/* WER SELBST SCROLLT, GEWINNT. `wheel`, `touchstart` und die
|
||||
Tastatur -- alle drei, weil es alle drei Arten gibt, eine Seite
|
||||
zu bewegen. */
|
||||
var aufhoeren = function () { abgebrochen = true; };
|
||||
window.addEventListener('wheel', aufhoeren, { passive: true, once: true });
|
||||
window.addEventListener('touchstart', aufhoeren, { passive: true, once: true });
|
||||
window.addEventListener('keydown', aufhoeren, { once: true });
|
||||
|
||||
(function versuchen() {
|
||||
if (abgebrochen) return;
|
||||
var hoch = Math.max(
|
||||
document.body ? document.body.scrollHeight : 0,
|
||||
document.documentElement ? document.documentElement.scrollHeight : 0);
|
||||
/* Reicht die Höhe schon? Dann hin und fertig. */
|
||||
if (hoch >= stand.y + window.innerHeight * 0.5) {
|
||||
window.scrollTo(0, stand.y);
|
||||
/* NOCH EINMAL IM NÄCHSTEN BILD. Manche Listen wachsen beim
|
||||
Zeichnen noch ein Stück (Bilder, Schriften), und dann sitzt
|
||||
der erste Sprung ein paar Pixel daneben. */
|
||||
requestAnimationFrame(function () {
|
||||
if (!abgebrochen) window.scrollTo(0, stand.y);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (Date.now() > bis) return; /* Aufgeben ist auch eine Antwort. */
|
||||
requestAnimationFrame(versuchen);
|
||||
})();
|
||||
}
|
||||
|
||||
if ('scrollRestoration' in history) {
|
||||
/* Wir machen es selbst -- sonst springt der Browser erst nach oben
|
||||
und wir gleich danach wieder hinunter, und das sieht man. */
|
||||
history.scrollRestoration = 'manual';
|
||||
}
|
||||
|
||||
if (kamVonZurueck()) {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', wiederherstellen);
|
||||
} else {
|
||||
wiederherstellen();
|
||||
}
|
||||
} else {
|
||||
/* Ein frisch geöffneter Bereich fängt oben an -- und die alte
|
||||
Stelle wird vergessen, damit sie nicht in einer halben Stunde
|
||||
überraschend zuschlägt. */
|
||||
try { sessionStorage.removeItem(SCHLUESSEL); } catch (e) { /* egal */ }
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -360,11 +360,54 @@
|
||||
meisten Bewegung nach oben stellt, ist eine Rangliste mit einem
|
||||
anderen Namen. Der Server sortiert nach Namen, und dabei bleibt es.
|
||||
===================================================================== */
|
||||
/* Was über einer Gruppe steht. Aus der Rolle abgeleitet, nicht
|
||||
abgeschrieben -- der Rollenname kommt vom Server. */
|
||||
const GRUPPENSATZ = {
|
||||
hand: 'Koordiniert den Alltag und schaut mit dir zusammen auf das Team.',
|
||||
modi: 'Die Leute, die im Chat und im Live stehen.',
|
||||
};
|
||||
|
||||
function listeBauen(d) {
|
||||
const wurzel = el('div', 'w-liste__gitter');
|
||||
const wurzel = el('div', 'w-liste');
|
||||
|
||||
/* ---- NACH ROLLE GRUPPIERT (19.09.2026) -------------------------
|
||||
|
||||
Filipe: „die reihenfolge der listen soll immer angepasst sein
|
||||
... die rechte hand rolle immer zuerst und dan die modis. die
|
||||
sollen auch schoen getrennt sein und verschieden aussehen also
|
||||
die rechte hand viel spezieller."
|
||||
|
||||
Die REIHENFOLGE macht der Server (ROLLEN_SORTIERUNG) -- hier
|
||||
wird nur gruppiert, wo sie wechselt. Eine eigene Sortierung im
|
||||
Browser waere die zweite Wahrheit, und die zweite ist die, die
|
||||
beim naechsten Umbau abweicht.
|
||||
|
||||
DAS IST KEINE RANGLISTE. Eine Sortierung nach Rolle sagt nichts
|
||||
darueber, wie gut jemand ist -- nur, welche Aufgabe er hat.
|
||||
Deshalb steht ueber jeder Gruppe ein Satz, der genau das
|
||||
erklaert, und keine Zahl. */
|
||||
let letzteRolle = null;
|
||||
let gitter = null;
|
||||
for (const p of d.liste) {
|
||||
if (p.rolle !== letzteRolle) {
|
||||
letzteRolle = p.rolle;
|
||||
const kopf = el('div', 'w-gruppe');
|
||||
kopf.dataset.rolle = p.rolle;
|
||||
kopf.append(el('h3', 'w-gruppe__name', p.rolle_name));
|
||||
if (GRUPPENSATZ[p.rolle]) {
|
||||
kopf.append(el('p', 'w-gruppe__satz', GRUPPENSATZ[p.rolle]));
|
||||
}
|
||||
wurzel.append(kopf);
|
||||
gitter = el('div', 'w-liste__gitter');
|
||||
gitter.dataset.rolle = p.rolle;
|
||||
wurzel.append(gitter);
|
||||
}
|
||||
const karte = el('button', 'w-karte');
|
||||
karte.type = 'button';
|
||||
/* Die Rolle haengt an der Karte, nicht an einer Klasse: So kann
|
||||
das CSS sie einfaerben, ohne dass hier eine zweite Liste von
|
||||
Rollennamen entsteht. */
|
||||
karte.dataset.rolle = p.rolle;
|
||||
karte.append(el('span', 'w-karte__name', p.name));
|
||||
karte.append(el('span', 'w-karte__rolle', p.rolle_name));
|
||||
|
||||
@@ -390,7 +433,7 @@
|
||||
}
|
||||
karte.append(zeilen);
|
||||
karte.addEventListener('click', () => karteZeigen(p.id).catch(() => {}));
|
||||
wurzel.append(karte);
|
||||
(gitter || wurzel).append(karte);
|
||||
}
|
||||
return wurzel;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user