@schukai/monster 4.45.5 → 4.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,20 +13,20 @@
13
13
  */
14
14
 
15
15
  import {
16
- CustomElement,
17
- getSlottedElements,
18
- registerCustomElement,
19
- assembleMethodSymbol,
16
+ CustomElement,
17
+ getSlottedElements,
18
+ registerCustomElement,
19
+ assembleMethodSymbol,
20
20
  } from "../../dom/customelement.mjs";
21
21
  import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
22
22
  import { SiteNavigationStyleSheet } from "./stylesheet/site-navigation.mjs";
23
23
  import {
24
- computePosition,
25
- autoUpdate,
26
- flip,
27
- shift,
28
- offset,
29
- size,
24
+ computePosition,
25
+ autoUpdate,
26
+ flip,
27
+ shift,
28
+ offset,
29
+ size,
30
30
  } from "@floating-ui/dom";
31
31
  import { fireCustomEvent } from "../../dom/events.mjs";
32
32
 
@@ -64,59 +64,59 @@ const hamburgerCloseButtonSymbol = Symbol("hamburgerCloseButton");
64
64
  * @fires monster-submenu-hide - Fired when a submenu is hidden. The event detail contains `{context, trigger, submenu, level}`.
65
65
  */
66
66
  class SiteNavigation extends CustomElement {
67
- static get [instanceSymbol]() {
68
- return Symbol.for("@schukai/monster/components/navigation/site@@instance");
69
- }
70
-
71
- /**
72
- * Configuration options for the SiteNavigation component.
73
- * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
74
- *
75
- * To set these options via an HTML tag, use the `data-monster-options` attribute.
76
- * The individual configuration values are detailed in the table below.
77
- *
78
- * @property {Object} templates - Template definitions.
79
- * @property {string} templates.main - The main HTML template for the component.
80
- * @property {string} interactionModel="auto" - Defines the interaction with submenus. Possible values: `auto`, `click`, `hover`. With `auto`, `hover` is used on desktop and `click` is used in the hamburger menu.
81
- * @property {Object} features - Container for additional feature flags.
82
- * @property {boolean} features.resetOnClose=true - If `true`, all open submenus within the hamburger menu will be reset when it is closed.
83
- */
84
- get defaults() {
85
- return Object.assign({}, super.defaults, {
86
- templates: { main: getTemplate() },
87
- interactionModel: "auto", // 'auto', 'click', 'hover'
88
- features: {
89
- resetOnClose: true,
90
- },
91
- });
92
- }
93
-
94
- [assembleMethodSymbol]() {
95
- super[assembleMethodSymbol]();
96
- initControlReferences.call(this);
97
- initEventHandler.call(this);
98
- }
99
-
100
- static getCSSStyleSheet() {
101
- return [SiteNavigationStyleSheet];
102
- }
103
-
104
- static getTag() {
105
- return "monster-site-navigation";
106
- }
107
-
108
- connectedCallback() {
109
- super.connectedCallback();
110
- attachResizeObserver.call(this);
111
- requestAnimationFrame(() => {
112
- populateTabs.call(this);
113
- });
114
- }
115
-
116
- disconnectedCallback() {
117
- super.disconnectedCallback();
118
- detachResizeObserver.call(this);
119
- }
67
+ static get [instanceSymbol]() {
68
+ return Symbol.for("@schukai/monster/components/navigation/site@@instance");
69
+ }
70
+
71
+ /**
72
+ * Configuration options for the SiteNavigation component.
73
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
74
+ *
75
+ * To set these options via an HTML tag, use the `data-monster-options` attribute.
76
+ * The individual configuration values are detailed in the table below.
77
+ *
78
+ * @property {Object} templates - Template definitions.
79
+ * @property {string} templates.main - The main HTML template for the component.
80
+ * @property {string} interactionModel="auto" - Defines the interaction with submenus. Possible values: `auto`, `click`, `hover`. With `auto`, `hover` is used on desktop and `click` is used in the hamburger menu.
81
+ * @property {Object} features - Container for additional feature flags.
82
+ * @property {boolean} features.resetOnClose=true - If `true`, all open submenus within the hamburger menu will be reset when it is closed.
83
+ */
84
+ get defaults() {
85
+ return Object.assign({}, super.defaults, {
86
+ templates: { main: getTemplate() },
87
+ interactionModel: "auto", // 'auto', 'click', 'hover'
88
+ features: {
89
+ resetOnClose: true,
90
+ },
91
+ });
92
+ }
93
+
94
+ [assembleMethodSymbol]() {
95
+ super[assembleMethodSymbol]();
96
+ initControlReferences.call(this);
97
+ initEventHandler.call(this);
98
+ }
99
+
100
+ static getCSSStyleSheet() {
101
+ return [SiteNavigationStyleSheet];
102
+ }
103
+
104
+ static getTag() {
105
+ return "monster-site-navigation";
106
+ }
107
+
108
+ connectedCallback() {
109
+ super.connectedCallback();
110
+ attachResizeObserver.call(this);
111
+ requestAnimationFrame(() => {
112
+ populateTabs.call(this);
113
+ });
114
+ }
115
+
116
+ disconnectedCallback() {
117
+ super.disconnectedCallback();
118
+ detachResizeObserver.call(this);
119
+ }
120
120
  }
121
121
 
122
122
  /**
@@ -125,22 +125,22 @@ class SiteNavigation extends CustomElement {
125
125
  * @this {SiteNavigation}
126
126
  */
