/* Kundenansicht neu: Router mit den History-Regeln der Hülle (Spec 5.2). Kein DOM-Zugriff beim Import. Verlaufsstempel: history.state = { kn: { tiefe, overlay } }; tiefe = Zahl der Einträge unter diesem innerhalb der Seite. Die URL der Website ist pathname + search ohne #. R1 Bereichs- und Reiterwechsel ersetzen den Eintrag (nav 'tab'), immer mit dem kanonischen Hash (#wartung wird #abo). R2 Von Ebene 1 in eine Etage und von Etage zu Etage wird ein Eintrag geschoben (nav 'push'); ersetzen: true erzwingt replaceState. R3 popstate: tiefer als vorher ist 'push', sonst 'back'. R4 Start: startHash anwenden, zeige einmal mit 'tab'; fehlt der Stempel, legt der Router die Anker-Kette an. R5 zurueck(): mit eigenem Eintrag history.back(), sonst das Elternziel per replaceState. R6 Overlay (Konto-Sheet, Bestätigungsseite): ein Eintrag mit overlay: true; Zurück schließt, Navigation ersetzt den Eintrag. R7 stempeln() schreibt den Stempel neu, nachdem ein Modul mit replaceState(null, …) die URL angefasst hat. */ import { routeAusHash, hashFuerRoute, startHash, ankerKette, elternVon } from './routen.js' const fenster = () => window // erst beim Aufruf, nie beim Import const leerer = () => ({ umg: null, zeige: null, aktuell: null, vorher: '', tiefe: 0, overlay: null, abmelden: null }) let z = leerer() const hashTeil = (hash) => (hash === '#' ? '' : hash) // ?abo= bleibt am Zieleintrag stehen (P2 liest es dort); die Anker davor tragen es nicht. Andere Parameter bleiben Zeichen für Zeichen. const ohneAbo = (search) => { const teile = (search || '').replace(/^\?/, '').split('&').filter((t) => t && t.split('=')[0] !== 'abo') return teile.length ? `?${teile.join('&')}` : '' } const urlFuer = (pfad, search, hash) => `${pfad}${search}${hashTeil(hash)}` const leseStempel = (state) => { const s = state?.kn return s && Number.isInteger(s.tiefe) && s.tiefe >= 0 ? { tiefe: s.tiefe, overlay: s.overlay === true } : null } const stempelFuer = (state, tiefe, overlay) => ({ ...(state && typeof state === 'object' ? state : {}), kn: { tiefe, overlay } }) function legeKetteAn(route) { const { location, history } = z.umg const pfad = location.pathname const search = location.search const kette = ankerKette(route) kette.forEach((hash, i) => { const url = urlFuer(pfad, i === kette.length - 1 ? search : ohneAbo(search), hash) if (i === 0) history.replaceState(stempelFuer(history.state, 0, false), '', url) else history.pushState(stempelFuer(null, i, false), '', url) }) z.tiefe = kette.length - 1 } // Stempel vorhanden (Neuladen): nichts anlegen, nur Overlay-Reste löschen und den Hash kanonisch schreiben function bereinige(route, stempel) { const { location, history } = z.umg const soll = urlFuer(location.pathname, location.search, hashFuerRoute(route)) z.tiefe = stempel.tiefe if (stempel.overlay || soll !== location.pathname + location.search + location.hash) history.replaceState(stempelFuer(history.state, stempel.tiefe, false), '', soll) } export function starteRouter({ zeige, umgebung = fenster() }) { z.abmelden?.() const eigener = leerer() const beiPopstate = () => { if (z === eigener) popstate() } umgebung.addEventListener('popstate', beiPopstate) Object.assign(eigener, { umg: umgebung, zeige, abmelden: () => umgebung.removeEventListener('popstate', beiPopstate) }) z = eigener const route = routeAusHash(startHash({ search: umgebung.location.search, hash: umgebung.location.hash })) const stempel = leseStempel(umgebung.history.state) if (stempel) bereinige(route, stempel) else legeKetteAn(route) z.aktuell = route zeige(route, { nav: 'tab' }) return { beende() { if (z === eigener) { eigener.abmelden(); z = leerer() } } } } function popstate() { const { location, history } = z.umg const stempel = leseStempel(history.state) const route = routeAusHash(location.hash) const ziel = hashFuerRoute(route) const alteTiefe = z.tiefe if (z.overlay) { const schliesse = z.overlay; z.overlay = null; schliesse() } // Mit Stempel entscheidet er; ohne Stempel ist es ein neuer Eintrag des Browsers (Fragment-Link im Modul): eine Ebene tiefer const tiefe = stempel ? stempel.tiefe : alteTiefe + 1 const nav = stempel ? (tiefe > alteTiefe ? 'push' : 'back') : (route.etage ? 'push' : 'tab') const soll = urlFuer(location.pathname, location.search, ziel) if (!stempel || stempel.overlay || soll !== location.pathname + location.search + location.hash) history.replaceState(stempelFuer(history.state, tiefe, false), '', soll) z.tiefe = tiefe if (ziel === hashFuerRoute(z.aktuell)) return // gleiche Route (z. B. Overlay geschlossen): nichts neu zeichnen z.vorher = hashFuerRoute(z.aktuell) z.aktuell = route z.zeige(route, { nav }) } function navigiere(route, { ersetzen = false, nav = null } = {}) { const { location, history } = z.umg const ziel = hashFuerRoute(route) const overlay = z.overlay if (overlay) { z.overlay = null; overlay() } // genau einmal if (ziel === hashFuerRoute(z.aktuell)) { if (overlay) { z.tiefe = Math.max(0, z.tiefe - 1); history.back() } // den Overlay-Eintrag loswerden z.zeige(route, { nav: 'same' }) return } const schieben = !overlay && !ersetzen && Boolean(route.etage) const tiefe = schieben ? z.tiefe + 1 : z.tiefe const url = urlFuer(location.pathname, location.search, ziel) if (schieben) history.pushState(stempelFuer(null, tiefe, false), '', url) else history.replaceState(stempelFuer(history.state, tiefe, false), '', url) z.tiefe = tiefe z.vorher = hashFuerRoute(z.aktuell) z.aktuell = route z.zeige(route, { nav: nav ?? (route.etage ? 'push' : 'tab') }) } /** Verlauf schreiben und danach zeige rufen. Reiter und Bereiche ersetzen, Etage 2 schiebt (ersetzen: true erzwingt replaceState). */ export function geheZu(hash, { ersetzen = false } = {}) { if (!z.umg) return navigiere(routeAusHash(hash), { ersetzen }) } /** Zurück der Etage: mit eigenem Verlaufseintrag history.back(), sonst das Elternziel per replaceState mit nav 'back'. */ export function zurueck() { if (!z.umg) return if (z.tiefe > 0) { z.umg.history.back(); return } const eltern = elternVon(z.aktuell, z.vorher) if (eltern) navigiere(eltern, { ersetzen: true, nav: 'back' }) } /** Overlay öffnen: schiebt einen Eintrag; Zurück ruft schliesse(). Ist schon eines offen, wechselt nur der Besitzer. */ export function oeffneOverlay({ schliesse }) { if (!z.umg) return const alt = z.overlay z.overlay = null if (alt) { alt(); z.overlay = schliesse; return } const { location, history } = z.umg z.tiefe += 1 history.pushState(stempelFuer(null, z.tiefe, true), '', location.pathname + location.search + location.hash) z.overlay = schliesse } /** Overlay vom Besitzer aus schließen: ruft schliesse() und nimmt den Verlaufseintrag zurück. */ export function beendeOverlay() { if (!z.umg || !z.overlay) return const schliesse = z.overlay z.overlay = null z.tiefe = Math.max(0, z.tiefe - 1) schliesse() z.umg.history.back() } /** Stempel neu schreiben, nachdem ein Modul mit replaceState(null, …) die URL angefasst hat (z. B. um die Query zu löschen). */ export function stempeln() { if (!z.umg) return const { location, history } = z.umg history.replaceState(stempelFuer(history.state, z.tiefe, z.overlay !== null), '') const route = routeAusHash(location.hash) if (hashFuerRoute({ ...route, query: '' }) === hashFuerRoute({ ...z.aktuell, query: '' })) z.aktuell = route // nur die Query hat sich geändert } export function aktuelleRoute() { return z.aktuell ? { ...z.aktuell } : null }