1/** 2 * DOMContentLoaded Event 3 */ 4document.addEventListener("DOMContentLoaded", () => { 5 // --- KONFIGURATION & SELEKTOREN --- 6 const CONFIG = { 7 mobileBreakpoint: 768, 8 classes: { 9 active: "active", 10 noScroll: "no-scroll", 11 mobileMenuVisible: "mobile-menu-visible", 12 spacer: "menu-spacer", 13 }, 14 selectors: { 15 level0: ".r-menu-item-level-0", 16 level1Items: ".r-menu-item-level-0 .r-menu-item", 17 submenuContainer: ".r-submenu-container", 18 backButton: ".back-to-button", 19 closeButton: ".r-submenu-close", 20 blankSpace: ".blank-menu-space", 21 }, 22 }; 23 24 // --- DOM ELEMENTE CACHEN --- 25 const topLevelItems = Array.from( 26 document.querySelectorAll(CONFIG.selectors.level0), 27 ); 28 const subMenuItem = Array.from( 29 document.querySelectorAll(CONFIG.selectors.level1Items), 30 ); 31 const backButtons = Array.from( 32 document.querySelectorAll(CONFIG.selectors.backButton), 33 ); 34 const closeButtons = Array.from( 35 document.querySelectorAll(CONFIG.selectors.closeButton), 36 ); 37 const blankSpace = Array.from( 38 document.querySelectorAll(CONFIG.selectors.blankSpace), 39 ); 40 let savedScrollPosition = 0; 41 42 // Speichert den zuletzt relevanten Hauptmenü-Link (Level 0), 43 // damit der Fokus beim SchlieÃen (ESC) nicht "ins Nichts" fällt. 44 let lastLevel0Trigger = null; 45 46 const rememberLastLevel0Trigger = (anyItemInMenu) => { 47 const level0 = anyItemInMenu?.closest?.(CONFIG.selectors.level0); 48 if (!level0) return; 49 50 const link = 51 level0.querySelector(":scope > a") || level0.querySelector("a"); 52 if (link) lastLevel0Trigger = link; 53 }; 54 55 // Mobile UI Elemente 56 const ui = { 57 mobileOpenBtn: document.getElementById("mobil-menu-icon"),
58 mobileCloseBtn: document.getElementById("mobil-menu-close"), 59 stickyHeader: document.getElementById("sticky-header"), 60 mainContent: document.querySelector("main"), 61 headerFirstSection: document.getElementById("header-first-section"), 62 logoFull: 63 document.getElementById("header-logo-container") || 64 document.querySelector(".sticky-logo-menu-container"), 65 logoSimple: document.getElementById("mobile-header-logo-container"), 66 }; 67 68 const isMobile = () => window.innerWidth < CONFIG.mobileBreakpoint; 69 70 // --- Screenreader Live Region (für Statusmeldungen wie "Menü geschlossen") --- 71 let srStatusEl = document.getElementById("menu-sr-status"); 72 73 if (!srStatusEl) { 74 srStatusEl = document.createElement("div"); 75 srStatusEl.id = "menu-sr-status"; 76 srStatusEl.setAttribute("role", "status"); 77 srStatusEl.setAttribute("aria-live", "polite"); 78 srStatusEl.setAttribute("aria-atomic", "true"); 79 80 srStatusEl.style.position = "absolute"; 81 srStatusEl.style.left = "-9999px"; 82 srStatusEl.style.width = "1px"; 83 srStatusEl.style.height = "1px"; 84 srStatusEl.style.overflow = "hidden"; 85 86 document.body.appendChild(srStatusEl); 87 } 88 89 const announceToSR = (msg) => { 90 if (!srStatusEl) return; 91 92 srStatusEl.textContent = ""; 93 setTimeout(() => { 94 srStatusEl.textContent = msg; 95 }, 10); 96 }; 97 98 // Alles schlieÃen und zurücksetzen 99 const closeAllMenus = () => { 100 // 1. Level 0 Active Klasse entfernen 101 document 102 .querySelectorAll(`nav.r-menu li.${CONFIG.classes.active}`) 103 .forEach((item) => { 104 item.classList.remove(CONFIG.classes.active); 105 item.setAttribute("aria-expanded", "false"); 106 }); 107 108 // 2. Submenüs schlieÃen (Inline Styles resetten) 109 document 110 .querySelectorAll(CONFIG.selectors.submenuContainer) 111 .forEach((sub) => { 112 if ( 113 sub.parentElement && 114 !sub.parentElement.classList.contains(CONFIG.classes.active) 115 ) { 116 sub.style.display = "none"; 117 } 118 }); 119 120 // 3. Aria-Expanded Reset für alle manuell geöffneten 121 document.querySelectorAll('[aria-expanded="true"]').forEach((el) => { 122 el.setAttribute("aria-expanded", "false"); 123 }); 124 }; 125 126 // Submenü öffnen (Level 0) 127 const openLevel0 = (element) => { 128 rememberLastLevel0Trigger(element); 129 if (element.classList.contains(CONFIG.classes.active)) return; 130 131 closeAllMenus(); 132 element.classList.add(CONFIG.classes.active); 133 element.setAttribute("aria-expanded", "true"); 134 135 const submenu = element.querySelector(CONFIG.selectors.submenuContainer); 136 137 if (submenu) { 138 submenu.style.display = ""; 139 140 const titleElement = submenu.querySelector(".back-to-bar-title"); 141 142 if (titleElement) { 143 const itemClone = element.cloneNode(true); 144 145 const subInClone = itemClone.querySelector( 146 CONFIG.selectors.submenuContainer, 147 ); 148 if (subInClone) subInClone.remove(); 149 150 titleElement.innerText = itemClone.textContent.trim(); 151 } 152 } 153 }; 154 155 // Submenü öffnen (Level 1 / Tiefere Ebene) 156 const openLevel1 = (event, item) => { 157 rememberLastLevel0Trigger(item); 158 const submenuElement = item.querySelector( 159 CONFIG.selectors.submenuContainer, 160 ); 161 if (!submenuElement) return; 162 163 const itemClone = item.cloneNode(true); 164 const subInClone = itemClone.querySelector( 165 CONFIG.selectors.submenuContainer, 166 ); 167 if (subInClone) subInClone.remove(); 168 169 const titleElement = submenuElement.querySelector(".back-to-bar-title"); 170 if (titleElement) { 171 titleElement.innerText = itemClone.textContent.trim(); 172 } 173 174 submenuElement.style.display = "block"; 175 submenuElement.style.marginTop = "-120px"; 176 item.setAttribute("aria-expanded", "true"); 177 178 if (event) { 179 event.preventDefault(); 180 event.stopImmediatePropagation(); 181 } 182 }; 183 184 // --- EVENT LISTENER LOGIK --- 185 186 // 1. Level 0 (Hauptmenüpunkte) 187 topLevelItems.forEach((item) => { 188 const hasSubmenu = 189 item.querySelector(".r-menu-item") || 190 item.querySelector(CONFIG.selectors.submenuContainer); 191 192 if (hasSubmenu) { 193 item.setAttribute("aria-haspopup", "true"); 194 item.setAttribute("aria-expanded", "false"); 195 } 196 197 item.addEventListener("mouseover", () => { 198 if (isMobile()) return; 199 if (document.body.classList.contains(CONFIG.classes.noScroll)) return; 200 openLevel0(item); 201 }); 202 const handleInteraction = (e) => { 203 if (!hasSubmenu) return; 204 205 if (e.type === "keydown" && e.key !== "Enter") return; 206 207 if (e.key === "Enter") e.preventDefault(); 208 209 if (item.classList.contains(CONFIG.classes.active)) { 210 if (e.key === "Enter") closeAllMenus(); 211 } else { 212 if (e.type === "click") e.preventDefault(); 213 openLevel0(item); 214 } 215 }; 216 217 item.addEventListener("click", handleInteraction); 218 item.addEventListener("keydown", handleInteraction); 219 item.addEventListener("focusin", () => { 220 rememberLastLevel0Trigger(item); 221 }); 222 }); 223 224 // 2. Level 1 (Untermenüpunkte) 225 subMenuItem.forEach((item) => { 226 const hasSub = item.querySelector(CONFIG.selectors.submenuContainer); 227 if (hasSub) { 228 item.setAttribute("aria-haspopup", "true"); 229 item.setAttribute("aria-expanded", "false"); 230 } 231 232 const handleSubInteraction = (e) => { 233 if (!hasSub) return; 234 if (e.type === "keydown" && e.key !== "Enter") return; 235 236 // Toggle Logik für Submenüs 237 const subDisplay = hasSub.style.display; 238 if (e.key === "Enter" && subDisplay === "block") { 239 hasSub.style.display = "none"; 240 item.setAttribute("aria-expanded", "false"); 241 } else { 242 openLevel1(e, item); 243 } 244 }; 245 246 item.addEventListener("click", handleSubInteraction); 247 item.addEventListener("keydown", handleSubInteraction); 248 }); 249 250 // 3. Level 2 Links (Propagation Stoppen) 251 document 252 .querySelectorAll(`${CONFIG.selectors.submenuContainer} a`) 253 .forEach((link) => { 254 const stopProp = (e) => { 255 if (e.type === "keydown" && e.key !== "Enter") return; 256 e.stopPropagation(); 257 }; 258 link.addEventListener("click", stopProp); 259 link.addEventListener("keydown", stopProp); 260 }); 261 262 // 4. Zurück Buttons 263 backButtons.forEach((btn) => { 264 const goBack = (e) => { 265 e.preventDefault(); 266 e.stopImmediatePropagation(); 267 268 const currentSubmenu = e.target.closest( 269 CONFIG.selectors.submenuContainer, 270 ); 271 if (!currentSubmenu) return; 272 273 currentSubmenu.style.display = "none"; 274 275 // Parent ARIA Reset 276 if (currentSubmenu.parentElement) { 277 currentSubmenu.parentElement.setAttribute("aria-expanded", "false"); 278 currentSubmenu.parentElement.focus(); 279 } 280 281 if ( 282 !currentSubmenu.parentElement.closest(CONFIG.selectors.submenuContainer) 283 ) { 284 closeAllMenus(); 285 } 286 }; 287 288 btn.addEventListener("click", goBack); 289 btn.addEventListener("keydown", (e) => { 290 if (e.key === "Enter") goBack(e); 291 }); 292 }); 293 294 // 5. Exit Strategie (Tab Out) 295 document 296 .querySelectorAll(CONFIG.selectors.submenuContainer) 297 .forEach((submenu) => { 298 const focusable = submenu.querySelectorAll("a, button"); 299 if (focusable.length === 0) return; 300
301 const lastEl = focusable[focusable.length - 1]; 302 303 lastEl.addEventListener("keydown", (e) => { 304 if (e.key === "Tab" && !e.shiftKey) { 305 submenu.style.display = "none"; 306 if (submenu.parentElement) { 307 submenu.parentElement.setAttribute("aria-expanded", "false"); 308 } 309 if ( 310 !submenu.parentElement.closest(CONFIG.selectors.submenuContainer) 311 ) { 312 closeAllMenus(); 313 } 314 } 315 }); 316 }); 317 318 // 6. Global Close Events (Blank Space & Close Buttons) 319 const triggerClose = (e) => { 320 if (e.type === "keydown" && e.key !== "Enter") return; 321 e.preventDefault(); 322 e.stopImmediatePropagation(); 323 closeAllMenus(); 324 }; 325 326 blankSpace.forEach((el) => el.addEventListener("mouseover", triggerClose)); 327 closeButtons.forEach((btn) => { 328 btn.addEventListener("click", triggerClose); 329 btn.addEventListener("keydown", triggerClose); 330 }); 331 332 // --- MOBILE MENU TOGGLE --- 333 const toggleMobileMenu = (show) => { 334 if (!ui.stickyHeader) return; 335 336 if (ui.mobileOpenBtn) { 337 ui.mobileOpenBtn.setAttribute("aria-expanded", show ? "true" : "false"); 338 } 339 340 if (show) { 341 savedScrollPosition = window.scrollY; 342 343 ui.stickyHeader.classList.add(CONFIG.classes.mobileMenuVisible); 344 document.body.classList.add(CONFIG.classes.noScroll); 345 346 document.body.style.position = "fixed"; 347 document.body.style.top = `-${savedScrollPosition}px`; 348 document.body.style.width = "100%"; 349 350 if (ui.mobileOpenBtn) ui.mobileOpenBtn.style.display = "none"; 351 if (ui.mobileCloseBtn) { 352 ui.mobileCloseBtn.classList.remove("mobil-disabled"); 353 ui.mobileCloseBtn.style.display = "block"; 354 ui.mobileCloseBtn.focus(); 355 } 356 } else { 357 ui.stickyHeader.classList.remove(CONFIG.classes.mobileMenuVisible); 358 document.body.classList.remove(CONFIG.classes.noScroll); 359 360 document.body.style.position = ""; 361 document.body.style.top = ""; 362 document.body.style.width = ""; 363 364 window.scrollTo(0, savedScrollPosition); 365 366 if (ui.mobileOpenBtn) ui.mobileOpenBtn.style.display = ""; 367 if (ui.mobileCloseBtn) { 368 ui.mobileCloseBtn.classList.add("mobil-disabled"); 369 ui.mobileCloseBtn.style.display = "none"; 370 } 371 closeAllMenus(); 372 } 373 }; 374 375 if (ui.mobileOpenBtn) { 376 const openAction = (e) => { 377 if (e.type === "keydown" && e.key !== "Enter") return; 378 e.preventDefault(); 379 toggleMobileMenu(true); 380 }; 381 ui.mobileOpenBtn.addEventListener("click", openAction); 382 ui.mobileOpenBtn.addEventListener("keydown", openAction); 383 } 384 385 if (ui.mobileCloseBtn) { 386 const closeAction = (e) => { 387 if (e.type === "keydown" && e.key !== "Enter") return; 388 e.preventDefault(); 389 toggleMobileMenu(false); 390 }; 391 ui.mobileCloseBtn.addEventListener("click", closeAction); 392 ui.mobileCloseBtn.addEventListener("keydown", closeAction); 393 } 394 395 // --- STICKY HEADER --- 396 let isScrolling = false; 397 398 function updateStickyHeader() { 399 if (!ui.stickyHeader) return; 400 401 const offset = ui.stickyHeader.offsetTop; 402 const scrollY = window.scrollY; 403 const mobileView = isMobile(); 404 405 // Sticky Klasse Logik (Desktop) 406 if (!mobileView) { 407 if (scrollY > offset) { 408 ui.stickyHeader.classList.add("sticky"); 409 ui.stickyHeader.classList.remove("sticky-relative-header"); 410 ui.mainContent?.classList.add("sticky-header-padding"); 411 } else { 412 ui.stickyHeader.classList.remove("sticky"); 413 ui.stickyHeader.classList.add("sticky-relative-header"); 414 ui.mainContent?.classList.remove("sticky-header-padding"); 415 } 416 if (ui.logoFull) ui.logoFull.style.display = ""; 417 } 418 // 2. Mobile Logik 419 else { 420 if (scrollY > offset) { 421 // Logo ausblenden, auÃer Menü ist offen 422 const menuIsOpen = document.body.classList.contains( 423 CONFIG.classes.noScroll, 424 ); 425 if (ui.logoFull) 426 ui.logoFull.style.display = menuIsOpen ? "block" : "none"; 427 428 if (ui.headerFirstSection) { 429 ui.headerFirstSection.style.position = "fixed"; 430 ui.headerFirstSection.style.background = "white"; 431 ui.headerFirstSection.style.width = "100%"; 432 } 433 if (ui.logoSimple) ui.logoSimple.style.display = "block"; 434 } else { 435 if (ui.headerFirstSection) 436 ui.headerFirstSection.style.position = "static"; 437 if (ui.logoFull) ui.logoFull.style.display = "block"; 438 if (ui.logoSimple) ui.logoSimple.style.display = "none"; 439 } 440 } 441 isScrolling = false;
442 } 443 444 // Performance: Request Animation Frame statt direktem Event 445 window.addEventListener("scroll", () => { 446 if (!isScrolling) { 447 window.requestAnimationFrame(updateStickyHeader); 448 isScrolling = true; 449 } 450 }); 451 452 updateStickyHeader(); 453 454 // --- RESIZE HANDLER --- 455 // SchlieÃt das Mobile Menü automatisch, wenn das Fenster groà gezogen wird (Desktop) 456 window.addEventListener("resize", () => { 457 if (!isMobile() && document.body.classList.contains("no-scroll")) { 458 document 459 .getElementById("sticky-header") 460 ?.classList.remove("mobile-menu-visible"); 461 document.body.classList.remove("no-scroll"); 462 463 const mobileOpenBtn = document.getElementById("mobil-menu-icon"); 464 const mobileCloseBtn = document.getElementById("mobil-menu-close"); 465 466 if (mobileOpenBtn) mobileOpenBtn.style.display = ""; 467 if (mobileCloseBtn) { 468 mobileCloseBtn.classList.add("mobil-disabled"); 469 mobileCloseBtn.style.display = "none"; 470 } 471 472 const activeItems = document.querySelectorAll("nav.r-menu li.active"); 473 activeItems.forEach((item) => item.classList.remove("active")); 474 } 475 }); 476 477 // --- ESC: NUR Submenüs schlieÃen + Fokus auf letzten Hauptmenüpunkt --- 478 document.addEventListener( 479 "keydown", 480 (e) => { 481 if (e.key !== "Escape" && e.key !== "Esc") return; 482 483 const openTrigger = document.querySelector( 484 "nav.r-menu [aria-expanded='true']", 485 ); 486 if (!openTrigger) return; 487 488 e.preventDefault(); 489 e.stopPropagation(); 490 if (typeof e.stopImmediatePropagation === "function") { 491 e.stopImmediatePropagation(); 492 } 493 494 const mobileWasOpen = document.body.classList.contains( 495 CONFIG.classes.noScroll, 496 ); 497 498 closeAllMenus(); 499 announceToSR("Menü geschlossen"); 500 501 if (mobileWasOpen) { 502 toggleMobileMenu(false); 503 return; 504 } 505 506 if (lastLevel0Trigger && typeof lastLevel0Trigger.focus === "function") { 507 requestAnimationFrame(() => { 508 lastLevel0Trigger.focus(); 509 setTimeout(() => lastLevel0Trigger?.focus?.(), 0); 510 }); 511 } 512 }, 513 true, 514 ); 515});
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.