127
127
  function initControlReferences() {
128
- if (!this.shadowRoot) throw new Error("Component requires a shadowRoot.");
129
- this[navElementSymbol] = this.shadowRoot.querySelector(
130
- '[data-monster-role="navigation"]',
131
- );
132
- this[visibleElementsSymbol] =
133
- this.shadowRoot.querySelector("#visible-elements");
134
- this[hiddenElementsSymbol] =
135
- this.shadowRoot.querySelector("#hidden-elements");
136
- this[hamburgerButtonSymbol] =
137
- this.shadowRoot.querySelector("#hamburger-button");
138
- this[hamburgerNavSymbol] = this.shadowRoot.querySelector(
139
- '[data-monster-role="hamburger-nav"]',
140
- );
141
- this[hamburgerCloseButtonSymbol] = this.shadowRoot.querySelector(
142
- '[part="hamburger-close-button"]',
143
- );
128
+ if (!this.shadowRoot) throw new Error("Component requires a shadowRoot.");
129
+ this[navElementSymbol] = this.shadowRoot.querySelector(
130
+ '[data-monster-role="navigation"]',
131
+ );
132
+ this[visibleElementsSymbol] =
133
+ this.shadowRoot.querySelector("#visible-elements");
134
+ this[hiddenElementsSymbol] =
135
+ this.shadowRoot.querySelector("#hidden-elements");
136
+ this[hamburgerButtonSymbol] =
137
+ this.shadowRoot.querySelector("#hamburger-button");
138
+ this[hamburgerNavSymbol] = this.shadowRoot.querySelector(
139
+ '[data-monster-role="hamburger-nav"]',
140
+ );
141
+ this[hamburgerCloseButtonSymbol] = this.shadowRoot.querySelector(
142
+ '[part="hamburger-close-button"]',
143
+ );
144
144
  }
145
145
 
146
146
  /**
@@ -149,122 +149,122 @@ function initControlReferences() {
149
149
  * @this {SiteNavigation}
150
150
  */
