1/*! 2 * mv-anchor-offset.js â Musikverein Wien 3 * Neu 2026-09-04. Ersetzt den Block âsmooth-scroll.js" in idl-code.js. 4 * v1.1 (2026-09-14): defaultPrevented-Sperre entfernt â siehe âREGRESSIONâ 5 * weiter unten. 6 * 7 * BEFUND (2026-09-04, live gemessen) 8 * --------------------------------------------------------------------------- 9 * Der bisherige Block rechnete mit `const offset = 80`. Die fixierte 10 * Kopfleiste ist aber nicht 80 px hoch: 11 * /2026-27/abonnements/.../musikverein-entdecken/ header 145 px 12 * /konzertbesuch/ header 99 px 13 * Nach dem Klick auf âAbo bestellen" lag der Kopf des Formularblocks damit 14 * unter der Leiste. Beim Aufruf mit #formular in der URL (Link von auÃen, 15 * Reload, Zurück-Button, geteilter Link) griff die Rechnung gar nicht â der 16 * Browser springt dort selbst, und ohne scroll-margin-top landete die 17 * Oberkante des Ziels auf Viewport-Position 0, also vollständig hinter der 18 * Kopfleiste. Auf /konzertbesuch/#kartenkauf derselbe Effekt. 19 * 20 * WAS DIESES SKRIPT TUT 21 * --------------------------------------------------------------------------- 22 * 1. Misst die Kopfleiste und schreibt sie nach --mv-header-h. Damit rechnen 23 * sowohl dieses Skript als auch scroll-margin-top in mv-anchor-offset.css 24 * mit demselben Wert â auf jedem Breakpoint, auch nach Resize. 25 * 2. Ein delegierter Klick-Handler am Dokument statt eines Handlers pro Link. 26 * Erfasst dadurch auch Anker, die erst später ins DOM kommen. 27 * 3. Setzt die URL per history.pushState â der bisherige preventDefault ohne 28 * Hash machte Anker-Links unteilbar und den Zurück-Button wirkungslos. 29 * 4. Ãbergibt den Fokus an das Sprungziel (WCAG 2.4.3). Der bisherige 30 * preventDefault unterband die native Fokusübergabe ersatzlos; das betraf 31 * auch den Skip-Link auf #inhalt. 32 * 5. Korrigiert die Position beim Laden mit #hash noch einige hundert 33 * Millisekunden nach â auf den Abo-Seiten schieben nachladende Bilder und 34 * die .component-fade-Animationen das Ziel nach dem ersten Sprung noch. 35 * Bricht ab, sobald der Besucher selbst scrollt. 36 * 6. Springt OHNE Animation (Vorgabe 04.09.2026). Der alte Block scrollte mit 37 * behavior:"smooth"; das ist hier ersatzlos raus. Zwei Stellen sind dafür 38 * nötig, weil zwei Ebenen mitreden: 39 * - hier behavior:"auto" bei jedem window.scrollTo 40 * - CSS html:root { scroll-behavior: auto } gegen Oxygens 41 * normalize.min.css, die html { scroll-behavior: smooth } setzt 42 * und damit auch den nativen Sprung beim Aufruf mit #hash 43 * animiert hätte 44 * Nebenwirkung, erwünscht: erfüllt WCAG 2.3.3 (Animation aus Interaktionen) 45 * ohne Umweg über prefers-reduced-motion. 46 * NICHT betroffen ist das Dots-Menü in idl-code.js â das ruft 47 * scrollIntoView({behavior:"smooth", block:"center"}) explizit auf und 48 * behält seine Animation. 49 * 50 * ABHÃNGIGKEITEN 51 * --------------------------------------------------------------------------- 52 * Reines Vanilla-JS, KEIN jQuery â hält das Skript aus der Defer-Falle vom 53 * 19.06.2026 heraus (jQuery darf nicht deferred werden, sonst bricht der 54 * Fluent-Forms-Datei-Upload). 55 * Braucht mv-anchor-offset.css für den Fall âAufruf mit #hash". 56 */ 57 58(function () { 59 'use strict'; 60 61 /* Die Kopfleiste ist <header class="⦠menu-section ⦠fixed">. Der zweite 62 Selektor ist nur ein Netz, falls die Klasse in Oxygen umbenannt wird. */ 63 var HEADER_SELECTORS = ['header.menu-section', 'header']; 64 var GAP = 24; // Luft unter der Kopfleiste, wie in der CSS 65 var FALLBACK_HEIGHT = 100; // nur falls gar nichts messbar ist 66 67 /* ---------------------------------------------- Kopfleiste messen */ 68 69 function headerElement() { 70 for (var i = 0; i < HEADER_SELECTORS.length; i++) { 71 var el = document.querySelector(HEADER_SELECTORS[i]); 72 if (el) return el; 73 } 74 return null; 75 } 76 77 function headerHeight() { 78 var el = headerElement(); 79 if (!el) return 0; 80 var cs = window.getComputedStyle(el); 81 /* Nur eine fixierte oder klebende Leiste verdeckt das Sprungziel. 82 Scrollt der Kopf mit, ist kein Offset nötig. */ 83 if (cs.position !== 'fixed' && cs.position !== 'sticky') return 0; 84 if (cs.display === 'none' || cs.visibility === 'hidden') return 0; 85 return Math.round(el.getBoundingClientRect().height); 86 } 87 88 function publishHeaderHeight() { 89 var h = headerHeight(); 90 /* 0 kann heiÃen âgerade ausgeblendet" â dann den letzten Wert behalten, 91 statt das Sprungziel kurzzeitig ohne Offset zu berechnen. */ 92 if (h > 0) { 93 document.documentElement.style.setProperty('--mv-header-h', h + 'px'); 94 } 95 return h; 96 } 97 98 function currentOffset() { 99 return (headerHeight() || FALLBACK_HEIGHT) + GAP; 100 } 101 102 /* ---------------------------------------------- Ziel ermitteln */ 103 104 function targetFromHash(hash) { 105 if (!hash || hash === '#' || hash === '#0') return null; 106 var id = hash.slice(1); 107 try { id = decodeURIComponent(id); } catch (e) { /* rohen Wert benutzen */ } 108 /* getElementById statt querySelector: verträgt IDs, die mit einer Ziffer 109 beginnen oder Sonderzeichen enthalten, ohne CSS.escape. */ 110 return document.getElementById(id) || 111 document.getElementsByName(id)[0] || 112 null; 113 } 114 115 /* ---------------------------------------------- Scrollen */ 116 117 function scrollToTarget(el) { 118 var top = el.getBoundingClientRect().top + window.pageYOffset - currentOffset(); 119 if (top < 0) top = 0; 120 var max = document.documentElement.scrollHeight - window.innerHeight; 121 if (top > max) top = max; 122 /* behavior: 'auto' ist hier nicht der Standardwert, sondern eine Ansage: 123 es überstimmt html { scroll-behavior: smooth } aus Oxygens 124 normalize.min.css. Der Sprung ist gewollt sofort. */ 125 window.scrollTo({ top: top, behavior: 'auto' }); 126 } 127 128 function focusTarget(el) { 129 /* Ohne tabindex nimmt ein <div>/<section> keinen Fokus an. Das Attribut 130 bleibt stehen â -1 macht das Element nicht tabbierbar, nur 131 programmatisch fokussierbar. Der Fokusring auf #inhalt ist in 132 mv-a11y-nav.css bereits unterdrückt. */ 133 if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '-1'); 134 try { el.focus({ preventScroll: true }); } catch (e) { /* egal */ } 135 } 136 137 /* ---------------------------------------------- Klicks */ 138 139 function isPlainLeftClick(e) { 140 return e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey; 141 } 142 143 function isSamePage(a) { 144 return a.origin === window.location.origin && 145 a.pathname === window.location.pathname && 146 a.search === window.location.search; 147 } 148 149 document.addEventListener('click', function (e) { 150 if (!isPlainLeftClick(e)) return; 151 152 /* REGRESSION 2026-09-14, live gemessen auf /konzertbesuch/ 153 ----------------------------------------------------------------- 154 Hier stand `if (e.defaultPrevented) return;`. Die Sprungnavigation 155 dieser Seite liegt in `ul.service > li.service-head > a[href^="#"]`, 156 und auf `li.service-head` hängt der Akkordeon-Handler aus
157 idl-code.js (Zeile ~125), der `e.preventDefault()` ruft. Der Klick 158 erreicht `document` also bereits als abgebrochen â dieser Handler 159 stieg aus, und weil der alte smooth-scroll-Block seit dem Austausch 160 der idl-code.js weg ist, scrollte gar nichts mehr: Hash in der URL, 161 Seite bleibt stehen. 162 163 Der alte Block prüfte `defaultPrevented` nie â er hing direkt am 164 Anker und lief in der Zielphase, also vor dem Akkordeon. Diese 165 Sperre hier war damit strenger als das, was sie ersetzt hat. 166 167 Der Handler bleibt trotzdem eng gefasst: er greift nur bei einfachem 168 Linksklick auf einen Link mit Hash, dessen Ziel auf derselben Seite 169 tatsächlich existiert. Links mit href="#" (reine Schalter) haben 170 keinen Hash und laufen unten in den Ausstieg. */ 171 172 var a = e.target && e.target.closest ? e.target.closest('a[href]') : null; 173 if (!a || !a.hash) return; 174 /* Links auf eine andere Seite normal navigieren lassen â dort greifen 175 scroll-margin-top und die Nachkorrektur beim Laden. */ 176 if (!isSamePage(a)) return; 177 178 var el = targetFromHash(a.hash); 179 if (!el) return; 180 181 e.preventDefault(); 182 scrollToTarget(el); 183 focusTarget(el); 184 185 /* Teilbare URL und funktionierender Zurück-Button. pushState löst keinen 186 zweiten, unkorrigierten Sprung aus â location.hash würde das tun. */ 187 if (window.history && window.history.pushState) { 188 window.history.pushState(null, '', a.hash); 189 } 190 }, false); 191 192 /* ---------------------------------------------- Aufruf mit #hash */ 193 194 /* Der Browser springt beim Laden selbst; scroll-margin-top aus der CSS 195 sorgt dort für den Offset. Auf den Abo-Seiten verschiebt sich das Ziel 196 danach aber noch: Bilder oberhalb laden nach, .component-fade blendet 197 Blöcke ein. Deshalb für knapp eine Sekunde nachkorrigieren â und sofort 198 aufhören, sobald der Besucher selbst scrollt. */ 199 function correctAfterLoad() { 200 var el = targetFromHash(window.location.hash); 201 if (!el) return; 202 203 var handedOver = false; 204 function handOver() { handedOver = true; } 205 ['wheel', 'touchstart', 'keydown', 'mousedown'].forEach(function (type) { 206 window.addEventListener(type, handOver, { once: true, passive: true }); 207 }); 208 209 var tries = 0; 210 (function step() { 211 if (handedOver) return; 212 scrollToTarget(el); 213 focusTarget(el); 214 if (++tries < 6) window.setTimeout(step, 150); 215 })(); 216 } 217 218 window.addEventListener('hashchange', function () { 219 var el = targetFromHash(window.location.hash); 220 if (el) { scrollToTarget(el); focusTarget(el); } 221 }); 222 223 window.addEventListener('popstate', function () { 224 var el = targetFromHash(window.location.hash); 225 if (el) { scrollToTarget(el); focusTarget(el); } 226 }); 227 228 /* ---------------------------------------------- Start */ 229 230 function init() { 231 publishHeaderHeight(); 232 233 /* Die Leiste ändert ihre Höhe mit dem Breakpoint. ResizeObserver fängt 234 auch Höhenänderungen ohne Fensterwechsel (z. B. Umbruch der Navigation). */ 235 if (window.ResizeObserver) { 236 var h = headerElement(); 237 if (h) new ResizeObserver(publishHeaderHeight).observe(h); 238 } 239 window.addEventListener('resize', publishHeaderHeight, { passive: true }); 240 window.addEventListener('orientationchange', publishHeaderHeight); 241 242 /* load statt DOMContentLoaded: erst dann stehen Bildhöhen fest. */ 243 if (document.readyState === 'complete') { 244 publishHeaderHeight(); 245 correctAfterLoad(); 246 } else { 247 window.addEventListener('load', function () { 248 publishHeaderHeight(); 249 correctAfterLoad(); 250 }); 251 } 252 } 253 254 if (document.readyState === 'loading') { 255 document.addEventListener('DOMContentLoaded', init); 256 } else { 257 init(); 258 } 259})();
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.