PageSourceSearch

https://musikverein.at/wp-content/plugins/IDL-Functions/mv-anchor-offset.js?ver=1789387035

js musikverein.at collected 2026-09-24 09:27:46 UTC 11,032 bytes, 259 lines download raw bytes

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.