@ecl/mega-menu 5.0.0-alpha.1

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