157 lines
7.5 KiB
JavaScript
157 lines
7.5 KiB
JavaScript
/* 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
|
|
}
|