PageSourceSearch

https://musikverein.at/wp-content/plugins/IDL-Functions/idl-code.js?ver=1789386760

js musikverein.at collected 2026-10-01 11:37:39 UTC 14,305 bytes, 310 lines download raw bytes

1/*!
2 * idl-code.js – Musikverein Wien
3 *
4 * ÄNDERUNG 2026-08-17 (Barrierefreiheit T2 / Baustein B2):
5 * Nur der letzte Block – „Footer-Menü Accordion" – wurde angepasst. Alles
6 * davor (smooth-scroll, Drop-Down-Logik, GSAP/.component-fade, Dots-Menü)
7 * ist unverändert.
8 *
9 * Grund: Die Footer-Spaltentitel sind in Oxygen jetzt <h2 class="menu-title">.
10 * Die bisherige Fassung setzte `role="button"` direkt auf den Titel – eine
11 * ARIA-Rolle überschreibt die implizite Semantik, die Überschrift wäre auf
12 * Mobile also verloren gegangen. Jetzt wird der Textinhalt in ein echtes
13 * <button class="menu-title-btn"> verschoben: <h2><button>…</button></h2>.
14 * Überschrift und Bedienelement sind damit gleichzeitig gültig.
15 *
16 * Unverändert erhalten: Paarbildung über previousElementSibling (der Button
17 * sitzt INNERHALB des Titels, die Nachbarschaft Titel↔Liste bleibt intakt),
18 * matchMedia-Gating, Fade-Logik mit doppeltem requestAnimationFrame,
19 * 300-ms-Timer, Safari-<14-Fallback.
20 *
21 * Benötigt den Button-Reset in mv-a11y-nav.css (Abschnitte „Footer-Akkordeon"
22 * und „Dropdown-Akkordeon").
23 *
24 * ÄNDERUNG 2026-08-17 (2): initDropdown() baut jetzt ebenfalls einen echten
25 * <button> in die Überschrift (Befund G7). Betrifft die 7 Akkordeons auf
26 * /foerdern/mitgliedschaft/ – die einzigen echten Vorkommen laut Inventur.
27 */
28
29// smooth-scroll.js  -- ENTFERNT 2026-09-04
30//
31// Der Block band an jeden a[href^="#"] einen Klick-Handler, rief
32// preventDefault() und scrollte mit einem fest verdrahteten Offset von 80 px.
33// Drei Probleme, alle am 04.09.2026 live nachgemessen:
34//
35//   1. Die fixierte Kopfleiste ist nicht 80 px hoch. Auf den Abo-Seiten
36//      145 px, auf /konzertbesuch/ 99 px. Nach dem Klick auf "Abo bestellen"
37//      lag der Kopf des Formularblocks damit unter der Leiste.
38//   2. Beim Aufruf mit #hash in der URL -- Link von aussen, Reload,
39//      Zurueck-Button, geteilter Link -- lief der Handler gar nicht. Der
40//      Browser sprang selbst, und ohne scroll-margin-top landete die
41//      Oberkante des Ziels auf Viewport-Position 0, also komplett hinter der
42//      Kopfleiste.
43//   3. preventDefault() ohne Ersatz: die URL bekam keinen Hash (Anker-Links
44//      waren nicht teilbar, der Zurueck-Button wirkungslos) und der Fokus
45//      wanderte nicht ans Ziel (WCAG 2.4.3) -- auch nicht beim Skip-Link.
46//
47// Nachfolger: mv-anchor-offset.js + mv-anchor-offset.css im selben Plugin.
48// Die Kopfleiste wird dort gemessen statt geschaetzt, beide Wege zum Anker
49// (Klick und #hash) bekommen denselben Offset. Enqueue in Abschnitt 7a der
50// idl-functions.php. Siehe Sprungziel-Anker-Offset.md.
51//
52// Das animierte Scrollen ist dabei ersatzlos entfallen (Vorgabe 04.09.2026):
53// der Nachfolger springt sofort, und mv-anchor-offset.css setzt dafür
54// html:root { scroll-behavior: auto } gegen Oxygens normalize.min.css.
55// Der Block "Dots Menu" weiter unten in dieser Datei ruft
56// scrollIntoView({behavior:"smooth"}) explizit auf und ist NICHT betroffen --
57// falls das ebenfalls unanimiert werden soll, ist das eine eigene Entscheidung.
58
59// Drop Down Menu Logic
60//
61// AENDERUNG 2026-08-17 - Barrierefreiheit B2 (Befund G7):
62// Die Toggles sind <h2>-Ueberschriften. Bisher lag der Klick-Handler direkt
63// darauf, ohne role, tabindex oder keydown - per Tastatur also nicht bedienbar
64// (2.1.1), und aria-expanded sass auf einem Element ohne Button-Rolle (4.1.2).
65//
66// Loesung wie im Footer-Akkordeon: der Textinhalt wandert in ein echtes
67// <button class="dropdown-toggle-btn"> INNERHALB der Ueberschrift.
68//   <h2><button aria-expanded aria-controls>Titel</button></h2>
69// Ueberschrift und Bedienelement sind damit gleichzeitig gueltig. Enter und
70// Leertaste erledigt der Browser selbst - kein eigener keydown-Handler, also
71// auch keine Gefahr doppelter Ausloesung.
72//
73// Unveraendert: Klick-Logik, "nur eines offen", slideUp/slideDown mit 700 ms,
74// Startzustand (erstes Element offen), Paarbildung ueber .next().
75// Der Button sitzt IM Header, die Geschwister-Beziehung Header->Panel bleibt.
76jQuery(function ($) {
77  var ddUid = 0;
78
79  function initDropdown(containerSelector, headerSelector, contentSelector) {
80    const $container = $(containerSelector);
81    const $headers = $container.find(headerSelector);
82    if (!$headers.length) return;   // z. B. .dropdown-slide existiert nirgends
83
84    // Textinhalt in einen echten <button> verschieben. Idempotent.
85    function ensureButton($header) {
86      let $btn = $header.children('button.dropdown-toggle-btn');
87      if ($btn.length) return $btn;
88      $btn = $('<button type="button" class="dropdown-toggle-btn"></button>');
89      $btn.append($header.contents());
90      $header.append($btn);
91      return $btn;
92    }
93
94    // Set up initial state
95    $headers.each(function (index) {
96      const $header  = $(this);
97      const $content = $header.next(contentSelector);
98      const $btn     = ensureButton($header);
99
100      // IDs fuer aria-controls / aria-labelledby
101      ddUid++;
102      if (!$header.attr('id'))  $header.attr('id',  'dropdown-title-' + ddUid);
103      if (!$content.attr('id')) $content.attr('id', 'dropdown-panel-' + ddUid);
104
105      $btn.attr('aria-controls', $content.attr('id'));
106      $content.attr('role', 'region').attr('aria-labelledby', $header.attr('id'));
107
108      // Altlasten am Header entfernen - aria-expanded gehoert an den Button,
109      // role/tabindex wuerden die Ueberschriftensemantik ueberschreiben.
110      $header.removeAttr('role').removeAttr('tabindex').removeAttr('aria-expanded');
111
112      if (index === 0) {
113        $header.addClass('open');
114        $btn.attr('aria-expanded', 'true');
115        $content.show().attr('aria-hidden', 'false');
116      } else {
117        $btn.attr('aria-expanded', 'false');
118        $content.hide().attr('aria-hidden', 'true');
119      }
120    });
121
122    // Event binding - am Header, Klicks aus dem Button bubbeln herauf.
123    // Enter/Leertaste loest der Browser am <button> selbst als Klick aus.
124    $headers.on('click', function (e) {
125      e.preventDefault();
126      const $clickedHeader = $(this);
127      const $clickedContent = $clickedHeader.next(contentSelector);
128      const isCurrentlyOpen = $clickedHeader.hasClass('open');
129      // Close all
130      $headers.removeClass('open');
131      $headers.children('button.dropdown-toggle-btn').attr('aria-expanded', 'false');
132      $container.find(contentSelector).slideUp(700).attr('aria-hidden', 'true');
133      if (!isCurrentlyOpen) {
134        // Open clicked
135        $clickedHeader.addClass('open');
136        $clickedHeader.children('button.dropdown-toggle-btn').attr('aria-expanded', 'true');
137        $clickedContent.stop(true, true).slideDown(700).attr('aria-hidden', 'false');
138      }
139    });
140  }
141  // Initialize all dropdown groups
142  initDropdown('.dropdown-footer', '.dropdown-header-footer', '.dropdown-content-footer');
143  initDropdown('.dropdown-slide', '.dropdown-header-slide', '.dropdown-content-slide');
144  initDropdown('.dropdown-members', '.dropdown-header-members', '.dropdown-content-members');
145});
146// GSAP Load + .component-fade
147document.addEventListener('DOMContentLoaded', function () {
148  gsap.registerPlugin(ScrollTrigger);
149  gsap.utils.toArray(".component-fade").forEach((el) => {
150    gsap.from(el, {
151      opacity: 0,
152      duration: 1.2,
153      scrollTrigger: {
154        trigger: el,
155        start: "top 80%",
156        toggleActions: "play none none none"
157      }
158    });
159  });
160});
161// Dots Menu
162const sections = document.querySelectorAll("div.page-section");
163const menu = document.getElementById("dotMenu");
164if (menu) {
165  sections.forEach((_, i) => {
166    const dot = document.createElement("div");
167    dot.classList.add("dot");
168    dot.addEventListener("click", () => {
169      sections[i].scrollIntoView({
170        behavior: "smooth",
171        block: "center"
172      });
173    });
174    menu.appendChild(dot);
175  });
176  const dots = document.querySelectorAll(".dot");
177  const setActiveDot = () => {
178    let closestIndex = 0;
179    let smallestDistance = Infinity;
180    const centerY = window.innerHeight / 2;
181    sections.forEach((section, i) => {
182      const rect = section.getBoundingClientRect();
183      const sectionCenter = rect.top + rect.height / 2;
184      const distance = Math.abs(sectionCenter - centerY);
185      if (distance < smallestDistance) {
186        smallestDistance = distance;
187        closestIndex = i;
188      }
189    });
190    dots.forEach(dot => dot.classList.remove("active"));
191    if (dots[closestIndex]) dots[closestIndex].classList.add("active");
192  };
193  window.addEventListener("scroll", setActiveDot);
194  setActiveDot();
195}
196// Footer-Menü Accordion (nur Mobil ≤767px)
197// Jede .footer-content-menu (Liste) + ihre vorausgehende .menu-title (Titel)
198// wird zu einem unabhängigen Toggle. Standard: alle eingeklappt.
199// Enhancement wird per matchMedia auf-/abgebaut: oberhalb 767px keine ARIA-/
200// Interaktions-Pollution (sonst läge aria-hidden auf sichtbaren Footer-Listen).
201//
202// 2026-08-17: Der Titel ist eine Überschrift (<h2 class="menu-title">). Das
203// Bedienelement ist deshalb ein echtes <button> IM Titel, nicht der Titel
204// selbst – sonst überschriebe role="button" die Überschriftensemantik.
205document.addEventListener('DOMContentLoaded', function () {
206  var MQ = window.matchMedia('(max-width: 767px)');
207  var pairs = [];
208  document.querySelectorAll('.footer-content-menu').forEach(function (panel) {
209    var title = panel.previousElementSibling;
210    if (!title || !title.classList.contains('menu-title')) return; // schließt z.B. Newsletter aus
211    pairs.push({ title: title, panel: panel, btn: null });
212  });
213  if (!pairs.length) return;
214  // Textinhalt des Titels in einen echten <button> verschieben.
215  // Idempotent – apply() läuft bei jedem Breakpoint-Wechsel erneut.
216  function wrap(p) {
217    var existing = p.title.querySelector('button.menu-title-btn');
218    if (existing) { p.btn = existing; return existing; }
219    var btn = document.createElement('button');
220    btn.type = 'button';
221    btn.className = 'menu-title-btn';
222    while (p.title.firstChild) btn.appendChild(p.title.firstChild);
223    p.title.appendChild(btn);
224    p.btn = btn;
225    return btn;
226  }
227  // Button auflösen, Textinhalt zurück in den Titel.
228  function unwrap(p) {
229    var btn = p.title.querySelector('button.menu-title-btn');
230    if (!btn) { p.btn = null; return; }
231    while (btn.firstChild) p.title.insertBefore(btn.firstChild, btn);
232    p.title.removeChild(btn);
233    p.btn = null;
234  }
235  function enhance(p, i) {
236    p.title.classList.add('footer-acc-toggle');
237    p.panel.classList.add('footer-acc-panel');
238    // eindeutige IDs für aria-controls / aria-labelledby
239    if (!p.panel.id) p.panel.id = 'footer-acc-panel-' + i;
240    if (!p.title.id) p.title.id = 'footer-acc-title-' + i;
241    // a11y – Bedienelement ist der Button, der Titel bleibt Überschrift
242    var btn = wrap(p);
243    btn.setAttribute('aria-expanded', p.title.classList.contains('is-open') ? 'true' : 'false');
244    btn.setAttribute('aria-controls', p.panel.id);
245    // Sicherheitsnetz: Reste einer älteren Fassung am Titel entfernen,
246    // sonst überschriebe role weiterhin die Überschriftensemantik
247    ['role', 'tabindex', 'aria-expanded', 'aria-controls'].forEach(function (a) {
248      p.title.removeAttribute(a);
249    });
250    p.panel.setAttribute('role', 'region');
251    p.panel.setAttribute('aria-labelledby', p.title.id);
252    p.panel.setAttribute('aria-hidden', p.title.classList.contains('is-open') ? 'false' : 'true');
253  }
254  function teardown(p) {
255    if (p.panel._accTimer) { clearTimeout(p.panel._accTimer); p.panel._accTimer = null; }
256    p.title.classList.remove('footer-acc-toggle', 'is-open');
257    p.panel.classList.remove('footer-acc-panel', 'is-open', 'is-visible');
258    ['role', 'tabindex', 'aria-expanded', 'aria-controls'].forEach(function (a) { p.title.removeAttribute(a); });
259    ['role', 'aria-labelledby', 'aria-hidden'].forEach(function (a) { p.panel.removeAttribute(a); });
260    unwrap(p); // am Desktop kein fokussierbares Element im Titel
261  }
262  function apply() {
263    pairs.forEach(function (p, i) {
264      if (MQ.matches) { enhance(p, i + 1); } else { teardown(p); }
265    });
266  }
267  function toggle(title, panel) {
268    if (!MQ.matches) return; // nur Mobil aktiv
269    if (panel._accTimer) { clearTimeout(panel._accTimer); panel._accTimer = null; }
270    var willOpen = !title.classList.contains('is-open');
271    if (willOpen) {
272      title.classList.add('is-open');
273      panel.classList.add('is-open');           // Höhe sofort
274      // Opazität erst im nächsten Frame -> Fade-In (sonst springt opacity auf 1)
275      requestAnimationFrame(function () {
276        requestAnimationFrame(function () { panel.classList.add('is-visible'); });
277      });
278    } else {
279      title.classList.remove('is-open');         // Chevron zurückdrehen
280      panel.classList.remove('is-visible');
280      // Fade-Out (Höhe bleibt vorerst)
281      panel._accTimer = setTimeout(function () { // nach dem Fade einklappen
282        panel.classList.remove('is-open');
283        panel._accTimer = null;
284      }, 300);
285    }
286    // aria-expanded gehört an das Bedienelement, nicht an die Überschrift
287    var btn = title.querySelector('button.menu-title-btn');
288    (btn || title).setAttribute('aria-expanded', willOpen ? 'true' : 'false');
289    panel.setAttribute('aria-hidden', willOpen ? 'false' : 'true');
290  }
291  // Listener einmalig am Titel binden (toggle() ist selbst Mobil-gegated).
292  // Klicks auf den Button bubbeln zum Titel – ein Handler genügt.
293  pairs.forEach(function (p) {
294    p.title.addEventListener('click', function () { toggle(p.title, p.panel); });
295    p.title.addEventListener('keydown', function (e) {
296      // Ein natives <button> löst bei Enter/Leertaste selbst einen Klick aus,
297      // der hier hochbubbelt. Ohne diese Sperre feuerte toggle() zweimal und
298      // das Panel klappte sofort wieder zu.
299      if (e.target && e.target.closest && e.target.closest('button.menu-title-btn')) return;
300      if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
301        e.preventDefault();
302        toggle(p.title, p.panel);
303      }
304    });
305  });
306  // Initialer Auf-/Abbau + auf Breakpoint-Wechsel reagieren
307  apply();
308  if (MQ.addEventListener) { MQ.addEventListener('change', apply); }
309  else if (MQ.addListener) { MQ.addListener(apply); } // Safari < 14 Fallback
310});

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.