151
151
  function initEventHandler() {
152
- if (!this.shadowRoot) throw new Error("Component requires a shadowRoot.");
153
-
154
- const hamburgerButton = this[hamburgerButtonSymbol];
155
- const hamburgerNav = this[hamburgerNavSymbol];
156
- const hamburgerCloseButton = this[hamburgerCloseButtonSymbol];
157
- let cleanup;
158
-
159
- if (!hamburgerButton || !hamburgerNav || !hamburgerCloseButton) return;
160
-
161
- const getBestPositionStrategy = (element) => {
162
- let parent = element.parentElement;
163
- while (parent) {
164
- const parentPosition = window.getComputedStyle(parent).position;
165
- if (["fixed", "sticky"].includes(parentPosition)) {
166
- return "fixed";
167
- }
168
- parent = parent.parentElement;
169
- }
170
- return "absolute";
171
- };
172
-
173
- const handleOutsideClick = (event) => {
174
- if (
175
- !hamburgerButton.contains(event.target) &&
176
- !hamburgerNav.contains(event.target)
177
- ) {
178
- hideMenu();
179
- }
180
- };
181
-
182
- const hideMenu = () => {
183
- hamburgerNav.style.display = "none";
184
- document.body.classList.remove("monster-navigation-open");
185
-
186
- fireCustomEvent(this, "monster-hamburger-hide", {
187
- button: hamburgerButton,
188
- menu: hamburgerNav,
189
- });
190
-
191
- if (this.getOption("features.resetOnClose") === true) {
192
- this[hiddenElementsSymbol]
193
- .querySelectorAll(".is-open")
194
- .forEach((submenu) => submenu.classList.remove("is-open"));
195
- }
196
-
197
- if (cleanup) {
198
- cleanup();
199
- cleanup = undefined;
200
- }
201
- document.removeEventListener("click", handleOutsideClick);
202
- };
203
-
204
- this[hideHamburgerMenuSymbol] = hideMenu;
205
-
206
- const showMenu = () => {
207
- this[activeSubmenuHiderSymbol]?.();
208
- hamburgerNav.style.display = "block";
209
- document.body.classList.add("monster-navigation-open");
210
-
211
- fireCustomEvent(this, "monster-hamburger-show", {
212
- button: hamburgerButton,
213
- menu: hamburgerNav,
214
- });
215
-
216
- hamburgerNav.scrollIntoView({ block: "start", behavior: "smooth" });
217
-
218
- cleanup = autoUpdate(hamburgerButton, hamburgerNav, () => {
219
- if (window.innerWidth > 768) {
220
- const strategy = getBestPositionStrategy(this);
221
-
222
- computePosition(hamburgerButton, hamburgerNav, {
223
- placement: "bottom-end",
224
- strategy: strategy,
225
- middleware: [
226
- offset(8),
227
- flip(),
228
- shift({ padding: 8 }),
229
- size({
230
- apply: ({ availableHeight, elements }) => {
231
- Object.assign(elements.floating.style, {
232
- maxHeight: `${availableHeight}px`,
233
- overflowY: "auto",
234
- });
235
- },
236
- padding: 8,
237
- }),
238
- ],
239
- }).then(({ x, y, strategy }) => {
240
- Object.assign(hamburgerNav.style, {
241
- position: strategy,
242
- left: `${x}px`,
243
- top: `${y}px`,
244
- });
245
- });
246
- } else {
247
- // Mobile view (fullscreen overlay), position is handled by CSS
248
- Object.assign(hamburgerNav.style, { position: "", left: "", top: "" });
249
- }
250
- });
251
- setTimeout(() => document.addEventListener("click", handleOutsideClick), 0);
252
- };
253
-
254
- hamburgerButton.addEventListener("click", (event) => {
255
- event.stopPropagation();
256
- const isVisible = hamburgerNav.style.display === "block";
257
- if (isVisible) {
258
- hideMenu();
259
- } else {
260
- showMenu();
261
- }
262
- });
263
-
264
- hamburgerCloseButton.addEventListener("click", (event) => {
265
- event.stopPropagation();
266
- hideMenu();
267
- });
152
+ if (!this.shadowRoot) throw new Error("Component requires a shadowRoot.");
153
+
154
+ const hamburgerButton = this[hamburgerButtonSymbol];
155
+ const hamburgerNav = this[hamburgerNavSymbol];
156
+ const hamburgerCloseButton = this[hamburgerCloseButtonSymbol];
157
+ let cleanup;
158
+
159
+ if (!hamburgerButton || !hamburgerNav || !hamburgerCloseButton) return;
160
+
161
+ const getBestPositionStrategy = (element) => {
162
+ let parent = element.parentElement;
163
+ while (parent) {
164
+ const parentPosition = window.getComputedStyle(parent).position;
165
+ if (["fixed", "sticky"].includes(parentPosition)) {
166
+ return "fixed";
167
+ }
168
+ parent = parent.parentElement;
169
+ }
170
+ return "absolute";
171
+ };
172
+
173
+ const handleOutsideClick = (event) => {
174
+ if (
175
+ !hamburgerButton.contains(event.target) &&
176
+ !hamburgerNav.contains(event.target)
177
+ ) {
178
+ hideMenu();
179
+ }
180
+ };
181
+
182
+ const hideMenu = () => {
183
+ hamburgerNav.style.display = "none";
184
+ document.body.classList.remove("monster-navigation-open");
185
+
186
+ fireCustomEvent(this, "monster-hamburger-hide", {
187
+ button: hamburgerButton,
188
+ menu: hamburgerNav,
189
+ });
190
+
191
+ if (this.getOption("features.resetOnClose") === true) {
192
+ this[hiddenElementsSymbol]
193
+ .querySelectorAll(".is-open")
194
+ .forEach((submenu) => submenu.classList.remove("is-open"));
195
+ }
196
+
197
+ if (cleanup) {
198
+ cleanup();
199
+ cleanup = undefined;
200
+ }
201
+ document.removeEventListener("click", handleOutsideClick);
202
+ };
203
+
204
+ this[hideHamburgerMenuSymbol] = hideMenu;
205
+
206
+ const showMenu = () => {
207
+ this[activeSubmenuHiderSymbol]?.();
208
+ hamburgerNav.style.display = "block";
209
+ document.body.classList.add("monster-navigation-open");
210
+
211
+ fireCustomEvent(this, "monster-hamburger-show", {
212
+ button: hamburgerButton,
213
+ menu: hamburgerNav,
214
+ });
215
+
216
+ hamburgerNav.scrollIntoView({ block: "start", behavior: "smooth" });
217
+
218
+ cleanup = autoUpdate(hamburgerButton, hamburgerNav, () => {
219
+ if (window.innerWidth > 768) {
220
+ const strategy = getBestPositionStrategy(this);
221
+
222
+ computePosition(hamburgerButton, hamburgerNav, {
223
+ placement: "bottom-end",
224
+ strategy: strategy,
225
+ middleware: [
226
+ offset(8),
227
+ flip(),
228
+ shift({ padding: 8 }),
229
+ size({
230
+ apply: ({ availableHeight, elements }) => {
231
+ Object.assign(elements.floating.style, {
232
+ maxHeight: `${availableHeight}px`,
233
+ overflowY: "auto",
234
+ });
235
+ },
236
+ padding: 8,
237
+ }),
238
+ ],
239
+ }).then(({ x, y, strategy }) => {
240
+ Object.assign(hamburgerNav.style, {
241
+ position: strategy,
242
+ left: `${x}px`,
243
+ top: `${y}px`,
244
+ });
245
+ });
246
+ } else {
247
+ // Mobile view (fullscreen overlay), position is handled by CSS
248
+ Object.assign(hamburgerNav.style, { position: "", left: "", top: "" });
249
+ }
250
+ });
251
+ setTimeout(() => document.addEventListener("click", handleOutsideClick), 0);
252
+ };
253
+
254
+ hamburgerButton.addEventListener("click", (event) => {
255
+ event.stopPropagation();
256
+ const isVisible = hamburgerNav.style.display === "block";
257
+ if (isVisible) {
258
+ hideMenu();
259
+ } else {
260
+ showMenu();
261
+ }
262
+ });
263
+
264
+ hamburgerCloseButton.addEventListener("click", (event) => {
265
+ event.stopPropagation();
266
+ hideMenu();
267
+ });
268
268
  }
269
269
 
270
270
  /**
@@ -274,22 +274,22 @@ function initEventHandler() {
274
274
  * @this {SiteNavigation}
275
275
  */
276
276
  function attachResizeObserver() {
277
- this[resizeObserverSymbol] = new ResizeObserver(() => {
278
- if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
279
- try {
280
- this[timerCallbackSymbol].touch();
281
- return;
282
- } catch (e) {
283
- delete this[timerCallbackSymbol];
284
- }
285
- }
286
- this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
287
- requestAnimationFrame(() => {
288
- populateTabs.call(this);
289
- });
290
- });
291
- });
292
- this[resizeObserverSymbol].observe(this);
277
+ this[resizeObserverSymbol] = new ResizeObserver(() => {
278
+ if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
279
+ try {
280
+ this[timerCallbackSymbol].touch();
281
+ return;
282
+ } catch (e) {
283
+ delete this[timerCallbackSymbol];
284
+ }
285
+ }
286
+ this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
287
+ requestAnimationFrame(() => {
288
+ populateTabs.call(this);
289
+ });
290
+ });
291
+ });
292
+ this[resizeObserverSymbol].observe(this);
293
293
  }
