1/* 2 * Seitenwechsel ohne Neuaufbau - Blog und statische Seiten, nach dem 3 * Vorbild von Stats4U (stats4u3/js/nav.js, 23.09.2026). 4 * 5 * Ein Klick auf einen Verweis holt dieselbe Adresse, die der Browser sonst 6 * geladen haette, und tauscht einen Teil der Seite. Zwei Arten: 7 * 8 * - INHALT (Blog zu Blog): nur der Teil zwischen <!--lw:inhalt--> (Ende 9 * header.php) und <!--/lw:inhalt--> (Anfang footer.php). Kopf, Menue und 10 * Fuss bleiben stehen; ihre Sprachverweise werden abgeglichen. 11 * - RAHMEN (sobald eine der statischen Seiten /{en,de,pl}/ oder 12 * /{en,de,pl}/projects/ beteiligt ist): der ganze .site-container samt 13 * dem CSS der Zielseite. Die statischen Seiten tragen ihr CSS im <head>, 14 * der Blog in style.css und in der Kopfzeile selbst - getauscht wird 15 * deshalb genau das, was die Zielseite beim gewoehnlichen Laden haette. 16 * Neue Stilblaetter laden vorher mit media="not all" und gelten erst im 17 * Moment des Tauschs; nichts blitzt ungestaltet auf. Die Skripte der 18 * Zielseite ausserhalb des Rahmens laufen neu (Burger und Schalter der 19 * statischen Seiten, Fussskripte von WordPress) - bis auf den Block aus 20 * inc/footer-tracker.php (Einwilligung, GTM, MousePlayer, Chat): der 21 * steht auf jeder Seite gleich und laeuft einfach weiter. 22 * 23 * In beiden Faellen ziehen die Angaben im <head> mit (Titel, Beschreibung, 24 * canonical, hreflang, og:, JSON-LD), ebenso <html lang> und die Klassen am 25 * <body>. 26 * 27 * FUER SUCHMASCHINEN AENDERT SICH NICHTS. Der Server liefert jede Adresse 28 * weiter als ganze Seite; es gibt keinen Teil-Endpunkt, der in den 29 * Seitencache geraten koennte. Ohne Klick tut das Skript nichts, und bei 30 * Strg-/Mittelklick, ohne JavaScript oder wenn ein Abruf schiefgeht, sind 31 * die Verweise gewoehnliche Seiten. 32 * 33 * WELCHE SEITEN. Blogseiten mit Marken (lw_weich() in der functions.php) 34 * und die sechs statischen Seiten. Werkzeuge haben keine Marken und laden 35 * immer ganz - ihre Skripte rechnen mit einem frischen Dokument; WERKZEUG 36 * unten erspart dafuer den Umweg ueber den Abruf. Ein Sprachwechsel laedt 37 * ebenfalls ganz. Auch dann laeuft der Balken: beim Klick in der alten 38 * Seite und auf der neuen zu Ende. Auf den Werkzeugseiten laedt WordPress 39 * dieses Skript ebenfalls, dort tut es nichts als das. Angemeldet (WP- 40 * Leiste im Blog, nicht auf den statischen Seiten) wechselt der Rahmen die 41 * Leiste mit. 42 * 43 * MESSUNG. GA4 (Enhanced Measurement, Verlaufsereignisse an), MousePlayer 44 * (umwickelt pushState) und reactive.chat (vergleicht die Adresse im Takt) 45 * erkennen den Wechsel selbst. Deshalb steht der neue Titel VOR pushState - 46 * GA4 liest ihn in dem Moment. 47 * 48 * SKRIPTE. Was neu eingesetzt wird, laeuft neu. Zuhoerer, die es dabei an 49 * window, document oder eine Medienabfrage haengt, und Beobachter, die es 50 * startet, loest dieses Skript beim naechsten Tausch desselben Teils selbst 51 * (mitSignal) - die sieben Kopien von Burger und Hell/Dunkel-Schalter 52 * muessen davon nichts wissen. Wer es ausdruecklich will: window.lwSeiteSignal 53 * (Inhalt) und window.lwRahmenSignal (Rahmen). Ein Skript, das auf 54 * DOMContentLoaded wartet, wartet nach einem Tausch ewig - es fragt vorher 55 * document.readyState. Skripte, die einmal geladen bleiben (Lupe, Suche, 56 * Sprachwahl, Chat-Schutz), hoeren auf lw:seite (jeder Tausch) und 57 * lw:rahmen (neuer Rahmen). Externe Skripte laedt ein Dokument nur einmal, 58 * eigene nach Pfad - lw-suche.js steht mit ?v= in den statischen Seiten und 59 * mit ?ver= im Blog. 60 */ 61(function () { 62 'use strict'; 63 64 var BLOG = /^\/blog\/(en|de|pl)\//; 65 var STATISCH = /^\/(en|de|pl)\/(?:projects\/)?$/; 66 var WERKZEUG = /^\/blog\/(?:en\/toolbox|de\/werkzeuge|pl\/narzedzia)\/[^\/]+/; 67 var KEINE_SEITE = /\/(?:feed|embed|trackback|wp-json)(?:\/|$)|\.[a-z0-9]{2,5}$/i; 68 // Erstes Element des Blocks aus inc/footer-tracker.php. Was danach kommt, 69 // steht auf jeder Seite gleich und wird nie neu eingesetzt: die 70 // Einwilligung koennte die gesperrten Skripte sonst ein zweites Mal 71 // freischalten. 72 var TRACKER = 'script[src*="consented.eu/sdk/dist/stub.js"]'; 73 74 // --- Ladebalken oben am Fenster ----------------------------------------- 75 // Sein Aussehen steht hier und nicht in style.css: die statischen Seiten
76 // laden style.css nicht. Farben aus den Marken, die alle sieben Kopien 77 // gleich fuehren. 78 if (!document.getElementById('lw-nav-stil')) { 79 var stil = document.createElement('style'); 80 stil.id = 'lw-nav-stil'; 81 stil.textContent = '.lw-balken{position:fixed;top:0;left:0;right:0;z-index:100000;height:3px;' 82 + 'overflow:hidden;pointer-events:none;opacity:0}' 83 + '.lw-balken i{display:block;height:100%;width:calc(var(--p,0)*100%);border-radius:0 3px 3px 0;' 84 + 'background:linear-gradient(90deg,var(--accent,#2ba6df),var(--accent-soft,#6fc0e6));' 85 + 'box-shadow:0 0 10px var(--accent,#2ba6df);transition:width .28s ease-out}' 86 + '.lw-balken.an{opacity:1;transition:opacity .1s}' 87 + '.lw-balken.an.fertig{opacity:0;transition:opacity .3s .2s}' 88 + '.lw-balken.neu,.lw-balken.neu i{transition:none}' 89 + 'main{transition:opacity .12s}html.lw-laedt main{opacity:.6}main:focus{outline:none}' 90 + '@media (prefers-reduced-motion:reduce){.lw-balken i,.lw-balken.an,.lw-balken.an.fertig,main{transition:none}}'; 91 document.head.appendChild(stil); 92 } 93 94 // Eine Seite aus dem Seitencache ist oft schon da, bevor der Balken 95 // gezeichnet wurde (Abruf beim Druecken, gemessen 48 ms zwischen an und 96 // fertig) - er blitzte dann nur halb auf. Deshalb steht er mindestens 97 // MINDEST ms, waehrend der Inhalt laengst getauscht ist. 98 var MINDEST = 350; 99 var bar = null, bp = 0, btick = null, baus = null, bseit = 0; 100 function setzen(p) { bp = p; if (bar) { bar.style.setProperty('--p', String(p)); } } 101 function balkenStart() { 102 bseit = Date.now(); 103 if (!bar) { 104 bar = document.createElement('div'); 105 bar.className = 'lw-balken'; 106 bar.setAttribute('aria-hidden', 'true'); 107 bar.appendChild(document.createElement('i')); 108 document.body.appendChild(bar); 109 } 110 clearInterval(btick); 111 clearTimeout(baus); 112 bar.classList.remove('an', 'fertig'); 113 bar.classList.add('neu'); 114 setzen(0); 115 void bar.offsetWidth; 116 bar.classList.remove('neu'); 117 bar.classList.add('an'); 118 setzen(0.15); 119 btick = setInterval(function () { setzen(bp + (0.92 - bp) * 0.14); }, 250); 120 } 121 function balkenEnde() { 122 if (!bar || !bar.classList.contains('an') || bar.classList.contains('fertig')) { return; } 123 var rest = MINDEST - (Date.now() - bseit); 124 clearTimeout(baus); 125 if (rest > 0) { baus = setTimeout(balkenEnde, rest); return; } 126 clearInterval(btick); 127 setzen(1); 128 bar.classList.add('fertig'); 129 baus = setTimeout(function () { bar.classList.remove('an', 'fertig'); setzen(0); }, 700); 130 } 131 132 // --- Ganze Seite, trotzdem mit Balken ------------------------------------- 133 // Nicht jeder Verweis darf tauschen: Werkzeuge, ein Sprachwechsel und 134 // alles, was von einer Werkzeugseite weggeht, laden ganz. Ohne Zutun sah 135 // das aus wie "geht nicht" - kein Balken, obwohl die Seite kam. Deshalb 136 // laeuft der Balken auch hier: beim Klick in der alten Seite (der Browser 137 // zeigt sie, bis die neue gezeichnet ist) und auf der neuen zu Ende 138 // (sessionStorage, zehn Sekunden gueltig). Dieser Teil laeuft auf jeder 139 // Seite, die lw-nav.js laedt - auch auf den Werkzeugen, die sonst nichts 140 // davon merken: kein Tausch, kein Abfangen, nur der Balken. 141 var notaus = null; 142 function nurBalken() { 143 balkenStart(); 144 clearTimeout(notaus); 145 // Bleibt die Seite doch stehen (Abbruch, Download): nach zehn 146 // Sekunden weg. 147 notaus = setTimeout(balkenEnde, 10000); 148 try { sessionStorage.setItem('lw-balken', String(Date.now())); } catch (e) { } 149 } 150 function seitenVerweis(e) { 151 if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { return false; } 152 var a = e.target.closest ? e.target.closest('a[href]') : null; 153 if (!a || (a.target && a.target !== '_self') || a.hasAttribute('download')) { return false; } 154 var u; 155 try { u = new URL(a.getAttribute('href'), location.href); } catch (x) { return false; } 156 if (u.origin !== location.origin || !/^https?:$/.test(u.protocol) || KEINE_SEITE.test(u.pathname)) { return false; } 157 // Nur ein Sprungziel auf derselben Seite: kein Laden. 158 if (u.pathname === location.pathname && u.search === location.search && u.hash) { return false; } 159 return true; 160 } 161 // Am window, also nach allen Zuhoerern am document: hat der Seitenwechsel 162 // den Klick schon uebernommen (preventDefault), passiert hier nichts. 163 window.addEventListener('click', function (e) { if (seitenVerweis(e)) { nurBalken(); } }); 164 // Zurueck aus dem Speicher des Browsers: die Seite steht, wie sie beim 165 // Verlassen war - mit laufendem Balken. Also wegraeumen. 166 window.addEventListener('pageshow', function (e) { 167 if (!e.persisted) { return; } 168 clearTimeout(notaus); 169 clearInterval(btick); 170 clearTimeout(baus); 171 if (bar) { bar.classList.remove('an', 'fertig'); setzen(0); } 172 document.documentElement.classList.remove('lw-laedt'); 173 }); 174 (function () { 175 var seit = 0; 176 try { seit = +sessionStorage.getItem('lw-balken') || 0; sessionStorage.removeItem('lw-balken'); } catch (e) { } 177 if (seit && Date.now() - seit < 10000) { 178 balkenStart(); 179 setzen(0.7); 180 balkenEnde(); 181 } 182 })(); 183 184 if (!window.fetch || !window.DOMParser || !window.AbortController || !window.URL 185 || !window.Promise || !history.pushState || !document.createTreeWalker) { return; } 186 187 function sprache(u) { var m = BLOG.exec(u.pathname) || STATISCH.exec(u.pathname); return m ? m[1] : null; } 188 var SPRACHE = sprache(location); 189 190 // Eine Adresse, die dieses Skript tauschen darf: gleicher Ursprung, Blog 191 // oder statische Seite in derselben Sprache, keine Abfrage, kein 192 // Werkzeug, keine Datei. 193 function tauschbar(u) { 194 return u.origin === location.origin && u.search === '' && SPRACHE !== null 195 && sprache(u) === SPRACHE && !WERKZEUG.test(u.pathname) && !KEINE_SEITE.test(u.pathname); 196 } 197 198 // Die Knoten zwischen den beiden Marken - in einem Dokument, das die 199 // Marken nicht (beide, als Geschwister) hat, null. 200 function inhalt(doc) { 201 var anf = null, ende = null; 202 var w = doc.createTreeWalker(doc.body, 128 /* NodeFilter.SHOW_COMMENT */); 203 var c; 204 while ((c = w.nextNode())) { 205 var t = c.data.trim(); 206 if (t === 'lw:inhalt' && !anf) { anf = c; } 207 else if (t === '/lw:inhalt' && anf) { ende = c; break; } 208 } 209 if (!anf || !ende || anf.parentNode !== ende.parentNode) { return null; } 210 var liste = []; 211 for (var n = anf.nextSibling; n && n !== ende; n = n.nextSibling) { liste.push(n); } 212 return { anf: anf, ende: ende, knoten: liste }; 213 } 214 function imTeil(teil, e) { 215 return teil.knoten.some(function (k) { return k === e || (k.nodeType === 1 && k.contains(e)); }); 216 } 217
218 // Der Rahmen: Kopf, Banner, Inhalt, Fuss. Auf allen drei Seitenarten 219 // derselbe aeussere Behaelter. 220 function rahmen(doc) { return doc.querySelector('body > .site-container'); } 221 // Eine Seite, deren Skripte einen Tausch vertragen: Blog mit Marken oder 222 // eine der statischen Seiten. 223 function geeignet(doc, pfad) { return !!inhalt(doc) || STATISCH.test(pfad); } 224 225 // Ab hier der Seitenwechsel selbst - nicht auf Werkzeugseiten und nicht 226 // ohne Marken: dort bleibt es beim Balken oben. 227 if (!tauschbar(new URL(location.href)) || !geeignet(document, location.pathname) || !rahmen(document)) { return; } 228 229 var ctl = new AbortController(), rctl = new AbortController(); 230 window.lwSeiteSignal = ctl.signal; 231 window.lwRahmenSignal = rctl.signal; 232 233 // Zurueck und Vor innerhalb der Sitzung rollt dieses Skript selbst - 234 // sonst springt der Browser, bevor der neue Inhalt steht. Beim Neuladen 235 // und beim Verlassen gilt wieder das Verhalten des Browsers: pagehide 236 // stellt den Eintrag, auf dem die Seite gerade steht, auf "auto" zurueck. 237 // Kommt ein Eintrag, den dieses Skript angelegt hat, spaeter als ganze 238 // Seite zurueck (Zurueck aus einem Werkzeug), traegt history.state die 239 // Hoehe. 240 function selbstRollen(ja) { 241 if ('scrollRestoration' in history) { history.scrollRestoration = ja ? 'manual' : 'auto'; } 242 } 243 selbstRollen(true); 244 window.addEventListener('pagehide', function () { selbstRollen(false); }); 245 window.addEventListener('pageshow', function (e) { if (e.persisted) { selbstRollen(true); } }); 246 try { 247 var nav = performance.getEntriesByType('navigation')[0]; 248 if (nav && nav.type === 'back_forward' && history.state && typeof history.state.y === 'number' 249 && history.state.y > 0 && window.scrollY === 0) { 250 window.scrollTo(0, history.state.y); 251 } 252 } catch (e) { } 253 254 function ohneHash(u) { return u.pathname + u.search; } 255 var gezeigt = ohneHash(new URL(location.href)); 256 var lauf = 0; 257 258 // --- Abruf --------------------------------------------------------------- 259 // Ergebnis: { t: Text, u: URL nach Weiterleitungen } - oder ein Fehler, 260 // wenn die Antwort kein HTML ist. Auch 404 ist eine Seite. 261 function abrufen(adr) { 262 return fetch(adr, { credentials: 'same-origin' }).then(function (r) { 263 if (!/text\/html/i.test(r.headers.get('content-type') || '')) { throw new Error('kein html'); } 264 var ziel = r.url ? new URL(r.url) : new URL(adr); 265 return r.text().then(function (t) { return { t: t, u: ziel }; }); 266 }); 267 } 268 269 // Beim Druecken auf einen Verweis beginnt der Abruf schon, der Klick 270 // kommt 80-150 ms spaeter. Das ersetzt fuer diese Verweise das Vorladen 271 // von WordPress (speculationrules, "conservative" - ebenfalls beim 272 // Druecken); dessen Antwort koennte fetch() nicht verwenden, und jeder 273 // Klick kostete den Pi zwei Abrufe. Deshalb bekommen tauschbare Verweise 274 // die Klasse no-prefetch, die die Regel von WordPress ausnimmt. 275 var vorab = null; 276 function vorabNehmen(u) { 277 var v = vorab; 278 vorab = null; 279 if (v && v.adr === ohneHash(u) && Date.now() - v.zeit < 10000) { return v.p; } 280 return null; 281 } 282 function sperren(a) { 283 try { 284 if (tauschbar(new URL(a.getAttribute('href'), location.href))) { a.classList.add('no-prefetch'); } 285 } catch (x) { } 286 } 287 function vorladenSperren(wurzel) { 288 if (wurzel.nodeType !== 1 && wurzel.nodeType !== 9) { return; } 289 if (wurzel.nodeType === 1 && wurzel.tagName === 'A' && wurzel.hasAttribute('href')) { sperren(wurzel); } 290 Array.prototype.forEach.call(wurzel.querySelectorAll('a[href]'), sperren); 291 } 292 vorladenSperren(document); 293 // Verweise, die erst spaeter entstehen - die Treffer der Suche baut 294 // lw-suche.js bei jedem Tastendruck neu -, bekommen die Klasse ebenso. 295 if (window.MutationObserver) { 296 new MutationObserver(function (aenderungen) { 297 aenderungen.forEach(function (m) { Array.prototype.forEach.call(m.addedNodes, vorladenSperren); }); 298 }).observe(document.body, { childList: true, subtree: true }); 299 } 300 301 // Zurueck und Vor zeigen Seiten, die schon einmal geholt wurden: aus 302 // dem Speicher, ohne Abruf. Die Beitraege aendern sich innerhalb eines 303 // Besuchs nicht; zehn Seiten sind auch bei der Werkzeuguebersicht 304 // (450 KB) wenige Megabyte. 305 var ablage = []; 306 function ablegen(adr, erg) { 307 ablage = ablage.filter(function (e) { return e.adr !== adr; });
308 ablage.push({ adr: adr, erg: erg }); 309 if (ablage.length > 10) { ablage.shift(); } 310 } 311 function ausAblage(adr) { 312 for (var i = 0; i < ablage.length; i++) { if (ablage[i].adr === adr) { return ablage[i].erg; } } 313 return null; 314 } 315 316 // --- Skripte ------------------------------------------------------------- 317 function abs(src) { try { return new URL(src, location.href).href; } catch (x) { return src; } } 318 // Eigene Skripte nach Pfad, fremde nach voller Adresse (reCAPTCHA traegt 319 // den Schluessel in der Abfrage). 320 function skriptSchluessel(src) { 321 try { 322 var u = new URL(src, location.href); 323 return u.origin === location.origin ? u.pathname : u.href; 324 } catch (x) { return src; } 325 } 326 var geladen = {}; 327 Array.prototype.forEach.call(document.querySelectorAll('script[src]'), function (s) { 328 geladen[skriptSchluessel(s.getAttribute('src'))] = true; 329 }); 330 331 function istJs(s) { 332 var t = (s.getAttribute('type') || '').trim().toLowerCase(); 333 return t === '' || t === 'module' || /^(?:text|application)\/(?:x-)?(?:java|ecma)script$/.test(t); 334 } 335 336 // Ein neues <script> mit denselben Angaben; das eingesetzte laeuft nicht 337 // von selbst. Externe laufen in ihrer Reihenfolge (async = false), jedes 338 // nur einmal je Dokument; das Versprechen haelt der Ladebalken. 339 function skriptNeu(alt, warten) { 340 var neu = document.createElement('script'); 341 for (var i = 0; i < alt.attributes.length; i++) { 342 neu.setAttribute(alt.attributes[i].name, alt.attributes[i].value); 343 } 344 if (alt.hasAttribute('src')) { 345 var schl = skriptSchluessel(alt.getAttribute('src')); 346 if (geladen[schl]) { return null; } 347 geladen[schl] = true; 348 neu.async = false; 349 warten.push(new Promise(function (fertig) { 350 neu.addEventListener('load', fertig); 351 neu.addEventListener('error', fertig); 352 })); 353 } else { 354 neu.textContent = alt.textContent; 355 } 356 return neu; 357 } 358 359 // Waehrend fn laeuft, bekommt jeder Zuhoerer an window, document oder 360 // einer Medienabfrage das Signal, und jeder Beobachter trennt sich, wenn 361 // es abbricht. Eingesetzte Inline-Skripte laufen beim Einsetzen, also 362 // genau in diesem Fenster. So haengen Burger, Schalter und Co. nach dem 363 // naechsten Tausch nicht mehr doppelt - ohne dass eine ihrer Kopien 364 // geaendert werden musste. 365 function mitSignal(signal, fn) { 366 var ET = window.EventTarget && window.EventTarget.prototype; 367 var orig = ET && ET.addEventListener; 368 var beobachter = []; 369 if (orig) { 370 ET.addEventListener = function (typ, h, opt) { 371 if (this === window || this === document || (window.MediaQueryList && this instanceof window.MediaQueryList)) { 372 if (opt === undefined || opt === null || typeof opt === 'boolean') { 373 opt = { capture: !!opt, signal: signal }; 374 } else if (!opt.signal) { 375 var o = {}; 376 for (var k in opt) { if (Object.prototype.hasOwnProperty.call(opt, k)) { o[k] = opt[k]; } } 377 o.signal = signal; 378 opt = o; 379 } 380 } 381 return orig.call(this, typ, h, opt); 382 }; 383 } 384 ['MutationObserver', 'IntersectionObserver', 'ResizeObserver'].forEach(function (n) { 385 var P = window[n] && window[n].prototype; 386 if (!P || !P.observe) { return; } 387 var o = P.observe; 388 beobachter.push([P, o]); 389 P.observe = function () { 390 var self = this; 391 if (!self.lwGetrennt) { 392 self.lwGetrennt = true; 393 signal.addEventListener('abort', function () { self.disconnect(); }); 394 } 395 return o.apply(this, arguments); 396 }; 397 }); 398 try { fn(); } finally { 399 if (orig) { ET.addEventListener = orig; } 400 beobachter.forEach(function (b) { b[0].observe = b[1]; }); 401 } 402 } 403 404 // signalFuer(altesSkript) -> Signal, an dem seine Zuhoerer haengen. 405 function skripteStarten(knoten, warten, signalFuer) { 406 knoten.forEach(function (k) { 407 if (k.nodeType !== 1) { return; } 408 var alle = k.tagName === 'SCRIPT' ? [k] : Array.prototype.slice.call(k.querySelectorAll('script')); 409 alle.forEach(function (alt) { 410 if (!istJs(alt)) { return; } 411 var neu = skriptNeu(alt, warten); 412 if (!neu) { return; } 413 mitSignal(signalFuer(alt), function () { alt.parentNode.replaceChild(neu, alt); }); 414 }); 415 }); 416 } 417 418 // Skripte ausserhalb des Rahmens bis zum Tracker-Block: Burger und 419 // Schalter der statischen Seiten, Suchtexte, Fussskripte von WordPress, 420 // die Einblend-Beobachter der Projektseite. Sie gehoeren zur Seite und 421 // werden beim Rahmentausch ersetzt. 422 function seitenSkripteVon(doc) { 423 var r = [], rah = rahmen(doc); 424 var alle = doc.body ? doc.body.querySelectorAll('script') : []; 425 for (var i = 0; i < alle.length; i++) { 426 if (alle[i].matches(TRACKER)) { break; } 427 if (rah && rah.contains(alle[i])) { continue; } 428 r.push(alle[i]); 429 } 430 return r; 431 } 432 var seitenSkripte = seitenSkripteVon(document); 433 434 // Skripte, die WordPress nur auf bestimmten Seiten einreiht (die Lupe 435 // nur bei Diagrammen, reCAPTCHA nur bei Beitraegen, Code Block Pro nur 436 // bei dessen Bloecken), stehen im Fuss - ausserhalb des Inhalts. Kommt 437 // die neue Seite mit einem, das hier fehlt: nachladen, samt der Zeilen 438 // davor (-js-extra, -js-before) und danach (-js-after). 439 function fussSkripte(doc, warten) { 440 Array.prototype.forEach.call(doc.querySelectorAll('script[id$="-js"][src]'), function (s) { 441 if (geladen[skriptSchluessel(s.getAttribute('src'))] || document.getElementById(s.id)) { return; } 442 var name = s.id.slice(0, -3); 443 [name + '-js-extra', name + '-js-before', s.id, name + '-js-after'].forEach(function (id) { 444 var alt = doc.getElementById(id); 445 if (!alt || document.getElementById(id) || !istJs(alt)) { return; } 446 var neu = skriptNeu(alt, warten); 447 if (!neu) { return; } 448 mitSignal(ctl.signal, function () { document.body.appendChild(neu); }); 449 seitenSkripte.push(neu); 450 }); 451 }); 452 } 453 454 // Beim Rahmentausch: die Seitenskripte der alten Seite aus dem Dokument 455 // (gelaufen sind sie; ihre Zuhoerer loest das Rahmensignal), die der
456 // neuen vor den Tracker-Block. Externe, die schon geladen sind, bleiben 457 // weg; speculationrules kommen mit, damit WordPress die Werkzeuge 458 // weiter vorlaedt. 459 function rahmenSkripte(doc, warten) { 460 seitenSkripte.forEach(function (s) { if (s.parentNode) { s.parentNode.removeChild(s); } }); 461 seitenSkripte = []; 462 var vor = document.querySelector(TRACKER); 463 seitenSkripteVon(doc).forEach(function (alt) { 464 var typ = (alt.getAttribute('type') || '').trim().toLowerCase(); 465 if (!istJs(alt) && typ !== 'speculationrules') { return; } 466 var neu = skriptNeu(alt, warten); 467 if (!neu) { return; } 468 mitSignal(rctl.signal, function () { 469 if (vor && vor.parentNode) { vor.parentNode.insertBefore(neu, vor); } else { document.body.appendChild(neu); } 470 }); 471 seitenSkripte.push(neu); 472 }); 473 } 474 475 // --- Kopf, Stile, Menue ---------------------------------------------------- 476 // Was im <head> je Seite verschieden ist. Erst die alten weg, dann die 477 // neuen hinein - so stehen canonical und hreflang nie doppelt. 478 var KOPF = 'title, meta[name="description"], meta[name="robots"], meta[name="author"],' 479 + ' meta[property^="og:"], meta[property^="article:"], meta[name^="twitter:"],' 480 + ' link[rel="canonical"], link[rel="alternate"], link[rel="next"], link[rel="prev"],' 481 + ' link[rel="shortlink"], script[type="application/ld+json"]'; 482 function kopfTauschen(doc) { 483 Array.prototype.forEach.call(document.head.querySelectorAll(KOPF), function (e) { e.parentNode.removeChild(e); }); 484 Array.prototype.forEach.call(doc.head.querySelectorAll(KOPF), function (e) { 485 document.head.appendChild(document.importNode(e, true)); 486 }); 487 var lang = doc.documentElement.getAttribute('lang'); 488 if (lang) { document.documentElement.setAttribute('lang', lang); } 489 } 490 function grundsprache(doc) { return (doc.documentElement.getAttribute('lang') || '').slice(0, 2).toLowerCase(); } 491 492 // Die Stilblaetter einer Seite, wie der Server sie liefert: im <head> und 493 // direkt im <body> (footer.php des Blogs). Eigene tragen keine id oder 494 // die von WordPress (...-css); was ein fremdes Skript mit eigener id 495 // einhaengt, bleibt unberuehrt, ebenso der Balken hier. 496 function istSeitenstil(e) { return e.id !== 'lw-nav-stil' && (!e.id || /-css$/.test(e.id)); } 497 function stileDerSeite(doc) { 498 return Array.prototype.filter.call(doc.querySelectorAll('head > style, head > link[rel~="stylesheet"],' 499 + ' body > style, body > link[rel~="stylesheet"]'), istSeitenstil); 500 } 501 function stilSchluessel(e) { 502 return e.tagName === 'LINK' ? 'L ' + abs(e.getAttribute('href')) 503 : 'S ' + (e.getAttribute('media') || '') + ' ' + e.textContent; 504 } 505 var seitenStile = stileDerSeite(document); 506 507 // Rahmentausch, Schritt 1: die neuen Stilblaetter laden, aber noch nicht 508 // gelten lassen (media="not all"). Fertig, wenn alle geladen sind - 509 // laengstens nach fuenf Sekunden. 510 function stileVorbereiten(doc) { 511 var liste = stileDerSeite(doc); 512 var da = seitenStile.map(stilSchluessel); 513 var neu = [], warten = []; 514 liste.forEach(function (e) { 515 if (da.indexOf(stilSchluessel(e)) !== -1) { return; } 516 var k = document.importNode(e, true); 517 var media = k.getAttribute('media'); 518 k.setAttribute('media', 'not all'); 519 if (k.tagName === 'LINK') { 520 warten.push(new Promise(function (fertig) { 521 k.addEventListener('load', fertig); 522 k.addEventListener('error', fertig); 523 setTimeout(fertig, 5000); 524 })); 525 } 526 document.head.appendChild(k); 527 neu.push({ el: k, media: media }); 528 }); 529 return Promise.all(warten).then(function () { return { liste: liste, neu: neu }; }); 530 } 531 // Schritt 2, im selben Zug wie der Tausch: alte weg, neue scharf. 532 function stileUmschalten(v) { 533 var soll = v.liste.map(stilSchluessel); 534 seitenStile = seitenStile.filter(function (e) { 535 if (e.parentNode && soll.indexOf(stilSchluessel(e)) !== -1) { return true; } 536 if (e.parentNode) { e.parentNode.removeChild(e); } 537 return false; 538 }); 539 v.neu.forEach(function (n) { 540 if (n.media === null) { n.el.removeAttribute('media'); } else { n.el.setAttribute('media', n.media); } 541 seitenStile.push(n.el); 542 }); 543 } 544 // Ein spaeterer Klick kam dazwischen: die vorbereiteten wieder weg. 545 function stileVerwerfen(v) { 546 v.neu.forEach(function (n) { if (n.el.parentNode) { n.el.parentNode.removeChild(n.el); } }); 547 } 548 549 // Inhaltstausch: Blockstile laedt WordPress je Seite (Listen, Tabellen, 550 // Code). Fehlt einer, kommt er dazu; entfernt wird keiner - sie gelten 551 // nur fuer ihre eigenen Blockklassen. Vor dem Einsetzen, damit nichts 552 // ungestaltet aufblitzt. 553 function stileNachziehen(doc, neuKnoten) { 554 var hrefs = Array.prototype.map.call(document.querySelectorAll('link[rel="stylesheet"]'), function (l) { return l.href; }); 555 Array.prototype.forEach.call(doc.querySelectorAll('style[id], link[rel="stylesheet"]'), function (e) { 556 // Was im getauschten Teil steht, kommt ohnehin mit. 557 if (neuKnoten.some(function (k) { return k === e || (k.nodeType === 1 && k.contains(e)); })) { return; } 558 var da = e.id ? document.getElementById(e.id) 559 : hrefs.indexOf(abs(e.getAttribute('href'))) !== -1; 560 if (da) { return; } 561 var k = document.importNode(e, true); 562 document.head.appendChild(k); 563 if (istSeitenstil(k)) { seitenStile.push(k); } 564 }); 565 } 566 567 // Die Klassen am <body> (single, postid-..., category-...) kommen vom 568 // Server. Klassen, die ein Skript gesetzt hat (lw-suche-offen), bleiben. 569 var serverKlassen = document.body.className.split(/\s+/).filter(
569Boolean); 570 function koerperKlassen(doc) { 571 var neu = doc.body.className.split(/\s+/).filter(Boolean); 572 serverKlassen.forEach(function (k) { if (neu.indexOf(k) === -1) { document.body.classList.remove(k); } }); 573 neu.forEach(function (k) { document.body.classList.add(k); }); 574 serverKlassen = neu; 575 } 576 577 // Menue und Fuss stehen beim Inhaltstausch ausserhalb, aendern sich aber 578 // je Seite: die Sprachverweise zeigen auf die Uebersetzung DIESER Seite. 579 // Paarweise nach Reihenfolge; weicht die Zahl ab, bleibt es stehen - die 580 // Sprachverweise gleichen sich darueber hinaus immer ueber hreflang ab. 581 var ATTR = ['href', 'class', 'aria-current', 'hreflang', 'lang']; 582 function angleichen(alt, neu) { 583 ATTR.forEach(function (attr) { 584 var w = neu.getAttribute(attr); 585 if (w === null) { alt.removeAttribute(attr); } 586 else if (alt.getAttribute(attr) !== w) { alt.setAttribute(attr, w); } 587 }); 588 } 589 function menueAbgleichen(doc) { 590 ['#site-nav', '.lw-sprachwahl', '.site-footer'].forEach(function (sel) { 591 var alt = document.querySelector(sel), neu = doc.querySelector(sel); 592 if (!alt || !neu) { return; } 593 var a = alt.querySelectorAll('a, li'), n = neu.querySelectorAll('a, li'); 594 if (a.length === n.length) { 595 for (var i = 0; i < a.length; i++) { if (a[i].tagName === n[i].tagName) { angleichen(a[i], n[i]); } } 596 } 597 Array.prototype.forEach.call(neu.querySelectorAll('a[hreflang]'), function (n) { 598 var a = alt.querySelector('a[hreflang="' + n.getAttribute('hreflang') + '"]'); 599 if (a) { a.setAttribute('href', n.getAttribute('href')); } 600 }); 601 }); 602 } 603 604 // Wer angemeldet ist, hat die Werkzeugleiste von WordPress (wp_footer, 605 // ausserhalb des Tauschs). Sie traegt Verweise auf DIESE Seite 606 // ("Beitrag bearbeiten", Anpassen, Yoast) und kommt deshalb ganz aus der 607 // neuen Seite. Ihre Untermenues oeffnet danach CSS: das Markup bringt die 608 // Klasse nojs mit, den Rueckfall von WordPress selbst (li:hover); admin- 609 // bar.js schliesst nur beim Laden an und wuerde die neue Leiste nicht mehr 610 // kennen. 611 // 612 // Beim Rahmentausch zwischen Blog und statischen Seiten kommt sie dazu 613 // oder faellt weg - die statischen Seiten zeigen sie nie. Ihre Stile 614 // (admin-bar-css, admin-bar-inline-css: der Abstand oben) wechseln mit 615 // den uebrigen, die Klasse admin-bar mit den Body-Klassen. 616 function leisteTauschen(doc) { 617 var alt = document.getElementById('wpadminbar'), neu = doc.getElementById('wpadminbar'); 618 if (alt && neu) { 619 alt.parentNode.replaceChild(document.importNode(neu, true), alt); 620 } else if (alt) { 621 alt.parentNode.removeChild(alt); 622 } else if (neu) { 623 var kopie = document.importNode(neu, true), vor = document.querySelector(TRACKER); 624 if (vor && vor.parentNode) { vor.parentNode.insertBefore(kopie, vor); } else { document.body.appendChild(kopie); } 625 } 626 } 627 628 // --- Tausch -------------------------------------------------------------- 629 // 'inhalt', 'rahmen' oder null (ganze Seite). Den Rahmen nur zwischen 630 // Seiten, deren Skripte das vertragen - ein Werkzeug hat einen Rahmen, 631 // aber keine Marken. 632 function tauschArt(doc, pfad) { 633 if (inhalt(doc) && inhalt(document)) { return 'inhalt'; } 634 if (rahmen(doc) && rahmen(document) && geeignet(doc, pfad) 635 && geeignet(document, new URL(gezeigt, location.href).pathname)) { return 'rahmen'; } 636 return null; 637 } 638 639 // schieben: neuer Eintrag im Verlauf (Klick) oder nicht (Zurueck/Vor); 640 // y: Hoehe, auf die danach gerollt wird (Zurueck/Vor). 641 function tauschen(u, schieben, y) { 642 // Ein spaeterer Klick macht das Ergebnis dieses Laufs wertlos; der 643 // Abruf selbst laeuft zu Ende (er kann schon beim Druecken begonnen 644 // haben und gehoert dann nicht diesem Lauf allein). 645 var nr = ++lauf; 646 var vorY = window.scrollY; 647 var adr = u.href; 648 var sprung = u.hash; 649 var ausSpeicher = !schieben ? ausAblage(ohneHash(u)) : null; 650 var p = ausSpeicher ? Promise.resolve(ausSpeicher) : (vorabNehmen(u) || abrufen(adr)); 651 if (!ausSpeicher) { 652 document.documentElement.classList.add('lw-laedt'); 653 balkenStart(); 654 } 655 function ganz() { 656 try { sessionStorage.setItem('lw-balken', String(Date.now())); } catch (e) { } 657 if (schieben) { location.href = adr; } else { location.replace(adr); } 658 } 659 p.then(function (erg) { 660 if (nr !== lauf) { return; } 661 // Die Adresse nach Weiterleitungen - fetch kennt kein Sprungziel, 662 // das kommt vom Verweis. 663 u = new URL(erg.u.href); 664 if (sprung && !u.hash) { u.hash = sprung; } 665 adr = u.href; 666 var doc = new DOMParser().parseFromString(erg.t, 'text/html'); 667 var art = tauschArt(doc, u.pathname); 668 // Kam oder ging eine Anmeldung, aendern sich Leiste, ihr Abstand 669 // oben und die Stile - das kann nur der Rahmentausch. 670 if (art === 'inhalt' && !document.getElementById('wpadminbar') !== !doc.getElementById('wpadminbar')) { 671 art = 'rahmen'; 672 } 673 // Kein passender Teil (Werkzeug, Fehlerseite des Servers), eine 674 // andere Sprache oder eine Weiterleitung ausserhalb: die ganze Seite. 675 if (!art || !tauschbar(u) || grundsprache(doc) !== grundsprache(document)) { 676 ganz(); 677 return; 678 } 679 ablegen(ohneHash(u), erg); 680 return (art === 'rahmen' ? stileVorbereiten(doc) : Promise.resolve(null)).then(function (stile) { 681 if (nr !== lauf) { if (stile) { stileVerwerfen(stile); } return; } 682 return einsetzen(doc, art, stile, u, adr, schieben && ohneHash(u) !== gezeigt, vorY, y, nr); 683 }); 684 }).catch(function () { 685 if (nr !== lauf) { return; } 686 // Faellt der Abruf aus, tut es der gewoehnliche Weg. 687 ganz(); 688 }); 689 } 690 691 function einsetzen(doc, art, stile, u, adr, schieben, vorY, y, nr) { 692 // Die alte Seite abmelden: Zuhoerer an window/document loesen - beim 693 // Rahmentausch auch die des Rahmens. Stats4U (s4u.js) schliesst dabei
694 // die Verweildauer der alten Seite ab, solange location noch sie 695 // zeigt - wie auf stats4u.net (js/nav.js). Die neue Adresse zaehlt 696 // danach inc/footer-tracker.php (Abschnitt 6). 697 try { document.dispatchEvent(new Event('s4u:seiteweg')); } catch (e) { } 698 window.s4uSeiteWeich = true; 699 ctl.abort(); 700 ctl = new AbortController(); 701 window.lwSeiteSignal = ctl.signal; 702 if (art === 'rahmen') { 703 rctl.abort(); 704 rctl = new AbortController(); 705 window.lwRahmenSignal = rctl.signal; 706 } 707 708 var warten = [], eingesetzt = [], neuerRahmen = null; 709 if (art === 'rahmen') { 710 stileUmschalten(stile); 711 neuerRahmen = document.importNode(rahmen(doc), true); 712 var alterRahmen = rahmen(document); 713 alterRahmen.parentNode.replaceChild(neuerRahmen, alterRahmen); 714 } else { 715 var neu = inhalt(doc), alt = inhalt(document); 716 stileNachziehen(doc, neu.knoten); 717 alt.knoten.forEach(function (k) { k.parentNode.removeChild(k); }); 718 neu.knoten.forEach(function (k) { 719 var kopie = document.importNode(k, true); 720 alt.ende.parentNode.insertBefore(kopie, alt.ende); 721 eingesetzt.push(kopie); 722 }); 723 } 724 kopfTauschen(doc); 725 koerperKlassen(doc); 726 if (art === 'inhalt') { menueAbgleichen(doc); } 727 leisteTauschen(doc); 728 if (schieben) { 729 var st = history.state && typeof history.state === 'object' ? history.state : {}; 730 var mit = {}; 731 for (var k in st) { if (Object.prototype.hasOwnProperty.call(st, k)) { mit[k] = st[k]; } } 732 mit.y = vorY; 733 history.replaceState(mit, ''); 734 history.pushState({ y: 0 }, '', adr); 735 } 736 gezeigt = ohneHash(u); 737 738 if (art === 'rahmen') { 739 // Skripte im Inhalt des neuen Rahmens haengen am Seitensignal, 740 // die uebrigen (Burger, Schalter, Banner) am Rahmensignal. 741 var teil = inhalt(document); 742 skripteStarten([neuerRahmen], warten, function (s) { return teil && imTeil(teil, s) ? ctl.signal : rctl.signal; }); 743 rahmenSkripte(doc, warten); 744 } else { 745 skripteStarten(eingesetzt, warten, function () { return ctl.signal; }); 746 fussSkripte(doc, warten); 747 } 748 // Auch Menue und Fuss: der Abgleich oben hat ihre Klassen neu gesetzt. 749 vorladenSperren(document); 750 try { document.dispatchEvent(new Event('lw:seite')); } catch (e) { } 751 if (art === 'rahmen') { 752 mitSignal(rctl.signal, function () { 753 try { document.dispatchEvent(new Event('lw:rahmen')); } catch (e) { } 754 }); 755 } 756 if (typeof window.codeBlockProInit === 'function') { 757 try { window.codeBlockProInit(); } catch (e) { } 758 } 759 760 // Hinrollen: zum Sprungziel, sonst nach oben - bei Zurueck auf die 761 // gemerkte Hoehe. Dann den Fokus auf den Inhalt, damit Tastatur und 762 // Vorleser dort weitermachen, wo der Leser hinsieht. 763 var ziel = u.hash && u.hash.length > 1 ? document.getElementById(decodeURIComponent(u.hash.slice(1))) : null; 764 if (ziel) { ziel.scrollIntoView(); } 765 else { window.scrollTo(0, typeof y === 'number' ? y : 0); } 766 var main = document.querySelector('main'); 767 if (main) { 768 if (!main.hasAttribute('tabindex')) { main.setAttribute('tabindex', '-1'); } 769 try { main.focus({ preventScroll: true }); } catch (e) { } 770 } 771 return Promise.all(warten).then(function () { 772 if (nr !== lauf) { return; } 773 document.documentElement.classList.remove('lw-laedt'); 774 balkenEnde(); 775 }); 776 } 777 778 function verweis(e) { 779 if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { return null; } 780 var a = e.target.closest ? e.target.closest('a[href]') : null; 781 if (!a || a.target || a.hasAttribute('download') || a.hasAttribute('hreflang') 782 || a.hasAttribute('data-voll') || /(?:^|\s)external(?:\s|$)/.test(a.getAttribute('rel') || '')) { return null; } 783 var u; 784 try { u = new URL(a.getAttribute('href'), location.href); } catch (x) { return null; } 785 if (!tauschbar(u)) { return null; } 786 // Nur ein Sprungziel auf derselben Seite: das kann der Browser selbst. 787 if (ohneHash(u) === ohneHash(new URL(location.href))) { return null; } 788 return u; 789 } 790 791 document.addEventListener('pointerdown', function (e) { 792 var u = verweis(e); 793 if (!u || (vorab && vorab.adr === ohneHash(u))) { return; } 794 var p = abrufen(u.href); 795 p.catch(function () { }); 796 vorab = { adr: ohneHash(u), zeit: Date.now(), p: p }; 797 }, true); 798 799 document.addEventListener('click', function (e) { 800 var u = verweis(e); 801 if (!u) { return; } 802 e.preventDefault(); 803 tauschen(u, true); 804 }); 805 806 window.addEventListener('popstate', function (e) { 807 var u = new URL(location.href); 808 if (ohneHash(u) === gezeigt) { return; } // nur das Sprungziel 809 if (!tauschbar(u)) { location.reload(); return; } 810 tauschen(u, false, e.state && typeof e.state.y === 'number' ? e.state.y : 0); 811 }); 812})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.