dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
menu.js
Go to the documentation of this file.
1(function ($) {
2
3 const LIB = "menu";
4 const MOBILE_BREAKPOINT = 991.98;
5
6 let _lastMobileState = null;
7
8 /* =====================================================
9 * HELPERS (UNVERÄNDERT)
10 * ===================================================== */
11
12 function parseQuery(url) {
13 const query = {};
14 const q = url.split('?')[1];
15 if (!q) return query;
16
17 q.split('&').forEach(part => {
18 const [key, val] = part.split('=');
19 if (key) query[key] = val || '';
20 });
21
22 return query;
23 }
24
25 function currentQuery() {
26 return parseQuery(window.location.search);
27 }
28
29 function linkQuery(href) {
30 return parseQuery(href);
31 }
32
33 function isQueryMatch(linkParams, currentParams) {
34 if (!linkParams || !Object.keys(linkParams).length) {
35 return false;
36 }
37
38 for (let key in linkParams) {
39 if (currentParams[key] !== linkParams[key]) {
40 return false;
41 }
42 }
43 return true;
44 }
45
46 function cleanPath(pathname) {
47 let path = String(pathname || '/').replace(/\/+$/, '');
48 return path || '/';
49 }
50
51 function urlFromHref(href) {
52 try {
53 return new URL(href, window.location.href);
54 } catch (e) {
55 return null;
56 }
57 }
58
59 function getHistory() {
60 if (!window.dbx || typeof dbx.uiGet !== 'function') {
61 return [];
62 }
63
64 const history = dbx.uiGet(LIB, 'history', 'items', []);
65 return Array.isArray(history) ? history : [];
66 }
67
68 function setHistory(history) {
69 if (!window.dbx || typeof dbx.uiSet !== 'function') {
70 return;
71 }
72
73 dbx.uiSet(LIB, 'history', 'items', Array.isArray(history) ? history : []);
74 }
75
76 function linkScore(href, currentUrl, currentParams) {
77 const linkUrl = urlFromHref(href);
78 if (!linkUrl) return -1;
79
80 if (linkUrl.origin !== currentUrl.origin) {
81 return -1;
82 }
83
84 const linkParams = linkQuery(href);
85 const hasQuery = Object.keys(linkParams).length > 0;
86 const pathMatch = cleanPath(linkUrl.pathname) === cleanPath(currentUrl.pathname);
87
88 if (hasQuery) {
89 if (!pathMatch || !isQueryMatch(linkParams, currentParams)) {
90 return -1;
91 }
92
93 return 1000 + Object.keys(linkParams).length;
94 }
95
96 if (pathMatch) {
97 return 500;
98 }
99
100 return -1;
101 }
102
103 function isMobileViewport() {
104 return window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches;
105 }
106
107 function getMenuLabel(cfg, id) {
108
109 const fallback = (typeof id === "string" && id.trim() !== "" && id !== "undef")
110 ? id
111 : "";
112
113 if (!cfg || typeof cfg.label !== "string") {
114 return fallback;
115 }
116
117 const label = cfg.label.trim();
118 const lower = label.toLowerCase();
119
120 if (label === "" || lower === "undefined" || lower === "null") {
121 return fallback;
122 }
123
124 return label;
125 }
126
127
128 /* =====================================================
129 * CORE LOG WRAPPER
130 * ===================================================== */
131 function log(...a) {
132 if (window.dbx) dbx.log("[menu]", ...a);
133 }
134
135 /* =====================================================
136 * MOBILE TOGGLE / RESPONSIVE STATE
137 * ===================================================== */
138
139 function ensureMobileToggle($menu, id, labelHtml) {
140
141 const menuId = $menu.attr('id') || ('dbx-menu-' + id);
142 const $parent = $menu.parent();
143
144 let toggleClass = 'dbx-menu-toggle-main';
145
146 if ($menu.hasClass('dbx-menu-admin')) {
147 toggleClass = 'dbx-menu-toggle-admin';
148 }
149
150 $menu.attr('id', menuId);
151
152 let $bar = $parent.children('.dbx-menu-mobile-bar').first();
153
154 if (!$bar.length) {
155 $bar = $('<div class="dbx-menu-mobile-bar"></div>');
156 $parent.prepend($bar);
157 log("mobile toggle bar created");
158 }
159
160 let $toggle = $bar.children('.dbx-menu-toggle[aria-controls="' + menuId + '"]').first();
161
162 if ($toggle.length) {
163 return;
164 }
165
166 const labelText = $('<div>').html(labelHtml).text().trim() || id;
167
168 $toggle = $(`
169 <button
170 type="button"
171 class="dbx-menu-toggle ${toggleClass}"
172 aria-controls="${menuId}"
173 aria-expanded="false"
174 aria-label="Menü ${labelText} öffnen"
175 >
176 <i class="bi bi-list" aria-hidden="true"></i>
177 <span class="dbx-menu-toggle-label">${labelHtml}</span>
178 </button>
179 `);
180
181 $bar.append($toggle);
182
183 log("mobile toggle created", menuId, labelText);
184 }
185
186 function closeMenu($menu) {
187
188 if (!$menu || !$menu.length) return;
189
190 $menu.removeClass('is-mobile-open');
191
192 $menu.find('.dbx-menu-item.is-open')
193 .removeClass('is-open');
194
195 $menu.find('.dbx-menu-link[aria-expanded]')
196 .attr('aria-expanded', 'false');
197
198 const menuId = $menu.attr('id');
199 if (!menuId) return;
200
201 const $toggle = $('.dbx-menu-toggle[aria-controls="' + menuId + '"]');
202
203 $toggle.attr('aria-expanded', 'false');
204
205 $toggle.find('i.bi')
206 .removeClass('bi-x')
207 .addClass('bi-list');
208 }
209
210 function closeAllMenus(preservePersistent) {
211 $('.dbx-menu-root').each(function () {
212 const $menu = $(this);
213
214 if (preservePersistent === true
215 && $menu.attr('data-dbx-menu-active-open') === '1'
216 ) {
217 return;
218 }
219
220 closeMenu($menu);
221 });
222 }
223
224 /**
225 * Schliesst einen kompletten Menuezweig. Auch verdeckte Unterebenen
226 * verlieren ihren Oeffnungszustand, damit sie spaeter nicht unerwartet
227 * wieder erscheinen.
228 */
229 function closeBranch($item) {
230
231 if (!$item || !$item.length) return;
232
233 const $branchItems = $item.add($item.find('.dbx-menu-item'));
234
235 $branchItems.removeClass('is-open');
236 $branchItems
237 .children('.dbx-menu-link[aria-expanded]')
238 .attr('aria-expanded', 'false');
239 }
240
241 /**
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.
246 */
247 function restoreActivePath($scope) {
248
249 if (!$scope || !$scope.length) return;
250
251 $scope
252 .find('.dbx-menu-item.has-children.is-active-path')
253 .addBack('.dbx-menu-item.has-children.is-active-path')
254 .each(function () {
255
256 const $item = $(this);
257
258 $item.addClass('is-open');
259 $item.children('.dbx-menu-link[aria-expanded]')
260 .attr('aria-expanded', 'true');
261 });
262 }
263
264 function syncResponsiveState(force) {
265
266 const mobile = isMobileViewport();
267
268 if (!force && _lastMobileState === mobile) {
269 return;
270 }
271
272 _lastMobileState = mobile;
273
274 closeAllMenus();
275
276 $('.dbx-menu-root[data-dbx-menu-active-open="1"]').each(function () {
277 restoreActivePath($(this));
278 });
279
280 log("responsive sync", mobile ? "mobile" : "desktop");
281 }
282
283 /* =====================================================
284 * MENU BUILD
285 * ===================================================== */
286
287 function buildMenu($menu) {
288
289 $menu.addClass('dbx-menu-root');
290
291 if ($menu.is('ul')) {
292 $menu.addClass('dbx-menu-list');
293 }
294
295 $menu.find('ul').addClass('dbx-menu-list');
296
297 $menu.find('li').each(function () {
298
299 const $li = $(this);
300 const $a = $li.children('a');
301
302 $li.addClass('dbx-menu-item');
303
304 if ($li.hasClass('align-right') || $li.data('align') === 'right') {
305 $li.addClass('dbx-menu-right');
306 }
307
308 if ($a.length) {
309 $a.addClass('dbx-menu-link');
310 }
311
312 if ($li.children('ul').length) {
313
314 $li.addClass('has-children');
315
316 if ($a.length) {
317
318 $a.attr('data-role', 'toggle');
319 $a.attr('aria-haspopup', 'true');
320 $a.attr('aria-expanded', 'false');
321 $a.attr('role', 'button');
322
323 if ($a.attr('href') === '#') {
324 $a.removeAttr('href');
325 }
326 if (!$a.attr('href')) {
327 $a.attr('tabindex', '0');
328 }
329 }
330
331 if ($a.length && !$a.find('.dbx-caret').length) {
332 $('<span class="dbx-caret"></span>').appendTo($a);
333 }
334 }
335 });
336 }
337
338 /* =====================================================
339 * ACTIVE STATE
340 * ===================================================== */
341
342 function activateByUrl(root) {
343
344 const currentParams = currentQuery();
345 const currentUrl = new URL(window.location.href);
346
347 let bestMatch = null;
348 let bestScore = -1;
349
350 $(root).find('.dbx-menu-link[href]').each(function () {
351
352 const $link = $(this);
353 const href = $link.attr('href');
354 if (!href || href === '#') return;
355
356 /*
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.
360 */
361 if ($link.is('.dbxLngOpt, .dbx-design-opt, .dbx-design-skin-opt, .dbx-skin-opt')) {
362 return;
363 }
364
365 const score = linkScore(href, currentUrl, currentParams);
366
367 if (score > bestScore) {
368 bestMatch = $(this);
369 bestScore = score;
370 }
371 });
372
373 if (!bestMatch) return;
374
375 const $item = bestMatch.closest('.dbx-menu-item');
376
377 $(root).find('.dbx-menu-item')
378 .removeClass('is-active is-active-path is-open');
379
380 $(root).find('.dbx-menu-link[aria-expanded]')
381 .attr('aria-expanded', 'false');
382
383 $item.addClass('is-active is-active-path');
384
385 $item.parents('.dbx-menu-item').each(function () {
386
387 const $parent = $(this);
388
389 /*
390 * Nur die aufgerufene Seite ist aktiv. Eltern markieren lediglich
391 * den Pfad und bleiben dadurch von der aktiven Seite unterscheidbar.
392 */
393 $parent.addClass('is-active-path');
394 });
395
396 storeHistory(bestMatch);
397 }
398
399 /* =====================================================
400 * HISTORY (UNVERÄNDERT)
401 * ===================================================== */
402
403 function storeHistory($link) {
404
405 const text = $link.text().trim();
406 const href = $link.attr('href');
407
408 if (!href || href === '#') return;
409
410 let history = getHistory();
411
412 history = history.filter(item => item.href !== href);
413
414 history.unshift({ text, href });
415
416 history = history.slice(0, 20);
417
418 setHistory(history);
419 }
420
421 function buildChronic() {
422
423 $('.chronic[data-dbx*="menu|chronic"]').each(function () {
424
425 const $el = $(this);
426
427 const deepMatch = $el.attr('data-dbx').match(/deep=(\d+)/);
428 const deep = deepMatch ? parseInt(deepMatch[1]) : 10;
429
430 let history = getHistory();
431
432 history = history.slice(0, deep);
433
434 const html = history.map(item =>
435 `<a href="${item.href}" class="chronic-item">${item.text}</a>`
436 ).join(' <span class="chronic-sep">›</span> ');
437
438 $el.html(html);
439 });
440 }
441
442 /* =====================================================
443 * EVENTS → CORE DELEGATION
444 * ===================================================== */
445
446 function bindEvents() {
447
448 if (bindEvents._bound) return;
449 bindEvents._bound = true;
450
451 dbx.on(
452 'click',
453 '.dbx-menu-item.has-children > .dbx-menu-link',
454 function (e, el) {
455
456 e.preventDefault();
457 e.stopPropagation();
458
459 const $link = $(el);
460 const $item = $link.parent();
461 const $parentList = $item.parent();
462 const willOpen = !$item.hasClass('is-open');
463
464 $parentList.children('.dbx-menu-item.is-open').not($item).each(function () {
465 closeBranch($(this));
466 });
467
468 if (willOpen) {
469 $item.addClass('is-open');
470 $link.attr('aria-expanded', 'true');
471 restoreActivePath($item);
472 } else {
473 closeBranch($item);
474 }
475
476 log("toggle item", willOpen ? "open" : "close");
477 }
478 );
479
480 dbx.on(
481 'keydown',
482 '.dbx-menu-item.has-children > .dbx-menu-link',
483 function (e, el) {
484 const key = e.key || e.which;
485 if (key !== 'Enter' && key !== ' ' && key !== 'Spacebar' && key !== 13 && key !== 32) {
486 return;
487 }
488
489 e.preventDefault();
490 el.click();
491 }
492 );
493
494 dbx.on(
495 'click',
496 '.dbx-menu-toggle',
497 function (e, el) {
498
499 e.preventDefault();
500 e.stopPropagation();
501
502 const $toggle = $(el);
503 const menuId = $toggle.attr('aria-controls');
504 const $menu = $('#' + menuId);
505
506 if (!$menu.length) return;
507
508 const willOpen = !$menu.hasClass('is-mobile-open');
509
510 if (!willOpen) {
511 closeMenu($menu);
512 } else {
513 $menu.addClass('is-mobile-open');
514 $toggle.attr('aria-expanded', 'true');
515 $toggle.find('i.bi')
516 .removeClass('bi-list')
517 .addClass('bi-x');
518 restoreActivePath($menu);
519 }
520
521 log("toggle mobile", menuId, willOpen ? "open" : "close");
522 }
523 );
524
525 dbx.on('click', 'body', function (e) {
526
527 if (!e.target.closest('.dbx-menu-root') && !e.target.closest('.dbx-menu-toggle')) {
528 closeAllMenus(true);
529 }
530 });
531
532 if (!bindEvents._resizeBound) {
533 bindEvents._resizeBound = true;
534
535 window.addEventListener('resize', function () {
536 syncResponsiveState(false);
537 });
538 }
539
540 log("events bound");
541 }
542
543 /* =====================================================
544 * FEATURE INIT
545 * ===================================================== */
546
547 dbx.feature.register(LIB, {
548
549 scope: "element",
550 priority: "veryfirst",
551
552 css: [
553 ["css", "design", "m-menu.css"],
554 ["css", "design", "c-menu.css"]
555 ],
556
557 init(el, cfg) {
558
559 const id = dbx.getLibId(cfg);
560 const $el = $(el);
561
562 if (!id || id === "undef") {
563 dbx.warn("menu → missing id");
564 return;
565 }
566
567 const label = getMenuLabel(cfg, id);
568
569 el.__dbxInitialized = el.__dbxInitialized || {};
570 el.__dbxInitialized[LIB] = true;
571
572 log("init", id);
573
574 buildMenu($el);
575 ensureMobileToggle($el, id, label);
576
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');
580
581 activateByUrl($el);
582 buildChronic();
583
584 bindEvents();
585 syncResponsiveState(true);
586 if ($el.attr('data-dbx-menu-active-open') === '1') {
587 restoreActivePath($el);
588 }
589
590 el.style.visibility = 'visible';
591 }
592
593 });
594
595})(jQuery);