@ecl/mega-menu 5.0.0-RC1
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/LICENSE +296 -0
- package/README.md +109 -0
- package/mega-menu-featured-item.html.twig +109 -0
- package/mega-menu-item.html.twig +387 -0
- package/mega-menu-print.scss +8 -0
- package/mega-menu.html.twig +189 -0
- package/mega-menu.js +1768 -0
- package/mega-menu.scss +1755 -0
- package/package.json +36 -0
package/mega-menu.js
ADDED
|
@@ -0,0 +1,1768 @@
|
|
|
1
|
+
import { queryOne, queryAll } from '@ecl/dom-utils';
|
|
2
|
+
import EventManager from '@ecl/event-manager';
|
|
3
|
+
import { createFocusTrap } from 'focus-trap';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {HTMLElement} element DOM element for component instantiation and scope
|
|
7
|
+
* @param {Object} options
|
|
8
|
+
* @param {String} options.openSelector Selector for the hamburger button
|
|
9
|
+
* @param {String} options.backSelector Selector for the back button
|
|
10
|
+
* @param {String} options.innerSelector Selector for the menu inner
|
|
11
|
+
* @param {String} options.itemSelector Selector for the menu item
|
|
12
|
+
* @param {String} options.linkSelector Selector for the menu link
|
|
13
|
+
* @param {String} options.subLinkSelector Selector for the menu sub link
|
|
14
|
+
* @param {String} options.megaSelector Selector for the mega menu
|
|
15
|
+
* @param {String} options.subItemSelector Selector for the menu sub items
|
|
16
|
+
* @param {String} options.labelOpenAttribute The data attribute for open label
|
|
17
|
+
* @param {String} options.labelCloseAttribute The data attribute for close label
|
|
18
|
+
* @param {Boolean} options.attachClickListener Whether or not to bind click events
|
|
19
|
+
* @param {Boolean} options.attachHoverListener Whether or not to bind hover events
|
|
20
|
+
* @param {Boolean} options.attachFocusListener Whether or not to bind focus events
|
|
21
|
+
* @param {Boolean} options.attachKeyListener Whether or not to bind keyboard events
|
|
22
|
+
* @param {Boolean} options.attachResizeListener Whether or not to bind resize events
|
|
23
|
+
*/
|
|
24
|
+
export class MegaMenu {
|
|
25
|
+
/**
|
|
26
|
+
* @static
|
|
27
|
+
* Shorthand for instance creation and initialisation.
|
|
28
|
+
*
|
|
29
|
+
* @param {HTMLElement} root DOM element for component instantiation and scope
|
|
30
|
+
*
|
|
31
|
+
* @return {Menu} An instance of Menu.
|
|
32
|
+
*/
|
|
33
|
+
static autoInit(root, { MEGA_MENU: defaultOptions = {} } = {}) {
|
|
34
|
+
const megaMenu = new MegaMenu(root, defaultOptions);
|
|
35
|
+
megaMenu.init();
|
|
36
|
+
root.ECLMegaMenu = megaMenu;
|
|
37
|
+
return megaMenu;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @event MegaMenu#onOpen
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* @event MegaMenu#onClose
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* @event MegaMenu#onOpenPanel
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* @event MegaMenu#onBack
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* @event MegaMenu#onItemClick
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* @event MegaMenu#onFocusTrapToggle
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* An array of supported events for this component.
|
|
61
|
+
*
|
|
62
|
+
* @type {Array<string>}
|
|
63
|
+
* @memberof MegaMenu
|
|
64
|
+
*/
|
|
65
|
+
supportedEvents = ['onOpen', 'onClose'];
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
element,
|
|
69
|
+
{
|
|
70
|
+
openSelector = '[data-ecl-mega-menu-open]',
|
|
71
|
+
backSelector = '[data-ecl-mega-menu-back]',
|
|
72
|
+
innerSelector = '[data-ecl-mega-menu-inner]',
|
|
73
|
+
itemSelector = '[data-ecl-mega-menu-item]',
|
|
74
|
+
linkSelector = '[data-ecl-mega-menu-link]',
|
|
75
|
+
subLinkSelector = '[data-ecl-mega-menu-sublink]',
|
|
76
|
+
megaSelector = '[data-ecl-mega-menu-mega]',
|
|
77
|
+
containerSelector = '[data-ecl-has-container]',
|
|
78
|
+
subItemSelector = '[data-ecl-mega-menu-subitem]',
|
|
79
|
+
featuredAttribute = '[data-ecl-mega-menu-featured]',
|
|
80
|
+
featuredLinkAttribute = '[data-ecl-mega-menu-featured-link]',
|
|
81
|
+
labelOpenAttribute = 'data-ecl-mega-menu-label-open',
|
|
82
|
+
labelCloseAttribute = 'data-ecl-mega-menu-label-close',
|
|
83
|
+
attachClickListener = true,
|
|
84
|
+
attachFocusListener = true,
|
|
85
|
+
attachKeyListener = true,
|
|
86
|
+
attachResizeListener = true,
|
|
87
|
+
} = {},
|
|
88
|
+
) {
|
|
89
|
+
// Check element
|
|
90
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
91
|
+
throw new TypeError(
|
|
92
|
+
'DOM element should be given to initialize this widget.',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.element = element;
|
|
97
|
+
this.eventManager = new EventManager();
|
|
98
|
+
|
|
99
|
+
// Options
|
|
100
|
+
this.openSelector = openSelector;
|
|
101
|
+
this.backSelector = backSelector;
|
|
102
|
+
this.innerSelector = innerSelector;
|
|
103
|
+
this.itemSelector = itemSelector;
|
|
104
|
+
this.linkSelector = linkSelector;
|
|
105
|
+
this.subLinkSelector = subLinkSelector;
|
|
106
|
+
this.megaSelector = megaSelector;
|
|
107
|
+
this.subItemSelector = subItemSelector;
|
|
108
|
+
this.containerSelector = containerSelector;
|
|
109
|
+
this.labelOpenAttribute = labelOpenAttribute;
|
|
110
|
+
this.labelCloseAttribute = labelCloseAttribute;
|
|
111
|
+
this.attachClickListener = attachClickListener;
|
|
112
|
+
this.attachFocusListener = attachFocusListener;
|
|
113
|
+
this.attachKeyListener = attachKeyListener;
|
|
114
|
+
this.attachResizeListener = attachResizeListener;
|
|
115
|
+
this.featuredAttribute = featuredAttribute;
|
|
116
|
+
this.featuredLinkAttribute = featuredLinkAttribute;
|
|
117
|
+
|
|
118
|
+
// Private variables
|
|
119
|
+
this.direction = 'ltr';
|
|
120
|
+
this.open = null;
|
|
121
|
+
this.toggleLabel = null;
|
|
122
|
+
this.back = null;
|
|
123
|
+
this.backItemLevel1 = null;
|
|
124
|
+
this.backItemLevel2 = null;
|
|
125
|
+
this.inner = null;
|
|
126
|
+
this.items = null;
|
|
127
|
+
this.links = null;
|
|
128
|
+
this.isOpen = false;
|
|
129
|
+
this.resizeTimer = null;
|
|
130
|
+
this.wrappers = null;
|
|
131
|
+
this.isKeyEvent = false;
|
|
132
|
+
this.isDesktop = false;
|
|
133
|
+
this.isLarge = false;
|
|
134
|
+
this.lastVisibleItem = null;
|
|
135
|
+
this.menuOverlay = null;
|
|
136
|
+
this.currentItem = null;
|
|
137
|
+
this.totalItemsWidth = 0;
|
|
138
|
+
this.breakpointDesktop = 1140;
|
|
139
|
+
this.breakpointLarge = 1368;
|
|
140
|
+
this.openPanel = { num: 0, item: {} };
|
|
141
|
+
this.infoLinks = null;
|
|
142
|
+
this.seeAllLinks = null;
|
|
143
|
+
this.featuredLinks = null;
|
|
144
|
+
|
|
145
|
+
// Bind `this` for use in callbacks
|
|
146
|
+
this.handleClickOnOpen = this.handleClickOnOpen.bind(this);
|
|
147
|
+
this.handleClickOnClose = this.handleClickOnClose.bind(this);
|
|
148
|
+
this.handleClickOnToggle = this.handleClickOnToggle.bind(this);
|
|
149
|
+
this.handleClickOnBack = this.handleClickOnBack.bind(this);
|
|
150
|
+
this.handleClickGlobal = this.handleClickGlobal.bind(this);
|
|
151
|
+
this.handleClickOnItem = this.handleClickOnItem.bind(this);
|
|
152
|
+
this.handleClickOnSubitem = this.handleClickOnSubitem.bind(this);
|
|
153
|
+
this.handleFocusOut = this.handleFocusOut.bind(this);
|
|
154
|
+
this.handleKeyboard = this.handleKeyboard.bind(this);
|
|
155
|
+
this.handleKeyboardGlobal = this.handleKeyboardGlobal.bind(this);
|
|
156
|
+
this.handleResize = this.handleResize.bind(this);
|
|
157
|
+
this.closeOpenDropdown = this.closeOpenDropdown.bind(this);
|
|
158
|
+
this.checkDropdownHeight = this.checkDropdownHeight.bind(this);
|
|
159
|
+
this.positionMenuOverlay = this.positionMenuOverlay.bind(this);
|
|
160
|
+
this.resetStyles = this.resetStyles.bind(this);
|
|
161
|
+
this.handleFirstPanel = this.handleFirstPanel.bind(this);
|
|
162
|
+
this.handleSecondPanel = this.handleSecondPanel.bind(this);
|
|
163
|
+
this.disableScroll = this.disableScroll.bind(this);
|
|
164
|
+
this.enableScroll = this.enableScroll.bind(this);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Initialise component.
|
|
169
|
+
*/
|
|
170
|
+
init() {
|
|
171
|
+
if (!ECL) {
|
|
172
|
+
throw new TypeError('Called init but ECL is not present');
|
|
173
|
+
}
|
|
174
|
+
ECL.components = ECL.components || new Map();
|
|
175
|
+
|
|
176
|
+
// Query elements
|
|
177
|
+
this.open = queryOne(this.openSelector, this.element);
|
|
178
|
+
this.back = queryOne(this.backSelector, this.element);
|
|
179
|
+
this.inner = queryOne(this.innerSelector, this.element);
|
|
180
|
+
this.btnPrevious = queryOne(this.buttonPreviousSelector, this.element);
|
|
181
|
+
this.btnNext = queryOne(this.buttonNextSelector, this.element);
|
|
182
|
+
this.items = queryAll(this.itemSelector, this.element);
|
|
183
|
+
this.subItems = queryAll(this.subItemSelector, this.element);
|
|
184
|
+
this.links = queryAll(this.linkSelector, this.element);
|
|
185
|
+
this.header = queryOne('.ecl-site-header', document);
|
|
186
|
+
this.headerBanner = queryOne('.ecl-site-header__banner', document);
|
|
187
|
+
this.wrappers = queryAll('.ecl-mega-menu__wrapper', this.element);
|
|
188
|
+
this.headerNotification = queryOne(
|
|
189
|
+
'.ecl-site-header__notification',
|
|
190
|
+
document,
|
|
191
|
+
);
|
|
192
|
+
this.toggleLabel = queryOne('.ecl-button__label', this.open);
|
|
193
|
+
this.menuOverlay = queryOne('.ecl-mega-menu__overlay', this.element);
|
|
194
|
+
|
|
195
|
+
// Check if we should use desktop display
|
|
196
|
+
this.isDesktop = window.innerWidth >= this.breakpointDesktop;
|
|
197
|
+
|
|
198
|
+
// Bind click events on buttons
|
|
199
|
+
if (this.attachClickListener) {
|
|
200
|
+
// Open
|
|
201
|
+
if (this.open) {
|
|
202
|
+
this.open.addEventListener('click', this.handleClickOnToggle);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Back
|
|
206
|
+
if (this.back) {
|
|
207
|
+
this.back.addEventListener('click', this.handleClickOnBack);
|
|
208
|
+
this.back.addEventListener('keyup', this.handleKeyboard);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Global click
|
|
212
|
+
if (this.attachClickListener) {
|
|
213
|
+
document.addEventListener('click', this.handleClickGlobal);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Bind event on menu links
|
|
218
|
+
if (this.links) {
|
|
219
|
+
this.links.forEach((link) => {
|
|
220
|
+
if (this.attachFocusListener) {
|
|
221
|
+
link.addEventListener('focusout', this.handleFocusOut);
|
|
222
|
+
}
|
|
223
|
+
if (this.attachKeyListener) {
|
|
224
|
+
link.addEventListener('keyup', this.handleKeyboard);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Bind event on sub menu links
|
|
230
|
+
if (this.subItems) {
|
|
231
|
+
this.subItems.forEach((subItem) => {
|
|
232
|
+
const subLink = queryOne('.ecl-mega-menu__sublink', subItem);
|
|
233
|
+
|
|
234
|
+
if (this.attachKeyListener && subLink) {
|
|
235
|
+
subLink.addEventListener('click', this.handleClickOnSubitem);
|
|
236
|
+
subLink.addEventListener('keyup', this.handleKeyboard);
|
|
237
|
+
}
|
|
238
|
+
if (this.attachFocusListener && subLink) {
|
|
239
|
+
subLink.addEventListener('focusout', this.handleFocusOut);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.infoLinks = queryAll('.ecl-mega-menu__info-link a', this.element);
|
|
245
|
+
if (this.infoLinks.length > 0) {
|
|
246
|
+
this.infoLinks.forEach((infoLink) => {
|
|
247
|
+
if (this.attachKeyListener) {
|
|
248
|
+
infoLink.addEventListener('keyup', this.handleKeyboard);
|
|
249
|
+
}
|
|
250
|
+
if (this.attachFocusListener) {
|
|
251
|
+
infoLink.addEventListener('blur', this.handleFocusOut);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.seeAllLinks = queryAll('.ecl-mega-menu__see-all a', this.element);
|
|
257
|
+
if (this.seeAllLinks.length > 0) {
|
|
258
|
+
this.seeAllLinks.forEach((seeAll) => {
|
|
259
|
+
if (this.attachKeyListener) {
|
|
260
|
+
seeAll.addEventListener('keyup', this.handleKeyboard);
|
|
261
|
+
}
|
|
262
|
+
if (this.attachFocusListener) {
|
|
263
|
+
seeAll.addEventListener('blur', this.handleFocusOut);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.featuredLinks = queryAll(this.featuredLinkAttribute, this.element);
|
|
269
|
+
if (this.featuredLinks.length > 0 && this.attachFocusListener) {
|
|
270
|
+
this.featuredLinks.forEach((featured) => {
|
|
271
|
+
featured.addEventListener('blur', this.handleFocusOut);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Bind global keyboard events
|
|
276
|
+
if (this.attachKeyListener) {
|
|
277
|
+
document.addEventListener('keyup', this.handleKeyboardGlobal);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Bind resize events
|
|
281
|
+
if (this.attachResizeListener) {
|
|
282
|
+
window.addEventListener('resize', this.handleResize);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Browse first level items
|
|
286
|
+
if (this.items) {
|
|
287
|
+
this.items.forEach((item) => {
|
|
288
|
+
// Check menu item display (right to left, full width, ...)
|
|
289
|
+
this.totalItemsWidth += item.offsetWidth;
|
|
290
|
+
|
|
291
|
+
if (
|
|
292
|
+
item.hasAttribute('data-ecl-has-children') ||
|
|
293
|
+
item.hasAttribute('data-ecl-has-container')
|
|
294
|
+
) {
|
|
295
|
+
// Bind click event on menu links
|
|
296
|
+
const link = queryOne(this.linkSelector, item);
|
|
297
|
+
if (this.attachClickListener && link) {
|
|
298
|
+
link.addEventListener('click', this.handleClickOnItem);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Create a focus trap around the menu
|
|
305
|
+
this.focusTrap = createFocusTrap(this.element, {
|
|
306
|
+
onActivate: () =>
|
|
307
|
+
this.element.classList.add('ecl-mega-menu-trap-is-active'),
|
|
308
|
+
onDeactivate: () =>
|
|
309
|
+
this.element.classList.remove('ecl-mega-menu-trap-is-active'),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
this.handleResize();
|
|
313
|
+
// Set ecl initialized attribute
|
|
314
|
+
this.element.setAttribute('data-ecl-auto-initialized', 'true');
|
|
315
|
+
ECL.components.set(this.element, this);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Register a callback function for a specific event.
|
|
320
|
+
*
|
|
321
|
+
* @param {string} eventName - The name of the event to listen for.
|
|
322
|
+
* @param {Function} callback - The callback function to be invoked when the event occurs.
|
|
323
|
+
* @returns {void}
|
|
324
|
+
* @memberof MegaMenu
|
|
325
|
+
* @instance
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* // Registering a callback for the 'onOpen' event
|
|
329
|
+
* megaMenu.on('onOpen', (event) => {
|
|
330
|
+
* console.log('Open event occurred!', event);
|
|
331
|
+
* });
|
|
332
|
+
*/
|
|
333
|
+
on(eventName, callback) {
|
|
334
|
+
this.eventManager.on(eventName, callback);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Trigger a component event.
|
|
339
|
+
*
|
|
340
|
+
* @param {string} eventName - The name of the event to trigger.
|
|
341
|
+
* @param {any} eventData - Data associated with the event.
|
|
342
|
+
* @memberof MegaMenu
|
|
343
|
+
*/
|
|
344
|
+
trigger(eventName, eventData) {
|
|
345
|
+
this.eventManager.trigger(eventName, eventData);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Destroy component.
|
|
350
|
+
*/
|
|
351
|
+
destroy() {
|
|
352
|
+
if (this.attachClickListener) {
|
|
353
|
+
if (this.open) {
|
|
354
|
+
this.open.removeEventListener('click', this.handleClickOnToggle);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (this.back) {
|
|
358
|
+
this.back.removeEventListener('click', this.handleClickOnBack);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
document.removeEventListener('click', this.handleClickGlobal);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (this.links) {
|
|
365
|
+
this.links.forEach((link) => {
|
|
366
|
+
if (this.attachClickListener) {
|
|
367
|
+
link.removeEventListener('click', this.handleClickOnItem);
|
|
368
|
+
}
|
|
369
|
+
if (this.attachFocusListener) {
|
|
370
|
+
link.removeEventListener('focusout', this.handleFocusOut);
|
|
371
|
+
}
|
|
372
|
+
if (this.attachKeyListener) {
|
|
373
|
+
link.removeEventListener('keyup', this.handleKeyboard);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (this.subItems) {
|
|
379
|
+
this.subItems.forEach((subItem) => {
|
|
380
|
+
const subLink = queryOne('.ecl-mega-menu__sublink', subItem);
|
|
381
|
+
if (this.attachKeyListener && subLink) {
|
|
382
|
+
subLink.removeEventListener('keyup', this.handleKeyboard);
|
|
383
|
+
}
|
|
384
|
+
if (this.attachClickListener && subLink) {
|
|
385
|
+
subLink.removeEventListener('click', this.handleClickOnSubitem);
|
|
386
|
+
}
|
|
387
|
+
if (this.attachFocusListener && subLink) {
|
|
388
|
+
subLink.removeEventListener('focusout', this.handleFocusOut);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (this.infoLinks) {
|
|
394
|
+
this.infoLinks.forEach((infoLink) => {
|
|
395
|
+
if (this.attachFocusListener) {
|
|
396
|
+
infoLink.removeEventListener('blur', this.handleFocusOut);
|
|
397
|
+
}
|
|
398
|
+
if (this.attachKeyListener) {
|
|
399
|
+
infoLink.removeEventListener('keyup', this.handleKeyboard);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (this.seeAllLinks) {
|
|
405
|
+
this.seeAllLinks.forEach((seeAll) => {
|
|
406
|
+
if (this.attachFocusListener) {
|
|
407
|
+
seeAll.removeEventListener('blur', this.handleFocusOut);
|
|
408
|
+
}
|
|
409
|
+
if (this.attachKeyListener) {
|
|
410
|
+
seeAll.removeEventListener('keyup', this.handleKeyboard);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (this.featuredLinks && this.attachFocusListener) {
|
|
416
|
+
this.featuredLinks.forEach((featuredLink) => {
|
|
417
|
+
featuredLink.removeEventListener('blur', this.handleFocusOut);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (this.attachKeyListener) {
|
|
422
|
+
document.removeEventListener('keyup', this.handleKeyboardGlobal);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (this.attachResizeListener) {
|
|
426
|
+
window.removeEventListener('resize', this.handleResize);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this.closeOpenDropdown();
|
|
430
|
+
this.enableScroll();
|
|
431
|
+
|
|
432
|
+
if (this.element) {
|
|
433
|
+
this.element.removeAttribute('data-ecl-auto-initialized');
|
|
434
|
+
ECL.components.delete(this.element);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Disable page scrolling
|
|
440
|
+
*/
|
|
441
|
+
disableScroll() {
|
|
442
|
+
document.body.classList.add('ecl-mega-menu-prevent-scroll');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Enable page scrolling
|
|
447
|
+
*/
|
|
448
|
+
enableScroll() {
|
|
449
|
+
document.body.classList.remove('ecl-mega-menu-prevent-scroll');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Reset the styles set by the script
|
|
454
|
+
*
|
|
455
|
+
* @param {string} desktop or mobile
|
|
456
|
+
*/
|
|
457
|
+
resetStyles(viewport, compact) {
|
|
458
|
+
const infoPanels = queryAll('.ecl-mega-menu__info', this.element);
|
|
459
|
+
const subLists = queryAll('.ecl-mega-menu__sublist', this.element);
|
|
460
|
+
|
|
461
|
+
// Remove display:none from the sublists
|
|
462
|
+
if (subLists && viewport === 'mobile') {
|
|
463
|
+
const megaMenus = queryAll('[data-ecl-mega-menu-mega]', this.element);
|
|
464
|
+
const featuredPanels = queryAll(
|
|
465
|
+
'[data-ecl-mega-menu-featured]',
|
|
466
|
+
this.element,
|
|
467
|
+
);
|
|
468
|
+
if (featuredPanels.length) {
|
|
469
|
+
megaMenus.push(...featuredPanels);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
megaMenus.forEach((menu) => {
|
|
473
|
+
menu.style.height = '';
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
if (subLists.length) {
|
|
477
|
+
subLists.forEach((list) => {
|
|
478
|
+
list.style.height = '';
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Reset top position and height of the wrappers
|
|
483
|
+
if (this.wrappers) {
|
|
484
|
+
this.wrappers.forEach((wrapper) => {
|
|
485
|
+
wrapper.style.top = '';
|
|
486
|
+
wrapper.style.height = '';
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (this.openPanel.num > 0) {
|
|
491
|
+
if (this.header) {
|
|
492
|
+
if (this.headerBanner) {
|
|
493
|
+
this.headerBanner.style.display = 'none';
|
|
494
|
+
}
|
|
495
|
+
if (this.headerNotification) {
|
|
496
|
+
this.headerNotification.style.display = 'none';
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Two panels are opened
|
|
502
|
+
if (this.openPanel.num === 2) {
|
|
503
|
+
const subItemExpanded = queryOne(
|
|
504
|
+
'.ecl-mega-menu__subitem--expanded',
|
|
505
|
+
this.element,
|
|
506
|
+
);
|
|
507
|
+
if (subItemExpanded) {
|
|
508
|
+
subItemExpanded.firstChild.classList.add(
|
|
509
|
+
'ecl-mega-menu__parent-link',
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
const menuItem = this.openPanel.item;
|
|
513
|
+
// Hide siblings
|
|
514
|
+
const siblings = menuItem.parentNode.childNodes;
|
|
515
|
+
siblings.forEach((sibling) => {
|
|
516
|
+
if (sibling !== menuItem) {
|
|
517
|
+
sibling.style.display = 'none';
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
} else if (subLists && viewport === 'desktop' && !compact) {
|
|
522
|
+
// Reset styles for the sublist and subitems
|
|
523
|
+
subLists.forEach((list) => {
|
|
524
|
+
list.classList.remove('ecl-mega-menu__sublist--scrollable');
|
|
525
|
+
list.childNodes.forEach((item) => {
|
|
526
|
+
item.style.display = '';
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
infoPanels.forEach((info) => {
|
|
531
|
+
info.style.top = '';
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// Check if we have an open item, if we don't hide the overlay and enable scroll
|
|
535
|
+
const currentItems = [];
|
|
536
|
+
const currentItem = queryOne(
|
|
537
|
+
'.ecl-mega-menu__subitem--expanded',
|
|
538
|
+
this.element,
|
|
539
|
+
);
|
|
540
|
+
if (currentItem) {
|
|
541
|
+
currentItem.firstElementChild.classList.remove(
|
|
542
|
+
'ecl-mega-menu__parent-link',
|
|
543
|
+
);
|
|
544
|
+
currentItems.push(currentItem);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const currentSubItem = queryOne(
|
|
548
|
+
'.ecl-mega-menu__item--expanded',
|
|
549
|
+
this.element,
|
|
550
|
+
);
|
|
551
|
+
if (currentSubItem) {
|
|
552
|
+
currentItems.push(currentSubItem);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (currentItems.length > 0) {
|
|
556
|
+
currentItems.forEach((current) => {
|
|
557
|
+
this.checkDropdownHeight(current);
|
|
558
|
+
});
|
|
559
|
+
} else {
|
|
560
|
+
this.element.removeAttribute('data-expanded');
|
|
561
|
+
this.open.setAttribute('aria-expanded', 'false');
|
|
562
|
+
this.enableScroll();
|
|
563
|
+
}
|
|
564
|
+
} else if (viewport === 'desktop' && compact) {
|
|
565
|
+
const currentSubItem = queryOne(
|
|
566
|
+
'.ecl-mega-menu__subitem--expanded',
|
|
567
|
+
this.element,
|
|
568
|
+
);
|
|
569
|
+
if (currentSubItem) {
|
|
570
|
+
currentSubItem.firstElementChild.classList.remove(
|
|
571
|
+
'ecl-mega-menu__parent-link',
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
infoPanels.forEach((info) => {
|
|
575
|
+
info.style.height = '';
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (viewport === 'desktop' && this.header) {
|
|
580
|
+
if (this.headerBanner) {
|
|
581
|
+
this.headerBanner.style.display = '';
|
|
582
|
+
}
|
|
583
|
+
if (this.headerNotification) {
|
|
584
|
+
this.headerNotification.style.display = '';
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Trigger events on resize
|
|
591
|
+
* Uses a debounce, for performance
|
|
592
|
+
*/
|
|
593
|
+
handleResize() {
|
|
594
|
+
clearTimeout(this.resizeTimer);
|
|
595
|
+
this.resizeTimer = setTimeout(() => {
|
|
596
|
+
const screenWidth = window.innerWidth;
|
|
597
|
+
if (this.prevScreenWidth !== undefined) {
|
|
598
|
+
// Check if the transition involves crossing the desktop breakpoint
|
|
599
|
+
const isTransition =
|
|
600
|
+
(this.prevScreenWidth <= this.breakpointDesktop &&
|
|
601
|
+
screenWidth > this.breakpointDesktop) ||
|
|
602
|
+
(this.prevScreenWidth > this.breakpointDesktop &&
|
|
603
|
+
screenWidth <= this.breakpointDesktop);
|
|
604
|
+
// If we are moving in or out the desktop breakpoint, reset the styles
|
|
605
|
+
if (isTransition) {
|
|
606
|
+
this.resetStyles(
|
|
607
|
+
screenWidth > this.breakpointDesktop ? 'desktop' : 'mobile',
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
if (
|
|
611
|
+
this.prevScreenWidth >= this.breakpointLarge &&
|
|
612
|
+
screenWidth >= this.breakpointDesktop
|
|
613
|
+
) {
|
|
614
|
+
this.resetStyles('desktop', true);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
this.isDesktop = window.innerWidth >= this.breakpointDesktop;
|
|
618
|
+
this.isLarge = window.innerWidth >= this.breakpointLarge;
|
|
619
|
+
// Update previous screen width
|
|
620
|
+
this.prevScreenWidth = screenWidth;
|
|
621
|
+
// RTL
|
|
622
|
+
this.direction = getComputedStyle(this.element).direction;
|
|
623
|
+
if (this.direction === 'rtl') {
|
|
624
|
+
this.element.classList.add('ecl-mega-menu--rtl');
|
|
625
|
+
} else {
|
|
626
|
+
this.element.classList.remove('ecl-mega-menu--rtl');
|
|
627
|
+
}
|
|
628
|
+
// Check droopdown height if needed
|
|
629
|
+
const expanded = queryOne('.ecl-mega-menu__item--expanded', this.element);
|
|
630
|
+
if (expanded && this.isDesktop) {
|
|
631
|
+
this.checkDropdownHeight(expanded);
|
|
632
|
+
}
|
|
633
|
+
// Check the menu position
|
|
634
|
+
this.positionMenuOverlay();
|
|
635
|
+
}, 200);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Calculate dropdown height dynamically
|
|
640
|
+
*
|
|
641
|
+
* @param {Node} menuItem
|
|
642
|
+
*/
|
|
643
|
+
checkDropdownHeight(menuItem, hide = true) {
|
|
644
|
+
const infoPanel = queryOne('.ecl-mega-menu__info', menuItem);
|
|
645
|
+
const mainPanel = queryOne('.ecl-mega-menu__mega', menuItem);
|
|
646
|
+
const expanded = queryOne('.ecl-mega-menu__subitem--expanded', menuItem);
|
|
647
|
+
let secondPanel = null;
|
|
648
|
+
if (expanded) {
|
|
649
|
+
secondPanel = queryOne('.ecl-mega-menu__mega--level-2', expanded);
|
|
650
|
+
}
|
|
651
|
+
// Hide the panels while calculating their heights
|
|
652
|
+
if (mainPanel && this.isDesktop && hide) {
|
|
653
|
+
mainPanel.style.opacity = 0;
|
|
654
|
+
}
|
|
655
|
+
if (infoPanel && this.isDesktop && hide) {
|
|
656
|
+
infoPanel.style.opacity = 0;
|
|
657
|
+
}
|
|
658
|
+
// Second panel has already zero opacity in reality, this is for consistency
|
|
659
|
+
if (secondPanel && this.isDesktop && hide) {
|
|
660
|
+
secondPanel.opacity = 0;
|
|
661
|
+
}
|
|
662
|
+
setTimeout(() => {
|
|
663
|
+
const viewportHeight = window.innerHeight;
|
|
664
|
+
let infoPanelHeight = 0;
|
|
665
|
+
|
|
666
|
+
if (this.isDesktop) {
|
|
667
|
+
const heights = [];
|
|
668
|
+
let height = 0;
|
|
669
|
+
let featuredPanel = null;
|
|
670
|
+
let featuredPanelFirst = null;
|
|
671
|
+
let itemsHeight = 0;
|
|
672
|
+
let subItemsHeight = 0;
|
|
673
|
+
let featuredHeight = 0;
|
|
674
|
+
let featuredFirstHeight = 0;
|
|
675
|
+
|
|
676
|
+
if (infoPanel) {
|
|
677
|
+
infoPanel.style.height = '';
|
|
678
|
+
infoPanelHeight = infoPanel.scrollHeight + 16;
|
|
679
|
+
}
|
|
680
|
+
if (infoPanel && this.isLarge) {
|
|
681
|
+
heights.push(infoPanelHeight);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (mainPanel) {
|
|
685
|
+
mainPanel.style.height = '';
|
|
686
|
+
const seeAll = queryOne('.ecl-mega-menu__see-all', mainPanel);
|
|
687
|
+
if (seeAll) {
|
|
688
|
+
seeAll.style.top = 0;
|
|
689
|
+
}
|
|
690
|
+
const mainTop = mainPanel.getBoundingClientRect().top;
|
|
691
|
+
const list = queryOne('.ecl-mega-menu__sublist', mainPanel);
|
|
692
|
+
if (!list) {
|
|
693
|
+
const isContainer = menuItem.classList.contains(
|
|
694
|
+
'ecl-mega-menu__item--has-container',
|
|
695
|
+
);
|
|
696
|
+
if (isContainer) {
|
|
697
|
+
const container = queryOne(
|
|
698
|
+
'.ecl-mega-menu__mega-container',
|
|
699
|
+
menuItem,
|
|
700
|
+
);
|
|
701
|
+
if (container) {
|
|
702
|
+
container.firstElementChild.style.height = `${viewportHeight - mainTop}px`;
|
|
703
|
+
mainPanel.style.opacity = 1;
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
} else {
|
|
708
|
+
const items = list.children;
|
|
709
|
+
if (
|
|
710
|
+
!list
|
|
711
|
+
.closest('.ecl-mega-menu__item')
|
|
712
|
+
.classList.contains('ecl-mega-menu__item--one-level-only')
|
|
713
|
+
) {
|
|
714
|
+
if (items.length > 0) {
|
|
715
|
+
Array.from(items).forEach((item) => {
|
|
716
|
+
itemsHeight += item.getBoundingClientRect().height;
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
if (items.length > 0) {
|
|
721
|
+
itemsHeight = list.offsetHeight;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
heights.push(itemsHeight);
|
|
726
|
+
}
|
|
727
|
+
featuredPanelFirst = queryOne(
|
|
728
|
+
':scope > .ecl-mega-menu__featured',
|
|
729
|
+
mainPanel,
|
|
730
|
+
);
|
|
731
|
+
if (featuredPanelFirst) {
|
|
732
|
+
// Get the elements inside the scrollable container and calculate their heights.
|
|
733
|
+
Array.from(featuredPanelFirst.firstElementChild.children).forEach(
|
|
734
|
+
(child) => {
|
|
735
|
+
const elStyle = window.getComputedStyle(child);
|
|
736
|
+
|
|
737
|
+
const marginHeight =
|
|
738
|
+
parseFloat(elStyle.marginTop) +
|
|
739
|
+
parseFloat(elStyle.marginBottom);
|
|
740
|
+
featuredFirstHeight += child.offsetHeight + marginHeight;
|
|
741
|
+
},
|
|
742
|
+
);
|
|
743
|
+
// Add 8px to the featured panel height to prevent scrollbar
|
|
744
|
+
featuredFirstHeight += 8;
|
|
745
|
+
heights.push(featuredFirstHeight);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (secondPanel) {
|
|
750
|
+
secondPanel.style.height = '';
|
|
751
|
+
const subItems = queryAll(`${this.subItemSelector}`, secondPanel);
|
|
752
|
+
if (subItems.length > 0) {
|
|
753
|
+
subItems.forEach((item) => {
|
|
754
|
+
subItemsHeight += item.getBoundingClientRect().height;
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
heights.push(subItemsHeight);
|
|
758
|
+
// Featured panel calculations.
|
|
759
|
+
featuredPanel = queryOne('.ecl-mega-menu__featured', expanded);
|
|
760
|
+
if (featuredPanel) {
|
|
761
|
+
// Get the elements inside the scrollable container and calculate their heights.
|
|
762
|
+
Array.from(featuredPanel.firstElementChild.children).forEach(
|
|
763
|
+
(child) => {
|
|
764
|
+
const elStyle = window.getComputedStyle(child);
|
|
765
|
+
const marginHeight =
|
|
766
|
+
parseFloat(elStyle.marginTop) +
|
|
767
|
+
parseFloat(elStyle.marginBottom);
|
|
768
|
+
featuredHeight += child.offsetHeight + marginHeight;
|
|
769
|
+
},
|
|
770
|
+
);
|
|
771
|
+
// Add 5px to the featured panel height to prevent scrollbar on hover
|
|
772
|
+
featuredHeight += 5;
|
|
773
|
+
heights.push(featuredHeight);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const maxHeight = Math.max(...heights);
|
|
778
|
+
const containerBounding = this.inner.getBoundingClientRect();
|
|
779
|
+
let containerBottom = containerBounding.bottom;
|
|
780
|
+
if (mainPanel && infoPanel && this.isDesktop && !this.isLarge) {
|
|
781
|
+
containerBottom += infoPanelHeight;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const availableHeight = window.innerHeight - containerBottom;
|
|
785
|
+
// By requirements, limit the height to the 70% of the available space.
|
|
786
|
+
const allowedHeight = availableHeight * 0.7;
|
|
787
|
+
const wrapper = queryOne('.ecl-mega-menu__wrapper', menuItem);
|
|
788
|
+
const wrapperTop =
|
|
789
|
+
parseFloat(getComputedStyle(wrapper).paddingTop) || 0;
|
|
790
|
+
|
|
791
|
+
if (maxHeight > availableHeight) {
|
|
792
|
+
height = availableHeight - wrapperTop;
|
|
793
|
+
} else {
|
|
794
|
+
height = maxHeight > allowedHeight ? allowedHeight : maxHeight;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (wrapper) {
|
|
798
|
+
wrapper.style.height =
|
|
799
|
+
infoPanel && !this.isLarge
|
|
800
|
+
? `${infoPanelHeight + height}px`
|
|
801
|
+
: `${height}px`;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (mainPanel) {
|
|
805
|
+
mainPanel.style.height = `${height}px`;
|
|
806
|
+
const seeAll = queryOne('.ecl-mega-menu__see-all', mainPanel);
|
|
807
|
+
const firstOnly = mainPanel
|
|
808
|
+
.closest('li')
|
|
809
|
+
.classList.contains('ecl-mega-menu__item--one-level-only');
|
|
810
|
+
if (seeAll && firstOnly) {
|
|
811
|
+
const remaining =
|
|
812
|
+
mainPanel.offsetHeight - (seeAll.offsetTop + seeAll.offsetHeight);
|
|
813
|
+
if (remaining > 0) {
|
|
814
|
+
seeAll.style.top = `${remaining}px`;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (infoPanel && this.isLarge) {
|
|
819
|
+
infoPanel.style.height = `${height}px`;
|
|
820
|
+
}
|
|
821
|
+
if (secondPanel) {
|
|
822
|
+
secondPanel.style.height = `${height}px`;
|
|
823
|
+
secondPanel.style.opacity = 1;
|
|
824
|
+
}
|
|
825
|
+
if (featuredPanelFirst) {
|
|
826
|
+
featuredPanelFirst.style.height = `${height}px`;
|
|
827
|
+
}
|
|
828
|
+
if (featuredPanel) {
|
|
829
|
+
featuredPanel.style.height = `${height}px`;
|
|
830
|
+
}
|
|
831
|
+
if (mainPanel) {
|
|
832
|
+
mainPanel.style.opacity = 1;
|
|
833
|
+
}
|
|
834
|
+
if (infoPanel) {
|
|
835
|
+
infoPanel.style.opacity = 1;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}, 100);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Dinamically set the position of the menu overlay
|
|
843
|
+
*/
|
|
844
|
+
positionMenuOverlay() {
|
|
845
|
+
let availableHeight = 0;
|
|
846
|
+
if (!this.isDesktop) {
|
|
847
|
+
// In mobile, we get the bottom position of the site header header
|
|
848
|
+
setTimeout(() => {
|
|
849
|
+
if (this.header) {
|
|
850
|
+
const position = this.header.getBoundingClientRect();
|
|
851
|
+
const bottomPosition = Math.round(position.bottom);
|
|
852
|
+
|
|
853
|
+
if (this.menuOverlay) {
|
|
854
|
+
this.menuOverlay.style.top = `${bottomPosition}px`;
|
|
855
|
+
}
|
|
856
|
+
if (this.inner) {
|
|
857
|
+
this.inner.style.top = `${bottomPosition}px`;
|
|
858
|
+
}
|
|
859
|
+
const item = queryOne('.ecl-mega-menu__item--expanded', this.element);
|
|
860
|
+
|
|
861
|
+
if (item) {
|
|
862
|
+
const hasFeatured = queryOne(
|
|
863
|
+
'.ecl-mega-menu__mega--has-featured',
|
|
864
|
+
item,
|
|
865
|
+
);
|
|
866
|
+
const info = queryOne('.ecl-mega-menu__info', item);
|
|
867
|
+
if (info && this.openPanel.num === 1) {
|
|
868
|
+
const bottomRect = info.getBoundingClientRect();
|
|
869
|
+
const bottomInfo = bottomRect.bottom;
|
|
870
|
+
availableHeight = window.innerHeight - bottomInfo - 16;
|
|
871
|
+
}
|
|
872
|
+
// When the subitem of first level defines a featured panel
|
|
873
|
+
if (hasFeatured) {
|
|
874
|
+
const hasFeaturedRect = hasFeatured.getBoundingClientRect();
|
|
875
|
+
const hasFeaturedTop = hasFeaturedRect.top;
|
|
876
|
+
availableHeight =
|
|
877
|
+
availableHeight || window.innerHeight - hasFeaturedTop;
|
|
878
|
+
hasFeatured.style.height = `${availableHeight}px`;
|
|
879
|
+
} else {
|
|
880
|
+
const subList = queryOne('.ecl-mega-menu__sublist', item);
|
|
881
|
+
// Check that we are showing the first panel, with no featured panel.
|
|
882
|
+
if (subList && this.openPanel.num === 1) {
|
|
883
|
+
const subListRect = subList.getBoundingClientRect();
|
|
884
|
+
const subListRectTop = subListRect.top;
|
|
885
|
+
subList.classList.add('ecl-mega-menu__sublist--scrollable');
|
|
886
|
+
availableHeight =
|
|
887
|
+
availableHeight || window.innerHeight - subListRectTop;
|
|
888
|
+
subList.style.height = `${availableHeight}px`;
|
|
889
|
+
} else if (subList) {
|
|
890
|
+
// Clean up the sublist, it is not the one being shown.
|
|
891
|
+
subList.classList.remove('ecl-mega-menu__sublist--scrollable');
|
|
892
|
+
subList.style.height = '';
|
|
893
|
+
}
|
|
894
|
+
// Second panel handling
|
|
895
|
+
if (this.openPanel.num === 2) {
|
|
896
|
+
const subItem = queryOne(
|
|
897
|
+
'.ecl-mega-menu__subitem--expanded',
|
|
898
|
+
this.element,
|
|
899
|
+
);
|
|
900
|
+
if (subItem) {
|
|
901
|
+
const subMega = queryOne(
|
|
902
|
+
'.ecl-mega-menu__mega--level-2',
|
|
903
|
+
subItem,
|
|
904
|
+
);
|
|
905
|
+
// If there is a featured panel is going to part of it.
|
|
906
|
+
if (subMega) {
|
|
907
|
+
const subMegaRect = subMega.getBoundingClientRect();
|
|
908
|
+
const subMegaTop = subMegaRect.top;
|
|
909
|
+
availableHeight = window.innerHeight - subMegaTop;
|
|
910
|
+
subMega.style.height = `${availableHeight}px`;
|
|
911
|
+
// Overflow on the child list doesn't work here, so we apply
|
|
912
|
+
// this class to the wrapper
|
|
913
|
+
subMega.classList.add('ecl-mega-menu__sublist--scrollable');
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (this.wrappers) {
|
|
918
|
+
this.wrappers.forEach((wrapper) => {
|
|
919
|
+
wrapper.style.top = '';
|
|
920
|
+
wrapper.style.height = '';
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}, 0);
|
|
927
|
+
} else {
|
|
928
|
+
setTimeout(() => {
|
|
929
|
+
// In desktop we get the bottom position of the whole site header
|
|
930
|
+
const siteHeader = queryOne('.ecl-site-header', document);
|
|
931
|
+
if (siteHeader) {
|
|
932
|
+
const headerRect = siteHeader.getBoundingClientRect();
|
|
933
|
+
const headerBottom = headerRect.bottom;
|
|
934
|
+
const item = queryOne(this.itemSelector, this.element);
|
|
935
|
+
const rect = item.getBoundingClientRect();
|
|
936
|
+
const rectHeight = rect.height;
|
|
937
|
+
|
|
938
|
+
if (this.wrappers) {
|
|
939
|
+
this.wrappers.forEach((wrapper) => {
|
|
940
|
+
wrapper.style.top = `${rectHeight}px`;
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
if (this.menuOverlay) {
|
|
944
|
+
this.menuOverlay.style.top = `${headerBottom}px`;
|
|
945
|
+
}
|
|
946
|
+
} else {
|
|
947
|
+
const bottomPosition = this.element.getBoundingClientRect().bottom;
|
|
948
|
+
if (this.menuOverlay) {
|
|
949
|
+
this.menuOverlay.style.top = `${bottomPosition}px`;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}, 0);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Handles keyboard events specific to the menu.
|
|
958
|
+
*
|
|
959
|
+
* @param {Event} e
|
|
960
|
+
*/
|
|
961
|
+
handleKeyboard(e) {
|
|
962
|
+
const element = e.target;
|
|
963
|
+
const cList = element.classList;
|
|
964
|
+
const menuExpanded = this.element.getAttribute('data-expanded');
|
|
965
|
+
|
|
966
|
+
// Detect press on Escape
|
|
967
|
+
if (e.key === 'Escape' || e.key === 'Esc') {
|
|
968
|
+
if (document.activeElement === element) {
|
|
969
|
+
element.blur();
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (!menuExpanded) {
|
|
973
|
+
this.closeOpenDropdown();
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
// Handle Keyboard on the first panel
|
|
978
|
+
if (cList.contains('ecl-mega-menu__info-link')) {
|
|
979
|
+
if (e.key === 'ArrowUp') {
|
|
980
|
+
if (this.isDesktop) {
|
|
981
|
+
// Focus on the expanded nav item
|
|
982
|
+
queryOne(
|
|
983
|
+
'.ecl-mega-menu__item--expanded button',
|
|
984
|
+
this.element,
|
|
985
|
+
).focus();
|
|
986
|
+
} else if (this.back && !this.isDesktop) {
|
|
987
|
+
// focus on the back button
|
|
988
|
+
this.back.focus();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
|
|
992
|
+
// First item in the open dropdown.
|
|
993
|
+
element.parentElement.parentElement.nextSibling.firstChild.firstChild.firstChild.focus();
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (cList.contains('ecl-mega-menu__parent-link')) {
|
|
998
|
+
if (e.key === 'ArrowUp') {
|
|
999
|
+
const back = queryOne('.ecl-mega-menu__back', this.element);
|
|
1000
|
+
back.focus();
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (e.key === 'ArrowDown') {
|
|
1004
|
+
const mega = e.target.nextSibling;
|
|
1005
|
+
mega.firstElementChild.firstElementChild.firstChild.focus();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Handle keyboard on the see all links
|
|
1011
|
+
if (element.parentElement.classList.contains('ecl-mega-menu__see-all')) {
|
|
1012
|
+
if (e.key === 'ArrowUp') {
|
|
1013
|
+
// Focus on the last element of the sub-list
|
|
1014
|
+
element.parentElement.previousSibling.firstChild.focus();
|
|
1015
|
+
}
|
|
1016
|
+
if (e.key === 'ArrowDown') {
|
|
1017
|
+
// Focus on the fi
|
|
1018
|
+
const featured = element.parentElement.parentElement.nextSibling;
|
|
1019
|
+
if (featured) {
|
|
1020
|
+
const focusableSelectors = [
|
|
1021
|
+
'a[href]',
|
|
1022
|
+
'button:not([disabled])',
|
|
1023
|
+
'input:not([disabled])',
|
|
1024
|
+
'select:not([disabled])',
|
|
1025
|
+
'textarea:not([disabled])',
|
|
1026
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
1027
|
+
];
|
|
1028
|
+
const focusableElements = queryAll(
|
|
1029
|
+
focusableSelectors.join(', '),
|
|
1030
|
+
featured,
|
|
1031
|
+
);
|
|
1032
|
+
if (focusableElements.length > 0) {
|
|
1033
|
+
focusableElements[0].focus();
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
// Handle keyboard on the back button
|
|
1039
|
+
if (cList.contains('ecl-mega-menu__back')) {
|
|
1040
|
+
if (e.key === 'ArrowDown') {
|
|
1041
|
+
e.preventDefault();
|
|
1042
|
+
const expanded = queryOne(
|
|
1043
|
+
'[aria-expanded="true"]',
|
|
1044
|
+
element.parentElement.nextSibling,
|
|
1045
|
+
);
|
|
1046
|
+
// We have an opened list
|
|
1047
|
+
if (expanded) {
|
|
1048
|
+
const innerExpanded = queryOne(
|
|
1049
|
+
'.ecl-mega-menu__subitem--expanded',
|
|
1050
|
+
expanded.parentElement,
|
|
1051
|
+
);
|
|
1052
|
+
// We have an opened sub-list
|
|
1053
|
+
if (innerExpanded) {
|
|
1054
|
+
const parentLink = queryOne(
|
|
1055
|
+
'.ecl-mega-menu__parent-link',
|
|
1056
|
+
innerExpanded,
|
|
1057
|
+
);
|
|
1058
|
+
if (parentLink) {
|
|
1059
|
+
parentLink.focus();
|
|
1060
|
+
}
|
|
1061
|
+
} else {
|
|
1062
|
+
const infoLink = queryOne(
|
|
1063
|
+
'.ecl-mega-menu__info-link',
|
|
1064
|
+
expanded.parentElement,
|
|
1065
|
+
);
|
|
1066
|
+
if (infoLink) {
|
|
1067
|
+
infoLink.focus();
|
|
1068
|
+
} else {
|
|
1069
|
+
queryOne(
|
|
1070
|
+
'.ecl-mega-menu__subitem:first-child .ecl-mega-menu__sublink',
|
|
1071
|
+
expanded.parentElement,
|
|
1072
|
+
).focus();
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
if (e.key === 'ArrowUp') {
|
|
1078
|
+
// Focus on the open button
|
|
1079
|
+
this.open.focus();
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
// Key actions to navigate between first level menu items
|
|
1083
|
+
if (cList.contains('ecl-mega-menu__link')) {
|
|
1084
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
1085
|
+
e.preventDefault();
|
|
1086
|
+
let prevItem = element.previousSibling;
|
|
1087
|
+
|
|
1088
|
+
if (prevItem && prevItem.classList.contains('ecl-mega-menu__link')) {
|
|
1089
|
+
prevItem.focus();
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
prevItem = element.parentElement.previousSibling;
|
|
1094
|
+
if (prevItem) {
|
|
1095
|
+
const prevLink = queryOne('.ecl-mega-menu__link', prevItem);
|
|
1096
|
+
|
|
1097
|
+
if (prevLink) {
|
|
1098
|
+
prevLink.focus();
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
1104
|
+
e.preventDefault();
|
|
1105
|
+
if (
|
|
1106
|
+
element.parentElement.getAttribute('aria-expanded') === 'true' &&
|
|
1107
|
+
e.key === 'ArrowDown'
|
|
1108
|
+
) {
|
|
1109
|
+
const infoLink = queryOne(
|
|
1110
|
+
'.ecl-mega-menu__info-link',
|
|
1111
|
+
element.parentElement,
|
|
1112
|
+
);
|
|
1113
|
+
if (infoLink) {
|
|
1114
|
+
infoLink.focus();
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const nextItem = element.parentElement.nextSibling;
|
|
1119
|
+
if (nextItem) {
|
|
1120
|
+
const nextLink = queryOne('.ecl-mega-menu__link', nextItem);
|
|
1121
|
+
|
|
1122
|
+
if (nextLink) {
|
|
1123
|
+
nextLink.focus();
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
// Key actions to navigate between the sub-links
|
|
1130
|
+
if (cList.contains('ecl-mega-menu__sublink')) {
|
|
1131
|
+
if (e.key === 'ArrowDown') {
|
|
1132
|
+
e.preventDefault();
|
|
1133
|
+
const nextItem = element.parentElement.nextSibling;
|
|
1134
|
+
let nextLink = '';
|
|
1135
|
+
if (nextItem) {
|
|
1136
|
+
nextLink = queryOne('.ecl-mega-menu__sublink', nextItem);
|
|
1137
|
+
if (
|
|
1138
|
+
!nextLink &&
|
|
1139
|
+
nextItem.classList.contains('ecl-mega-menu__spacer')
|
|
1140
|
+
) {
|
|
1141
|
+
nextLink = nextItem.nextSibling.firstElementChild;
|
|
1142
|
+
}
|
|
1143
|
+
if (nextLink) {
|
|
1144
|
+
nextLink.focus();
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (e.key === 'ArrowUp') {
|
|
1150
|
+
e.preventDefault();
|
|
1151
|
+
const prevItem = element.parentElement.previousSibling;
|
|
1152
|
+
if (prevItem) {
|
|
1153
|
+
const prevLink = queryOne('.ecl-mega-menu__sublink', prevItem);
|
|
1154
|
+
|
|
1155
|
+
if (prevLink) {
|
|
1156
|
+
prevLink.focus();
|
|
1157
|
+
}
|
|
1158
|
+
} else {
|
|
1159
|
+
const moreLink = queryOne(
|
|
1160
|
+
'.ecl-mega-menu__info-link',
|
|
1161
|
+
element.parentElement.parentElement.parentElement.previousSibling,
|
|
1162
|
+
);
|
|
1163
|
+
if (moreLink) {
|
|
1164
|
+
moreLink.focus();
|
|
1165
|
+
} else if (this.openPanel.num === 2) {
|
|
1166
|
+
const parent = e.target.closest(
|
|
1167
|
+
'.ecl-mega-menu__mega',
|
|
1168
|
+
).previousSibling;
|
|
1169
|
+
if (parent) {
|
|
1170
|
+
parent.focus();
|
|
1171
|
+
}
|
|
1172
|
+
} else if (this.back) {
|
|
1173
|
+
this.back.focus();
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (e.key === 'ArrowRight') {
|
|
1179
|
+
const expanded =
|
|
1180
|
+
element.parentElement.getAttribute('aria-expanded') === 'true';
|
|
1181
|
+
if (expanded) {
|
|
1182
|
+
e.preventDefault();
|
|
1183
|
+
// Focus on the first element in the second panel
|
|
1184
|
+
element.nextSibling.firstElementChild.firstChild.firstChild.focus();
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Handles global keyboard events, triggered outside of the menu.
|
|
1191
|
+
*
|
|
1192
|
+
* @param {Event} e
|
|
1193
|
+
*/
|
|
1194
|
+
handleKeyboardGlobal(e) {
|
|
1195
|
+
// Detect press on Escape
|
|
1196
|
+
if (e.key === 'Escape' || e.key === 'Esc') {
|
|
1197
|
+
if (this.isOpen) {
|
|
1198
|
+
this.closeOpenDropdown(true);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Open menu list.
|
|
1205
|
+
*
|
|
1206
|
+
* @param {Event} e
|
|
1207
|
+
*
|
|
1208
|
+
* @fires MegaMenu#onOpen
|
|
1209
|
+
*/
|
|
1210
|
+
handleClickOnOpen(e) {
|
|
1211
|
+
if (this.isOpen) {
|
|
1212
|
+
this.handleClickOnClose(e);
|
|
1213
|
+
} else {
|
|
1214
|
+
e.preventDefault();
|
|
1215
|
+
this.disableScroll();
|
|
1216
|
+
this.element.setAttribute('data-expanded', true);
|
|
1217
|
+
this.element.classList.add('ecl-mega-menu--start-panel');
|
|
1218
|
+
this.element.classList.remove(
|
|
1219
|
+
'ecl-mega-menu--one-panel',
|
|
1220
|
+
'ecl-mega-menu--two-panels',
|
|
1221
|
+
);
|
|
1222
|
+
this.open.setAttribute('aria-expanded', 'true');
|
|
1223
|
+
this.inner.setAttribute('aria-hidden', 'false');
|
|
1224
|
+
this.isOpen = true;
|
|
1225
|
+
|
|
1226
|
+
if (this.header) {
|
|
1227
|
+
this.header.classList.add(
|
|
1228
|
+
'ecl-site-header--open-menu',
|
|
1229
|
+
'ecl-site-header--open-menu-start',
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Update label
|
|
1234
|
+
const closeLabel = this.element.getAttribute(this.labelCloseAttribute);
|
|
1235
|
+
if (this.toggleLabel && closeLabel) {
|
|
1236
|
+
this.toggleLabel.innerHTML = closeLabel;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
this.positionMenuOverlay();
|
|
1240
|
+
|
|
1241
|
+
// Focus first element
|
|
1242
|
+
if (this.links.length > 0) {
|
|
1243
|
+
this.links[0].focus();
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
this.trigger('onOpen', e);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* Close menu list.
|
|
1252
|
+
*
|
|
1253
|
+
* @param {Event} e
|
|
1254
|
+
*
|
|
1255
|
+
* @fires Menu#onClose
|
|
1256
|
+
*/
|
|
1257
|
+
handleClickOnClose(e) {
|
|
1258
|
+
if (this.element.getAttribute('data-expanded')) {
|
|
1259
|
+
this.focusTrap.deactivate();
|
|
1260
|
+
this.closeOpenDropdown();
|
|
1261
|
+
this.trigger('onClose', e);
|
|
1262
|
+
} else {
|
|
1263
|
+
this.handleClickOnOpen(e);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Toggle menu list.
|
|
1269
|
+
*
|
|
1270
|
+
* @param {Event} e
|
|
1271
|
+
*/
|
|
1272
|
+
handleClickOnToggle(e) {
|
|
1273
|
+
e.preventDefault();
|
|
1274
|
+
|
|
1275
|
+
if (this.isOpen) {
|
|
1276
|
+
this.handleClickOnClose(e);
|
|
1277
|
+
} else {
|
|
1278
|
+
this.handleClickOnOpen(e);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Get back to previous list (on mobile)
|
|
1284
|
+
*
|
|
1285
|
+
* @fires MegaMenu#onBack
|
|
1286
|
+
*/
|
|
1287
|
+
handleClickOnBack() {
|
|
1288
|
+
const infoPanels = queryAll('.ecl-mega-menu__info', this.element);
|
|
1289
|
+
infoPanels.forEach((info) => {
|
|
1290
|
+
info.style.top = '';
|
|
1291
|
+
});
|
|
1292
|
+
const level2 = queryOne('.ecl-mega-menu__subitem--expanded', this.element);
|
|
1293
|
+
if (level2) {
|
|
1294
|
+
this.element.classList.remove(
|
|
1295
|
+
'ecl-mega-menu--two-panels',
|
|
1296
|
+
'ecl-mega-menu--start-panel',
|
|
1297
|
+
);
|
|
1298
|
+
this.element.classList.add('ecl-mega-menu--one-panel');
|
|
1299
|
+
this.element.classList.remove('ecl-mega-menu--has-secondary-featured');
|
|
1300
|
+
level2.setAttribute('aria-expanded', 'false');
|
|
1301
|
+
level2.classList.remove(
|
|
1302
|
+
'ecl-mega-menu__subitem--expanded',
|
|
1303
|
+
'ecl-mega-menu__subitem--current',
|
|
1304
|
+
);
|
|
1305
|
+
const itemLink = queryOne(this.subLinkSelector, level2);
|
|
1306
|
+
itemLink.setAttribute('aria-expanded', 'false');
|
|
1307
|
+
itemLink.classList.remove('ecl-mega-menu__parent-link');
|
|
1308
|
+
const siblings = level2.parentElement.childNodes;
|
|
1309
|
+
if (siblings) {
|
|
1310
|
+
siblings.forEach((sibling) => {
|
|
1311
|
+
sibling.style.display = '';
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
if (this.header) {
|
|
1315
|
+
this.header.classList.remove('ecl-site-header--open-menu-start');
|
|
1316
|
+
}
|
|
1317
|
+
// Move the focus to the previously selected item
|
|
1318
|
+
if (this.backItemLevel2) {
|
|
1319
|
+
this.backItemLevel2.firstElementChild.focus();
|
|
1320
|
+
}
|
|
1321
|
+
this.openPanel.num = 1;
|
|
1322
|
+
} else {
|
|
1323
|
+
if (this.header) {
|
|
1324
|
+
if (this.headerBanner) {
|
|
1325
|
+
this.headerBanner.style.display = '';
|
|
1326
|
+
}
|
|
1327
|
+
if (this.headerNotification) {
|
|
1328
|
+
this.headerNotification.style.display = '';
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
// Remove expanded class from inner menu
|
|
1332
|
+
this.inner.classList.remove('ecl-mega-menu__inner--expanded');
|
|
1333
|
+
this.element.classList.remove('ecl-mega-menu--one-panel');
|
|
1334
|
+
// Remove css class and attribute from menu items
|
|
1335
|
+
this.items.forEach((item) => {
|
|
1336
|
+
item.classList.remove(
|
|
1337
|
+
'ecl-mega-menu__item--expanded',
|
|
1338
|
+
'ecl-mega-menu__item--current',
|
|
1339
|
+
);
|
|
1340
|
+
const itemLink = queryOne(this.linkSelector, item);
|
|
1341
|
+
itemLink.setAttribute('aria-expanded', 'false');
|
|
1342
|
+
});
|
|
1343
|
+
// Move the focus to the previously selected item
|
|
1344
|
+
if (this.backItemLevel1) {
|
|
1345
|
+
this.backItemLevel1.firstElementChild.focus();
|
|
1346
|
+
} else {
|
|
1347
|
+
this.items[0].firstElementChild.focus();
|
|
1348
|
+
}
|
|
1349
|
+
this.openPanel.num = 0;
|
|
1350
|
+
this.element.classList.add('ecl-mega-menu--start-panel');
|
|
1351
|
+
if (this.header) {
|
|
1352
|
+
this.header.classList.add('ecl-site-header--open-menu-start');
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
this.positionMenuOverlay();
|
|
1357
|
+
this.trigger('onBack', { level: level2 ? 2 : 1 });
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Show/hide the first panel
|
|
1362
|
+
*
|
|
1363
|
+
* @param {Node} menuItem
|
|
1364
|
+
* @param {string} op (expand or collapse)
|
|
1365
|
+
*
|
|
1366
|
+
* @fires MegaMenu#onOpenPanel
|
|
1367
|
+
*/
|
|
1368
|
+
handleFirstPanel(menuItem, op) {
|
|
1369
|
+
switch (op) {
|
|
1370
|
+
case 'expand': {
|
|
1371
|
+
this.inner.classList.add('ecl-mega-menu__inner--expanded');
|
|
1372
|
+
this.positionMenuOverlay();
|
|
1373
|
+
this.checkDropdownHeight(menuItem);
|
|
1374
|
+
this.element.setAttribute('data-expanded', true);
|
|
1375
|
+
this.element.classList.add('ecl-mega-menu--one-panel');
|
|
1376
|
+
this.element.classList.remove('ecl-mega-menu--start-panel');
|
|
1377
|
+
this.open.setAttribute('aria-expanded', 'true');
|
|
1378
|
+
if (this.header) {
|
|
1379
|
+
this.header.classList.add('ecl-site-header--open-menu');
|
|
1380
|
+
this.header.classList.remove('ecl-site-header--open-menu-start');
|
|
1381
|
+
if (!this.isDesktop) {
|
|
1382
|
+
if (this.headerBanner) {
|
|
1383
|
+
this.headerBanner.style.display = 'none';
|
|
1384
|
+
}
|
|
1385
|
+
if (this.headerNotification) {
|
|
1386
|
+
this.headerNotification.style.display = 'none';
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
this.disableScroll();
|
|
1391
|
+
this.isOpen = true;
|
|
1392
|
+
this.items.forEach((item) => {
|
|
1393
|
+
const itemLink = queryOne(this.linkSelector, item);
|
|
1394
|
+
if (itemLink && itemLink.hasAttribute('aria-expanded')) {
|
|
1395
|
+
if (item === menuItem) {
|
|
1396
|
+
item.classList.add(
|
|
1397
|
+
'ecl-mega-menu__item--expanded',
|
|
1398
|
+
'ecl-mega-menu__item--current',
|
|
1399
|
+
);
|
|
1400
|
+
itemLink.setAttribute('aria-expanded', 'true');
|
|
1401
|
+
this.backItemLevel1 = item;
|
|
1402
|
+
} else {
|
|
1403
|
+
itemLink.setAttribute('aria-expanded', 'false');
|
|
1404
|
+
item.classList.remove(
|
|
1405
|
+
'ecl-mega-menu__item--current',
|
|
1406
|
+
'ecl-mega-menu__item--expanded',
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
if (!this.isDesktop && this.back) {
|
|
1413
|
+
this.back.focus();
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
this.openPanel = {
|
|
1417
|
+
num: 1,
|
|
1418
|
+
item: menuItem,
|
|
1419
|
+
};
|
|
1420
|
+
const details = { panel: 1, item: menuItem };
|
|
1421
|
+
this.trigger('OnOpenPanel', details);
|
|
1422
|
+
break;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
case 'collapse':
|
|
1426
|
+
this.closeOpenDropdown();
|
|
1427
|
+
break;
|
|
1428
|
+
|
|
1429
|
+
default:
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Show/hide the second panel
|
|
1435
|
+
*
|
|
1436
|
+
* @param {Node} menuItem
|
|
1437
|
+
* @param {string} op (expand or collapse)
|
|
1438
|
+
*
|
|
1439
|
+
* @fires MegaMenu#onOpenPanel
|
|
1440
|
+
*/
|
|
1441
|
+
handleSecondPanel(menuItem, op) {
|
|
1442
|
+
const infoPanel = queryOne(
|
|
1443
|
+
'.ecl-mega-menu__info',
|
|
1444
|
+
menuItem.closest('.ecl-container'),
|
|
1445
|
+
);
|
|
1446
|
+
let siblings;
|
|
1447
|
+
switch (op) {
|
|
1448
|
+
case 'expand': {
|
|
1449
|
+
this.element.classList.remove(
|
|
1450
|
+
'ecl-mega-menu--one-panel',
|
|
1451
|
+
'ecl-mega-menu--start-panel',
|
|
1452
|
+
);
|
|
1453
|
+
this.element.classList.add('ecl-mega-menu--two-panels');
|
|
1454
|
+
this.subItems.forEach((item) => {
|
|
1455
|
+
const itemLink = queryOne(this.subLinkSelector, item);
|
|
1456
|
+
if (item === menuItem) {
|
|
1457
|
+
if (itemLink && itemLink.hasAttribute('aria-expanded')) {
|
|
1458
|
+
itemLink.setAttribute('aria-expanded', 'true');
|
|
1459
|
+
const mega = queryOne('.ecl-mega-menu__mega', item);
|
|
1460
|
+
if (!this.isDesktop) {
|
|
1461
|
+
// We use this class mainly to recover the default behavior of the link.
|
|
1462
|
+
itemLink.classList.add('ecl-mega-menu__parent-link');
|
|
1463
|
+
if (this.back) {
|
|
1464
|
+
this.back.focus();
|
|
1465
|
+
}
|
|
1466
|
+
} else {
|
|
1467
|
+
// Hide the panel since it will be resized later.
|
|
1468
|
+
mega.style.opacity = 0;
|
|
1469
|
+
}
|
|
1470
|
+
item.classList.add('ecl-mega-menu__subitem--expanded');
|
|
1471
|
+
}
|
|
1472
|
+
item.classList.add('ecl-mega-menu__subitem--current');
|
|
1473
|
+
const hasFeatured = queryOne('.ecl-mega-menu__featured', item);
|
|
1474
|
+
if (hasFeatured) {
|
|
1475
|
+
this.element.classList.add(
|
|
1476
|
+
'ecl-mega-menu--has-secondary-featured',
|
|
1477
|
+
);
|
|
1478
|
+
} else {
|
|
1479
|
+
this.element.classList.remove(
|
|
1480
|
+
'ecl-mega-menu--has-secondary-featured',
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
this.backItemLevel2 = item;
|
|
1484
|
+
} else {
|
|
1485
|
+
if (itemLink && itemLink.hasAttribute('aria-expanded')) {
|
|
1486
|
+
itemLink.setAttribute('aria-expanded', 'false');
|
|
1487
|
+
itemLink.classList.remove('ecl-mega-menu__parent-link');
|
|
1488
|
+
item.classList.remove('ecl-mega-menu__subitem--expanded');
|
|
1489
|
+
}
|
|
1490
|
+
item.classList.remove('ecl-mega-menu__subitem--current');
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
this.openPanel = { num: 2, item: menuItem };
|
|
1495
|
+
siblings = menuItem.parentNode.childNodes;
|
|
1496
|
+
if (this.isDesktop) {
|
|
1497
|
+
// Reset style for the siblings, in case they were hidden
|
|
1498
|
+
siblings.forEach((sibling) => {
|
|
1499
|
+
if (sibling !== menuItem) {
|
|
1500
|
+
sibling.style.display = '';
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
} else {
|
|
1504
|
+
// Hide other items in the sublist
|
|
1505
|
+
siblings.forEach((sibling) => {
|
|
1506
|
+
if (sibling !== menuItem) {
|
|
1507
|
+
sibling.style.display = 'none';
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
this.positionMenuOverlay();
|
|
1512
|
+
this.checkDropdownHeight(
|
|
1513
|
+
menuItem.closest('.ecl-mega-menu__item'),
|
|
1514
|
+
false,
|
|
1515
|
+
);
|
|
1516
|
+
|
|
1517
|
+
const details = { panel: 2, item: menuItem };
|
|
1518
|
+
this.trigger('OnOpenPanel', details);
|
|
1519
|
+
break;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
case 'collapse':
|
|
1523
|
+
this.element.classList.remove('ecl-mega-menu--two-panels');
|
|
1524
|
+
this.element.classList.remove('ecl-mega-menu--has-secondary-featured');
|
|
1525
|
+
this.openPanel = { num: 1 };
|
|
1526
|
+
// eslint-disable-next-line no-case-declarations
|
|
1527
|
+
const itemLink = queryOne(this.subLinkSelector, menuItem);
|
|
1528
|
+
itemLink.setAttribute('aria-expanded', 'false');
|
|
1529
|
+
menuItem.classList.remove(
|
|
1530
|
+
'ecl-mega-menu__subitem--expanded',
|
|
1531
|
+
'ecl-mega-menu__subitem--current',
|
|
1532
|
+
);
|
|
1533
|
+
if (infoPanel) {
|
|
1534
|
+
infoPanel.style.top = '';
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
this.positionMenuOverlay();
|
|
1538
|
+
this.checkDropdownHeight(
|
|
1539
|
+
menuItem.closest('.ecl-mega-menu__item'),
|
|
1540
|
+
false,
|
|
1541
|
+
);
|
|
1542
|
+
break;
|
|
1543
|
+
|
|
1544
|
+
default:
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Click on a menu item
|
|
1550
|
+
*
|
|
1551
|
+
* @param {Event} e
|
|
1552
|
+
*
|
|
1553
|
+
* @fires MegaMenu#onItemClick
|
|
1554
|
+
*/
|
|
1555
|
+
handleClickOnItem(e) {
|
|
1556
|
+
let isInTheContainer = false;
|
|
1557
|
+
const menuItem = e.target.closest('li');
|
|
1558
|
+
|
|
1559
|
+
const container = queryOne(
|
|
1560
|
+
'.ecl-mega-menu__mega-container-scrollable',
|
|
1561
|
+
menuItem,
|
|
1562
|
+
);
|
|
1563
|
+
if (container) {
|
|
1564
|
+
isInTheContainer = container.contains(e.target);
|
|
1565
|
+
}
|
|
1566
|
+
// We need to ensure that the click doesn't come from a parent link
|
|
1567
|
+
// or from an open container, in that case we do not act.
|
|
1568
|
+
if (
|
|
1569
|
+
!e.target.classList.contains(
|
|
1570
|
+
'ecl-mega-menu__mega-container-scrollable',
|
|
1571
|
+
) &&
|
|
1572
|
+
!isInTheContainer
|
|
1573
|
+
) {
|
|
1574
|
+
this.trigger('onItemClick', { item: menuItem, event: e });
|
|
1575
|
+
const hasChildren =
|
|
1576
|
+
menuItem.firstElementChild.getAttribute('aria-expanded');
|
|
1577
|
+
if (hasChildren && menuItem.classList.contains('ecl-mega-menu__item')) {
|
|
1578
|
+
e.preventDefault();
|
|
1579
|
+
e.stopPropagation();
|
|
1580
|
+
if (!this.isDesktop) {
|
|
1581
|
+
this.handleFirstPanel(menuItem, 'expand');
|
|
1582
|
+
} else {
|
|
1583
|
+
const isOpen = hasChildren === 'true';
|
|
1584
|
+
if (isOpen) {
|
|
1585
|
+
this.handleFirstPanel(menuItem, 'collapse');
|
|
1586
|
+
} else {
|
|
1587
|
+
this.closeOpenDropdown();
|
|
1588
|
+
this.handleFirstPanel(menuItem, 'expand');
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* Click on a subitem
|
|
1597
|
+
*
|
|
1598
|
+
* @param {Event} e
|
|
1599
|
+
*/
|
|
1600
|
+
handleClickOnSubitem(e) {
|
|
1601
|
+
const menuItem = e.target.closest(this.subItemSelector);
|
|
1602
|
+
if (menuItem && menuItem.firstElementChild.hasAttribute('aria-expanded')) {
|
|
1603
|
+
const parentLink = queryOne('.ecl-mega-menu__parent-link', menuItem);
|
|
1604
|
+
if (parentLink) {
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
e.preventDefault();
|
|
1608
|
+
e.stopPropagation();
|
|
1609
|
+
const isExpanded =
|
|
1610
|
+
menuItem.firstElementChild.getAttribute('aria-expanded') === 'true';
|
|
1611
|
+
|
|
1612
|
+
if (isExpanded) {
|
|
1613
|
+
this.handleSecondPanel(menuItem, 'collapse');
|
|
1614
|
+
} else {
|
|
1615
|
+
this.handleSecondPanel(menuItem, 'expand');
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* Deselect any opened menu item
|
|
1622
|
+
*
|
|
1623
|
+
* @param {boolean} esc, whether the call was originated by a press on Esc
|
|
1624
|
+
*
|
|
1625
|
+
* @fires MegaMenu#onFocusTrapToggle
|
|
1626
|
+
*/
|
|
1627
|
+
closeOpenDropdown(esc = false) {
|
|
1628
|
+
this.element.classList.remove('ecl-mega-menu--has-secondary-featured');
|
|
1629
|
+
if (this.header) {
|
|
1630
|
+
this.header.classList.remove(
|
|
1631
|
+
'ecl-site-header--open-menu',
|
|
1632
|
+
'ecl-site-header--open-menu-start',
|
|
1633
|
+
);
|
|
1634
|
+
if (this.headerBanner) {
|
|
1635
|
+
this.headerBanner.style.display = '';
|
|
1636
|
+
}
|
|
1637
|
+
if (this.headerNotification) {
|
|
1638
|
+
this.headerNotification.style.display = '';
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
this.enableScroll();
|
|
1642
|
+
this.element.removeAttribute('data-expanded');
|
|
1643
|
+
this.element.classList.remove(
|
|
1644
|
+
'ecl-mega-menu--start-panel',
|
|
1645
|
+
'ecl-mega-menu--two-panels',
|
|
1646
|
+
'ecl-mega-menu--one-panel',
|
|
1647
|
+
);
|
|
1648
|
+
this.open.setAttribute('aria-expanded', 'false');
|
|
1649
|
+
// Remove css class and attribute from inner menu
|
|
1650
|
+
this.inner.classList.remove('ecl-mega-menu__inner--expanded');
|
|
1651
|
+
|
|
1652
|
+
// Reset heights
|
|
1653
|
+
const megaMenus = queryAll('[data-ecl-mega-menu-mega]', this.element);
|
|
1654
|
+
megaMenus.forEach((mega) => {
|
|
1655
|
+
mega.style.height = '';
|
|
1656
|
+
mega.style.top = '';
|
|
1657
|
+
mega.style.opacity = '';
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
if (this.wrappers) {
|
|
1661
|
+
this.wrappers.forEach((wrapper) => {
|
|
1662
|
+
wrapper.style = '';
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
let currentItem = false;
|
|
1666
|
+
// Remove css class and attribute from menu items
|
|
1667
|
+
this.items.forEach((item) => {
|
|
1668
|
+
item.classList.remove('ecl-mega-menu__item--current');
|
|
1669
|
+
const itemLink = queryOne(this.linkSelector, item);
|
|
1670
|
+
if (itemLink.getAttribute('aria-expanded') === 'true') {
|
|
1671
|
+
item.classList.remove('ecl-mega-menu__item--expanded');
|
|
1672
|
+
itemLink.setAttribute('aria-expanded', 'false');
|
|
1673
|
+
currentItem = itemLink;
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
// Remove css class and attribute from menu subitems
|
|
1677
|
+
this.subItems.forEach((item) => {
|
|
1678
|
+
item.classList.remove('ecl-mega-menu__subitem--current');
|
|
1679
|
+
item.style.display = '';
|
|
1680
|
+
const itemLink = queryOne(this.subLinkSelector, item);
|
|
1681
|
+
if (itemLink && itemLink.hasAttribute('aria-expanded')) {
|
|
1682
|
+
item.classList.remove('ecl-mega-menu__subitem--expanded');
|
|
1683
|
+
item.style.display = '';
|
|
1684
|
+
itemLink.setAttribute('aria-expanded', 'false');
|
|
1685
|
+
itemLink.classList.remove('ecl-mega-menu__parent-link');
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
// Remove styles set for the sublists
|
|
1689
|
+
const sublists = queryAll('.ecl-mega-menu__sublist');
|
|
1690
|
+
if (sublists) {
|
|
1691
|
+
sublists.forEach((sublist) => {
|
|
1692
|
+
sublist.classList.remove(
|
|
1693
|
+
'ecl-mega-menu__sublist--no-border',
|
|
1694
|
+
'ecl-mega-menu__sublist--scrollable',
|
|
1695
|
+
);
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
// Update label
|
|
1699
|
+
const openLabel = this.element.getAttribute(this.labelOpenAttribute);
|
|
1700
|
+
if (this.toggleLabel && openLabel) {
|
|
1701
|
+
this.toggleLabel.innerHTML = openLabel;
|
|
1702
|
+
}
|
|
1703
|
+
this.openPanel = {
|
|
1704
|
+
num: 0,
|
|
1705
|
+
item: false,
|
|
1706
|
+
};
|
|
1707
|
+
// If the focus trap is active, deactivate it
|
|
1708
|
+
this.focusTrap.deactivate();
|
|
1709
|
+
// Focus on the open button in mobile or on the formerly expanded item in desktop.
|
|
1710
|
+
if (!this.isDesktop && this.open && esc) {
|
|
1711
|
+
this.open.focus();
|
|
1712
|
+
} else if (this.isDesktop && currentItem && esc) {
|
|
1713
|
+
currentItem.focus();
|
|
1714
|
+
}
|
|
1715
|
+
this.trigger('onFocusTrapToggle', { active: false });
|
|
1716
|
+
this.isOpen = false;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Focus out of a menu link
|
|
1721
|
+
*
|
|
1722
|
+
* @param {Event} e
|
|
1723
|
+
*
|
|
1724
|
+
* @fires MegaMenu#onFocusTrapToggle
|
|
1725
|
+
*/
|
|
1726
|
+
handleFocusOut(e) {
|
|
1727
|
+
const element = e.target;
|
|
1728
|
+
const menuExpanded = this.element.getAttribute('data-expanded');
|
|
1729
|
+
|
|
1730
|
+
// Specific focus action for mobile menu
|
|
1731
|
+
// Loop through the items and go back to close button
|
|
1732
|
+
if (menuExpanded && !this.isDesktop) {
|
|
1733
|
+
const nextItem = element.parentElement.nextSibling;
|
|
1734
|
+
|
|
1735
|
+
if (!nextItem) {
|
|
1736
|
+
const nextFocusTarget = e.relatedTarget;
|
|
1737
|
+
if (!this.element.contains(nextFocusTarget)) {
|
|
1738
|
+
// This is the last item, go back to close button
|
|
1739
|
+
this.focusTrap.activate();
|
|
1740
|
+
this.trigger('onFocusTrapToggle', {
|
|
1741
|
+
active: true,
|
|
1742
|
+
lastFocusedEl: element.parentElement,
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
/**
|
|
1750
|
+
* Handles global click events, triggered outside of the menu.
|
|
1751
|
+
*
|
|
1752
|
+
* @param {Event} e
|
|
1753
|
+
*/
|
|
1754
|
+
handleClickGlobal(e) {
|
|
1755
|
+
if (
|
|
1756
|
+
!e.target.classList.contains(
|
|
1757
|
+
'ecl-mega-menu__mega-container-scrollable',
|
|
1758
|
+
) &&
|
|
1759
|
+
(e.target.classList.contains('ecl-mega-menu__overlay') ||
|
|
1760
|
+
!this.element.contains(e.target)) &&
|
|
1761
|
+
this.isOpen
|
|
1762
|
+
) {
|
|
1763
|
+
this.closeOpenDropdown();
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
export default MegaMenu;
|