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() {
211 $('.dbx-menu-root').each(function () {
216 function syncResponsiveState(force) {
218 const mobile = isMobileViewport();
220 if (!force && _lastMobileState === mobile) {
224 _lastMobileState = mobile;
228 log("responsive sync", mobile ? "mobile" : "desktop");
231 /* =====================================================
233 * ===================================================== */
235 function buildMenu($menu) {
237 $menu.addClass('dbx-menu-root');
239 if ($menu.is('ul')) {
240 $menu.addClass('dbx-menu-list');
243 $menu.find('ul').addClass('dbx-menu-list');
245 $menu.find('li').each(function () {
248 const $a = $li.children('a');
250 $li.addClass('dbx-menu-item');
252 if ($li.hasClass('align-right') || $li.data('align') === 'right') {
253 $li.addClass('dbx-menu-right');
257 $a.addClass('dbx-menu-link');
260 if ($li.children('ul').length) {
262 $li.addClass('has-children');
266 $a.attr('data-role', 'toggle');
267 $a.attr('aria-haspopup', 'true');
268 $a.attr('aria-expanded', 'false');
269 $a.attr('role', 'button');
271 if ($a.attr('href') === '#') {
272 $a.removeAttr('href');
274 if (!$a.attr('href')) {
275 $a.attr('tabindex', '0');
279 if ($a.length && !$a.find('.dbx-caret').length) {
280 $('<span class="dbx-caret"></span>').appendTo($a);
286 /* =====================================================
288 * ===================================================== */
290 function activateByUrl(root) {
292 const currentParams = currentQuery();
293 const currentUrl = new URL(window.location.href);
295 let bestMatch = null;
298 $(root).find('.dbx-menu-link[href]').each(function () {
300 const href = $(this).attr('href');
301 if (!href || href === '#') return;
303 const score = linkScore(href, currentUrl, currentParams);
305 if (score > bestScore) {
311 if (!bestMatch) return;
313 const $item = bestMatch.closest('.dbx-menu-item');
315 $(root).find('.dbx-menu-item')
316 .removeClass('is-active is-active-path is-open');
318 $(root).find('.dbx-menu-link[aria-expanded]')
319 .attr('aria-expanded', 'false');
321 $item.addClass('is-active is-active-path');
323 $item.parents('.dbx-menu-item').each(function () {
325 const $parent = $(this);
327 $parent.addClass('is-active is-active-path');
329 const $link = $parent.children('.dbx-menu-link');
331 $parent.addClass('is-active');
335 storeHistory(bestMatch);
338 /* =====================================================
339 * HISTORY (UNVERÄNDERT)
340 * ===================================================== */
342 function storeHistory($link) {
344 const text = $link.text().trim();
345 const href = $link.attr('href');
347 if (!href || href === '#') return;
349 let history = getHistory();
351 history = history.filter(item => item.href !== href);
353 history.unshift({ text, href });
355 history = history.slice(0, 20);
360 function buildChronic() {
362 $('.chronic[data-dbx*="menu|chronic"]').each(function () {
366 const deepMatch = $el.attr('data-dbx').match(/deep=(\d+)/);
367 const deep = deepMatch ? parseInt(deepMatch[1]) : 10;
369 let history = getHistory();
371 history = history.slice(0, deep);
373 const html = history.map(item =>
374 `<a href="${item.href}" class="chronic-item">${item.text}</a>`
375 ).join(' <span class="chronic-sep">›</span> ');
381 /* =====================================================
382 * EVENTS → CORE DELEGATION
383 * ===================================================== */
385 function bindEvents() {
387 if (bindEvents._bound) return;
388 bindEvents._bound = true;
392 '.dbx-menu-item.has-children > .dbx-menu-link',
399 const $item = $link.parent();
400 const $parentList = $item.parent();
401 const willOpen = !$item.hasClass('is-open');
403 $parentList.children('.dbx-menu-item.is-open').not($item).each(function () {
405 .removeClass('is-open')
406 .children('.dbx-menu-link')
407 .attr('aria-expanded', 'false');
410 $item.toggleClass('is-open', willOpen);
411 $link.attr('aria-expanded', willOpen ? 'true' : 'false');
413 log("toggle item", willOpen ? "open" : "close");
419 '.dbx-menu-item.has-children > .dbx-menu-link',
421 const key = e.key || e.which;
422 if (key !== 'Enter' && key !== ' ' && key !== 'Spacebar' && key !== 13 && key !== 32) {
439 const $toggle = $(el);
440 const menuId = $toggle.attr('aria-controls');
441 const $menu = $('#' + menuId);
443 if (!$menu.length) return;
445 const willOpen = !$menu.hasClass('is-mobile-open');
450 $menu.addClass('is-mobile-open');
451 $toggle.attr('aria-expanded', 'true');
453 .removeClass('bi-list')
457 log("toggle mobile", menuId, willOpen ? "open" : "close");
461 dbx.on('click', 'body', function (e) {
463 if (!e.target.closest('.dbx-menu-root') && !e.target.closest('.dbx-menu-toggle')) {
468 if (!bindEvents._resizeBound) {
469 bindEvents._resizeBound = true;
471 window.addEventListener('resize', function () {
472 syncResponsiveState(false);
479 /* =====================================================
481 * ===================================================== */
483 dbx.feature.register(LIB, {
486 priority: "veryfirst",
489 ["css", "design", "m-menu.css"],
490 ["css", "design", "c-menu.css"]
495 const id = dbx.getLibId(cfg);
498 if (!id || id === "undef") {
499 dbx.warn("menu → missing id");
503 const label = getMenuLabel(cfg, id);
505 el.__dbxInitialized = el.__dbxInitialized || {};
506 el.__dbxInitialized[LIB] = true;
511 ensureMobileToggle($el, id, label);
513 $el.find('.dbx-menu-item').removeClass('is-open');
514 $el.find('.dbx-menu-link[aria-expanded]').attr('aria-expanded', 'false');
515 $el.removeClass('is-mobile-open');
521 syncResponsiveState(true);
523 el.style.visibility = 'visible';