294
294
 
295
295
  /**
@@ -298,10 +298,10 @@ function attachResizeObserver() {
298
298
  * @this {SiteNavigation}
299
299
  */
300
300
  function detachResizeObserver() {
301
- if (this[resizeObserverSymbol] instanceof ResizeObserver) {
302
- this[resizeObserverSymbol].disconnect();
303
- delete this[resizeObserverSymbol];
304
- }
301
+ if (this[resizeObserverSymbol] instanceof ResizeObserver) {
302
+ this[resizeObserverSymbol].disconnect();
303
+ delete this[resizeObserverSymbol];
304
+ }
305
305
  }
306
306
 
307
307
  /**
@@ -314,240 +314,240 @@ function detachResizeObserver() {
314
314
  * @param {number} level The nesting level of the submenu (starts at 1).
315
315
  */
316
316
  function setupSubmenu(parentLi, context = "visible", level = 1) {
317
- const submenu = parentLi.querySelector(
318
- ":scope > ul, :scope > div[part='mega-menu']",
319
- );
320
- if (!submenu) return;
321
-
322
- if (submenu.tagName === "UL") {
323
- submenu.setAttribute("part", "submenu");
324
- }
325
-
326
- const interaction = this.getOption("interactionModel", "auto");
327
- const isTouchDevice =
328
- "ontouchstart" in window || navigator.maxTouchPoints > 0;
329
-
330
- const effectiveInteraction =
331
- interaction === "auto"
332
- ? context === "visible"
333
- ? "hover"
334
- : "click"
335
- : interaction;
336
-
337
- const component = this;
338
- let cleanup;
339
-
340
- const immediateHide = () => {
341
- submenu.style.display = "none";
342
- Object.assign(submenu.style, {
343
- maxHeight: "",
344
- overflowY: "",
345
- });
346
- submenu
347
- .querySelectorAll(
348
- "ul[style*='display: block'], div[part='mega-menu'][style*='display: block']",
349
- )
350
- .forEach((sub) => {
351
- sub.style.display = "none";
352
- });
353
- fireCustomEvent(this, "monster-submenu-hide", {
354
- context,
355
- trigger: parentLi,
356
- submenu,
357
- level,
358
- });
359
- if (cleanup) {
360
- cleanup();
361
- cleanup = null;
362
- }
363
- if (level === 1 && component[activeSubmenuHiderSymbol] === immediateHide) {
364
- component[activeSubmenuHiderSymbol] = null;
365
- }
366
- };
367
-
368
- const show = () => {
369
- component[hideHamburgerMenuSymbol]?.();
370
- if (level === 1) {
371
- if (
372
- component[activeSubmenuHiderSymbol] &&
373
- component[activeSubmenuHiderSymbol] !== immediateHide
374
- ) {
375
- component[activeSubmenuHiderSymbol]();
376
- }
377
- component[activeSubmenuHiderSymbol] = immediateHide;
378
- } else {
379
- [...parentLi.parentElement.children]
380
- .filter((li) => li !== parentLi)
381
- .forEach((sibling) => {
382
- const siblingSubmenu = sibling.querySelector(
383
- ":scope > ul, :scope > div[part='mega-menu']",
384
- );
385
- if (siblingSubmenu) {
386
- siblingSubmenu.style.display = "none";
387
- }
388
- });
389
- }
390
- submenu.style.display = "block";
391
- fireCustomEvent(this, "monster-submenu-show", {
392
- context,
393
- trigger: parentLi,
394
- submenu,
395
- level,
396
- });
397
- if (!cleanup) {
398
- cleanup = autoUpdate(parentLi, submenu, () => {
399
- const middleware = [offset(8), flip(), shift({ padding: 8 })];
400
- const containsSubmenus = submenu.querySelector(
401
- "ul, div[part='mega-menu']",
402
- );
403
- if (!containsSubmenus) {
404
- middleware.push(
405
- size({
406
- apply: ({ availableHeight, elements }) => {
407
- Object.assign(elements.floating.style, {
408
- maxHeight: `${availableHeight}px`,
409
- overflowY: "auto",
410
- });
411
- },
412
- padding: 8,
413
- }),
414
- );
415
- }
416
- computePosition(parentLi, submenu, {
417
- placement: level === 1 ? "bottom-start" : "right-start",
418
- middleware: middleware,
419
- }).then(({ x, y, strategy }) => {
420
- Object.assign(submenu.style, {
421
- position: strategy,
422
- left: `${x}px`,
423
- top: `${y}px`,
424
- });
425
- });
426
- });
427
- }
428
- };
429
-
430
- if (effectiveInteraction === "hover" && isTouchDevice) {
431
- let lastTap = 0;
432
- const DOUBLE_TAP_DELAY = 300;
433
- const anchor = parentLi.querySelector(":scope > a");
434
-
435
- if (!anchor) return;
436
-
437
- const handleOutsideClickForSubmenu = (event) => {
438
- if (
439
- submenu.style.display === "block" &&
440
- !parentLi.contains(event.target)
441
- ) {
442
- immediateHide();
443
- document.removeEventListener(
444
- "click",
445
- handleOutsideClickForSubmenu,
446
- true,
447
- );
448
- }
449
- };
450
-
451
- anchor.addEventListener("click", (event) => {
452
- const now = Date.now();
453
- const timeSinceLastTap = now - lastTap;
454
- lastTap = now;
455
-
456
- if (timeSinceLastTap < DOUBLE_TAP_DELAY && timeSinceLastTap > 0) {
457
- lastTap = 0;
458
- document.removeEventListener(
459
- "click",
460
- handleOutsideClickForSubmenu,
461
- true,
462
- );
463
- immediateHide();
464
- } else {
465
- event.preventDefault();
466
- event.stopPropagation();
467
-
468
- const isMenuOpen = submenu.style.display === "block";
469
-
470
- if (isMenuOpen) {
471
- document.removeEventListener(
472
- "click",
473
- handleOutsideClickForSubmenu,
474
- true,
475
- );
476
- immediateHide();
477
- } else {
478
- show();
479
- setTimeout(() => {
480
- document.addEventListener(
481
- "click",
482
- handleOutsideClickForSubmenu,
483
- true,
484
- );
485
- }, 0);
486
- }
487
- }
488
- });
489
- } else if (effectiveInteraction === "hover" && !isTouchDevice) {
490
- let hideTimeout;
491
- let isHovering = false;
492
-
493
- const handleMouseEnter = () => {
494
- isHovering = true;
495
- clearTimeout(hideTimeout);
496
- if (submenu.style.display !== "block") {
497
- show();
498
- }
499
- };
500
-
501
- const handleMouseLeave = () => {
502
- isHovering = false;
503
- hideTimeout = setTimeout(() => {
504
- if (!isHovering) {
505
- immediateHide();
506
- }
507
- }, 250);
508
- };
509
-
510
- parentLi.addEventListener("mouseenter", handleMouseEnter);
511
- parentLi.addEventListener("mouseleave", handleMouseLeave);
512
- submenu.addEventListener("mouseenter", handleMouseEnter);
513
- submenu.addEventListener("mouseleave", handleMouseLeave);
514
- } else {
515
- const anchor = parentLi.querySelector(":scope > a");
516
- if (anchor) {
517
- anchor.addEventListener("click", (event) => {
518
- event.preventDefault();
519
- event.stopPropagation();
520
- if (!submenu.classList.contains("is-open")) {
521
- [...parentLi.parentElement.children]
522
- .filter((li) => li !== parentLi)
523
- .forEach((sibling) => {
524
- const siblingSubmenu = sibling.querySelector(
525
- ":scope > ul, :scope > div[part='mega-menu']",
526
- );
527
- if (siblingSubmenu) {
528
- siblingSubmenu.classList.remove("is-open");
529
- }
530
- });
531
- }
532
- const isOpen = submenu.classList.toggle("is-open");
533
- const eventName = isOpen
534
- ? "monster-submenu-show"
535
- : "monster-submenu-hide";
536
- fireCustomEvent(this, eventName, {
537
- context,
538
- trigger: parentLi,
539
- submenu,
540
- level,
541
- });
542
- });
543
- }
544
- }
545
-
546
- if (submenu.tagName === "UL") {
547
- submenu
548
- .querySelectorAll(":scope > li")
549
- .forEach((li) => setupSubmenu.call(this, li, context, level + 1));
550
- }
317
+ const submenu = parentLi.querySelector(
318
+ ":scope > ul, :scope > div[part='mega-menu']",
319
+ );
320
+ if (!submenu) return;
321
+
322
+ if (submenu.tagName === "UL") {
323
+ submenu.setAttribute("part", "submenu");
324
+ }
325
+
326
+ const interaction = this.getOption("interactionModel", "auto");
327
+ const isTouchDevice =
328
+ "ontouchstart" in window || navigator.maxTouchPoints > 0;
329
+
330
+ const effectiveInteraction =
331
+ interaction === "auto"
332
+ ? context === "visible"
333
+ ? "hover"
334
+ : "click"
335
+ : interaction;
336
+
337
+ const component = this;
338
+ let cleanup;
339
+
340
+ const immediateHide = () => {
341
+ submenu.style.display = "none";
342
+ Object.assign(submenu.style, {
343
+ maxHeight: "",
344
+ overflowY: "",
345
+ });
346
+ submenu
347
+ .querySelectorAll(
348
+ "ul[style*='display: block'], div[part='mega-menu'][style*='display: block']",
349
+ )
350
+ .forEach((sub) => {
351
+ sub.style.display = "none";
352
+ });
353
+ fireCustomEvent(this, "monster-submenu-hide", {
354
+ context,
355
+ trigger: parentLi,
356
+ submenu,
357
+ level,
358
+ });
359
+ if (cleanup) {
360
+ cleanup();
361
+ cleanup = null;
362
+ }
363
+ if (level === 1 && component[activeSubmenuHiderSymbol] === immediateHide) {
364
+ component[activeSubmenuHiderSymbol] = null;
365
+ }
366
+ };
367
+
368
+ const show = () => {
369
+ component[hideHamburgerMenuSymbol]?.();
370
+ if (level === 1) {
371
+ if (
372
+ component[activeSubmenuHiderSymbol] &&
373
+ component[activeSubmenuHiderSymbol] !== immediateHide
374
+ ) {
375
+ component[activeSubmenuHiderSymbol]();
376
+ }
377
+ component[activeSubmenuHiderSymbol] = immediateHide;
378
+ } else {
379
+ [...parentLi.parentElement.children]
380
+ .filter((li) => li !== parentLi)
381
+ .forEach((sibling) => {
382
+ const siblingSubmenu = sibling.querySelector(
383
+ ":scope > ul, :scope > div[part='mega-menu']",
384
+ );
385
+ if (siblingSubmenu) {
386
+ siblingSubmenu.style.display = "none";
387
+ }
388
+ });
389
+ }
390
+ submenu.style.display = "block";
391
+ fireCustomEvent(this, "monster-submenu-show", {
392
+ context,
393
+ trigger: parentLi,
394
+ submenu,
395
+ level,
396
+ });
397
+ if (!cleanup) {
398
+ cleanup = autoUpdate(parentLi, submenu, () => {
399
+ const middleware = [offset(8), flip(), shift({ padding: 8 })];
400
+ const containsSubmenus = submenu.querySelector(
401
+ "ul, div[part='mega-menu']",
402
+ );
403
+ if (!containsSubmenus) {
404
+ middleware.push(
405
+ size({
406
+ apply: ({ availableHeight, elements }) => {
407
+ Object.assign(elements.floating.style, {
408
+ maxHeight: `${availableHeight}px`,
409
+ overflowY: "auto",
410
+ });
411
+ },
412
+ padding: 8,
413
+ }),
414
+ );
415
+ }
416
+ computePosition(parentLi, submenu, {
417
+ placement: level === 1 ? "bottom-start" : "right-start",
418
+ middleware: middleware,
419
+ }).then(({ x, y, strategy }) => {
420
+ Object.assign(submenu.style, {
421
+ position: strategy,
422
+ left: `${x}px`,
423
+ top: `${y}px`,
424
+ });
425
+ });
426
+ });
427
+ }
428
+ };
429
+
430
+ if (effectiveInteraction === "hover" && isTouchDevice) {
431
+ let lastTap = 0;
432
+ const DOUBLE_TAP_DELAY = 300;
433
+ const anchor = parentLi.querySelector(":scope > a");
434
+
435
+ if (!anchor) return;
436
+
437
+ const handleOutsideClickForSubmenu = (event) => {
438
+ if (
439
+ submenu.style.display === "block" &&
440
+ !parentLi.contains(event.target)
441
+ ) {
442
+ immediateHide();
443
+ document.removeEventListener(
444
+ "click",
445
+ handleOutsideClickForSubmenu,
446
+ true,
447
+ );
448
+ }
449
+ };
450
+
451
+ anchor.addEventListener("click", (event) => {
452
+ const now = Date.now();
453
+ const timeSinceLastTap = now - lastTap;
454
+ lastTap = now;
455
+
456
+ if (timeSinceLastTap < DOUBLE_TAP_DELAY && timeSinceLastTap > 0) {
457
+ lastTap = 0;
458
+ document.removeEventListener(
459
+ "click",
460
+ handleOutsideClickForSubmenu,
461
+ true,
462
+ );
463
+ immediateHide();
464
+ } else {
465
+ event.preventDefault();
466
+ event.stopPropagation();
467
+
468
+ const isMenuOpen = submenu.style.display === "block";
469
+
470
+ if (isMenuOpen) {
471
+ document.removeEventListener(
472
+ "click",
473
+ handleOutsideClickForSubmenu,
474
+ true,
475
+ );
476
+ immediateHide();
477
+ } else {
478
+ show();
479
+ setTimeout(() => {
480
+ document.addEventListener(
481
+ "click",
482
+ handleOutsideClickForSubmenu,
483
+ true,
484
+ );
485
+ }, 0);
486
+ }
487
+ }
488
+ });
489
+ } else if (effectiveInteraction === "hover" && !isTouchDevice) {
490
+ let hideTimeout;
491
+ let isHovering = false;
492
+
493
+ const handleMouseEnter = () => {
494
+ isHovering = true;
495
+ clearTimeout(hideTimeout);
496
+ if (submenu.style.display !== "block") {
497
+ show();
498
+ }
499
+ };
500
+
501
+ const handleMouseLeave = () => {
502
+ isHovering = false;
503
+ hideTimeout = setTimeout(() => {
504
+ if (!isHovering) {
505
+ immediateHide();
506
+ }
507
+ }, 250);
508
+ };
509
+
510
+ parentLi.addEventListener("mouseenter", handleMouseEnter);
511
+ parentLi.addEventListener("mouseleave", handleMouseLeave);
512
+ submenu.addEventListener("mouseenter", handleMouseEnter);
513
+ submenu.addEventListener("mouseleave", handleMouseLeave);
514
+ } else {
515
+ const anchor = parentLi.querySelector(":scope > a");
516
+ if (anchor) {
517
+ anchor.addEventListener("click", (event) => {
518
+ event.preventDefault();
519
+ event.stopPropagation();
520
+ if (!submenu.classList.contains("is-open")) {
521
+ [...parentLi.parentElement.children]
522
+ .filter((li) => li !== parentLi)
523
+ .forEach((sibling) => {
524
+ const siblingSubmenu = sibling.querySelector(
525
+ ":scope > ul, :scope > div[part='mega-menu']",
526
+ );
527
+ if (siblingSubmenu) {
528
+ siblingSubmenu.classList.remove("is-open");
529
+ }
530
+ });
531
+ }
532
+ const isOpen = submenu.classList.toggle("is-open");
533
+ const eventName = isOpen
534
+ ? "monster-submenu-show"
535
+ : "monster-submenu-hide";
536
+ fireCustomEvent(this, eventName, {
537
+ context,
538
+ trigger: parentLi,
539
+ submenu,
540
+ level,
541
+ });
542
+ });
543
+ }
544
+ }
545
+
546
+ if (submenu.tagName === "UL") {
547
+ submenu
548
+ .querySelectorAll(":scope > li")
549
+ .forEach((li) => setupSubmenu.call(this, li, context, level + 1));
550
+ }
551
551
  }
