PageSourceSearch

https://www.zm-online.de/typo3conf/ext/lns_sitepackage/Resources/Public/JavaScript/a11y-navi.js?1790159505

js zm-online.de collected 2026-09-24 09:20:51 UTC 8,043 bytes, 222 lines download raw bytes

1(function () {
2  'use strict';
3
4  // --- Burger -------------------------------------------------------------------
5  const initBurger = () => {
6    const burger = document.getElementById('mainmenu-mobile');
7    const nav    = document.getElementById('lns-nav');
8    if (!burger || !nav) return;
9
10    const open = () => {
11      burger.setAttribute('aria-expanded', 'true');
12      document.body.classList.add('mainmenu-isopen');
13      nav.querySelector('a, button')?.focus();
14    };
15
16    const close = () => {
17      burger.setAttribute('aria-expanded', 'false');
18      document.body.classList.remove('mainmenu-isopen');
19      closeAllSubmenus();
20    };
21
22    burger.addEventListener('click', () => {
23      burger.getAttribute('aria-expanded') === 'true' ? close() : open();
24    });
25
26    document.addEventListener('keydown', (e) => {
27      if (e.key === 'Escape' && burger.getAttribute('aria-expanded') === 'true') {
28        close();
29        burger.focus();
30      }
31    });
32  };
33
34  // --- Submenu open/close -------------------------------------------------------
35  const openSubmenu = (btn) => {
36    const li    = btn.closest('li');
37    const panel = btn.nextElementSibling;
38    if (!li || !panel) return;
39
40    btn.setAttribute('aria-expanded', 'true');
41    // Static label — aria-expanded conveys the state
42    li.classList.add('submenuopen');
43    panel.setAttribute('aria-hidden', 'false');
44  };
45
46  const closeSubmenu = (btn) => {
47    const li    = btn.closest('li');
48    const panel = btn.nextElementSibling;
49    if (!li || !panel) return;
50
51    btn.setAttribute('aria-expanded', 'false');
52    li.classList.remove('submenuopen');
53    panel.setAttribute('aria-hidden', 'true');
54  };
55
56  const closeAllSubmenus = () => {
57    document.querySelectorAll('nav.main-menu .menu-sub[aria-expanded="true"]')
58      .forEach(closeSubmenu);
59  };
60
61  const bindSubmenuButtons = () => {
62    document.querySelectorAll('nav.main-menu .menu-sub').forEach(btn => {
63      // Set initial ARIA state
64      btn.setAttribute('aria-expanded', 'false');
65      btn.nextElementSibling?.setAttribute('aria-hidden', 'true');
66      // Label names the target, aria-expanded delivers the state
67      btn.setAttribute('aria-label',
68        'Untermenü: ' + btn.closest('li')?.querySelector(':scope > a')?.textContent?.trim()
69      );
70
71      btn.addEventListener('click', () => {
72        btn.getAttribute('aria-expanded') === 'true'
73          ? closeSubmenu(btn)
74          : openSubmenu(btn);
75      });
76    });
77  };
78
79  // --- Desktop keyboard navigation ----------------------------------------------
80  const initDesktopKeyboard = () => {
81    const topItems = document.querySelectorAll('nav.main-menu > ul > li');
82
83    topItems.forEach((li, index) => {
84      const topLink = li.querySelector(':scope > a');
85      const subBtn  = li.querySelector(':scope > .menu-sub');
86      const lvl1    = li.querySelector(':scope > .lvl-1');
87
88      if (topLink) {
89        topLink.addEventListener('keydown', (e) => {
90          switch (e.key) {
91            case 'ArrowRight':
92              topItems[(index + 1) % topItems.length]?.querySelector(':scope > a')?.focus();
93              e.preventDefault(); break;
94            case 'ArrowLeft':
95              topItems[index - 1]?.querySelector(':scope > a')?.focus();
96              e.preventDefault(); break;
97            case 'ArrowDown':
98              if (subBtn) {
99                openSubmenu(subBtn);
100                lvl1?.querySelector('.container .navi-column li > a')?.focus();
101              }
102              e.preventDefault(); break;
103            case 'Escape':
104              if (subBtn) closeSubmenu(subBtn);
105              e.preventDefault(); break;
106          }
107        });
108      }
109
110      if (lvl1) {
111        const getColumns = () => [...lvl1.querySelectorAll('.container .navi-column')];
112
113        // --- lvl-1 column headers -----------------------------------------------
114        lvl1.addEventListener('keydown', (e) => {
115          const current       = document.activeElement;
116          const columns       = getColumns();
117          const currentColumn = current.closest('.navi-column');
118          if (!currentColumn) return;
119
120          const colIndex = columns.indexOf(currentColumn);
121          const lvl1Link = currentColumn.querySelector(':scope > li > a');
122
123          switch (e.key) {
124            case 'ArrowRight':
125              columns[colIndex + 1]?.querySelector('li > a')?.focus();
126              e.preventDefault(); break;
127
128            case 'ArrowLeft':
129              columns[colIndex - 1]?.querySelector('li > a')?.focus();
130              e.preventDefault(); break;
131
132            case 'ArrowDown':
133              // Only handle ArrowDown when focus is on the lvl-1 column header link
134              if (current === lvl1Link) {
135                const firstLvl2Link = currentColumn.querySelector('.lvl-2 li > a');
136                if (firstLvl2Link) {
137                  // Column has lvl-2 items → move into them
138                  firstLvl2Link.focus();
139                } else {
140                  // No lvl-2 items → jump to next column header
141                  columns[colIndex + 1]?.querySelector('li > a')?.focus();
142                }
143                e.preventDefault();
144              }
145              break;
146
147            case 'Escape':
148              if (subBtn) { closeSubmenu(subBtn); topLink?.focus(); }
149              e.preventDefault(); break;
150          }
151        });
152
153        // --- lvl-2 items --------------------------------------------------------
154        lvl1.querySelectorAll('.lvl-2').forEach((lvl2) => {
155          lvl2.addEventListener('keydown', (e) => {
156            const links         = [...lvl2.querySelectorAll('li > a')];
157            const pos           = links.indexOf(document.activeElement);
158            const columns       = getColumns();
159            const currentColumn = lvl2.closest('.navi-column');
160            const colIndex      = columns.indexOf(currentColumn);
161
162            switch (e.key) {
163              case 'ArrowDown': {
164                const nextLink = links[pos + 1];
165                if (nextLink) {
166                  // Next item within the same lvl-2
167                  nextLink.focus();
168                } else {
169                  // End of lvl-2 → jump to next column's lvl-1 header
170                  columns[colIndex + 1]?.querySelector(':scope > li > a')?.focus();
171                }
172                e.preventDefault();
173                e.stopPropagation(); // Prevent lvl-1 handler from firing again
174                break;
175              }
176
177              case 'ArrowUp': {
178                const prevLink = links[pos - 1];
179                if (prevLink) {
180                  // Previous item within the same lvl-2
181                  prevLink.focus();
182                } else {
183                  // Top of lvl-2 → back to this column's lvl-1 header
184                  currentColumn.querySelector(':scope > li > a')?.focus();
185                }
186                e.preventDefault();
187                e.stopPropagation(); // Prevent lvl-1 handler from firing again
188                break;
189              }
190
191              case 'ArrowLeft':
192                // Jump back to this column's lvl-1 header
193                currentColumn.querySelector(':scope > li > a')?.focus();
194                e.preventDefault();
195                e.stopPropagation(); // Prevent lvl-1 handler from firing again
196                break;
197
198              case 'Escape':
199                if (subBtn) { closeSubmenu(subBtn); topLink?.focus(); }
200                e.preventDefault();
201                e.stopPropagation();
202                break;
203            }
204          });
205        });
206
207        // Close submenu when focus leaves the top-level item entirely
208        li.addEventListener('focusout', (e) => {
209          if (!li.contains(e.relatedTarget) && subBtn) closeSubmenu(subBtn);
210        });
211      }
212    });
213  };
214
215  // --- Init --------------------------------------------------------------------
216  document.addEventListener('DOMContentLoaded', () => {
217    initBurger();
218    bindSubmenuButtons();
219    initDesktopKeyboard();
220  });
221
222})();

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.