PageSourceSearch

https://www.landkreis-schweinfurt.de/_assets/06642057893baf8ff53f4…/Javascript/redesign/header.js?1787913307

js landkreis-schweinfurt.de collected 2026-10-02 17:24:14 UTC 15,821 bytes, 515 lines download raw bytes

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.