Ö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>
This commit is contained in:
1 parent
a2d11ad6c4
commit
a6a4eae294
3 files changed
+984
No files matched your search
@@ -0,0 +1,57 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Öffnungszeiten Testseite</title>
|
||||
<style>
|
||||
body{margin:0;font-family:system-ui,sans-serif;background:#f6f7f9;color:#1f2937;line-height:1.5}
|
||||
header,footer{padding:16px 24px;background:#e8ebf0}
|
||||
main{max-width:760px;margin:0 auto;padding:32px 16px}
|
||||
h2{margin:32px 0 12px}
|
||||
.ersatz{margin:0;padding:12px 16px;border:2px dashed #94a3b8;border-radius:12px;background:#fff}
|
||||
.ersatz table{border-collapse:collapse}
|
||||
.ersatz th,.ersatz td{padding:.25em .75em .25em 0;text-align:left}
|
||||
/* Gestaltung ueber CSS-Variablen der Seite (alle optional): [data-webklar-zeiten]{ --wkz-offen:#0f766e; --wkz-heute:#fde68a; } */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>Testseite der Kundenwebsite (intern, nicht verlinkt)</header>
|
||||
<main>
|
||||
<h1>Öffnungszeiten</h1>
|
||||
<p>Der siteKey unten ist ein Platzhalter: für echte Daten <code>data-site</code> durch den siteKey des Projekts ersetzen (und bei Bedarf <code>data-api</code> am Script-Tag ergänzen). Ohne Daten (oder bei einem Fehler) bleibt überall das Ersatz-HTML stehen.</p>
|
||||
|
||||
<h2>Tabelle (Hauptplan)</h2>
|
||||
<div data-webklar-zeiten data-modus="tabelle">
|
||||
<div class="ersatz">
|
||||
<table>
|
||||
<caption>Öffnungszeiten</caption>
|
||||
<tr><th scope="row">Montag</th><td>09:00 – 18:00</td></tr>
|
||||
<tr><th scope="row">Dienstag</th><td>09:00 – 18:00</td></tr>
|
||||
<tr><th scope="row">Mittwoch</th><td>09:00 – 18:00</td></tr>
|
||||
<tr><th scope="row">Donnerstag</th><td>09:00 – 18:00</td></tr>
|
||||
<tr><th scope="row">Freitag</th><td>09:00 – 18:00</td></tr>
|
||||
<tr><th scope="row">Samstag</th><td>09:00 – 13:00</td></tr>
|
||||
<tr><th scope="row">Sonntag</th><td>geschlossen</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Status (Hauptplan)</h2>
|
||||
<div data-webklar-zeiten data-modus="status">
|
||||
<p class="ersatz">Mo–Fr 09:00 – 18:00 Uhr, Sa 09:00 – 13:00 Uhr.</p>
|
||||
</div>
|
||||
|
||||
<h2>Badge (Plan „haupt“, im Fließtext)</h2>
|
||||
<p>Wir haben gerade <span data-webklar-zeiten data-modus="badge" data-plan="haupt">Mo–Fr 09:00 – 18:00 Uhr</span> für Sie da.</p>
|
||||
|
||||
<h2>Unbekannter Plan (Ersatz-HTML bleibt, Warnung in der Konsole)</h2>
|
||||
<div data-webklar-zeiten data-modus="status" data-plan="gibt-es-nicht">
|
||||
<p class="ersatz">Dieser Platzhalter nennt einen Plan, den es nicht gibt.</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>Fußbereich</footer>
|
||||
<script src="/embed/zeiten.js" data-site="abcdefghijklmnop" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,524 @@
|
||||
/*! 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()
|
||||
})()
|
||||
Reference in new issue
Block a user