Files
Webklar-Kundenbereich/public/embed/zeiten.js
T
ClaudeandClaude Opus 5.5 a6a4eae294 Öffnungszeiten: Website-Skript mit Status und Tabelle
public/embed/zeiten.js ruft die öffentliche Zeiten-API einmal je Seite ab und füllt
die Platzhalter [data-webklar-zeiten] (tabelle, status, badge; data-plan, Hauptplan
ohne Angabe). Reine Funktionen (waehleWechsel, naechsterWechsel, waehleHinweis,
jetztMitServer, tabelleDaten) stehen zwischen den Marken @rein-anfang/@rein-ende.
Timer auf den nächsten Wechsel (jede Wartezeit auf 10 Minuten begrenzt), Neuladen
höchstens alle 10 Minuten, Daten älter als 24 Stunden, Fehler oder aktiv:false
lassen das Ersatz-HTML stehen bzw. stellen es wieder her. Testseite zeiten-demo.html.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 04:01:31 +00:00

525 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*! WEBklar Oeffnungszeiten live: Einbau-Skript fuer Kundenwebsites.
* <script src=".../embed/zeiten.js" data-site="SITEKEY" defer></script>
* data-api="ADRESSE" am Script-Tag ist optional (sonst die Herkunft des Skripts); den siteKey darf auch ein Platzhalter tragen
* (data-webklar-zeiten="SITEKEY").
*
* Platzhalter (vom Team gesetzt, das Ersatz-HTML darin bleibt ohne JavaScript, ohne Daten und bei Fehlern unveraendert stehen):
* <div data-webklar-zeiten data-modus="tabelle|status|badge" data-plan="KURZNAME">Ersatz-HTML</div>
* tabelle Wochentabelle (<table> mit Caption, Zeilenkoepfen und heutigem Tag mit aria-current="date") plus die Abweichungen der
* naechsten 14 Tage
* status Punkt und Text ("Jetzt geoeffnet · schliesst um 18 Uhr"), ohne aria-live
* badge Kurzform ("Geoeffnet", "Geschlossen", "Nach Vereinbarung")
* Ohne data-plan gilt der Hauptplan; ein unbekannter data-plan laesst das Ersatz-HTML stehen und schreibt eine Konsole-Warnung.
*
* Gestaltung: Light DOM, alle Klassen wkz-..., Stile in @layer wkz (jede Regel der Seite schlaegt sie), Schrift und Farben erbt die Seite.
* Variablen (alle optional, an den Platzhalter oder :root): --wkz-offen, --wkz-zu, --wkz-unklar (Punktfarben), --wkz-offen-hell,
* --wkz-zu-hell, --wkz-unklar-hell, --wkz-offen-text, --wkz-zu-text, --wkz-unklar-text (Badge), --wkz-rand, --wkz-heute.
*
* Ein Abruf je Seite fuer alle Teile; keine Abhaengigkeiten, keine Cookies, kein Storage, keine Anfragen an Dritte. Kundentexte nur
* ueber textContent. Das Skript rechnet nie mit Feiertagen, Kalender oder Zeitzone des Browsers: "jetzt" ist Date.now() (weicht die
* Uhr laut Date-Kopf um mehr als 5 Minuten ab, die Serverzeit), alles andere kommt als Zeichenkette vom Server. Es laedt hoechstens
* alle 10 Minuten neu (und beim Zurueckkehren in den Tab); Daten aelter als 24 Stunden oder ein Fehler lassen das Ersatz-HTML stehen. */
(function () {
'use strict'
if (window.__wkZeiten) return
window.__wkZeiten = true
var SELF = document.currentScript
var STYLE_ID = 'wkz-style'
var API_PFAD = '/api/public/zeiten/v1/'
var SELEKTOR = '[data-webklar-zeiten]'
var TIMER_MAX_MS = 600000 // jede Wartezeit hoechstens 10 Minuten (setTimeout laeuft ab 2^31-1 ms ueber; "ohne Ende" ist 8.64e15)
var TIMER_PUFFER_MS = 25 // ein Timer darf nie vor dem Wechsel feuern
var RELOAD_MS = 600000 // hoechstens alle 10 Minuten ein Abruf
var ALTER_MAX_MS = 24 * 60 * 60 * 1000 // aeltere Daten gelten nicht mehr
var ABRUF_TIMEOUT_MS = 15000
// @rein-anfang (reine Funktionen: kein Zugriff auf Seite oder Uhr; test/zeitenEmbedStatic.test.js fuehrt sie einzeln in einem leeren Kontext aus)
var UHR_TOLERANZ_MS = 5 * 60 * 1000 // groessere Abweichung der Uhr laut Date-Kopf: Serverzeit
var ENDLOS_MS = 8.64e15 // "ohne Ende" der Hinweise (letzter darstellbarer Zeitpunkt)
var TAG_ZEILE = /^\d{4}-\d{2}-\d{2}$/
var WOCHENTAGE = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
var ART_NAMEN = { feiertag: 'Feiertag', urlaub: 'Betriebsurlaub', sondertag: 'Sondertag' }
function istZahl (v) { return typeof v === 'number' && v > -Infinity && v < Infinity }
function artName (art) { return Object.prototype.hasOwnProperty.call(ART_NAMEN, art) ? ART_NAMEN[art] : '' }
// Der Eintrag, der jetzt gilt: der letzte mit ab <= jetzt (null, wenn noch keiner begonnen hat).
function waehleWechsel (wechsel, jetztMs) {
if (!Array.isArray(wechsel)) return null
var best = null
for (var i = 0; i < wechsel.length; i++) {
var w = wechsel[i]
if (w && istZahl(w.ab) && w.ab <= jetztMs && (best === null || w.ab >= best.ab)) best = w
}
return best
}
// Der naechste Zeitpunkt nach jetzt, an dem sich etwas aendert: Statuswechsel, Hinweis-Beginn oder Hinweis-Ende (null, wenn keiner folgt;
// "ohne Ende" zaehlt nicht).
function naechsterWechsel (wechsel, hinweise, jetztMs) {
var best = null
function pruefe (ms) {
if (istZahl(ms) && ms > jetztMs && ms < ENDLOS_MS && (best === null || ms < best)) best = ms
}
var i
if (Array.isArray(wechsel)) for (i = 0; i < wechsel.length; i++) if (wechsel[i]) pruefe(wechsel[i].ab)
if (Array.isArray(hinweise)) {
for (i = 0; i < hinweise.length; i++) {
if (!hinweise[i]) continue
pruefe(hinweise[i].ab)
pruefe(hinweise[i].bis)
}
}
return best
}
// Der Hinweis, der jetzt gilt (ab <= jetzt < bis): hoechster Rang, bei Gleichstand der spaetere Beginn.
function waehleHinweis (hinweise, jetztMs) {
if (!Array.isArray(hinweise)) return null
var best = null
for (var i = 0; i < hinweise.length; i++) {
var h = hinweise[i]
if (!h || !istZahl(h.rang) || !istZahl(h.ab) || !istZahl(h.bis) || h.ab > jetztMs || h.bis <= jetztMs) continue
if (best === null || h.rang > best.rang || (h.rang === best.rang && h.ab > best.ab)) best = h
}
return best
}
// "Jetzt" in Millisekunden: die lokale Uhr, ausser sie weicht laut Date-Kopf der Antwort um mehr als 5 Minuten von der Serverzeit ab.
function jetztMitServer (datumsKopf, lokalMs) {
var server = typeof datumsKopf === 'string' ? Date.parse(datumsKopf) : NaN
if (!istZahl(server) || Math.abs(server - lokalMs) <= UHR_TOLERANZ_MS) return lokalMs
return server
}
// 'YYYY-MM-DD' -> Tage seit 1970-01-01 (reine Ganzzahlrechnung auf der Zeichenkette des Servers, ohne Kalender des Browsers)
function tageNr (tag) {
var m = parseInt(tag.slice(5, 7), 10)
var y = parseInt(tag.slice(0, 4), 10) - (m <= 2 ? 1 : 0)
var era = Math.floor(y / 400)
var yoe = y - era * 400
var doy = Math.floor((153 * (m > 2 ? m - 3 : m + 9) + 2) / 5) + parseInt(tag.slice(8, 10), 10) - 1
return era * 146097 + yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy - 719468
}
// ISO-Wochentag eines Tages 'YYYY-MM-DD': 1 = Montag ... 7 = Sonntag (der 1.1.1970 war ein Donnerstag)
function wochentagVon (tag) { return ((((tageNr(tag) + 3) % 7) + 7) % 7) + 1 }
// 'Montag, 12.10.'
function datumKurz (tag) { return WOCHENTAGE[wochentagVon(tag) - 1] + ', ' + tag.slice(8, 10) + '.' + tag.slice(5, 7) + '.' }
// Tageszeiten als Text; nie '24:00' (ein Ende um Mitternacht heisst 00:00).
function zeitenText (modus, fenster) {
if (!Array.isArray(fenster)) fenster = []
if (modus === 'rund' || (modus === 'zeiten' && fenster.length === 1 && fenster[0] && fenster[0][0] === '00:00' && fenster[0][1] === '24:00')) return 'rund um die Uhr'
if (modus === 'vereinbarung') return 'nach Vereinbarung'
if (modus !== 'zeiten' || fenster.length === 0) return 'geschlossen'
var teile = []
for (var i = 0; i < fenster.length; i++) {
var f = fenster[i]
if (Array.isArray(f) && typeof f[0] === 'string' && typeof f[1] === 'string') teile.push(f[0] + '–' + (f[1] === '24:00' ? '00:00' : f[1]))
}
return teile.length ? teile.join(', ') : 'geschlossen'
}
// Daten der Tabelle: woche = sieben Zeilen Montag bis Sonntag { tag, name, modus, fenster, text, heute } (heute = der Wochentag von
// heuteTag, dem Berliner Datum aus dem aktuellen Wechsel) und abweichungen14 = die Tage von heuteTag bis heuteTag + 13, an denen etwas
// vom Wochenplan abweicht (Feiertag, Urlaub, Sondertag oder andere Zeiten): { datum, art, name, modus, fenster, text }.
function tabelleDaten (plan, heuteTag) {
var heuteOk = typeof heuteTag === 'string' && TAG_ZEILE.test(heuteTag)
var heuteWt = heuteOk ? wochentagVon(heuteTag) : 0
var roh = plan && Array.isArray(plan.woche) ? plan.woche : []
var woche = []
var t, i
for (t = 1; t <= 7; t++) {
var e = null
for (i = 0; i < roh.length; i++) if (roh[i] && roh[i].tag === t) e = roh[i]
var modus = e ? e.modus : 'zu'
var fenster = e && Array.isArray(e.fenster) ? e.fenster : []
woche.push({ tag: t, name: WOCHENTAGE[t - 1], modus: modus, fenster: fenster, text: zeitenText(modus, fenster), heute: t === heuteWt })
}
var abweichungen14 = []
if (heuteOk && plan && Array.isArray(plan.tage)) {
var start = tageNr(heuteTag)
for (i = 0; i < plan.tage.length; i++) {
var z = plan.tage[i]
if (!z || typeof z.datum !== 'string' || !TAG_ZEILE.test(z.datum)) continue
var abstand = tageNr(z.datum) - start
if (abstand < 0 || abstand > 13) continue
var text = zeitenText(z.modus, z.fenster)
if (!artName(z.art) && text === woche[wochentagVon(z.datum) - 1].text) continue
abweichungen14.push({
datum: z.datum,
art: z.art,
name: typeof z.name === 'string' && z.name !== '' ? z.name : null,
modus: z.modus,
fenster: Array.isArray(z.fenster) ? z.fenster : [],
text: text
})
}
abweichungen14.sort(function (a, b) { return a.datum < b.datum ? -1 : a.datum > b.datum ? 1 : 0 })
}
return { woche: woche, abweichungen14: abweichungen14 }
}
// Aufeinanderfolgende Tage mit gleicher Art, gleichem Namen und gleichen Zeiten in einer Zeile: [{ von, bis, art, name, text }]
function gruppiereAbweichungen (liste) {
var gruppen = []
if (!Array.isArray(liste)) return gruppen
for (var i = 0; i < liste.length; i++) {
var a = liste[i]
var g = gruppen.length ? gruppen[gruppen.length - 1] : null
if (g && g.art === a.art && g.name === a.name && g.text === a.text && tageNr(a.datum) - tageNr(g.bis) === 1) g.bis = a.datum
else gruppen.push({ von: a.datum, bis: a.datum, art: a.art, name: a.name, text: a.text })
}
return gruppen
}
// Eine Zeile der Abweichungen: { datum: 'Dienstag, 06.10. – Donnerstag, 08.10.', text: 'Betriebsurlaub: geschlossen' }
function abweichungsZeile (g) {
var datum = datumKurz(g.von)
if (g.bis && g.bis !== g.von) datum += ' – ' + datumKurz(g.bis)
return { datum: datum, text: (g.name || artName(g.art) || 'Abweichung') + ': ' + g.text }
}
// @rein-ende
// @telefon-anfang (reine Funktion, test/zeitenEmbedStatic.test.js fuehrt sie einzeln aus; dieselbe Regel wie in public/embed/termine.js; fuer Kontakt-Leiste und Hinweis-Knoepfe)
// Telefonnummer des Betriebs (freier Text) als tel:-Adresse. Steht ein Buchstabe oder ein anderes Zeichen als Ziffern, Leerzeichen,
// + - ( ) / . darin ("06301 1234 (ab 9 Uhr)", "0171 1, 0171 2"), ist es keine Nummer, die man waehlen kann: dann '' und der Text bleibt
// Text. Sonst bleiben ein fuehrendes + und die Ziffern; die nationale Null "(0)" hinter der Landesvorwahl entfaellt.
function telefonLink (nr) {
if (typeof nr !== 'string') return ''
var s = nr.replace(/^\s+|\s+$/g, '')
if (!/^[0-9 +()\/.-]+$/.test(s)) return ''
var lv = /^((?:\+|00)\s*\d{1,3})[\s.\/-]*\(\s*0\s*\)/.exec(s)
if (lv) s = lv[1] + s.slice(lv[0].length)
var ziffern = s.replace(/[^0-9]/g, '')
if (ziffern.length < 3) return ''
return 'tel:' + (s.charAt(0) === '+' ? '+' : '') + ziffern
}
// @telefon-ende
var CSS = [
'@layer wkz{',
'.wkz-status,.wkz-badge{--wkz-i-farbe:var(--wkz-unklar,#a16207);--wkz-i-hell:var(--wkz-unklar-hell,#fef3c7);--wkz-i-dunkel:var(--wkz-unklar-text,#78350f)}',
'.wkz-zustand-offen{--wkz-i-farbe:var(--wkz-offen,#15803d);--wkz-i-hell:var(--wkz-offen-hell,#dcfce7);--wkz-i-dunkel:var(--wkz-offen-text,#14532d)}',
'.wkz-zustand-zu{--wkz-i-farbe:var(--wkz-zu,#b91c1c);--wkz-i-hell:var(--wkz-zu-hell,#fee2e2);--wkz-i-dunkel:var(--wkz-zu-text,#7f1d1d)}',
'.wkz-status{display:inline-flex;align-items:center;gap:.5em;font:inherit;color:inherit;line-height:1.4}',
'.wkz-punkt{flex:none;width:.65em;height:.65em;border-radius:50%;background:var(--wkz-i-farbe)}',
'.wkz-badge{display:inline-flex;align-items:center;gap:.45em;padding:.1em .75em;border-radius:999px;font:inherit;font-size:.9em;font-weight:600;line-height:1.5;background:var(--wkz-i-hell);color:var(--wkz-i-dunkel)}',
'.wkz-tabelle{border-collapse:collapse;width:100%;max-width:34rem;font:inherit;color:inherit}',
'.wkz-caption{caption-side:top;text-align:left;font-weight:700;padding:0 0 .5em}',
'.wkz-tabelle th,.wkz-tabelle td{padding:.4em .75em;text-align:left;vertical-align:top;border-bottom:1px solid var(--wkz-rand,rgba(128,128,128,.35))}',
'.wkz-tabelle th{font-weight:600;white-space:nowrap}',
'.wkz-heute{background:var(--wkz-heute,rgba(128,128,128,.14));font-weight:700}',
'.wkz-heute th{font-weight:700}',
'.wkz-abw{margin-top:1em;max-width:34rem;font:inherit;color:inherit}',
'.wkz-abw-titel{margin:0 0 .35em;font-weight:700}',
'.wkz-abw-liste{margin:0;padding:0;list-style:none}',
'.wkz-abw-eintrag{padding:.3em 0;border-bottom:1px solid var(--wkz-rand,rgba(128,128,128,.35))}',
'.wkz-abw-datum{font-weight:600}',
'}'
].join('\n')
// ---------------------------------------------------------------- Zustand (nur im Speicher der Seite)
var daten = null // letzte gueltige Antwort { plaene, hinweise } oder null (aktiv: false, Fehler, noch kein Abruf)
var geladenMs = 0 // Date.now() beim letzten ERFOLGREICHEN Abruf (nicht body.stand: ein 304 liefert alte Daten)
var versatz = 0 // Serverzeit minus lokale Uhr, nur bei mehr als 5 Minuten Abweichung (sonst 0)
var versuchMs = 0 // Beginn des letzten Abrufs, ob erfolgreich oder nicht
var laeuft = false
var timerId = 0
var platzhalter = []
var siteKey = ''
var api = ''
function timer (fn, ms) {
var d = Number(ms)
if (d !== d) d = TIMER_MAX_MS
return setTimeout(function () { fn() }, Math.max(0, Math.min(TIMER_MAX_MS, d)))
}
function jetzt () { return Date.now() + versatz }
function frisch () { return daten !== null && Math.abs(Date.now() - geladenMs) <= ALTER_MAX_MS }
function warne (text) {
if (typeof console !== 'undefined' && console.warn) console.warn('WEBklar Öffnungszeiten: ' + text)
}
function injectStyle () {
if (document.getElementById(STYLE_ID)) return
var st = document.createElement('style')
st.id = STYLE_ID
st.textContent = CSS
;(document.head || document.documentElement).appendChild(st)
}
function el (tag, cls, text) {
var e = document.createElement(tag)
if (cls) e.className = cls
if (text != null) e.textContent = text
return e
}
function attr (node, name) { return String(node.getAttribute(name) || '').replace(/^\s+|\s+$/g, '') }
// ---------------------------------------------------------------- Abruf
function planGueltig (p) {
return !!p && typeof p === 'object' && typeof p.kurz === 'string' && Array.isArray(p.woche) && Array.isArray(p.wechsel) && p.wechsel.length > 0
}
// Antwort uebernehmen. { aktiv: false } ist ein erfolgreicher Abruf (die Platzhalter bekommen ihr Ersatz-HTML zurueck); alles andere als ein
// Objekt zaehlt als Fehler (die alten Daten bleiben bis zur 24-Stunden-Grenze).
function uebernimm (body, datumsKopf) {
if (!body || typeof body !== 'object' || Array.isArray(body)) throw new Error('Antwort')
var lokal = Date.now()
geladenMs = lokal
versatz = jetztMitServer(datumsKopf, lokal) - lokal
if (body.aktiv !== true || !Array.isArray(body.plaene)) {
daten = null
return
}
daten = { plaene: body.plaene.filter(planGueltig), hinweise: Array.isArray(body.hinweise) ? body.hinweise : [] }
}
function lade () {
if (laeuft || !siteKey || !api) return
laeuft = true
versuchMs = Date.now()
var opts = { credentials: 'omit' }
var ctrl = typeof AbortController === 'function' ? new AbortController() : null
var abbruch = 0
if (ctrl) {
opts.signal = ctrl.signal
abbruch = timer(function () { ctrl.abort() }, ABRUF_TIMEOUT_MS)
}
function fertig () {
if (abbruch) clearTimeout(abbruch)
laeuft = false
try { render() } finally { plane() }
}
fetch(api + API_PFAD + encodeURIComponent(siteKey), opts).then(function (r) {
if (!r.ok) throw new Error('Status ' + r.status)
var kopf = r.headers && typeof r.headers.get === 'function' ? r.headers.get('Date') : null
return r.json().then(function (body) { uebernimm(body, kopf) })
}).then(fertig, fertig)
}
// ---------------------------------------------------------------- Platzhalter
function neuerPlatzhalter (root) {
// Kopie des Ersatz-HTML: das Original bleibt stehen, bis es ersetzt wird, und kommt (als weitere Kopie) zurueck
var ausweich = document.createDocumentFragment()
for (var c = root.firstChild; c; c = c.nextSibling) ausweich.appendChild(c.cloneNode(true))
var modus = attr(root, 'data-modus')
if (modus !== 'tabelle' && modus !== 'status' && modus !== 'badge') {
warne('data-modus="' + modus + '" ist unbekannt (tabelle, status oder badge): das Ersatz-HTML bleibt stehen.')
modus = ''
}
return { root: root, ausweich: ausweich, modus: modus, plan: attr(root, 'data-plan'), sig: '', gewarnt: '' }
}
function sammle () {
var knoten = document.querySelectorAll(SELEKTOR)
for (var i = 0; i < knoten.length; i++) {
if (knoten[i].hasAttribute('data-wkz-init')) continue
knoten[i].setAttribute('data-wkz-init', '1')
platzhalter.push(neuerPlatzhalter(knoten[i]))
}
}
function leere (root) {
while (root.firstChild) root.removeChild(root.firstChild)
}
function ersetze (p, knoten, sig) {
injectStyle()
leere(p.root)
p.root.appendChild(knoten)
p.sig = sig
}
// Das Ersatz-HTML zurueck, falls das Skript es ersetzt hatte (sonst bleibt alles, wie es ist)
function stelleWiederHer (p) {
if (p.sig === '') return
leere(p.root)
p.root.appendChild(p.ausweich.cloneNode(true))
p.sig = ''
}
function findePlan (d, kurz) {
var i
if (kurz) {
for (i = 0; i < d.plaene.length; i++) if (d.plaene[i].kurz === kurz) return d.plaene[i]
return null
}
for (i = 0; i < d.plaene.length; i++) if (d.plaene[i].haupt === true) return d.plaene[i]
return d.plaene.length ? d.plaene[0] : null
}
// Der Eintrag, der jetzt gilt. Geht die Uhr der Seite (innerhalb der Toleranz) nach, ist der erste Eintrag (Beginn der Minute des Abrufs)
// noch "in der Zukunft": er gilt trotzdem.
function aktuellerEintrag (plan, jetztMs) {
var e = waehleWechsel(plan.wechsel, jetztMs)
if (e) return e
var erster = plan.wechsel[0]
return erster && istZahl(erster.ab) && erster.ab - jetztMs <= UHR_TOLERANZ_MS ? erster : null
}
function zustandKlasse (offen) {
return offen === true ? 'wkz-zustand-offen' : offen === false ? 'wkz-zustand-zu' : 'wkz-zustand-unklar'
}
function zeigeTabelle (p, plan, eintrag) {
var t = tabelleDaten(plan, eintrag.tag)
var gruppen = gruppiereAbweichungen(t.abweichungen14)
var name = typeof plan.name === 'string' && plan.name !== '' ? plan.name : 'Öffnungszeiten'
var sig = 'tabelle|' + JSON.stringify([name, t.woche.map(function (w) { return [w.text, w.heute] }), gruppen])
if (p.sig === sig) return
var tabelle = el('table', 'wkz-tabelle')
tabelle.appendChild(el('caption', 'wkz-caption', name))
var tbody = el('tbody')
for (var i = 0; i < t.woche.length; i++) {
var w = t.woche[i]
var tr = el('tr', w.heute ? 'wkz-zeile wkz-heute' : 'wkz-zeile')
if (w.heute) tr.setAttribute('aria-current', 'date')
var th = el('th', 'wkz-tag', w.name)
th.setAttribute('scope', 'row')
tr.appendChild(th)
tr.appendChild(el('td', 'wkz-zeiten', w.text))
tbody.appendChild(tr)
}
tabelle.appendChild(tbody)
var box = document.createDocumentFragment()
box.appendChild(tabelle)
if (gruppen.length) {
var abw = el('div', 'wkz-abw')
abw.appendChild(el('p', 'wkz-abw-titel', 'Abweichungen in den nächsten 14 Tagen'))
var liste = el('ul', 'wkz-abw-liste')
for (var k = 0; k < gruppen.length; k++) {
var z = abweichungsZeile(gruppen[k])
var li = el('li', 'wkz-abw-eintrag')
li.appendChild(el('span', 'wkz-abw-datum', z.datum))
li.appendChild(document.createTextNode(' '))
li.appendChild(el('span', 'wkz-abw-text', z.text))
liste.appendChild(li)
}
abw.appendChild(liste)
box.appendChild(abw)
}
ersetze(p, box, sig)
}
// Punkt und Text in einem Element (Status: der ausfuehrliche Text, Badge: die Kurzform); kein aria-live
function zeigeZustand (p, cls, text, eintrag) {
if (typeof text !== 'string' || text === '') {
stelleWiederHer(p)
return
}
var sig = cls + '|' + String(eintrag.offen) + '|' + text
if (p.sig === sig) return
var box = el('span', cls + ' ' + zustandKlasse(eintrag.offen))
var punkt = el('span', 'wkz-punkt')
punkt.setAttribute('aria-hidden', 'true')
box.appendChild(punkt)
box.appendChild(el('span', 'wkz-text', text))
ersetze(p, box, sig)
}
function fuelle (p, d, jetztMs) {
var plan = d ? findePlan(d, p.plan) : null
if (d && d.plaene.length && p.plan && !plan && p.gewarnt !== p.plan) {
p.gewarnt = p.plan
warne('Plan "' + p.plan + '" nicht gefunden: das Ersatz-HTML bleibt stehen.')
}
var eintrag = plan && p.modus ? aktuellerEintrag(plan, jetztMs) : null
if (!eintrag) {
stelleWiederHer(p)
return
}
if (p.modus === 'tabelle') zeigeTabelle(p, plan, eintrag)
else if (p.modus === 'status') zeigeZustand(p, 'wkz-status', eintrag.text || eintrag.kurz, eintrag)
else zeigeZustand(p, 'wkz-badge', eintrag.kurz, eintrag)
}
function render () {
sammle()
platzhalter = platzhalter.filter(function (p) { return p.root.isConnected !== false })
var d = frisch() ? daten : null
var t = jetzt()
for (var i = 0; i < platzhalter.length; i++) fuelle(platzhalter[i], d, t)
// Task 26: Hinweis-Leiste, Kontakt-Leiste und JSON-LD werden hier mit denselben Daten (d) gezeichnet
}
// ---------------------------------------------------------------- Timer
// Ein Timer auf den naechsten Wechsel (Status, Hinweis-Beginn oder -Ende) oder das naechste erlaubte Neuladen, was frueher kommt.
function plane () {
if (timerId) clearTimeout(timerId)
var t = jetzt()
var alle = []
if (daten) for (var i = 0; i < daten.plaene.length; i++) alle = alle.concat(daten.plaene[i].wechsel)
var naechster = naechsterWechsel(alle, daten ? daten.hinweise : [], t)
// laeuft ein Abruf, plant dessen Ende neu (sonst wuerde ein faelliges Neuladen den Timer auf 0 setzen)
var bisReload = laeuft ? Infinity : versuchMs + RELOAD_MS - Date.now()
var warte = naechster === null ? bisReload : Math.min(naechster - t, bisReload)
timerId = timer(tick, warte + TIMER_PUFFER_MS)
}
function tick () {
if (timerId) clearTimeout(timerId)
timerId = 0
if (!laeuft && Date.now() - versuchMs >= RELOAD_MS) {
lade() // zeichnet und plant danach neu
return
}
render()
plane()
}
// ---------------------------------------------------------------- Start
function eigenesSkript () {
return SELF || document.querySelector('script[data-wk-zeiten], script[src*="/embed/zeiten.js"]')
}
function bestimmeSiteKey (skript) {
var k = skript ? attr(skript, 'data-site') : ''
for (var i = 0; !k && i < platzhalter.length; i++) k = attr(platzhalter[i].root, 'data-webklar-zeiten')
return k
}
function bestimmeApi (skript) {
var a = skript ? attr(skript, 'data-api') : ''
for (var i = 0; !a && i < platzhalter.length; i++) a = attr(platzhalter[i].root, 'data-api')
a = a.replace(/\/+$/, '')
if (a) return a
if (skript && skript.src) {
try { return new URL(skript.src).origin } catch (e) { return '' }
}
return ''
}
function start () {
sammle()
var skript = eigenesSkript()
siteKey = bestimmeSiteKey(skript)
api = bestimmeApi(skript)
if (!siteKey || !api || typeof fetch !== 'function') {
if (platzhalter.length) warne('siteKey (data-site) oder Adresse der Schnittstelle fehlt: die Platzhalter bleiben beim Ersatz-HTML.')
return
}
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') tick()
})
lade()
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start)
else start()
})()