dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
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() {
211 $('.dbx-menu-root').each(function () {
212 closeMenu($(this));
213 });
214 }
215
216 function syncResponsiveState(force) {
217
218 const mobile = isMobileViewport();
219
220 if (!force && _lastMobileState === mobile) {
221 return;
222 }
223
224 _lastMobileState = mobile;
225
226 closeAllMenus();
227
228 log("responsive sync", mobile ? "mobile" : "desktop");
229 }
230
231 /* =====================================================
232 * MENU BUILD
233 * ===================================================== */
234
235 function buildMenu($menu) {
236
237 $menu.addClass('dbx-menu-root');
238
239 if ($menu.is('ul')) {
240 $menu.addClass('dbx-menu-list');
241 }
242
243 $menu.find('ul').addClass('dbx-menu-list');
244
245 $menu.find('li').each(function () {
246
247 const $li = $(this);
248 const $a = $li.children('a');
249
250 $li.addClass('dbx-menu-item');
251
252 if ($li.hasClass('align-right') || $li.data('align') === 'right') {
253 $li.addClass('dbx-menu-right');
254 }
255
256 if ($a.length) {
257 $a.addClass('dbx-menu-link');
258 }
259
260 if ($li.children('ul').length) {
261
262 $li.addClass('has-children');
263
264 if ($a.length) {
265
266 $a.attr('data-role', 'toggle');
267 $a.attr('aria-haspopup', 'true');
268 $a.attr('aria-expanded', 'false');
269 $a.attr('role', 'button');
270
271 if ($a.attr('href') === '#') {
272 $a.removeAttr('href');
273 }
274 if (!$a.attr('href')) {
275 $a.attr('tabindex', '0');
276 }
277 }
278
279 if ($a.length && !$a.find('.dbx-caret').length) {
280 $('<span class="dbx-caret"></span>').appendTo($a);
281 }
282 }
283 });
284 }
285
286 /* =====================================================
287 * ACTIVE STATE
288 * ===================================================== */
289
290 function activateByUrl(root) {
291
292 const currentParams = currentQuery();
293 const currentUrl = new URL(window.location.href);
294
295 let bestMatch = null;
296 let bestScore = -1;
297
298 $(root).find('.dbx-menu-link[href]').each(function () {
299
300 const href = $(this).attr('href');
301 if (!href || href === '#') return;
302
303 const score = linkScore(href, currentUrl, currentParams);
304
305 if (score > bestScore) {
306 bestMatch = $(this);
307 bestScore = score;
308 }
309 });
310
311 if (!bestMatch) return;
312
313 const $item = bestMatch.closest('.dbx-menu-item');
314
315 $(root).find('.dbx-menu-item')
316 .removeClass('is-active is-active-path is-open');
317
318 $(root).find('.dbx-menu-link[aria-expanded]')
319 .attr('aria-expanded', 'false');
320
321 $item.addClass('is-active is-active-path');
322
323 $item.parents('.dbx-menu-item').each(function () {
324
325 const $parent = $(this);
326
327 $parent.addClass('is-active is-active-path');
328
329 const $link = $parent.children('.dbx-menu-link');
330 if ($link.length) {
331 $parent.addClass('is-active');
332 }
333 });
334
335 storeHistory(bestMatch);
336 }
337
338 /* =====================================================
339 * HISTORY (UNVERÄNDERT)
340 * ===================================================== */
341
342 function storeHistory($link) {
343
344 const text = $link.text().trim();
345 const href = $link.attr('href');
346
347 if (!href || href === '#') return;
348
349 let history = getHistory();
350
351 history = history.filter(item => item.href !== href);
352
353 history.unshift({ text, href });
354
355 history = history.slice(0, 20);
356
357 setHistory(history);
358 }
359
360 function buildChronic() {
361
362 $('.chronic[data-dbx*="menu|chronic"]').each(function () {
363
364 const $el = $(this);
365
366 const deepMatch = $el.attr('data-dbx').match(/deep=(\d+)/);
367 const deep = deepMatch ? parseInt(deepMatch[1]) : 10;
368
369 let history = getHistory();
370
371 history = history.slice(0, deep);
372
373 const html = history.map(item =>
374 `<a href="${item.href}" class="chronic-item">${item.text}</a>`
375 ).join(' <span class="chronic-sep">›</span> ');
376
377 $el.html(html);
378 });
379 }
380
381 /* =====================================================
382 * EVENTS → CORE DELEGATION
383 * ===================================================== */
384
385 function bindEvents() {
386
387 if (bindEvents._bound) return;
388 bindEvents._bound = true;
389
390 dbx.on(
391 'click',
392 '.dbx-menu-item.has-children > .dbx-menu-link',
393 function (e, el) {
394
395 e.preventDefault();
396 e.stopPropagation();
397
398 const $link = $(el);
399 const $item = $link.parent();
400 const $parentList = $item.parent();
401 const willOpen = !$item.hasClass('is-open');
402
403 $parentList.children('.dbx-menu-item.is-open').not($item).each(function () {
404 $(this)
405 .removeClass('is-open')
406 .children('.dbx-menu-link')
407 .attr('aria-expanded', 'false');
408 });
409
410 $item.toggleClass('is-open', willOpen);
411 $link.attr('aria-expanded', willOpen ? 'true' : 'false');
412
413 log("toggle item", willOpen ? "open" : "close");
414 }
415 );
416
417 dbx.on(
418 'keydown',
419 '.dbx-menu-item.has-children > .dbx-menu-link',
420 function (e, el) {
421 const key = e.key || e.which;
422 if (key !== 'Enter' && key !== ' ' && key !== 'Spacebar' && key !== 13 && key !== 32) {
423 return;
424 }
425
426 e.preventDefault();
427 el.click();
428 }
429 );
430
431 dbx.on(
432 'click',
433 '.dbx-menu-toggle',
434 function (e, el) {
435
436 e.preventDefault();
437 e.stopPropagation();
438
439 const $toggle = $(el);
440 const menuId = $toggle.attr('aria-controls');
441 const $menu = $('#' + menuId);
442
443 if (!$menu.length) return;
444
445 const willOpen = !$menu.hasClass('is-mobile-open');
446
447 if (!willOpen) {
448 closeMenu($menu);
449 } else {
450 $menu.addClass('is-mobile-open');
451 $toggle.attr('aria-expanded', 'true');
452 $toggle.find('i.bi')
453 .removeClass('bi-list')
454 .addClass('bi-x');
455 }
456
457 log("toggle mobile", menuId, willOpen ? "open" : "close");
458 }
459 );
460
461 dbx.on('click', 'body', function (e) {
462
463 if (!e.target.closest('.dbx-menu-root') && !e.target.closest('.dbx-menu-toggle')) {
464 closeAllMenus();
465 }
466 });
467
468 if (!bindEvents._resizeBound) {
469 bindEvents._resizeBound = true;
470
471 window.addEventListener('resize', function () {
472 syncResponsiveState(false);
473 });
474 }
475
476 log("events bound");
477 }
478
479 /* =====================================================
480 * FEATURE INIT
481 * ===================================================== */
482
483 dbx.feature.register(LIB, {
484
485 scope: "element",
486 priority: "veryfirst",
487
488 css: [
489 ["css", "design", "m-menu.css"],
490 ["css", "design", "c-menu.css"]
491 ],
492
493 init(el, cfg) {
494
495 const id = dbx.getLibId(cfg);
496 const $el = $(el);
497
498 if (!id || id === "undef") {
499 dbx.warn("menu → missing id");
500 return;
501 }
502
503 const label = getMenuLabel(cfg, id);
504
505 el.__dbxInitialized = el.__dbxInitialized || {};
506 el.__dbxInitialized[LIB] = true;
507
508 log("init", id);
509
510 buildMenu($el);
511 ensureMobileToggle($el, id, label);
512
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');
516
517 activateByUrl($el);
518 buildChronic();
519
520 bindEvents();
521 syncResponsiveState(true);
522
523 el.style.visibility = 'visible';
524 }
525
526 });
527
528})(jQuery);