@schukai/monster 4.43.3 → 4.43.5
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.
- package/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/source/components/navigation/site-navigation.mjs +562 -367
- package/source/components/navigation/style/site-navigation.pcss +131 -22
- package/source/components/navigation/stylesheet/site-navigation.mjs +7 -14
- package/source/components/style/property.css +1 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright © schukai GmbH and all contributing authors,
|
|
2
|
+
* Copyright © schukai GmbH and all contributing authors, 2025. All rights reserved.
|
|
3
3
|
* Node module: @schukai/monster
|
|
4
4
|
*
|
|
5
5
|
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
|
|
@@ -13,20 +13,22 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
computePosition,
|
|
25
|
+
autoUpdate,
|
|
26
|
+
flip,
|
|
27
|
+
shift,
|
|
28
|
+
offset,
|
|
29
|
+
size,
|
|
29
30
|
} from "@floating-ui/dom";
|
|
31
|
+
import { fireCustomEvent } from "../../dom/events.mjs";
|
|
30
32
|
|
|
31
33
|
export { SiteNavigation };
|
|
32
34
|
|
|
@@ -39,421 +41,614 @@ const hamburgerButtonSymbol = Symbol("hamburgerButton");
|
|
|
39
41
|
const hamburgerNavSymbol = Symbol("hamburgerNav");
|
|
40
42
|
const instanceSymbol = Symbol("instanceSymbol");
|
|
41
43
|
const activeSubmenuHiderSymbol = Symbol("activeSubmenuHider");
|
|
44
|
+
const hideHamburgerMenuSymbol = Symbol("hideHamburgerMenu");
|
|
45
|
+
const hamburgerCloseButtonSymbol = Symbol("hamburgerCloseButton");
|
|
42
46
|
|
|
43
47
|
/**
|
|
44
|
-
*
|
|
48
|
+
* A responsive site navigation that automatically moves menu items into a hamburger menu
|
|
49
|
+
* when there isn't enough available space.
|
|
45
50
|
*
|
|
51
|
+
* @summary An adaptive navigation component that supports hover and click interactions for submenus.
|
|
46
52
|
* @fragments /fragments/components/navigation/site-navigation/
|
|
47
53
|
*
|
|
48
|
-
* @example /examples/components/navigation/site-navigation-simple
|
|
54
|
+
* @example /examples/components/navigation/site-navigation-simple Simple Navigation
|
|
55
|
+
* @example /examples/components/navigation/site-navigation-with-submenus Navigation with Submenus
|
|
56
|
+
* @example /examples/components/navigation/site-navigation-with-mega-menu Navigation with Mega Menu
|
|
49
57
|
*
|
|
50
|
-
* @
|
|
51
|
-
*
|
|
52
|
-
* @
|
|
58
|
+
* @issue https://localhost.alvine.dev:8440/development/issues/closed/336.html
|
|
59
|
+
*
|
|
60
|
+
* @fires monster-layout-change - Fired when the layout of menu items changes. The event detail contains `{visibleItems, hiddenItems}`.
|
|
61
|
+
* @fires monster-hamburger-show - Fired when the hamburger menu is shown. The event detail contains `{button, menu}`.
|
|
62
|
+
* @fires monster-hamburger-hide - Fired when the hamburger menu is hidden. The event detail contains `{button, menu}`.
|
|
63
|
+
* @fires monster-submenu-show - Fired when a submenu is shown. The event detail contains `{context, trigger, submenu, level}`.
|
|
64
|
+
* @fires monster-submenu-hide - Fired when a submenu is hidden. The event detail contains `{context, trigger, submenu, level}`.
|
|
53
65
|
*/
|
|
54
66
|
class SiteNavigation extends CustomElement {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
setTimeout(() => populateTabs.call(this), 0);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Called when the component is disconnected from the DOM.
|
|
111
|
-
* @returns {void}
|
|
112
|
-
*/
|
|
113
|
-
disconnectedCallback() {
|
|
114
|
-
super.disconnectedCallback();
|
|
115
|
-
detachResizeObserver.call(this);
|
|
116
|
-
}
|
|
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
|
+
setTimeout(() => populateTabs.call(this), 0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
disconnectedCallback() {
|
|
115
|
+
super.disconnectedCallback();
|
|
116
|
+
detachResizeObserver.call(this);
|
|
117
|
+
}
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
/**
|
|
120
|
-
*
|
|
121
|
+
* Queries the shadow DOM for essential elements and stores their references.
|
|
121
122
|
* @private
|
|
122
123
|
* @this {SiteNavigation}
|
|
123
|
-
* @returns {void}
|
|
124
124
|
*/
|
|
125
125
|
function initControlReferences() {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
126
|
+
if (!this.shadowRoot) throw new Error("Component requires a shadowRoot.");
|
|
127
|
+
this[navElementSymbol] = this.shadowRoot.querySelector(
|
|
128
|
+
'[data-monster-role="navigation"]',
|
|
129
|
+
);
|
|
130
|
+
this[visibleElementsSymbol] =
|
|
131
|
+
this.shadowRoot.querySelector("#visible-elements");
|
|
132
|
+
this[hiddenElementsSymbol] =
|
|
133
|
+
this.shadowRoot.querySelector("#hidden-elements");
|
|
134
|
+
this[hamburgerButtonSymbol] =
|
|
135
|
+
this.shadowRoot.querySelector("#hamburger-button");
|
|
136
|
+
this[hamburgerNavSymbol] = this.shadowRoot.querySelector(
|
|
137
|
+
'[data-monster-role="hamburger-nav"]',
|
|
138
|
+
);
|
|
139
|
+
this[hamburgerCloseButtonSymbol] = this.shadowRoot.querySelector(
|
|
140
|
+
'[part="hamburger-close-button"]',
|
|
141
|
+
);
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
/**
|
|
142
|
-
* Initializes event handlers for hamburger menu and
|
|
145
|
+
* Initializes event handlers for the hamburger menu button and its functionality.
|
|
143
146
|
* @private
|
|
144
147
|
* @this {SiteNavigation}
|
|
145
|
-
* @returns {void}
|
|
146
148
|
*/
|
|
147
149
|
function initEventHandler() {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
150
|
+
if (!this.shadowRoot) throw new Error("Component requires a shadowRoot.");
|
|
151
|
+
|
|
152
|
+
const hamburgerButton = this[hamburgerButtonSymbol];
|
|
153
|
+
const hamburgerNav = this[hamburgerNavSymbol];
|
|
154
|
+
const hamburgerCloseButton = this[hamburgerCloseButtonSymbol];
|
|
155
|
+
let cleanup;
|
|
156
|
+
|
|
157
|
+
if (!hamburgerButton || !hamburgerNav || !hamburgerCloseButton) return;
|
|
158
|
+
|
|
159
|
+
const handleOutsideClick = (event) => {
|
|
160
|
+
if (
|
|
161
|
+
!hamburgerButton.contains(event.target) &&
|
|
162
|
+
!hamburgerNav.contains(event.target)
|
|
163
|
+
) {
|
|
164
|
+
hideMenu();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const hideMenu = () => {
|
|
169
|
+
hamburgerNav.style.display = "none";
|
|
170
|
+
document.body.classList.remove("monster-navigation-open");
|
|
171
|
+
|
|
172
|
+
fireCustomEvent(this, "monster-hamburger-hide", {
|
|
173
|
+
button: hamburgerButton,
|
|
174
|
+
menu: hamburgerNav,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (this.getOption("features.resetOnClose") === true) {
|
|
178
|
+
this[hiddenElementsSymbol]
|
|
179
|
+
.querySelectorAll(".is-open")
|
|
180
|
+
.forEach((submenu) => submenu.classList.remove("is-open"));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (cleanup) {
|
|
184
|
+
cleanup();
|
|
185
|
+
cleanup = undefined;
|
|
186
|
+
}
|
|
187
|
+
document.removeEventListener("click", handleOutsideClick);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
this[hideHamburgerMenuSymbol] = hideMenu;
|
|
191
|
+
|
|
192
|
+
const showMenu = () => {
|
|
193
|
+
this[activeSubmenuHiderSymbol]?.();
|
|
194
|
+
hamburgerNav.style.display = "block";
|
|
195
|
+
document.body.classList.add("monster-navigation-open");
|
|
196
|
+
|
|
197
|
+
fireCustomEvent(this, "monster-hamburger-show", {
|
|
198
|
+
button: hamburgerButton,
|
|
199
|
+
menu: hamburgerNav,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
cleanup = autoUpdate(hamburgerButton, hamburgerNav, () => {
|
|
203
|
+
// Desktop view where the hamburger menu is a dropdown
|
|
204
|
+
if (window.innerWidth > 768) {
|
|
205
|
+
computePosition(hamburgerButton, hamburgerNav, {
|
|
206
|
+
placement: "bottom-end",
|
|
207
|
+
strategy: "fixed",
|
|
208
|
+
middleware: [
|
|
209
|
+
offset(8),
|
|
210
|
+
flip(),
|
|
211
|
+
shift({ padding: 8 }),
|
|
212
|
+
size({
|
|
213
|
+
apply: ({ availableHeight, elements }) => {
|
|
214
|
+
Object.assign(elements.floating.style, {
|
|
215
|
+
maxHeight: `${availableHeight}px`,
|
|
216
|
+
overflowY: "auto",
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
padding: 8,
|
|
220
|
+
}),
|
|
221
|
+
],
|
|
222
|
+
}).then(({ x, y, strategy }) => {
|
|
223
|
+
Object.assign(hamburgerNav.style, {
|
|
224
|
+
position: strategy,
|
|
225
|
+
left: `${x}px`,
|
|
226
|
+
top: `${y}px`,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
} else {
|
|
230
|
+
// Mobile view (fullscreen overlay), position is handled by CSS
|
|
231
|
+
Object.assign(hamburgerNav.style, { position: "", left: "", top: "" });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
setTimeout(() => document.addEventListener("click", handleOutsideClick), 0);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
hamburgerButton.addEventListener("click", (event) => {
|
|
238
|
+
event.stopPropagation();
|
|
239
|
+
const isVisible = hamburgerNav.style.display === "block";
|
|
240
|
+
if (isVisible) {
|
|
241
|
+
hideMenu();
|
|
242
|
+
} else {
|
|
243
|
+
showMenu();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
hamburgerCloseButton.addEventListener("click", (event) => {
|
|
248
|
+
event.stopPropagation();
|
|
249
|
+
hideMenu();
|
|
250
|
+
});
|
|
198
251
|
}
|
|
199
252
|
|
|
200
253
|
/**
|
|
201
|
-
* Attaches a ResizeObserver to
|
|
254
|
+
* Attaches a ResizeObserver to the main navigation element to recalculate
|
|
255
|
+
* tab distribution on size changes. A DeadMansSwitch is used for debouncing.
|
|
202
256
|
* @private
|
|
203
257
|
* @this {SiteNavigation}
|
|
204
|
-
* @returns {void}
|
|
205
258
|
*/
|
|
206
259
|
function attachResizeObserver() {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
260
|
+
this[resizeObserverSymbol] = new ResizeObserver(() => {
|
|
261
|
+
if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
|
|
262
|
+
try {
|
|
263
|
+
this[timerCallbackSymbol].touch();
|
|
264
|
+
return;
|
|
265
|
+
} catch (e) {
|
|
266
|
+
delete this[timerCallbackSymbol];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
this[timerCallbackSymbol] = new DeadMansSwitch(200, () =>
|
|
270
|
+
populateTabs.call(this),
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
this[resizeObserverSymbol].observe(this[navElementSymbol]);
|
|
221
274
|
}
|
|
222
275
|
|
|
223
276
|
/**
|
|
224
|
-
*
|
|
277
|
+
* Disconnects and cleans up the ResizeObserver instance.
|
|
225
278
|
* @private
|
|
226
279
|
* @this {SiteNavigation}
|
|
227
|
-
* @returns {void}
|
|
228
280
|
*/
|
|
229
281
|
function detachResizeObserver() {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
282
|
+
if (this[resizeObserverSymbol] instanceof ResizeObserver) {
|
|
283
|
+
this[resizeObserverSymbol].disconnect();
|
|
284
|
+
delete this[resizeObserverSymbol];
|
|
285
|
+
}
|
|
234
286
|
}
|
|
235
287
|
|
|
236
288
|
/**
|
|
237
|
-
* Sets up
|
|
289
|
+
* Sets up interaction logic (hover or click) for a submenu.
|
|
290
|
+
* This function is called recursively for nested submenus.
|
|
238
291
|
* @private
|
|
239
292
|
* @this {SiteNavigation}
|
|
240
|
-
* @param {
|
|
241
|
-
* @param {'visible'
|
|
242
|
-
* @
|
|
293
|
+
* @param {HTMLLIElement} parentLi The list item containing the submenu.
|
|
294
|
+
* @param {'visible'|'hidden'} context The context (main nav or hamburger).
|
|
295
|
+
* @param {number} level The nesting level of the submenu (starts at 1).
|
|
296
|
+
*/
|
|
297
|
+
function setupSubmenu(parentLi, context = "visible", level = 1) {
|
|
298
|
+
const submenu = parentLi.querySelector(
|
|
299
|
+
":scope > ul, :scope > div[part='mega-menu']",
|
|
300
|
+
);
|
|
301
|
+
if (!submenu) return;
|
|
302
|
+
|
|
303
|
+
if (submenu.tagName === "UL") {
|
|
304
|
+
submenu.setAttribute("part", "submenu");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const interaction = this.getOption("interactionModel", "auto");
|
|
308
|
+
const useHover =
|
|
309
|
+
interaction === "hover" ||
|
|
310
|
+
(interaction === "auto" && context === "visible");
|
|
311
|
+
|
|
312
|
+
if (useHover) {
|
|
313
|
+
const component = this;
|
|
314
|
+
let cleanup;
|
|
315
|
+
let hideTimeout;
|
|
316
|
+
|
|
317
|
+
const immediateHide = () => {
|
|
318
|
+
submenu.style.display = "none";
|
|
319
|
+
submenu
|
|
320
|
+
.querySelectorAll(
|
|
321
|
+
"ul[style*='display: block'], div[part='mega-menu'][style*='display: block']",
|
|
322
|
+
)
|
|
323
|
+
.forEach((sub) => {
|
|
324
|
+
sub.style.display = "none";
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
fireCustomEvent(this, "monster-submenu-hide", {
|
|
328
|
+
context,
|
|
329
|
+
trigger: parentLi,
|
|
330
|
+
submenu,
|
|
331
|
+
level,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (cleanup) {
|
|
335
|
+
cleanup();
|
|
336
|
+
cleanup = null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (
|
|
340
|
+
level === 1 &&
|
|
341
|
+
component[activeSubmenuHiderSymbol] === immediateHide
|
|
342
|
+
) {
|
|
343
|
+
component[activeSubmenuHiderSymbol] = null;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const show = () => {
|
|
348
|
+
component[hideHamburgerMenuSymbol]?.();
|
|
349
|
+
clearTimeout(hideTimeout);
|
|
350
|
+
|
|
351
|
+
if (level === 1) {
|
|
352
|
+
if (
|
|
353
|
+
component[activeSubmenuHiderSymbol] &&
|
|
354
|
+
component[activeSubmenuHiderSymbol] !== immediateHide
|
|
355
|
+
) {
|
|
356
|
+
component[activeSubmenuHiderSymbol]();
|
|
357
|
+
}
|
|
358
|
+
component[activeSubmenuHiderSymbol] = immediateHide;
|
|
359
|
+
} else {
|
|
360
|
+
[...parentLi.parentElement.children]
|
|
361
|
+
.filter((li) => li !== parentLi)
|
|
362
|
+
.forEach((sibling) => {
|
|
363
|
+
const siblingSubmenu = sibling.querySelector(
|
|
364
|
+
":scope > ul, :scope > div[part='mega-menu']",
|
|
365
|
+
);
|
|
366
|
+
if (siblingSubmenu) {
|
|
367
|
+
siblingSubmenu.style.display = "none";
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
submenu.style.display = "block";
|
|
373
|
+
fireCustomEvent(this, "monster-submenu-show", {
|
|
374
|
+
context,
|
|
375
|
+
trigger: parentLi,
|
|
376
|
+
submenu,
|
|
377
|
+
level,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
if (level === 1 && !cleanup) {
|
|
381
|
+
cleanup = autoUpdate(parentLi, submenu, () => {
|
|
382
|
+
computePosition(parentLi, submenu, {
|
|
383
|
+
placement: "bottom-start",
|
|
384
|
+
strategy: "fixed",
|
|
385
|
+
middleware: [
|
|
386
|
+
offset(8),
|
|
387
|
+
flip({ fallbackPlacements: ["top-start"] }),
|
|
388
|
+
shift({ padding: 8 }),
|
|
389
|
+
size({
|
|
390
|
+
apply: ({ availableHeight, elements }) => {
|
|
391
|
+
Object.assign(elements.floating.style, {
|
|
392
|
+
maxHeight: `${availableHeight}px`,
|
|
393
|
+
overflowY: "auto",
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
padding: 8,
|
|
397
|
+
}),
|
|
398
|
+
],
|
|
399
|
+
}).then(({ x, y, strategy }) => {
|
|
400
|
+
Object.assign(submenu.style, {
|
|
401
|
+
position: strategy,
|
|
402
|
+
left: `${x}px`,
|
|
403
|
+
top: `${y}px`,
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const hideWithDelay = () => {
|
|
411
|
+
hideTimeout = setTimeout(immediateHide, 200);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
parentLi.addEventListener("mouseenter", show);
|
|
415
|
+
parentLi.addEventListener("mouseleave", hideWithDelay);
|
|
416
|
+
submenu.addEventListener("mouseenter", () => clearTimeout(hideTimeout));
|
|
417
|
+
submenu.addEventListener("mouseleave", hideWithDelay);
|
|
418
|
+
} else {
|
|
419
|
+
// Click behavior
|
|
420
|
+
const anchor = parentLi.querySelector(":scope > a");
|
|
421
|
+
if (anchor) {
|
|
422
|
+
anchor.addEventListener("click", (event) => {
|
|
423
|
+
event.preventDefault();
|
|
424
|
+
event.stopPropagation();
|
|
425
|
+
|
|
426
|
+
if (!submenu.classList.contains("is-open")) {
|
|
427
|
+
[...parentLi.parentElement.children]
|
|
428
|
+
.filter((li) => li !== parentLi)
|
|
429
|
+
.forEach((sibling) => {
|
|
430
|
+
const siblingSubmenu = sibling.querySelector(
|
|
431
|
+
":scope > ul, :scope > div[part='mega-menu']",
|
|
432
|
+
);
|
|
433
|
+
if (siblingSubmenu) {
|
|
434
|
+
siblingSubmenu.classList.remove("is-open");
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const isOpen = submenu.classList.toggle("is-open");
|
|
440
|
+
const eventName = isOpen
|
|
441
|
+
? "monster-submenu-show"
|
|
442
|
+
: "monster-submenu-hide";
|
|
443
|
+
fireCustomEvent(this, eventName, {
|
|
444
|
+
context,
|
|
445
|
+
trigger: parentLi,
|
|
446
|
+
submenu,
|
|
447
|
+
level,
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Recursive call for nested lists
|
|
454
|
+
if (submenu.tagName === "UL") {
|
|
455
|
+
submenu
|
|
456
|
+
.querySelectorAll(":scope > li")
|
|
457
|
+
.forEach((li) => setupSubmenu.call(this, li, context, level + 1));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Creates a clone of a navigation list item, setting appropriate part attributes
|
|
463
|
+
* for styling and handling active states.
|
|
464
|
+
* @private
|
|
465
|
+
* @param {HTMLLIElement} item The original list item to clone.
|
|
466
|
+
* @returns {HTMLLIElement} The cloned and configured list item.
|
|
243
467
|
*/
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
cleanup = null;
|
|
260
|
-
}
|
|
261
|
-
if (component[activeSubmenuHiderSymbol] === immediateHide) {
|
|
262
|
-
component[activeSubmenuHiderSymbol] = null;
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
const show = () => {
|
|
267
|
-
clearTimeout(hideTimeout);
|
|
268
|
-
if (
|
|
269
|
-
component[activeSubmenuHiderSymbol] &&
|
|
270
|
-
component[activeSubmenuHiderSymbol] !== immediateHide
|
|
271
|
-
) {
|
|
272
|
-
component[activeSubmenuHiderSymbol]();
|
|
273
|
-
}
|
|
274
|
-
submenu.style.display = "block";
|
|
275
|
-
if (!cleanup) {
|
|
276
|
-
cleanup = autoUpdate(parentLi, submenu, () => {
|
|
277
|
-
computePosition(parentLi, submenu, {
|
|
278
|
-
placement: "bottom-start",
|
|
279
|
-
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
280
|
-
}).then(({ x, y }) => {
|
|
281
|
-
Object.assign(submenu.style, { left: `${x}px`, top: `${y}px` });
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
component[activeSubmenuHiderSymbol] = immediateHide;
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
const hideWithDelay = () => {
|
|
289
|
-
hideTimeout = setTimeout(immediateHide, 200);
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
parentLi.addEventListener("mouseenter", show);
|
|
293
|
-
parentLi.addEventListener("mouseleave", hideWithDelay);
|
|
294
|
-
submenu.addEventListener("mouseenter", () => clearTimeout(hideTimeout));
|
|
295
|
-
submenu.addEventListener("mouseleave", hideWithDelay);
|
|
296
|
-
} else {
|
|
297
|
-
submenu.style.display = "none";
|
|
298
|
-
const anchor = parentLi.querySelector("a");
|
|
299
|
-
if (anchor) {
|
|
300
|
-
anchor.addEventListener("click", (event) => {
|
|
301
|
-
event.preventDefault();
|
|
302
|
-
event.stopPropagation();
|
|
303
|
-
const isVisible = submenu.style.display === "block";
|
|
304
|
-
submenu.style.display = isVisible ? "none" : "block";
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
468
|
+
function cloneNavItem(item) {
|
|
469
|
+
const liClone = item.cloneNode(true);
|
|
470
|
+
const aClone = liClone.querySelector("a");
|
|
471
|
+
let navItemPart = "nav-item";
|
|
472
|
+
let navLinkPart = "nav-link";
|
|
473
|
+
|
|
474
|
+
if (item.classList.contains("active")) {
|
|
475
|
+
navItemPart += " nav-item-active";
|
|
476
|
+
if (aClone) navLinkPart += " nav-link-active";
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
liClone.setAttribute("part", navItemPart);
|
|
480
|
+
if (aClone) aClone.setAttribute("part", navLinkPart);
|
|
481
|
+
|
|
482
|
+
return liClone;
|
|
308
483
|
}
|
|
309
484
|
|
|
310
485
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
486
|
+
* Measures available space and distributes slotted navigation items between
|
|
487
|
+
* the visible list and the hidden hamburger menu list.
|
|
313
488
|
* @private
|
|
314
489
|
* @this {SiteNavigation}
|
|
315
|
-
* @returns {void}
|
|
316
490
|
*/
|
|
317
491
|
function populateTabs() {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (item.classList.contains("active")) {
|
|
419
|
-
liClone.setAttribute("part", "nav-item nav-item-active");
|
|
420
|
-
if (aClone) aClone.setAttribute("part", "nav-link nav-link-active");
|
|
421
|
-
}
|
|
422
|
-
return liClone;
|
|
423
|
-
});
|
|
424
|
-
hiddenList.append(...clonedHiddenItems);
|
|
425
|
-
hamburgerButton.style.display = "flex";
|
|
426
|
-
hiddenList
|
|
427
|
-
.querySelectorAll("li")
|
|
428
|
-
.forEach((li) => setupSubmenu.call(this, li, "hidden"));
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
visibleList.style.visibility = "visible";
|
|
492
|
+
const visibleList = this[visibleElementsSymbol];
|
|
493
|
+
const hiddenList = this[hiddenElementsSymbol];
|
|
494
|
+
const hamburgerButton = this[hamburgerButtonSymbol];
|
|
495
|
+
const hamburgerNav = this[hamburgerNavSymbol];
|
|
496
|
+
const topLevelUl = [...getSlottedElements.call(this, "ul")].find(
|
|
497
|
+
(ul) => ul.parentElement === this,
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
if (!topLevelUl) {
|
|
501
|
+
visibleList.style.visibility = "visible";
|
|
502
|
+
hamburgerButton.style.display = "none";
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const sourceItems = Array.from(topLevelUl.children).filter(
|
|
507
|
+
(item) => item.tagName === "LI",
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
visibleList.style.visibility = "hidden";
|
|
511
|
+
hamburgerButton.style.display = "none";
|
|
512
|
+
hamburgerNav.style.display = "none";
|
|
513
|
+
visibleList.innerHTML = "";
|
|
514
|
+
hiddenList.innerHTML = "";
|
|
515
|
+
|
|
516
|
+
const originalDisplay = hamburgerButton.style.display;
|
|
517
|
+
hamburgerButton.style.visibility = "hidden";
|
|
518
|
+
hamburgerButton.style.display = "flex";
|
|
519
|
+
const hamburgerWidth = hamburgerButton.offsetWidth;
|
|
520
|
+
hamburgerButton.style.display = originalDisplay;
|
|
521
|
+
hamburgerButton.style.visibility = "visible";
|
|
522
|
+
|
|
523
|
+
const navWidth = this[navElementSymbol].clientWidth;
|
|
524
|
+
if (navWidth === 0) {
|
|
525
|
+
visibleList.style.visibility = "visible";
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const measurementList = visibleList.cloneNode(true);
|
|
530
|
+
Object.assign(measurementList.style, {
|
|
531
|
+
position: "absolute",
|
|
532
|
+
left: "0",
|
|
533
|
+
top: "0",
|
|
534
|
+
width: "auto",
|
|
535
|
+
visibility: "hidden",
|
|
536
|
+
});
|
|
537
|
+
this.shadowRoot.appendChild(measurementList);
|
|
538
|
+
|
|
539
|
+
const itemsToMove = [];
|
|
540
|
+
const availableWidthForTabs = navWidth - hamburgerWidth;
|
|
541
|
+
|
|
542
|
+
for (let i = 0; i < sourceItems.length; i++) {
|
|
543
|
+
const item = sourceItems[i];
|
|
544
|
+
const itemClone = item.cloneNode(true);
|
|
545
|
+
const submenu = itemClone.querySelector("ul, div[part='mega-menu']");
|
|
546
|
+
if (submenu) submenu.style.display = "none";
|
|
547
|
+
|
|
548
|
+
measurementList.appendChild(itemClone);
|
|
549
|
+
const isLastItem = i === sourceItems.length - 1;
|
|
550
|
+
const effectiveMaxWidth = isLastItem ? navWidth : availableWidthForTabs;
|
|
551
|
+
|
|
552
|
+
if (measurementList.scrollWidth > effectiveMaxWidth) {
|
|
553
|
+
break;
|
|
554
|
+
} else {
|
|
555
|
+
itemsToMove.push(item);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
this.shadowRoot.removeChild(measurementList);
|
|
559
|
+
|
|
560
|
+
const visibleItems = itemsToMove;
|
|
561
|
+
const hiddenItems = sourceItems.slice(visibleItems.length);
|
|
562
|
+
|
|
563
|
+
if (visibleItems.length > 0) {
|
|
564
|
+
const clonedVisibleItems = visibleItems.map(cloneNavItem);
|
|
565
|
+
visibleList.append(...clonedVisibleItems);
|
|
566
|
+
visibleList
|
|
567
|
+
.querySelectorAll(":scope > li")
|
|
568
|
+
.forEach((li) => setupSubmenu.call(this, li, "visible", 1));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (hiddenItems.length > 0) {
|
|
572
|
+
const clonedHiddenItems = hiddenItems.map(cloneNavItem);
|
|
573
|
+
hiddenList.append(...clonedHiddenItems);
|
|
574
|
+
hamburgerButton.style.display = "flex";
|
|
575
|
+
hiddenList
|
|
576
|
+
.querySelectorAll(":scope > li")
|
|
577
|
+
.forEach((li) => setupSubmenu.call(this, li, "hidden", 1));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
visibleList.style.visibility = "visible";
|
|
581
|
+
fireCustomEvent(this, "monster-layout-change", { visibleItems, hiddenItems });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* A simple template literal tag function for clarity.
|
|
586
|
+
* @private
|
|
587
|
+
* @param {TemplateStringsArray} strings
|
|
588
|
+
* @returns {string} The combined string.
|
|
589
|
+
*/
|
|
590
|
+
function html(strings) {
|
|
591
|
+
return strings.join("");
|
|
432
592
|
}
|
|
433
593
|
|
|
434
594
|
/**
|
|
435
595
|
* Returns the HTML template for the component's shadow DOM.
|
|
436
596
|
* @private
|
|
437
|
-
* @returns {string}
|
|
597
|
+
* @returns {string} The HTML template string.
|
|
438
598
|
*/
|
|
439
599
|
function getTemplate() {
|
|
440
|
-
|
|
441
|
-
<
|
|
442
|
-
<
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
<
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
600
|
+
return html`<div data-monster-role="control" part="control">
|
|
601
|
+
<nav data-monster-role="navigation" role="navigation" part="nav">
|
|
602
|
+
<ul id="visible-elements" part="visible-list"></ul>
|
|
603
|
+
</nav>
|
|
604
|
+
<div data-monster-role="hamburger-container" part="hamburger-container">
|
|
605
|
+
<button
|
|
606
|
+
id="hamburger-button"
|
|
607
|
+
part="hamburger-button"
|
|
608
|
+
aria-label="More navigation items"
|
|
609
|
+
>
|
|
610
|
+
<svg
|
|
611
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
612
|
+
width="16"
|
|
613
|
+
height="16"
|
|
614
|
+
fill="currentColor"
|
|
615
|
+
viewBox="0 0 16 16"
|
|
616
|
+
>
|
|
617
|
+
<path
|
|
618
|
+
fill-rule="evenodd"
|
|
619
|
+
d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5"
|
|
620
|
+
/>
|
|
621
|
+
</svg>
|
|
622
|
+
</button>
|
|
623
|
+
<nav
|
|
624
|
+
data-monster-role="hamburger-nav"
|
|
625
|
+
role="navigation"
|
|
626
|
+
part="hamburger-nav"
|
|
627
|
+
style="display: none;"
|
|
628
|
+
>
|
|
629
|
+
<div part="hamburger-header">
|
|
630
|
+
<button part="hamburger-close-button" aria-label="Close navigation">
|
|
631
|
+
<svg
|
|
632
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
633
|
+
width="24"
|
|
634
|
+
height="24"
|
|
635
|
+
viewBox="0 0 24 24"
|
|
636
|
+
fill="none"
|
|
637
|
+
stroke="currentColor"
|
|
638
|
+
stroke-width="2"
|
|
639
|
+
stroke-linecap="round"
|
|
640
|
+
stroke-linejoin="round"
|
|
641
|
+
>
|
|
642
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
643
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
449
644
|
</svg>
|
|
450
645
|
</button>
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
646
|
+
</div>
|
|
647
|
+
<ul id="hidden-elements" part="hidden-list"></ul>
|
|
648
|
+
</nav>
|
|
649
|
+
</div>
|
|
650
|
+
<slot class="hidden-slot" style="display: none;"></slot>
|
|
651
|
+
</div>`;
|
|
457
652
|
}
|
|
458
653
|
|
|
459
654
|
registerCustomElement(SiteNavigation);
|