552
552
 
553
553
  /**
@@ -558,20 +558,20 @@ function setupSubmenu(parentLi, context = "visible", level = 1) {
558
558
  * @returns {HTMLLIElement} The cloned and configured list item.
559
559
  */
560
560
  function cloneNavItem(item) {
561
- const liClone = item.cloneNode(true);
562
- const aClone = liClone.querySelector("a");
563
- let navItemPart = "nav-item";
564
- let navLinkPart = "nav-link";
561
+ const liClone = item.cloneNode(true);
562
+ const aClone = liClone.querySelector("a");
563
+ let navItemPart = "nav-item";
564
+ let navLinkPart = "nav-link";
565
565
 
566
- if (item.classList.contains("active")) {
567
- navItemPart += " nav-item-active";
568
- if (aClone) navLinkPart += " nav-link-active";
569
- }
566
+ if (item.classList.contains("active")) {
567
+ navItemPart += " nav-item-active";
568
+ if (aClone) navLinkPart += " nav-link-active";
569
+ }
570
570
 
571
- liClone.setAttribute("part", navItemPart);
572
- if (aClone) aClone.setAttribute("part", navLinkPart);
571
+ liClone.setAttribute("part", navItemPart);
572
+ if (aClone) aClone.setAttribute("part", navLinkPart);
573
573
 
574
- return liClone;
574
+ return liClone;
575
575
  }
