Öffnungszeiten: Fixrunde Website-Skript und Editor (JSON-LD, Kopf beim Scrollen, späte Platzhalter, Links, Bestätigung)

- JSON-LD wird ohne "<" geschrieben (<), ein </script> im Firmennamen bricht keinen Block mehr
- verschiebe(): ist die Hinweis-Leiste weggescrollt, bekommen feste Elemente ihren eigenen Inline-top zurück
  (Köpfe, die per Klasse beim Scrollen wegfahren, funktionieren wieder)
- Späte Platzhalter (Einzelseiten-Apps): entprellter MutationObserver auf body zeichnet neue oder wieder
  eingehängte Platzhalter nach
- sichererLink lehnt Zugangsdaten in der Adresse ab; die Adresse der Schnittstelle bestimmt nur das eigene
  Script-Tag, kein Platzhalter mehr (der siteKey am Platzhalter bleibt laut Spec 4.2)
- Editor: die Bestätigung klebt unter der Kopfleiste und bleibt nach dem Speichern eines unteren Abschnitts sichtbar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:10:02 +00:00
1 parent ecf67d6978
commit aa9ee35ab6
6 files changed
+221 -12

No files matched your search

+69 -9
View File
@@ -49,6 +49,8 @@
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
var BEOBACHTER_MS = 200 // Entprellung: nach Aenderungen der Seite hoechstens alle 200 ms nach neuen Platzhaltern sehen
var GETRENNT_MAX = 20
// @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
@@ -221,11 +223,12 @@
'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.
// Eine Adresse aus der Schnittstelle: nur https mit Host, ohne Leer- und Steuerzeichen und ohne Zugangsdaten (sonst ''; wie der Server und
// linkOk des Portals). Ein @ vor dem ersten /, ?, # oder \ gehoert zu Benutzer und Kennwort (die Rueckwaertsschraege zaehlt wie / im Browser).
// 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 : ''
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.
@@ -336,6 +339,10 @@
}
return erg
}
// JSON-LD als Text fuer ein <script type="application/ld+json">: ohne "<" (\u003c), damit ein </script> im Firmennamen oder in einem schon
// vorhandenen Block den Block nie beendet, wenn die Seite serialisiert wird (Suchroboter, "Seite speichern"). JSON.parse liest es zurueck.
function jsonLdText (wert) { return JSON.stringify(wert).replace(/</g, '\\u003c') }
// @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)
@@ -385,6 +392,7 @@
var laeuft = false
var timerId = 0
var platzhalter = []
var getrennt = [] // Platzhalter, die gerade nicht mehr auf der Seite stehen (hoechstens GETRENNT_MAX): kommt derselbe Knoten zurueck, gilt er wieder
var siteKey = ''
var api = ''
var HANDY_MQ = '(max-width: 768px)' // Kontakt-Leiste nur auf schmalen Schirmen
@@ -632,9 +640,31 @@
function fuelleSicher (p, d, t) { sicher(function () { fuelle(p, d, t) }) }
// Entfernte Platzhalter (Seitenwechsel einer Einzelseiten-App) wandern in `getrennt`; ein wieder eingehaengter Knoten kommt mit seinem
// Ersatz-HTML zurueck und wird neu gezeichnet (sig leer), denn die Seite hat seinen Inhalt unterdessen vielleicht zurueckgesetzt
function pflegePlatzhalter () {
var i, p
var bleibt = []
for (i = 0; i < platzhalter.length; i++) {
p = platzhalter[i]
if (p.root.isConnected !== false) bleibt.push(p)
else getrennt.push(p)
}
platzhalter = bleibt
var rest = []
for (i = 0; i < getrennt.length; i++) {
p = getrennt[i]
if (p.root.isConnected !== false) {
p.sig = ''
platzhalter.push(p)
} else rest.push(p)
}
getrennt = rest.slice(-GETRENNT_MAX)
}
function render () {
sammle()
platzhalter = platzhalter.filter(function (p) { return p.root.isConnected !== false })
pflegePlatzhalter()
var d = frisch() ? daten : null
var t = jetzt()
for (var i = 0; i < platzhalter.length; i++) fuelleSicher(platzhalter[i], d, t)
@@ -760,11 +790,18 @@
return liste
}
// Feste Elemente um max(0, Leistenhoehe - Scrollposition) nach unten ruecken (Inline-top, hat Vorrang vor den Regeln der Seite)
// Feste Elemente um max(0, Leistenhoehe - Scrollposition) nach unten ruecken (Inline-top, hat Vorrang vor den Regeln der Seite). Ist die
// Leiste weggescrollt (s = 0), bekommt das Element seinen eigenen Inline-top zurueck (oder keinen): Koepfe, die per Klasse oder Skript beim
// Scrollen weg- und wieder einfahren (top: -80px), arbeiten dann wie auf der Seite gedacht.
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')
for (var i = 0; i < l.fest.length; i++) {
var f = l.fest[i]
if (s > 0) f.el.style.setProperty('top', (f.basis + s) + 'px', 'important')
else if (f.wert) f.el.style.setProperty('top', f.wert, f.prio)
else f.el.style.removeProperty('top')
}
}
function setzeFesteZurueck (l) {
@@ -985,7 +1022,7 @@
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]])
for (i = 0; i < erg.geaendert.length; i++) skripte[erg.geaendert[i]].textContent = jsonLdText(bloecke[erg.geaendert[i]])
if (erg.neu) {
if (!ldEigenes) {
ldEigenes = document.createElement('script')
@@ -993,7 +1030,7 @@
ldEigenes.setAttribute('data-wkz-jsonld', '')
;(document.head || document.documentElement).appendChild(ldEigenes)
}
ldEigenes.textContent = JSON.stringify(erg.neu)
ldEigenes.textContent = jsonLdText(erg.neu)
} else if (ldEigenes) {
if (ldEigenes.parentNode) ldEigenes.parentNode.removeChild(ldEigenes)
ldEigenes = null
@@ -1039,8 +1076,8 @@
}
function bestimmeApi (skript) {
// nur das eigene Script-Tag (oder dessen Herkunft) bestimmt die Adresse: ein Platzhalter der Seite kann die Anfragen nicht umleiten
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) {
@@ -1049,6 +1086,28 @@
return ''
}
// Einzelseiten-Apps (Vite/React) haengen Platzhalter erst nach einem Seitenwechsel ein. Ein Beobachter auf body (childList, subtree) sieht
// einen neuen (noch nicht eingetragenen) oder wieder eingehaengten Platzhalter und zeichnet nach, hoechstens alle BEOBACHTER_MS; er bleibt
// fuer die Lebensdauer der Seite (die eigenen Aenderungen des Skripts loesen nur diese billige Pruefung aus, nie ein Zeichnen).
function pruefeNeue () {
var neu = document.querySelector(SELEKTOR + ':not([data-wkz-init])')
for (var i = 0; !neu && i < getrennt.length; i++) if (getrennt[i].root.isConnected !== false) neu = getrennt[i].root
if (neu) render()
}
function beobachteSeite () {
if (typeof MutationObserver !== 'function' || !document.body) return
var wartet = false
new MutationObserver(function () {
if (wartet) return
wartet = true
timer(function () {
wartet = false
sicher(pruefeNeue)
}, BEOBACHTER_MS)
}).observe(document.body, { childList: true, subtree: true })
}
function start () {
sammle()
var skript = eigenesSkript()
@@ -1067,6 +1126,7 @@
}
tick()
})
beobachteSeite()
lade()
}
+5
View File
@@ -25,6 +25,11 @@
/* Editor: Zeitplaene, Feiertage, Ausnahmen (zeiten-editor.js, zeiten-plaene.js, zeiten-ausnahmen.js) */
.zt-editor > * + * { margin-top: 16px; }
.zt-editor > .tm-meldung:empty, .zt-editor > .tm-box[hidden] { margin: 0; }
/* Die Bestätigung (erstes Kind des Editors) klebt unter der Kopfleiste: nach dem Speichern eines unteren Abschnitts springt der Fokus auf den Knopf,
und „gespeichert“ läge sonst ausser Sicht. Undurchsichtig, damit der Inhalt darunter nicht durchscheint; leer klebt nichts. */
.zt-editor > .tm-meldung:not(:empty) { position: sticky; top: calc(var(--kb-appbar-h, 0px) + 8px); z-index: 5; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45); }
.zt-editor > .tm-meldung-ok:not(:empty) { background: linear-gradient(rgba(74, 222, 128, 0.1), rgba(74, 222, 128, 0.1)), #0c0c0e; }
.zt-editor > .tm-meldung-fehler:not(:empty) { background: linear-gradient(rgba(248, 113, 113, 0.1), rgba(248, 113, 113, 0.1)), #0c0c0e; }
.zt-kopfbereich:empty { display: none; }
.zt-spaeter { margin: 0 4px; line-height: 1.5; }
.zt-plaene { display: grid; gap: 12px; }