4 const MOBILE_BREAKPOINT = 991.98;
6 let _lastMobileState = null;
8 /* =====================================================
9 * HELPERS (UNVERÄNDERT)
10 * ===================================================== */
12 function parseQuery(url) {
14 const q = url.split('?')[1];
17 q.split('&').forEach(part => {
18 const [key, val] = part.split('=');
19 if (key) query[key] = val || '';
25 function currentQuery() {
26 return parseQuery(window.location.search);
29 function linkQuery(href) {
30 return parseQuery(href);
33 function isQueryMatch(linkParams, currentParams) {
34 if (!linkParams || !Object.keys(linkParams).length) {
38 for (let key in linkParams) {
39 if (currentParams[key] !== linkParams[key]) {
46 function cleanPath(pathname) {
47 let path = String(pathname || '/').replace(/\/+$/, '');
51 function urlFromHref(href) {
53 return new URL(href, window.location.href);
59 function getHistory() {
60 if (!window.dbx || typeof dbx.uiGet !== 'function') {
64 const history = dbx.uiGet(LIB, 'history', 'items', []);
65 return Array.isArray(history) ? history : [];
68 function setHistory(history) {
69 if (!window.dbx || typeof dbx.uiSet !== 'function') {
73 dbx.uiSet(LIB, 'history', 'items', Array.isArray(history) ? history : []);
76 function linkScore(href, currentUrl, currentParams) {
77 const linkUrl = urlFromHref(href);
78 if (!linkUrl) return -1;
80 if (linkUrl.origin !== currentUrl.origin) {
84 const linkParams = linkQuery(href);
85 const hasQuery = Object.keys(linkParams).length > 0;
86 const pathMatch = cleanPath(linkUrl.pathname) === cleanPath(currentUrl.pathname);
89 if (!pathMatch || !isQueryMatch(linkParams, currentParams)) {
93 return 1000 + Object.keys(linkParams).length;
103 function isMobileViewport() {
104 return window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches;
107 function getMenuLabel(cfg, id) {
109 const fallback = (typeof id === "string" && id.trim() !== "" && id !== "undef")
113 if (!cfg || typeof cfg.label !== "string") {
117 const label = cfg.label.trim();
118 const lower = label.toLowerCase();
120 if (label === "" || lower === "undefined" || lower === "null") {
128 /* =====================================================
130 * ===================================================== */
132 if (window.dbx) dbx.log("[menu]", ...a);
135 /* =====================================================
136 * MOBILE TOGGLE / RESPONSIVE STATE
137 * ===================================================== */
139 function ensureMobileToggle($menu, id, labelHtml) {
141 const menuId = $menu.attr('id') || ('dbx-menu-' + id);
142 const $parent = $menu.parent();
144 let toggleClass = 'dbx-menu-toggle-main';
146 if ($menu.hasClass('dbx-menu-admin')) {
147 toggleClass = 'dbx-menu-toggle-admin';
150 $menu.attr('id', menuId);
152 let $bar = $parent.children('.dbx-menu-mobile-bar').first();
155 $bar = $('<div class="dbx-menu-mobile-bar"></div>');
156 $parent.prepend($bar);
157 log("mobile toggle bar created");
160 let $toggle = $bar.children('.dbx-menu-toggle[aria-controls="' + menuId + '"]').first();
162 if ($toggle.length) {
166 const labelText = $('<div>').html(labelHtml).text().trim() || id;
171 class="dbx-menu-toggle ${toggleClass}"
172 aria-controls="${menuId}"
173 aria-expanded="false"
174 aria-label="Menü ${labelText} öffnen"
176 <i class="bi bi-list" aria-hidden="true"></i>
177 <span class="dbx-menu-toggle-label">${labelHtml}</span>
181 $bar.append($toggle);
183 log("mobile toggle created", menuId, labelText);
186 function closeMenu($menu) {
188 if (!$menu || !$menu.length) return;
190 $menu.removeClass('is-mobile-open');
192 $menu.find('.dbx-menu-item.is-open')
193 .removeClass('is-open');
195 $menu.find('.dbx-menu-link[aria-expanded]')
196 .attr('aria-expanded', 'false');
198 const menuId = $menu.attr('id');
201 const $toggle = $('.dbx-menu-toggle[aria-controls="' + menuId + '"]');
203 $toggle.attr('aria-expanded', 'false');
207 .addClass('bi-list');
210 function closeAllMenus(preservePersistent) {
211 $('.dbx-menu-root').each(function () {
212 const $menu = $(this);
214 if (preservePersistent === true
215 && $menu.attr('data-dbx-menu-active-open') === '1'
225 * Schliesst einen kompletten Menuezweig. Auch verdeckte Unterebenen
226 * verlieren ihren Oeffnungszustand, damit sie spaeter nicht unerwartet
229 function closeBranch($item) {
231 if (!$item || !$item.length) return;
233 const $branchItems = $item.add($item.find('.dbx-menu-item'));
235 $branchItems.removeClass('is-open');
237 .children('.dbx-menu-link[aria-expanded]')
238 .attr('aria-expanded', 'false');
242 * Oeffnet innerhalb des angegebenen Menuebereichs alle Eltern des
243 * aktuell aktiven Links. Die URL-Aktivierung markiert diese Elemente mit
244 * is-active-path; beim erneuten Oeffnen des Menues muss der Benutzer den
245 * Pfad dadurch nicht noch einmal Ebene fuer Ebene aufklappen.
247 function restoreActivePath($scope) {
249 if (!$scope || !$scope.length) return;
252 .find('.dbx-menu-item.has-children.is-active-path')
253 .addBack('.dbx-menu-item.has-children.is-active-path')
256 const $item = $(this);
258 $item.addClass('is-open');
259 $item.children('.dbx-menu-link[aria-expanded]')
260 .attr('aria-expanded', 'true');
264 function syncResponsiveState(force) {
266 const mobile = isMobileViewport();
268 if (!force && _lastMobileState === mobile) {
272 _lastMobileState = mobile;
276 $('.dbx-menu-root[data-dbx-menu-active-open="1"]').each(function () {
277 restoreActivePath($(this));
280 log("responsive sync", mobile ? "mobile" : "desktop");
283 /* =====================================================
285 * ===================================================== */
287 function buildMenu($menu) {
289 $menu.addClass('dbx-menu-root');
291 if ($menu.is('ul')) {
292 $menu.addClass('dbx-menu-list');
295 $menu.find('ul').addClass('dbx-menu-list');
297 $menu.find('li').each(function () {
300 const $a = $li.children('a');
302 $li.addClass('dbx-menu-item');
304 if ($li.hasClass('align-right') || $li.data('align') === 'right') {
305 $li.addClass('dbx-menu-right');
309 $a.addClass('dbx-menu-link');
312 if ($li.children('ul').length) {
314 $li.addClass('has-children');
318 $a.attr('data-role', 'toggle');
319 $a.attr('aria-haspopup', 'true');
320 $a.attr('aria-expanded', 'false');
321 $a.attr('role', 'button');
323 if ($a.attr('href') === '#') {
324 $a.removeAttr('href');
326 if (!$a.attr('href')) {
327 $a.attr('tabindex', '0');
331 if ($a.length && !$a.find('.dbx-caret').length) {
332 $('<span class="dbx-caret"></span>').appendTo($a);
338 /* =====================================================
340 * ===================================================== */
342 function activateByUrl(root) {
344 const currentParams = currentQuery();
345 const currentUrl = new URL(window.location.href);
347 let bestMatch = null;
350 $(root).find('.dbx-menu-link[href]').each(function () {
352 const $link = $(this);
353 const href = $link.attr('href');
354 if (!href || href === '#') return;
357 * Sprach- und Designoptionen ändern nur die Darstellung der
358 * aktuellen Route. Sie dürfen deshalb nicht den eigentlichen
359 * Seitenlink als aktiven Menüpunkt verdrängen.
361 if ($link.is('.dbxLngOpt, .dbx-design-opt, .dbx-design-skin-opt, .dbx-skin-opt')) {
365 const score = linkScore(href, currentUrl, currentParams);
367 if (score > bestScore) {
373 if (!bestMatch) return;
375 const $item = bestMatch.closest('.dbx-menu-item');
377 $(root).find('.dbx-menu-item')
378 .removeClass('is-active is-active-path is-open');
380 $(root).find('.dbx-menu-link[aria-expanded]')
381 .attr('aria-expanded', 'false');
383 $item.addClass('is-active is-active-path');
385 $item.parents('.dbx-menu-item').each(function () {
387 const $parent = $(this);
390 * Nur die aufgerufene Seite ist aktiv. Eltern markieren lediglich
391 * den Pfad und bleiben dadurch von der aktiven Seite unterscheidbar.
393 $parent.addClass('is-active-path');
396 storeHistory(bestMatch);
399 /* =====================================================
400 * HISTORY (UNVERÄNDERT)
401 * ===================================================== */
403 function storeHistory($link) {
405 const text = $link.text().trim();
406 const href = $link.attr('href');
408 if (!href || href === '#') return;
410 let history = getHistory();
412 history = history.filter(item => item.href !== href);
414 history.unshift({ text, href });
416 history = history.slice(0, 20);
421 function buildChronic() {
423 $('.chronic[data-dbx*="menu|chronic"]').each(function () {
427 const deepMatch = $el.attr('data-dbx').match(/deep=(\d+)/);
428 const deep = deepMatch ? parseInt(deepMatch[1]) : 10;
430 let history = getHistory();
432 history = history.slice(0, deep);
434 const html = history.map(item =>
435 `<a href="${item.href}" class="chronic-item">${item.text}</a>`
436 ).join(' <span class="chronic-sep">›</span> ');
442 /* =====================================================
443 * EVENTS → CORE DELEGATION
444 * ===================================================== */
446 function bindEvents() {
448 if (bindEvents._bound) return;
449 bindEvents._bound = true;
453 '.dbx-menu-item.has-children > .dbx-menu-link',
460 const $item = $link.parent();
461 const $parentList = $item.parent();
462 const willOpen = !$item.hasClass('is-open');
464 $parentList.children('.dbx-menu-item.is-open').not($item).each(function () {
465 closeBranch($(this));
469 $item.addClass('is-open');
470 $link.attr('aria-expanded', 'true');
471 restoreActivePath($item);
476 log("toggle item", willOpen ? "open" : "close");
482 '.dbx-menu-item.has-children > .dbx-menu-link',
484 const key = e.key || e.which;
485 if (key !== 'Enter' && key !== ' ' && key !== 'Spacebar' && key !== 13 && key !== 32) {
502 const $toggle = $(el);
503 const menuId = $toggle.attr('aria-controls');
504 const $menu = $('#' + menuId);
506 if (!$menu.length) return;
508 const willOpen = !$menu.hasClass('is-mobile-open');
513 $menu.addClass('is-mobile-open');
514 $toggle.attr('aria-expanded', 'true');
516 .removeClass('bi-list')
518 restoreActivePath($menu);
521 log("toggle mobile", menuId, willOpen ? "open" : "close");
525 dbx.on('click', 'body', function (e) {
527 if (!e.target.closest('.dbx-menu-root') && !e.target.closest('.dbx-menu-toggle')) {
532 if (!bindEvents._resizeBound) {
533 bindEvents._resizeBound = true;
535 window.addEventListener('resize', function () {
536 syncResponsiveState(false);
543 /* =====================================================
545 * ===================================================== */
547 dbx.feature.register(LIB, {
550 priority: "veryfirst",
553 ["css", "design", "m-menu.css"],
554 ["css", "design", "c-menu.css"]
559 const id = dbx.getLibId(cfg);
562 if (!id || id === "undef") {
563 dbx.warn("menu → missing id");
567 const label = getMenuLabel(cfg, id);
569 el.__dbxInitialized = el.__dbxInitialized || {};
570 el.__dbxInitialized[LIB] = true;
575 ensureMobileToggle($el, id, label);
577 $el.find('.dbx-menu-item').removeClass('is-open');
578 $el.find('.dbx-menu-link[aria-expanded]').attr('aria-expanded', 'false');
579 $el.removeClass('is-mobile-open');
585 syncResponsiveState(true);
586 if ($el.attr('data-dbx-menu-active-open') === '1') {
587 restoreActivePath($el);
590 el.style.visibility = 'visible';