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.