@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/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;