Öffnungszeiten: Hinweis-Leiste, Kontakt-Leiste und JSON-LD im Website-Skript

zeiten.js zeichnet mit denselben Daten wie die Platzhalter eine Hinweis-Leiste
(Shadow DOM, erstes Kind von body, rückt feste und klebende Köpfe, scroll-padding-top),
eine Kontakt-Leiste nur auf schmalen Schirmen (Abstandhalter, scroll-padding-bottom,
entfällt bei eigener Anruf-Leiste) und ergänzt das JSON-LD der Seite (nur die beiden
Zeiten-Felder, sonst eigener Block). Alle Adressen aus der Schnittstelle werden im
Browser erneut geprüft. Ruling R29: tick mit try/finally, sofort werfender fetch,
erst zeichnen dann laden beim Zurückkehren in den Tab.

Tests: statische Funktionstests der reinen Funktionen und eine Playwright-Prüfung
gegen den UI-Testserver (echte öffentliche Schnittstelle, Nachbau von Appwrite).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 04:27:04 +00:00
1 parent a6a4eae294
commit ed99234ae4
4 files changed
+1542 -8

No files matched your search

+559 -8
View File
@@ -15,6 +15,22 @@
* 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.
*
* Leisten und JSON-LD (kommen mit den Daten, ohne Platzhalter; das Team kann sie je Seite abschalten):
* Hinweis-Leiste Shadow-DOM-Host [data-wkz-leiste="hinweis"] als erstes Kind von <body>, im Fluss (schiebt den Inhalt). Zeigt den
* gueltigen Hinweis mit dem hoechsten Rang (Info, Aktion, Urlaub, Wichtig); role="region" aria-label="Hinweis",
* kein Schliessen-Knopf. Feste und klebende Elemente mit top: 0 (und die des Team-Selektors navSelektor) rutschen per
* Inline-top um max(0, Leistenhoehe - Scrollposition) nach unten (beim Entfernen wie vorher), scroll-padding-top des
* Dokuments waechst um die Hoehe. Ein festes Element, das schon ueber der Leiste endet (die Seite hat seinen Platz per
* body-Polster freigehalten, die Leiste steht darunter), bleibt, wo es ist. Mit prefers-reduced-motion ohne Einblendung.
* Kontakt-Leiste nur bei (max-width: 768px): Shadow-DOM-Host [data-wkz-leiste="kontakt"] unten fest (z-index 40) mit Anrufen, Route
* und WhatsApp (nur, was angegeben ist), dazu ein Abstandhalter [data-wkz-abstand] am Ende von <body> und
* scroll-padding-bottom. Entfaellt, wenn die Seite selbst ein festes Element am unteren Rand mit tel:-Link hat.
* JSON-LD ein vorhandener application/ld+json-Block mit einem Unternehmen vor Ort (LocalBusiness oder Untertyp, auch in @graph)
* bekommt nur openingHoursSpecification und specialOpeningHoursSpecification neu; gibt es keinen, legt das Skript
* einen Block [data-wkz-jsonld] an. Unlesbare Bloecke bleiben unberuehrt.
* Jede Adresse aus der Schnittstelle wird im Browser noch einmal geprueft (https:, tel: nur ueber telefonLink); Links tragen
* rel="noopener noreferrer".
*
* 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
@@ -183,6 +199,143 @@
if (g.bis && g.bis !== g.von) datum += ' – ' + datumKurz(g.bis)
return { datum: datum, text: (g.name || artName(g.art) || 'Abweichung') + ': ' + g.text }
}
function istObjekt (v) { return v !== null && typeof v === 'object' && !Array.isArray(v) }
// Farben der Hinweis-Leiste je Art (Hintergrund, Schrift): Kontrast jeweils mindestens 4,5:1 (geprueft in test/zeitenEmbedStatic.test.js)
var HINWEIS_FARBEN = {
info: { bg: '#e0f2fe', fg: '#0c4a6e' },
aktion: { bg: '#dcfce7', fg: '#14532d' },
urlaub: { bg: '#fef3c7', fg: '#78350f' },
wichtig: { bg: '#b91c1c', fg: '#ffffff' }
}
// schema.org: LocalBusiness und seine Untertypen (Unternehmen vor Ort); Person, Organization, WebSite, WebPage u. a. gehoeren nicht dazu
var LOKALE_TYPEN = ' LocalBusiness AnimalShelter ArchiveOrganization AutomotiveBusiness AutoBodyShop AutoDealer AutoPartsStore AutoRental AutoRepair AutoWash GasStation MotorcycleDealer MotorcycleRepair ' +
'ChildCare Dentist DryCleaningOrLaundry EmergencyService FireStation Hospital PoliceStation EmploymentAgency EntertainmentBusiness AdultEntertainment AmusementPark ArtGallery Casino ComedyClub MovieTheater NightClub ' +
'FinancialService AccountingService AutomatedTeller BankOrCreditUnion InsuranceAgency FoodEstablishment Bakery BarOrPub Brewery CafeOrCoffeeShop Distillery FastFoodRestaurant IceCreamShop Restaurant Winery ' +
'GovernmentOffice PostOffice HealthAndBeautyBusiness BeautySalon DaySpa HairSalon HealthClub NailSalon TattooParlor HomeAndConstructionBusiness Electrician GeneralContractor HVACBusiness HousePainter Locksmith MovingCompany Plumber RoofingContractor ' +
'InternetCafe LegalService Attorney Notary Library LodgingBusiness BedAndBreakfast Campground Hostel Hotel Motel Resort SkiResort VacationRental ' +
'MedicalBusiness CommunityHealth Dermatology DietNutrition Emergency Geriatric Gynecologic MedicalClinic Midwifery Nursing Obstetric Oncologic Optician Optometric Otolaryngologic Pediatric Pharmacy Physician Physiotherapy PlasticSurgery Podiatric PrimaryCare Psychiatric PublicHealth ' +
'ProfessionalService RadioStation RealEstateAgent RecyclingCenter SelfStorage ShoppingCenter SportsActivityLocation BowlingAlley ExerciseGym GolfCourse PublicSwimmingPool SportsClub StadiumOrArena TennisComplex ' +
'Store BikeStore BookStore ClothingStore ComputerStore ConvenienceStore DepartmentStore ElectronicsStore Florist FurnitureStore GardenStore GroceryStore HardwareStore HobbyShop HomeGoodsStore JewelryStore LiquorStore MensClothingStore MobilePhoneStore MovieRentalStore MusicStore OfficeEquipmentStore OutletStore PawnShop PetStore ShoeStore SportingGoodsStore TireShop ToyStore WholesaleStore ' +
'TelevisionStation TouristInformationCenter TravelAgency '
// Eine Adresse aus der Schnittstelle: nur https mit Host, ohne Leer- und Steuerzeichen (sonst ''). Es steht keine feste Webadresse im Skript,
// nur diese Pruefung des Schemas.
function sichererLink (url) {
if (typeof url !== 'string' || url.length > 2000 || /[\s\u0000-\u001f\u007f]/.test(url)) return ''
return /^https:\/\/[^\/?#]+/i.test(url) ? url : ''
}
// Knopf eines Hinweises { art: 'link'|'anruf', ziel, text } als { art, href, text } oder null. Links: nur https; Anruf: tel: nur ueber telefonLink.
function hinweisKnopf (knopf) {
if (!istObjekt(knopf)) return null
var href = knopf.art === 'link' ? sichererLink(knopf.ziel) : knopf.art === 'anruf' ? telefonLink(knopf.ziel) : ''
if (!href) return null
var text = typeof knopf.text === 'string' ? knopf.text.replace(/^\s+|\s+$/g, '') : ''
return { art: knopf.art, href: href, text: text || (knopf.art === 'anruf' ? 'Anrufen' : 'Mehr erfahren') }
}
// Knoepfe der Kontakt-Leiste aus { telefon, routeUrl, whatsappUrl }: [{ art: 'anruf'|'route'|'whatsapp', href, text }], nur mit brauchbaren Angaben.
// telefon = Ziffern mit Laendervorwahl ohne "+" (E.164), daraus tel:+... ueber telefonLink; Route und WhatsApp nur als https-Adresse.
function kontaktKnoepfe (kontakt) {
var liste = []
if (!istObjekt(kontakt)) return liste
var tel = typeof kontakt.telefon === 'string' && /^[1-9][0-9]{5,14}$/.test(kontakt.telefon) ? telefonLink('+' + kontakt.telefon) : ''
if (tel) liste.push({ art: 'anruf', href: tel, text: 'Anrufen' })
var route = sichererLink(kontakt.routeUrl)
if (route) liste.push({ art: 'route', href: route, text: 'Route' })
var wa = sichererLink(kontakt.whatsappUrl)
if (wa) liste.push({ art: 'whatsapp', href: wa, text: 'WhatsApp' })
return liste
}
// Hat die Seite schon eine eigene Anruf-Leiste? elemente = die festen/klebenden Vorfahren aller a[href^="tel:"] der Seite als
// { position, bottom (berechnet), oben, unten (Rand im Fenster), fenster (Fensterhoehe), sichtbar }. Zaehlt: sichtbar, fest (oder klebend mit
// bottom) und in der unteren Haelfte, hoechstens 160 px ueber dem unteren Rand (auch ein schwebender Anruf-Knopf). Kopfzeilen, bildschirmhohe
// Seitenleisten und Ausgeblendetes zaehlen nicht.
function istEigeneAnrufLeiste (elemente) {
if (!Array.isArray(elemente)) return false
for (var i = 0; i < elemente.length; i++) {
var e = elemente[i]
if (!e || e.sichtbar !== true || !istZahl(e.oben) || !istZahl(e.unten) || !istZahl(e.fenster)) continue
if (e.position !== 'fixed' && !(e.position === 'sticky' && e.bottom !== 'auto')) continue
if (e.oben >= e.fenster / 2 && e.oben < e.fenster && e.unten >= e.fenster - 160) return true
}
return false
}
function typVon (t) { return typeof t === 'string' ? t.replace(/^.*[\/:#]/, '') : '' }
// Ein Knoten ist ein Unternehmen vor Ort, wenn einer seiner @type (Text oder Liste, auch mit Praefix oder als Adresse) LocalBusiness oder ein Untertyp ist.
function istLokalesUnternehmen (knoten) {
var typen = Array.isArray(knoten['@type']) ? knoten['@type'] : [knoten['@type']]
for (var i = 0; i < typen.length; i++) {
var t = typVon(typen[i])
if (/^[A-Za-z]+$/.test(t) && LOKALE_TYPEN.indexOf(' ' + t + ' ') >= 0) return true
}
return false
}
// Alle Objekte eines geparsten JSON-LD-Blocks: er selbst (Objekt oder Liste von Objekten) und die Knoten seines @graph.
function sammleKnoten (wert, aus, tiefe) {
if (tiefe > 4) return
if (Array.isArray(wert)) {
for (var i = 0; i < wert.length; i++) sammleKnoten(wert[i], aus, tiefe + 1)
return
}
if (!istObjekt(wert)) return
aus.push(wert)
if (Array.isArray(wert['@graph'])) sammleKnoten(wert['@graph'], aus, tiefe + 1)
}
function kopie (v) { return JSON.parse(JSON.stringify(v)) }
// Zeiten fuer schema.org: bloecke = geparste application/ld+json-Inhalte der Seite (unparsbare als null), jsonld = { openingHoursSpecification,
// specialOpeningHoursSpecification, neuerBlock } der Schnittstelle, seite = { name, adresse, url }. In jedem Unternehmen vor Ort der Bloecke
// werden NUR openingHoursSpecification und specialOpeningHoursSpecification ersetzt (leere Listen entfernen das Feld); die Bloecke werden
// dabei veraendert, geaendert = ihre Indizes (nur wo sich etwas aendert). Gibt es kein Unternehmen vor Ort, ist neu der neuerBlock des Servers
// mit name, address (wenn bekannt) und url (ohne Namen: null, ein LocalBusiness ohne name taugt nicht). Ohne jsonld bleibt alles, wie es ist.
function jsonLdEinbauen (bloecke, jsonld, seite) {
var erg = { geaendert: [], neu: null }
if (!istObjekt(jsonld)) return erg
if (!Array.isArray(bloecke)) bloecke = []
var zeiten = Array.isArray(jsonld.openingHoursSpecification) ? jsonld.openingHoursSpecification : []
var sonder = Array.isArray(jsonld.specialOpeningHoursSpecification) ? jsonld.specialOpeningHoursSpecification : []
var gefunden = false
var i, k
function setze (knoten, schluessel, liste) {
if (liste.length) knoten[schluessel] = kopie(liste)
else delete knoten[schluessel]
}
for (i = 0; i < bloecke.length; i++) {
var knoten = []
sammleKnoten(bloecke[i], knoten, 0)
var geaendert = false
for (k = 0; k < knoten.length; k++) {
if (!istLokalesUnternehmen(knoten[k])) continue
gefunden = true
var vorher = JSON.stringify([knoten[k].openingHoursSpecification, knoten[k].specialOpeningHoursSpecification])
setze(knoten[k], 'openingHoursSpecification', zeiten)
setze(knoten[k], 'specialOpeningHoursSpecification', sonder)
if (JSON.stringify([knoten[k].openingHoursSpecification, knoten[k].specialOpeningHoursSpecification]) !== vorher) geaendert = true
}
if (geaendert) erg.geaendert.push(i)
}
var s = istObjekt(seite) ? seite : {}
var name = typeof s.name === 'string' ? s.name.replace(/^\s+|\s+$/g, '') : ''
if (!gefunden && name && istObjekt(jsonld.neuerBlock)) {
var neu = kopie(jsonld.neuerBlock)
neu.name = name
if (typeof s.adresse === 'string' && s.adresse.replace(/^\s+|\s+$/g, '')) neu.address = s.adresse.replace(/^\s+|\s+$/g, '')
if (typeof s.url === 'string' && s.url) neu.url = s.url
if (Array.isArray(neu.openingHoursSpecification) && !neu.openingHoursSpecification.length) delete neu.openingHoursSpecification
if (Array.isArray(neu.specialOpeningHoursSpecification) && !neu.specialOpeningHoursSpecification.length) delete neu.specialOpeningHoursSpecification
erg.neu = neu
}
return erg
}
// @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)
@@ -225,7 +378,7 @@
// ---------------------------------------------------------------- Zustand (nur im Speicher der Seite)
var daten = null // letzte gueltige Antwort { plaene, hinweise } oder null (aktiv: false, Fehler, noch kein Abruf)
var daten = null // letzte gueltige Antwort { plaene, hinweise, kontakt, jsonld, firma, team } 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
@@ -234,6 +387,18 @@
var platzhalter = []
var siteKey = ''
var api = ''
var HANDY_MQ = '(max-width: 768px)' // Kontakt-Leiste nur auf schmalen Schirmen
var hinweisLeiste = null // { host, sig, nav, hoehe, fest } der Hinweis-Leiste auf der Seite (null: keine)
var kontaktLeiste = null // { host, abstand, sig } der Kontakt-Leiste samt Abstandhalter (null: keine)
var SCROLL_PADDING = { top: 'scroll-padding-top', bottom: 'scroll-padding-bottom' }
var scrollPadding = { top: null, bottom: null } // was die Seite vor uns in scroll-padding-top/-bottom hatte: { wert, prio, basis }
var handyAbfrage = null
var hoererAn = false
var beobachter = null
var scrollWartet = false
var groesseWartet = false
var ldSig = ''
var ldEigenes = null // der von uns angelegte JSON-LD-Block (nur wenn die Seite keinen eigenen hat)
function timer (fn, ms) {
var d = Number(ms)
@@ -283,7 +448,14 @@
daten = null
return
}
daten = { plaene: body.plaene.filter(planGueltig), hinweise: Array.isArray(body.hinweise) ? body.hinweise : [] }
daten = {
plaene: body.plaene.filter(planGueltig),
hinweise: Array.isArray(body.hinweise) ? body.hinweise : [],
kontakt: istObjekt(body.kontakt) ? body.kontakt : null,
jsonld: istObjekt(body.jsonld) ? body.jsonld : null,
firma: istObjekt(body.firma) ? body.firma : {},
team: istObjekt(body.team) ? body.team : {}
}
}
function lade () {
@@ -302,7 +474,14 @@
laeuft = false
try { render() } finally { plane() }
}
fetch(api + API_PFAD + encodeURIComponent(siteKey), opts).then(function (r) {
var anfrage
try {
anfrage = fetch(api + API_PFAD + encodeURIComponent(siteKey), opts)
} catch (e) {
fertig() // ein sofort werfender fetch beendet den Abruf wie ein Fehler (laeuft bleibt nie haengen)
return
}
anfrage.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) })
@@ -446,13 +625,380 @@
else zeigeZustand(p, 'wkz-badge', eintrag.kurz, eintrag)
}
// Ein Fehler in einem Teil (Platzhalter, Hinweis-Leiste, Kontakt-Leiste, JSON-LD) darf die anderen nicht aufhalten
function sicher (fn) {
try { fn() } catch (e) { warne('Fehler beim Zeichnen: ' + (e && e.message ? e.message : e)) }
}
function fuelleSicher (p, d, t) { sicher(function () { fuelle(p, d, t) }) }
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
for (var i = 0; i < platzhalter.length; i++) fuelleSicher(platzhalter[i], d, t)
zeichneLeisten(d, t)
sicher(function () { zeichneJsonLd(d) })
}
// ---------------------------------------------------------------- Leisten (Shadow DOM, vom Skript gezeichnet)
var LEISTE_SCHRIFT = 'system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'
function hinweisCss () {
var teile = [
':host{all:initial;display:block}',
'.wkz-leiste{box-sizing:border-box;display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:4px 16px;margin:0;padding:8px 16px;font:500 15px/1.4 ' + LEISTE_SCHRIFT + ';text-align:center;background:var(--bg);color:var(--fg);animation:wkz-ein .3s ease-out}',
'.wkz-text{margin:0;min-width:0;overflow-wrap:anywhere}',
'.wkz-knopf{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-width:44px;min-height:44px;padding:0 18px;border-radius:999px;background:var(--fg);color:var(--bg);font:inherit;font-weight:700;text-decoration:none}',
'.wkz-knopf:focus-visible{outline:3px solid var(--fg);outline-offset:3px}',
'@keyframes wkz-ein{from{opacity:0;transform:translateY(-100%)}to{opacity:1;transform:none}}',
'@media (prefers-reduced-motion:reduce){.wkz-leiste{animation:none}}'
]
for (var art in HINWEIS_FARBEN) {
if (Object.prototype.hasOwnProperty.call(HINWEIS_FARBEN, art)) teile.push('.wkz-' + art + '{--bg:' + HINWEIS_FARBEN[art].bg + ';--fg:' + HINWEIS_FARBEN[art].fg + '}')
}
return teile.join('\n')
}
function kontaktCss () {
return [
':host{all:initial;display:block}',
'.wkz-leiste{box-sizing:border-box;display:flex;gap:8px;margin:0;padding:8px 12px calc(8px + env(safe-area-inset-bottom,0px));background:#fff;border-top:1px solid #d1d5db;box-shadow:0 -2px 10px rgba(0,0,0,.15);font:600 16px/1.2 ' + LEISTE_SCHRIFT + '}',
'.wkz-knopf{box-sizing:border-box;flex:1 1 0;min-width:44px;min-height:48px;display:flex;align-items:center;justify-content:center;padding:0 8px;border:2px solid #111827;border-radius:10px;background:#fff;color:#111827;font:inherit;text-decoration:none;text-align:center}',
'.wkz-anruf{background:#111827;color:#fff}',
'.wkz-knopf:focus-visible{outline:3px solid #2563eb;outline-offset:2px}'
].join('\n')
}
// Host mit Shadow DOM und eigenem Stil (null, wo der Browser kein Shadow DOM kann: dann gibt es keine Leiste)
function neueLeiste (art, css) {
var host = document.createElement('div')
if (typeof host.attachShadow !== 'function') return null
host.setAttribute('data-wkz-leiste', art)
var wurzel = host.attachShadow({ mode: 'open' })
var st = document.createElement('style')
st.textContent = css
wurzel.appendChild(st)
return { host: host, wurzel: wurzel }
}
// Link aus einem { href, text } der reinen Funktionen (href ist dort schon geprueft); nie ueber HTML-Text
function linkKnopf (k, klasse, neuerTab) {
var a = el('a', klasse, k.text)
a.setAttribute('href', k.href)
a.setAttribute('rel', 'noopener noreferrer')
if (neuerTab) a.setAttribute('target', '_blank')
return a
}
function naechstesBild (fn) {
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(fn)
else fn()
}
function pixel (wert) { return /^-?\d+(\.\d+)?px$/.test(wert) ? parseFloat(wert) : 0 }
// scroll-padding-top/-bottom des Dokuments um hoehePx erhoehen (der Wert der Seite wird gemerkt und beim Entfernen zurueckgesetzt)
function erhoeheScrollPadding (seite, hoehePx) {
var prop = SCROLL_PADDING[seite]
var st = document.documentElement.style
if (!scrollPadding[seite]) {
scrollPadding[seite] = { wert: st.getPropertyValue(prop), prio: st.getPropertyPriority(prop), basis: pixel(getComputedStyle(document.documentElement).getPropertyValue(prop)) }
}
st.setProperty(prop, (scrollPadding[seite].basis + hoehePx) + 'px', 'important')
}
function stelleScrollPaddingWieder (seite) {
var merk = scrollPadding[seite]
if (!merk) return
var prop = SCROLL_PADDING[seite]
var st = document.documentElement.style
if (merk.wert) st.setProperty(prop, merk.wert, merk.prio)
else st.removeProperty(prop)
scrollPadding[seite] = null
}
function istFest (cs) { return cs.position === 'fixed' || cs.position === 'sticky' }
function merke (e, cs) {
var top = parseFloat(cs.top)
return { el: e, wert: e.style.getPropertyValue('top'), prio: e.style.getPropertyPriority('top'), basis: top === top ? top : 0 }
}
// Feste und klebende Elemente, die die Leiste verdecken wuerde: die des Team-Selektors nav (jedes feste/klebende, auch mit anderem top) und
// alle mit top: 0, sichtbar und hoechstens halb so hoch wie das Fenster (kein Menue-Panel, kein Overlay). Ein festes Element, das schon
// ueber der Leiste endet (die Seite hat ihren Kopf per Polster oder Rand von body freigehalten, die Leiste steht erst darunter), bleibt,
// wo es ist: barOben ist der Abstand der Leiste vom Seitenanfang. nav kann ungueltig sein: nur in try/catch.
function sucheFeste (nav, barOben) {
var liste = []
var bekannt = []
var i, e, cs
if (nav) {
var treffer = []
try { treffer = document.querySelectorAll(nav) } catch (fehler) { treffer = [] }
for (i = 0; i < treffer.length; i++) {
cs = getComputedStyle(treffer[i])
if (!istFest(cs)) continue
liste.push(merke(treffer[i], cs))
bekannt.push(treffer[i])
}
}
var alle = document.body.getElementsByTagName('*')
var hoeheFenster = window.innerHeight || 0
for (i = 0; i < alle.length && i < 4000; i++) {
e = alle[i]
if (bekannt.indexOf(e) >= 0 || e.hasAttribute('data-wkz-leiste')) continue
cs = getComputedStyle(e)
if (!istFest(cs) || parseFloat(cs.top) !== 0 || cs.display === 'none') continue
var r = e.getBoundingClientRect()
if (r.height <= 0 || r.height > hoeheFenster * 0.5) continue
if (cs.position === 'fixed' && r.bottom <= barOben + 0.5) continue
liste.push(merke(e, cs))
}
return liste
}
// Feste Elemente um max(0, Leistenhoehe - Scrollposition) nach unten ruecken (Inline-top, hat Vorrang vor den Regeln der Seite)
function verschiebe (l) {
var y = window.pageYOffset || document.documentElement.scrollTop || 0
var s = Math.max(0, l.hoehe - Math.max(0, y))
for (var i = 0; i < l.fest.length; i++) l.fest[i].el.style.setProperty('top', (l.fest[i].basis + s) + 'px', 'important')
}
function setzeFesteZurueck (l) {
for (var i = 0; i < l.fest.length; i++) {
var f = l.fest[i]
if (f.wert) f.el.style.setProperty('top', f.wert, f.prio)
else f.el.style.removeProperty('top')
}
l.fest = []
}
// Hoehe der Hinweis-Leiste neu messen, feste Elemente neu suchen (erst die alten zuruecksetzen: berechnet wird immer der Stand der Seite) und ruecken
function misstHinweis () {
var l = hinweisLeiste
if (!l) return
setzeFesteZurueck(l)
var r = l.host.getBoundingClientRect()
l.hoehe = r.height
l.fest = sucheFeste(l.nav, r.top + (window.pageYOffset || document.documentElement.scrollTop || 0))
erhoeheScrollPadding('top', l.hoehe)
verschiebe(l)
}
function misstKontakt () {
var l = kontaktLeiste
if (!l) return
var h = l.host.getBoundingClientRect().height
l.abstand.style.cssText = 'display:block;width:100%;height:' + h + 'px;margin:0;padding:0;border:0;visibility:hidden'
erhoeheScrollPadding('bottom', h)
}
function beiScroll () {
if (!hinweisLeiste || scrollWartet) return
scrollWartet = true
naechstesBild(function () {
scrollWartet = false
if (hinweisLeiste) verschiebe(hinweisLeiste)
})
}
// Fenster oder Leiste aendern ihre Groesse: hoechstens alle 120 ms neu messen und suchen (beim Ziehen am Fenster nicht in jedem Bild)
function beiGroesse () {
if (groesseWartet) return
groesseWartet = true
timer(function () {
groesseWartet = false
sicher(misstHinweis)
sicher(misstKontakt)
}, 120)
}
// Scroll (nur gemerkt, ein Bild spaeter gerueckt) und Groesse (Fenster oder Leiste) werden einmal beobachtet, sobald es eine Leiste gibt
function sorgeFuerHoerer () {
if (hoererAn) return
hoererAn = true
window.addEventListener('scroll', beiScroll, { passive: true })
window.addEventListener('resize', beiGroesse)
if (typeof ResizeObserver === 'function') beobachter = new ResizeObserver(beiGroesse)
}
function entferneLeiste (host) {
if (beobachter) beobachter.unobserve(host)
if (host.parentNode) host.parentNode.removeChild(host)
}
function zeigeHinweisLeiste (h, nav) {
var art = Object.prototype.hasOwnProperty.call(HINWEIS_FARBEN, h.art) ? h.art : 'info'
var knopf = hinweisKnopf(h.knopf)
var sig = JSON.stringify([art, h.text, knopf, nav])
var l = hinweisLeiste
var body = document.body
if (l && l.sig === sig && l.host.parentNode === body) {
// unveraendert: nur dafuer sorgen, dass sie erstes Kind von body bleibt (die Seite kann danach etwas davorgesetzt haben)
if (body.firstChild !== l.host) {
body.insertBefore(l.host, body.firstChild)
misstHinweis()
}
return
}
entferneHinweisLeiste()
var teile = neueLeiste('hinweis', hinweisCss())
if (!teile) return
var leiste = el('div', 'wkz-leiste wkz-' + art)
leiste.setAttribute('role', 'region')
leiste.setAttribute('aria-label', 'Hinweis')
leiste.appendChild(el('p', 'wkz-text', h.text))
if (knopf) leiste.appendChild(linkKnopf(knopf, 'wkz-knopf', false))
teile.wurzel.appendChild(leiste)
body.insertBefore(teile.host, body.firstChild)
hinweisLeiste = { host: teile.host, sig: sig, nav: nav, hoehe: 0, fest: [] }
sorgeFuerHoerer()
if (beobachter) beobachter.observe(teile.host)
misstHinweis()
}
function entferneHinweisLeiste () {
var l = hinweisLeiste
if (!l) return
hinweisLeiste = null
setzeFesteZurueck(l)
stelleScrollPaddingWieder('top')
entferneLeiste(l.host)
}
function zeigeKontaktLeiste (knoepfe) {
var sig = JSON.stringify(knoepfe)
var l = kontaktLeiste
var body = document.body
if (l && l.sig === sig && l.host.parentNode === body && l.abstand.parentNode === body) {
if (l.abstand.nextSibling) body.appendChild(l.abstand) // der Abstandhalter bleibt das Letzte in body
return
}
entferneKontaktLeiste()
var teile = neueLeiste('kontakt', kontaktCss())
if (!teile) return
var leiste = el('div', 'wkz-leiste')
leiste.setAttribute('role', 'region')
leiste.setAttribute('aria-label', 'Kontakt')
for (var i = 0; i < knoepfe.length; i++) leiste.appendChild(linkKnopf(knoepfe[i], 'wkz-knopf wkz-' + knoepfe[i].art, knoepfe[i].art !== 'anruf'))
teile.wurzel.appendChild(leiste)
teile.host.style.cssText = 'position:fixed;left:0;right:0;bottom:0;z-index:40;display:block;margin:0;padding:0;border:0'
var abstand = document.createElement('div')
abstand.setAttribute('data-wkz-abstand', '')
abstand.setAttribute('aria-hidden', 'true')
body.appendChild(teile.host)
body.appendChild(abstand)
kontaktLeiste = { host: teile.host, abstand: abstand, sig: sig }
sorgeFuerHoerer()
if (beobachter) beobachter.observe(teile.host)
misstKontakt()
}
function entferneKontaktLeiste () {
var l = kontaktLeiste
if (!l) return
kontaktLeiste = null
stelleScrollPaddingWieder('bottom')
entferneLeiste(l.host)
if (l.abstand.parentNode) l.abstand.parentNode.removeChild(l.abstand)
}
function beiHandyWechsel () { sicher(render) }
function istHandy () {
if (typeof window.matchMedia !== 'function') return false
if (!handyAbfrage) {
handyAbfrage = window.matchMedia(HANDY_MQ)
if (typeof handyAbfrage.addEventListener === 'function') handyAbfrage.addEventListener('change', beiHandyWechsel)
else if (typeof handyAbfrage.addListener === 'function') handyAbfrage.addListener(beiHandyWechsel)
}
return handyAbfrage.matches
}
// Die festen/klebenden Vorfahren aller sichtbaren tel:-Links, als Beschreibung fuer istEigeneAnrufLeiste
function eigeneAnrufLeiste () {
var links = document.querySelectorAll('a[href^="tel:"]')
var elemente = []
var fenster = window.innerHeight || 0
for (var i = 0; i < links.length && i < 50; i++) {
if (!links[i].getClientRects().length) continue // ausgeblendet
for (var n = links[i]; n && n !== document.documentElement; n = n.parentElement) {
var cs = getComputedStyle(n)
if (cs.position !== 'fixed' && cs.position !== 'sticky') continue
var r = n.getBoundingClientRect()
elemente.push({ position: cs.position, bottom: cs.bottom, oben: r.top, unten: r.bottom, fenster: fenster, sichtbar: cs.display !== 'none' && cs.visibility !== 'hidden' && r.width > 0 && r.height > 0 })
break
}
}
return istEigeneAnrufLeiste(elemente)
}
// Beide Leisten aus denselben Daten (d = null: Fehler, aktiv: false oder Daten zu alt: keine Leiste). Hinweis: der gueltige mit dem hoechsten
// Rang, nicht bei team.hinweisleisteAus. Kontakt: nur mit Daten, auf schmalen Schirmen, nicht bei team.kontaktleisteAus und nicht neben einer
// eigenen Anruf-Leiste der Seite.
function zeichneLeisten (d, t) {
var team = d && istObjekt(d.team) ? d.team : {}
sicher(function () {
var h = d && team.hinweisleisteAus !== true && document.body ? waehleHinweis(d.hinweise, t) : null
if (h && typeof h.text === 'string' && h.text !== '') zeigeHinweisLeiste(h, typeof team.navSelektor === 'string' ? team.navSelektor : '')
else entferneHinweisLeiste()
})
sicher(function () {
var knoepfe = d && team.kontaktleisteAus !== true && document.body ? kontaktKnoepfe(d.kontakt) : []
if (knoepfe.length && istHandy() && !eigeneAnrufLeiste()) zeigeKontaktLeiste(knoepfe)
else entferneKontaktLeiste()
})
}
// ---------------------------------------------------------------- JSON-LD
function ldSkripte () {
var alle = document.getElementsByTagName('script')
var liste = []
for (var i = 0; i < alle.length; i++) {
if (alle[i] !== ldEigenes && String(alle[i].getAttribute('type') || '').toLowerCase().replace(/\s+/g, '') === 'application/ld+json') liste.push(alle[i])
}
return liste
}
function seitenUrl () {
var o = window.location && window.location.origin
return typeof o === 'string' && o !== '' && o !== 'null' ? o + '/' : ''
}
// Zeiten ins JSON-LD der Seite: vorhandene Unternehmens-Bloecke bekommen nur die beiden Zeiten-Felder neu (Text per JSON.stringify), sonst legt das
// Skript einen eigenen Block an. Ohne jsonld in der Antwort (noch keine Zeiten gespeichert) bleibt die Seite voellig unberuehrt.
function zeichneJsonLd (d) {
var jd = d && istObjekt(d.jsonld) ? d.jsonld : null
if (!jd) return
var firma = istObjekt(d.firma) ? d.firma : {}
var seite = { name: firma.name, adresse: firma.adresse, url: seitenUrl() }
var sig = JSON.stringify([jd, seite])
if (sig === ldSig) return
var skripte = ldSkripte()
var bloecke = []
var i
for (i = 0; i < skripte.length; i++) {
try { bloecke.push(JSON.parse(skripte[i].textContent)) } catch (e) { bloecke.push(null) }
}
var erg = jsonLdEinbauen(bloecke, jd, seite)
for (i = 0; i < erg.geaendert.length; i++) skripte[erg.geaendert[i]].textContent = JSON.stringify(bloecke[erg.geaendert[i]])
if (erg.neu) {
if (!ldEigenes) {
ldEigenes = document.createElement('script')
ldEigenes.setAttribute('type', 'application/ld+json')
ldEigenes.setAttribute('data-wkz-jsonld', '')
;(document.head || document.documentElement).appendChild(ldEigenes)
}
ldEigenes.textContent = JSON.stringify(erg.neu)
} else if (ldEigenes) {
if (ldEigenes.parentNode) ldEigenes.parentNode.removeChild(ldEigenes)
ldEigenes = null
}
ldSig = sig
}
// ---------------------------------------------------------------- Timer
@@ -477,8 +1023,7 @@
lade() // zeichnet und plant danach neu
return
}
render()
plane()
try { render() } finally { plane() }
}
// ---------------------------------------------------------------- Start
@@ -514,7 +1059,13 @@
return
}
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible') tick()
if (document.visibilityState !== 'visible') return
// Zeit ist vergangen: ist ein Neuladen faellig, erst mit den alten Daten zeichnen (Hinweis, Status und die 24-Stunden-Grenze stimmen sofort), dann laden
if (!laeuft && Date.now() - versuchMs >= RELOAD_MS) {
try { render() } finally { lade() }
return
}
tick()
})
lade()
}