576
576
 
577
577
  /**
@@ -588,122 +588,122 @@ function cloneNavItem(item) {
588
588
  * @this {SiteNavigation}
589
589
  */
590
590
  function populateTabs() {
591
- const visibleList = this[visibleElementsSymbol];
592
- const hiddenList = this[hiddenElementsSymbol];
593
- const hamburgerButton = this[hamburgerButtonSymbol];
594
- const navEl = this[navElementSymbol];
595
-
596
- const topLevelUl = [...getSlottedElements.call(this, "ul")].find(
597
- (ul) => ul.parentElement === this,
598
- );
599
-
600
- visibleList.innerHTML = "";
601
- hiddenList.innerHTML = "";
602
- hamburgerButton.style.display = "none";
603
- this.style.visibility = "hidden";
604
-
605
- if (!topLevelUl) {
606
- this.style.visibility = "visible";
607
- return; // Nichts zu tun
608
- }
609
- const sourceItems = Array.from(topLevelUl.children).filter(
610
- (n) => n.tagName === "LI",
611
- );
612
- if (sourceItems.length === 0) {
613
- this.style.visibility = "visible";
614
- return;
615
- }
616
-
617
- const navWidth = navEl.clientWidth;
618
-
619
- const originalDisplay = hamburgerButton.style.display;
620
- hamburgerButton.style.visibility = "hidden";
621
- hamburgerButton.style.display = "flex";
622
- const hamburgerWidth =
623
- Math.ceil(hamburgerButton.getBoundingClientRect().width) || 0;
624
- hamburgerButton.style.display = originalDisplay;
625
- hamburgerButton.style.visibility = "visible";
626
-
627
- navEl.style.overflow = "hidden";
628
- visibleList.style.flexWrap = "nowrap";
629
- visibleList.style.visibility = "hidden"; // Inhalt der Liste während Manipulation ausblenden
630
-
631
- const fit = [];
632
- const rest = [];
633
- let hasOverflow = false;
634
-
635
- for (let i = 0; i < sourceItems.length; i++) {
636
- const item = sourceItems[i];
637
-
638
- if (hasOverflow) {
639
- rest.push(item);
640
- continue;
641
- }
642
-
643
- const liClone = cloneNavItem(item);
644
- visibleList.appendChild(liClone);
645
-
646
- const requiredWidth = liClone.offsetLeft + liClone.offsetWidth;
647
- const availableWidth = navWidth - hamburgerWidth;
648
- const SAFETY_MARGIN = 1; // 1px Sicherheitsmarge für Subpixel-Rendering
649
-
650
- if (requiredWidth > availableWidth + SAFETY_MARGIN) {
651
- hasOverflow = true;
652
- rest.push(item);
653
- } else {
654
- fit.push(item);
655
- }
656
- }
657
-
658
- if (fit.length > 0 && rest.length > 0) {
659
- const lastVisibleItem = visibleList.children[fit.length - 1];
660
- const visibleItemsWidth =
661
- lastVisibleItem.offsetLeft + lastVisibleItem.offsetWidth;
662
-
663
- const firstHiddenItemClone = cloneNavItem(rest[0]);
664
- const submenu = firstHiddenItemClone.querySelector(
665
- "ul, div[part='mega-menu']",
666
- );
667
- if (submenu) submenu.style.display = "none";
668
-
669
- visibleList.appendChild(firstHiddenItemClone);
670
- const firstHiddenItemWidth =
671
- firstHiddenItemClone.getBoundingClientRect().width;
672
- visibleList.removeChild(firstHiddenItemClone);
673
-
674
- const gap = parseFloat(getComputedStyle(visibleList).gap || "0") || 0;
675
- if (visibleItemsWidth + gap + firstHiddenItemWidth <= navWidth) {
676
- fit.push(rest.shift());
677
- }
678
- }
679
-
680
- navEl.style.overflow = "";
681
- visibleList.style.flexWrap = "";
682
- visibleList.innerHTML = "";
683
-
684
- if (fit.length) {
685
- const clonedVisible = fit.map(cloneNavItem);
686
- visibleList.append(...clonedVisible);
687
- visibleList
688
- .querySelectorAll(":scope > li")
689
- .forEach((li) => setupSubmenu.call(this, li, "visible", 1));
690
- }
691
-
692
- if (rest.length) {
693
- const clonedHidden = rest.map(cloneNavItem);
694
- hiddenList.append(...clonedHidden);
695
- hamburgerButton.style.display = "flex";
696
- hiddenList
697
- .querySelectorAll(":scope > li")
698
- .forEach((li) => setupSubmenu.call(this, li, "hidden", 1));
699
- }
700
-
701
- visibleList.style.visibility = "visible";
702
- this.style.visibility = "visible";
703
- fireCustomEvent(this, "monster-layout-change", {
704
- visibleItems: fit,
705
- hiddenItems: rest,
706
- });
591
+ const visibleList = this[visibleElementsSymbol];
592
+ const hiddenList = this[hiddenElementsSymbol];
593
+ const hamburgerButton = this[hamburgerButtonSymbol];
594
+ const navEl = this[navElementSymbol];
595
+
596
+ const topLevelUl = [...getSlottedElements.call(this, "ul")].find(
597
+ (ul) => ul.parentElement === this,
598
+ );
599
+
600
+ visibleList.innerHTML = "";
601
+ hiddenList.innerHTML = "";
602
+ hamburgerButton.style.display = "none";
603
+ this.style.visibility = "hidden";
604
+
605
+ if (!topLevelUl) {
606
+ this.style.visibility = "visible";
607
+ return; // Nichts zu tun
608
+ }
609
+ const sourceItems = Array.from(topLevelUl.children).filter(
610
+ (n) => n.tagName === "LI",
611
+ );
612
+ if (sourceItems.length === 0) {
613
+ this.style.visibility = "visible";
614
+ return;
615
+ }
616
+
617
+ const navWidth = navEl.clientWidth;
618
+
619
+ const originalDisplay = hamburgerButton.style.display;
620
+ hamburgerButton.style.visibility = "hidden";
621
+ hamburgerButton.style.display = "flex";
622
+ const hamburgerWidth =
623
+ Math.ceil(hamburgerButton.getBoundingClientRect().width) || 0;
624
+ hamburgerButton.style.display = originalDisplay;
625
+ hamburgerButton.style.visibility = "visible";
626
+
627
+ navEl.style.overflow = "hidden";
628
+ visibleList.style.flexWrap = "nowrap";
629
+ visibleList.style.visibility = "hidden"; // Inhalt der Liste während Manipulation ausblenden
630
+
631
+ const fit = [];
632
+ const rest = [];
633
+ let hasOverflow = false;
634
+
635
+ for (let i = 0; i < sourceItems.length; i++) {
636
+ const item = sourceItems[i];
637
+
638
+ if (hasOverflow) {
639
+ rest.push(item);
640
+ continue;
641
+ }
642
+
643
+ const liClone = cloneNavItem(item);
644
+ visibleList.appendChild(liClone);
645
+
646
+ const requiredWidth = liClone.offsetLeft + liClone.offsetWidth;
647
+ const availableWidth = navWidth - hamburgerWidth;
648
+ const SAFETY_MARGIN = 1; // 1px Sicherheitsmarge für Subpixel-Rendering
649
+
650
+ if (requiredWidth > availableWidth + SAFETY_MARGIN) {
651
+ hasOverflow = true;
652
+ rest.push(item);
653
+ } else {
654
+ fit.push(item);
655
+ }
656
+ }
657
+
658
+ if (fit.length > 0 && rest.length > 0) {
659
+ const lastVisibleItem = visibleList.children[fit.length - 1];
660
+ const visibleItemsWidth =
661
+ lastVisibleItem.offsetLeft + lastVisibleItem.offsetWidth;
662
+
663
+ const firstHiddenItemClone = cloneNavItem(rest[0]);
664
+ const submenu = firstHiddenItemClone.querySelector(
665
+ "ul, div[part='mega-menu']",
666
+ );
667
+ if (submenu) submenu.style.display = "none";
668
+
669
+ visibleList.appendChild(firstHiddenItemClone);
670
+ const firstHiddenItemWidth =
671
+ firstHiddenItemClone.getBoundingClientRect().width;
672
+ visibleList.removeChild(firstHiddenItemClone);
673
+
674
+ const gap = parseFloat(getComputedStyle(visibleList).gap || "0") || 0;
675
+ if (visibleItemsWidth + gap + firstHiddenItemWidth <= navWidth) {
676
+ fit.push(rest.shift());
677
+ }
678
+ }
679
+
680
+ navEl.style.overflow = "";
681
+ visibleList.style.flexWrap = "";
682
+ visibleList.innerHTML = "";
683
+
684
+ if (fit.length) {
685
+ const clonedVisible = fit.map(cloneNavItem);
686
+ visibleList.append(...clonedVisible);
687
+ visibleList
688
+ .querySelectorAll(":scope > li")
689
+ .forEach((li) => setupSubmenu.call(this, li, "visible", 1));
690
+ }
691
+
692
+ if (rest.length) {
693
+ const clonedHidden = rest.map(cloneNavItem);
694
+ hiddenList.append(...clonedHidden);
695
+ hamburgerButton.style.display = "flex";
696
+ hiddenList
697
+ .querySelectorAll(":scope > li")
698
+ .forEach((li) => setupSubmenu.call(this, li, "hidden", 1));
699
+ }
700
+
701
+ visibleList.style.visibility = "visible";
702
+ this.style.visibility = "visible";
703
+ fireCustomEvent(this, "monster-layout-change", {
704
+ visibleItems: fit,
705
+ hiddenItems: rest,
706
+ });
707
707
  }
708
708
 
709
709
  /**
@@ -713,7 +713,7 @@ function populateTabs() {
713
713
  * @returns {string} The combined string.
714
714
  */
715
715
  function html(strings) {
716
- return strings.join("");
716
+ return strings.join("");
717
717
  }
718
718
 
719
719
  /**
@@ -722,7 +722,7 @@ function html(strings) {
722
722
  * @returns {string} The HTML template string.
723
723
  */
724
724
  function getTemplate() {
725
- return html`<div data-monster-role="control" part="control">
725
+ return html`<div data-monster-role="control" part="control">
726
726
  <nav data-monster-role="navigation" role="navigation" part="nav">
727
727
  <ul id="visible-elements" part="visible-list"></ul>
728
728
  </nav>