/* eslint-disable class-methods-use-this */
import { queryOne, queryAll } from '@ecl/dom-utils';
import EventManager from '@ecl/event-manager';
import isMobile from 'mobile-device-detect';
import { createFocusTrap } from 'focus-trap';
/**
* @param {HTMLElement} element DOM element for component instantiation and scope
* @param {Object} options
* @param {String} options.openSelector Selector for the hamburger button
* @param {String} options.backSelector Selector for the back button
* @param {String} options.innerSelector Selector for the menu inner
* @param {String} options.itemSelector Selector for the menu item
* @param {String} options.linkSelector Selector for the menu link
* @param {String} options.subLinkSelector Selector for the menu sub link
* @param {String} options.megaSelector Selector for the mega menu
* @param {String} options.subItemSelector Selector for the menu sub items
* @param {String} options.labelOpenAttribute The data attribute for open label
* @param {String} options.labelCloseAttribute The data attribute for close label
* @param {Boolean} options.attachClickListener Whether or not to bind click events
* @param {Boolean} options.attachHoverListener Whether or not to bind hover events
* @param {Boolean} options.attachFocusListener Whether or not to bind focus events
* @param {Boolean} options.attachKeyListener Whether or not to bind keyboard events
* @param {Boolean} options.attachResizeListener Whether or not to bind resize events
*/
export class MegaMenu {
/**
* @static
* Shorthand for instance creation and initialisation.
*
* @param {HTMLElement} root DOM element for component instantiation and scope
*
* @return {Menu} An instance of Menu.
*/
static autoInit(root, { MEGA_MENU: defaultOptions = {} } = {}) {
const megaMenu = new MegaMenu(root, defaultOptions);
megaMenu.init();
root.ECLMegaMenu = megaMenu;
return megaMenu;
}
/**
* @event MegaMenu#onOpen
*/
/**
* @event MegaMenu#onClose
*/
/**
* @event MegaMenu#onOpenPanel
*/
/**
* @event MegaMenu#onBack
*/
/**
* @event MegaMenu#onItemClick
*/
/**
* @event MegaMenu#onFocusTrapToggle
*/
/**
* An array of supported events for this component.
*
* @type {Array<string>}
* @memberof MegaMenu
*/
supportedEvents = ['onOpen', 'onClose'];
constructor(
element,
{
openSelector = '[data-ecl-mega-menu-open]',
backSelector = '[data-ecl-mega-menu-back]',
innerSelector = '[data-ecl-mega-menu-inner]',
itemSelector = '[data-ecl-mega-menu-item]',
linkSelector = '[data-ecl-mega-menu-link]',
subLinkSelector = '[data-ecl-mega-menu-sublink]',
megaSelector = '[data-ecl-mega-menu-mega]',
containerSelector = '[data-ecl-has-container]',
subItemSelector = '[data-ecl-mega-menu-subitem]',
featuredAttribute = '[data-ecl-mega-menu-featured]',
featuredLinkAttribute = '[data-ecl-mega-menu-featured-link]',
labelOpenAttribute = 'data-ecl-mega-menu-label-open',
labelCloseAttribute = 'data-ecl-mega-menu-label-close',
attachClickListener = true,
attachFocusListener = true,
attachKeyListener = true,
attachResizeListener = true,
} = {},
) {
// Check element
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
throw new TypeError(
'DOM element should be given to initialize this widget.',
);
}
this.element = element;
this.eventManager = new EventManager();
// Options
this.openSelector = openSelector;
this.backSelector = backSelector;
this.innerSelector = innerSelector;
this.itemSelector = itemSelector;
this.linkSelector = linkSelector;
this.subLinkSelector = subLinkSelector;
this.megaSelector = megaSelector;
this.subItemSelector = subItemSelector;
this.containerSelector = containerSelector;
this.labelOpenAttribute = labelOpenAttribute;
this.labelCloseAttribute = labelCloseAttribute;
this.attachClickListener = attachClickListener;
this.attachFocusListener = attachFocusListener;
this.attachKeyListener = attachKeyListener;
this.attachResizeListener = attachResizeListener;
this.featuredAttribute = featuredAttribute;
this.featuredLinkAttribute = featuredLinkAttribute;
// Private variables
this.direction = 'ltr';
this.open = null;
this.toggleLabel = null;
this.back = null;
this.backItemLevel1 = null;
this.backItemLevel2 = null;
this.inner = null;
this.items = null;
this.links = null;
this.isOpen = false;
this.resizeTimer = null;
this.isKeyEvent = false;
this.isDesktop = false;
this.isLarge = false;
this.lastVisibleItem = null;
this.currentItem = null;
this.totalItemsWidth = 0;
this.breakpointL = 996;
this.openPanel = { num: 0, item: {} };
this.infoLinks = null;
this.seeAllLinks = null;
this.featuredLinks = null;
// Bind `this` for use in callbacks
this.handleClickOnOpen = this.handleClickOnOpen.bind(this);
this.handleClickOnClose = this.handleClickOnClose.bind(this);
this.handleClickOnToggle = this.handleClickOnToggle.bind(this);
this.handleClickOnBack = this.handleClickOnBack.bind(this);
this.handleClickGlobal = this.handleClickGlobal.bind(this);
this.handleClickOnItem = this.handleClickOnItem.bind(this);
this.handleClickOnSubitem = this.handleClickOnSubitem.bind(this);
this.handleFocusOut = this.handleFocusOut.bind(this);
this.handleKeyboard = this.handleKeyboard.bind(this);
this.handleKeyboardGlobal = this.handleKeyboardGlobal.bind(this);
this.handleResize = this.handleResize.bind(this);
this.useDesktopDisplay = this.useDesktopDisplay.bind(this);
this.closeOpenDropdown = this.closeOpenDropdown.bind(this);
this.checkDropdownHeight = this.checkDropdownHeight.bind(this);
this.positionMenuOverlay = this.positionMenuOverlay.bind(this);
this.resetStyles = this.resetStyles.bind(this);
this.handleFirstPanel = this.handleFirstPanel.bind(this);
this.handleSecondPanel = this.handleSecondPanel.bind(this);
this.disableScroll = this.disableScroll.bind(this);
this.enableScroll = this.enableScroll.bind(this);
}
/**
* Initialise component.
*/
init() {
if (!ECL) {
throw new TypeError('Called init but ECL is not present');
}
ECL.components = ECL.components || new Map();
// Query elements
this.open = queryOne(this.openSelector, this.element);
this.back = queryOne(this.backSelector, this.element);
this.inner = queryOne(this.innerSelector, this.element);
this.btnPrevious = queryOne(this.buttonPreviousSelector, this.element);
this.btnNext = queryOne(this.buttonNextSelector, this.element);
this.items = queryAll(this.itemSelector, this.element);
this.subItems = queryAll(this.subItemSelector, this.element);
this.links = queryAll(this.linkSelector, this.element);
this.header = queryOne('.ecl-site-header', document);
this.headerBanner = queryOne('.ecl-site-header__banner', document);
this.headerNotification = queryOne(
'.ecl-site-header__notification',
document,
);
this.toggleLabel = queryOne('.ecl-button__label', this.open);
// Check if we should use desktop display (it does not rely only on breakpoints)
this.isDesktop = this.useDesktopDisplay();
// Bind click events on buttons
if (this.attachClickListener) {
// Open
if (this.open) {
this.open.addEventListener('click', this.handleClickOnToggle);
}
// Back
if (this.back) {
this.back.addEventListener('click', this.handleClickOnBack);
this.back.addEventListener('keyup', this.handleKeyboard);
}
// Global click
if (this.attachClickListener) {
document.addEventListener('click', this.handleClickGlobal);
}
}
// Bind event on menu links
if (this.links) {
this.links.forEach((link) => {
if (this.attachFocusListener) {
link.addEventListener('focusout', this.handleFocusOut);
}
if (this.attachKeyListener) {
link.addEventListener('keyup', this.handleKeyboard);
}
});
}
// Bind event on sub menu links
if (this.subItems) {
this.subItems.forEach((subItem) => {
const subLink = queryOne('.ecl-mega-menu__sublink', subItem);
if (this.attachKeyListener && subLink) {
subLink.addEventListener('click', this.handleClickOnSubitem);
subLink.addEventListener('keyup', this.handleKeyboard);
}
if (this.attachFocusListener && subLink) {
subLink.addEventListener('focusout', this.handleFocusOut);
}
});
}
this.infoLinks = queryAll('.ecl-mega-menu__info-link a', this.element);
if (this.infoLinks.length > 0) {
this.infoLinks.forEach((infoLink) => {
if (this.attachKeyListener) {
infoLink.addEventListener('keyup', this.handleKeyboard);
}
if (this.attachFocusListener) {
infoLink.addEventListener('blur', this.handleFocusOut);
}
});
}
this.seeAllLinks = queryAll('.ecl-mega-menu__see-all a', this.element);
if (this.seeAllLinks.length > 0) {
this.seeAllLinks.forEach((seeAll) => {
if (this.attachKeyListener) {
seeAll.addEventListener('keyup', this.handleKeyboard);
}
if (this.attachFocusListener) {
seeAll.addEventListener('blur', this.handleFocusOut);
}
});
}
this.featuredLinks = queryAll(this.featuredLinkAttribute, this.element);
if (this.featuredLinks.length > 0 && this.attachFocusListener) {
this.featuredLinks.forEach((featured) => {
featured.addEventListener('blur', this.handleFocusOut);
});
}
// Bind global keyboard events
if (this.attachKeyListener) {
document.addEventListener('keyup', this.handleKeyboardGlobal);
}
// Bind resize events
if (this.attachResizeListener) {
window.addEventListener('resize', this.handleResize);
}
// Browse first level items
if (this.items) {
this.items.forEach((item) => {
// Check menu item display (right to left, full width, ...)
this.totalItemsWidth += item.offsetWidth;
if (
item.hasAttribute('data-ecl-has-children') ||
item.hasAttribute('data-ecl-has-container')
) {
// Bind click event on menu links
const link = queryOne(this.linkSelector, item);
if (this.attachClickListener && link) {
link.addEventListener('click', this.handleClickOnItem);
}
}
});
}
// Create a focus trap around the menu
this.focusTrap = createFocusTrap(this.element, {
onActivate: () =>
this.element.classList.add('ecl-mega-menu-trap-is-active'),
onDeactivate: () =>
this.element.classList.remove('ecl-mega-menu-trap-is-active'),
});
this.handleResize();
// Set ecl initialized attribute
this.element.setAttribute('data-ecl-auto-initialized', 'true');
ECL.components.set(this.element, this);
}
/**
* Register a callback function for a specific event.
*
* @param {string} eventName - The name of the event to listen for.
* @param {Function} callback - The callback function to be invoked when the event occurs.
* @returns {void}
* @memberof MegaMenu
* @instance
*
* @example
* // Registering a callback for the 'onOpen' event
* megaMenu.on('onOpen', (event) => {
* console.log('Open event occurred!', event);
* });
*/
on(eventName, callback) {
this.eventManager.on(eventName, callback);
}
/**
* Trigger a component event.
*
* @param {string} eventName - The name of the event to trigger.
* @param {any} eventData - Data associated with the event.
* @memberof MegaMenu
*/
trigger(eventName, eventData) {
this.eventManager.trigger(eventName, eventData);
}
/**
* Destroy component.
*/
destroy() {
if (this.attachClickListener) {
if (this.open) {
this.open.removeEventListener('click', this.handleClickOnToggle);
}
if (this.back) {
this.back.removeEventListener('click', this.handleClickOnBack);
}
if (this.attachClickListener) {
document.removeEventListener('click', this.handleClickGlobal);
}
}
if (this.links) {
this.links.forEach((link) => {
if (this.attachClickListener) {
link.removeEventListener('click', this.handleClickOnItem);
}
if (this.attachFocusListener) {
link.removeEventListener('focusout', this.handleFocusOut);
}
if (this.attachKeyListener) {
link.removeEventListener('keyup', this.handleKeyboard);
}
});
}
if (this.subItems) {
this.subItems.forEach((subItem) => {
const subLink = queryOne('.ecl-mega-menu__sublink', subItem);
if (this.attachKeyListener && subLink) {
subLink.removeEventListener('keyup', this.handleKeyboard);
}
if (this.attachClickListener && subLink) {
subLink.removeEventListener('click', this.handleClickOnSubitem);
}
if (this.attachFocusListener && subLink) {
subLink.removeEventListener('focusout', this.handleFocusOut);
}
});
}
if (this.infoLinks) {
this.infoLinks.forEach((infoLink) => {
if (this.attachFocusListener) {
infoLink.removeEventListener('blur', this.handleFocusOut);
}
if (this.attachKeyListener) {
infoLink.removeEventListener('keyup', this.handleKeyboard);
}
});
}
if (this.seeAllLinks) {
this.seeAllLinks.forEach((seeAll) => {
if (this.attachFocusListener) {
seeAll.removeEventListener('blur', this.handleFocusOut);
}
if (this.attachKeyListener) {
seeAll.removeEventListener('keyup', this.handleKeyboard);
}
});
}
if (this.featuredLinks && this.attachFocusListener) {
this.featuredLinks.forEach((featuredLink) => {
featuredLink.removeEventListener('blur', this.handleFocusOut);
});
}
if (this.attachKeyListener) {
document.removeEventListener('keyup', this.handleKeyboardGlobal);
}
if (this.attachResizeListener) {
window.removeEventListener('resize', this.handleResize);
}
this.closeOpenDropdown();
this.enableScroll();
if (this.element) {
this.element.removeAttribute('data-ecl-auto-initialized');
ECL.components.delete(this.element);
}
}
/**
* Disable page scrolling
*/
disableScroll() {
const scrollBarWidth =
window.innerWidth - document.documentElement.clientWidth;
document.body.classList.add('ecl-mega-menu-prevent-scroll');
document.body.style.paddingRight = `${scrollBarWidth}px`;
}
/**
* Enable page scrolling
*/
enableScroll() {
document.body.classList.remove('ecl-mega-menu-prevent-scroll');
document.body.style.paddingRight = '';
}
/**
* Check if desktop display has to be used
* - not using a phone or tablet (whatever the screen size is)
* - not having hamburger menu on screen
*/
useDesktopDisplay() {
// Detect mobile devices
if (isMobile.isMobileOnly) {
return false;
}
// Force mobile display on tablet
if (isMobile.isTablet) {
this.element.classList.add('ecl-mega-menu--forced-mobile');
return false;
}
// After all that, check if the hamburger button is displayed
if (window.innerWidth < this.breakpointL) {
return false;
}
// Everything is fine to use desktop display
this.element.classList.remove('ecl-mega-menu--forced-mobile');
return true;
}
/**
* Reset the styles set by the script
*
* @param {string} desktop or mobile
*/
resetStyles(viewport, compact) {
const infoPanels = queryAll('.ecl-mega-menu__info', this.element);
const subLists = queryAll('.ecl-mega-menu__sublist', this.element);
// Remove display:none from the sublists
if (subLists && viewport === 'mobile') {
const megaMenus = queryAll(
'.ecl-mega-menu__item > .ecl-mega-menu__wrapper > .ecl-container > [data-ecl-mega-menu-mega]',
this.element,
);
megaMenus.forEach((menu) => {
menu.style.height = '';
});
// Reset top position and height of the wrappers
const wrappers = queryAll('.ecl-mega-menu__wrapper', this.element);
if (wrappers) {
wrappers.forEach((wrapper) => {
wrapper.style.top = '';
wrapper.style.height = '';
});
}
if (this.openPanel.num > 0) {
if (this.header) {
if (this.headerBanner) {
this.headerBanner.style.display = 'none';
}
if (this.headerNotification) {
this.headerNotification.style.display = 'none';
}
}
}
// Two panels are opened
if (this.openPanel.num === 2) {
const subItemExpanded = queryOne(
'.ecl-mega-menu__subitem--expanded',
this.element,
);
if (subItemExpanded) {
subItemExpanded.firstChild.classList.add(
'ecl-mega-menu__parent-link',
);
}
const menuItem = this.openPanel.item;
// Hide siblings
const siblings = menuItem.parentNode.childNodes;
siblings.forEach((sibling) => {
if (sibling !== menuItem) {
sibling.style.display = 'none';
}
});
}
} else if (subLists && viewport === 'desktop' && !compact) {
// Reset styles for the sublist and subitems
subLists.forEach((list) => {
list.classList.remove('ecl-mega-menu__sublist--scrollable');
list.childNodes.forEach((item) => {
item.style.display = '';
});
});
infoPanels.forEach((info) => {
info.style.top = '';
});
// Check if we have an open item, if we don't hide the overlay and enable scroll
const currentItems = [];
const currentItem = queryOne(
'.ecl-mega-menu__subitem--expanded',
this.element,
);
if (currentItem) {
currentItem.firstElementChild.classList.remove(
'ecl-mega-menu__parent-link',
);
currentItems.push(currentItem);
}
const currentSubItem = queryOne(
'.ecl-mega-menu__item--expanded',
this.element,
);
if (currentSubItem) {
currentItems.push(currentSubItem);
}
if (currentItems.length > 0) {
currentItems.forEach((current) => {
this.checkDropdownHeight(current);
});
} else {
this.element.setAttribute('aria-expanded', 'false');
this.element.removeAttribute('data-expanded');
this.open.setAttribute('aria-expanded', 'false');
this.enableScroll();
}
} else if (viewport === 'desktop' && compact) {
const currentSubItem = queryOne(
'.ecl-mega-menu__subitem--expanded',
this.element,
);
if (currentSubItem) {
currentSubItem.firstElementChild.classList.remove(
'ecl-mega-menu__parent-link',
);
}
infoPanels.forEach((info) => {
info.style.height = '';
});
}
if (viewport === 'desktop' && this.header) {
if (this.headerBanner) {
this.headerBanner.style.display = 'flex';
}
if (this.headerNotification) {
this.headerNotification.style.display = 'flex';
}
}
}
/**
* Trigger events on resize
* Uses a debounce, for performance
*/
handleResize() {
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
const screenWidth = window.innerWidth;
if (this.prevScreenWidth !== undefined) {
// Check if the transition involves crossing the L breakpoint
const isTransition =
(this.prevScreenWidth <= this.breakpointL &&
screenWidth > this.breakpointL) ||
(this.prevScreenWidth > this.breakpointL &&
screenWidth <= this.breakpointL);
// If we are moving in or out the L breakpoint, reset the styles
if (isTransition) {
this.resetStyles(
screenWidth > this.breakpointL ? 'desktop' : 'mobile',
);
}
if (this.prevScreenWidth > 1140 && screenWidth > 996) {
this.resetStyles('desktop', true);
}
}
this.isDesktop = this.useDesktopDisplay();
this.isLarge = window.innerWidth > 1140;
// Update previous screen width
this.prevScreenWidth = screenWidth;
this.element.classList.remove('ecl-mega-menu--forced-mobile');
// RTL
this.direction = getComputedStyle(this.element).direction;
if (this.direction === 'rtl') {
this.element.classList.add('ecl-mega-menu--rtl');
} else {
this.element.classList.remove('ecl-mega-menu--rtl');
}
// Check droopdown height if needed
const expanded = queryOne('.ecl-mega-menu__item--expanded', this.element);
if (expanded && this.isDesktop) {
this.checkDropdownHeight(expanded);
}
// Check the menu position
this.positionMenuOverlay();
}, 200);
}
/**
* Calculate dropdown height dynamically
*
* @param {Node} menuItem
*/
checkDropdownHeight(menuItem) {
const infoPanel = queryOne('.ecl-mega-menu__info', menuItem);
const mainPanel = queryOne('.ecl-mega-menu__mega', menuItem);
// Hide the panels while calculating their heights
if (mainPanel && this.isDesktop) {
mainPanel.style.opacity = 0;
}
if (infoPanel && this.isDesktop) {
infoPanel.style.opacity = 0;
}
setTimeout(() => {
const viewportHeight = window.innerHeight;
let infoPanelHeight = 0;
if (this.isDesktop) {
const heights = [];
let height = 0;
let secondPanel = null;
let featuredPanel = null;
let itemsHeight = 0;
let subItemsHeight = 0;
if (infoPanel) {
infoPanelHeight = infoPanel.scrollHeight + 16;
}
if (infoPanel && this.isLarge) {
heights.push(infoPanelHeight);
} else if (infoPanel && this.isDesktop) {
itemsHeight = infoPanelHeight;
subItemsHeight = infoPanelHeight;
}
if (mainPanel) {
const mainTop = mainPanel.getBoundingClientRect().top;
const list = queryOne('.ecl-mega-menu__sublist', mainPanel);
if (!list) {
const isContainer = menuItem.classList.contains(
'ecl-mega-menu__item--has-container',
);
if (isContainer) {
const container = queryOne(
'.ecl-mega-menu__mega-container',
menuItem,
);
if (container) {
container.firstElementChild.style.height = `${viewportHeight - mainTop}px`;
return;
}
}
} else {
const items = list.children;
if (items.length > 0) {
Array.from(items).forEach((item) => {
itemsHeight += item.getBoundingClientRect().height;
});
heights.push(itemsHeight);
}
}
}
const expanded = queryOne(
'.ecl-mega-menu__subitem--expanded',
menuItem,
);
if (expanded) {
secondPanel = queryOne('.ecl-mega-menu__mega--level-2', expanded);
if (secondPanel) {
const subItems = queryAll(`${this.subItemSelector} a`, secondPanel);
if (subItems.length > 0) {
subItems.forEach((item) => {
subItemsHeight += item.getBoundingClientRect().height;
});
}
heights.push(subItemsHeight);
featuredPanel = queryOne('.ecl-mega-menu__featured', expanded);
if (featuredPanel) {
heights.push(featuredPanel.scrollHeight);
}
}
}
const maxHeight = Math.max(...heights);
const containerBounding = this.inner.getBoundingClientRect();
const containerBottom = containerBounding.bottom;
// By requirements, limit the height to the 70% of the available space.
const availableHeight = (window.innerHeight - containerBottom) * 0.7;
if (maxHeight > availableHeight) {
height = availableHeight;
} else {
height = maxHeight;
}
const wrapper = queryOne('.ecl-mega-menu__wrapper', menuItem);
if (wrapper) {
wrapper.style.height = `${height}px`;
}
if (mainPanel && this.isLarge) {
mainPanel.style.height = `${height}px`;
} else if (mainPanel && infoPanel && this.isDesktop) {
mainPanel.style.height = `${height - infoPanelHeight}px`;
}
if (infoPanel && this.isLarge) {
infoPanel.style.height = `${height}px`;
}
if (secondPanel && this.isLarge) {
secondPanel.style.height = `${height}px`;
} else if (secondPanel && this.isDesktop) {
secondPanel.style.height = `${height - infoPanelHeight}px`;
}
if (featuredPanel && this.isLarge) {
featuredPanel.style.height = `${height}px`;
} else if (featuredPanel && this.isDesktop) {
featuredPanel.style.height = `${height - infoPanelHeight}px`;
}
}
if (mainPanel && this.isDesktop) {
mainPanel.style.opacity = 1;
}
if (infoPanel && this.isDesktop) {
infoPanel.style.opacity = 1;
}
}, 100);
}
/**
* Dinamically set the position of the menu overlay
*/
positionMenuOverlay() {
const menuOverlay = queryOne('.ecl-mega-menu__overlay', this.element);
let availableHeight = 0;
if (!this.isDesktop) {
// In mobile, we get the bottom position of the site header header
setTimeout(() => {
if (this.header) {
const position = this.header.getBoundingClientRect();
const bottomPosition = Math.round(position.bottom);
if (menuOverlay) {
menuOverlay.style.top = `${bottomPosition}px`;
}
if (this.inner) {
this.inner.style.top = `${bottomPosition}px`;
}
const item = queryOne('.ecl-mega-menu__item--expanded', this.element);
if (item) {
const subList = queryOne('.ecl-mega-menu__sublist', item);
if (subList && this.openPanel.num === 1) {
const info = queryOne('.ecl-mega-menu__info', item);
if (info) {
const bottomRect = info.getBoundingClientRect();
const bottomInfo = bottomRect.bottom;
availableHeight = window.innerHeight - bottomInfo - 16;
subList.classList.add('ecl-mega-menu__sublist--scrollable');
subList.style.height = `${availableHeight}px`;
}
} else if (subList) {
subList.classList.remove('ecl-mega-menu__sublist--scrollable');
subList.style.height = '';
}
}
if (this.openPanel.num === 2) {
const subItem = queryOne(
'.ecl-mega-menu__subitem--expanded',
this.element,
);
if (subItem) {
const subMega = queryOne(
'.ecl-mega-menu__mega--level-2',
subItem,
);
if (subMega) {
const subMegaRect = subMega.getBoundingClientRect();
const subMegaTop = subMegaRect.top;
availableHeight = window.innerHeight - subMegaTop;
subMega.style.height = `${availableHeight}px`;
}
}
}
const wrappers = queryAll('.ecl-mega-menu__wrapper', this.element);
if (wrappers) {
wrappers.forEach((wrapper) => {
wrapper.style.top = '';
wrapper.style.height = '';
});
}
}
}, 0);
} else {
setTimeout(() => {
// In desktop we get the bottom position of the whole site header
const siteHeader = queryOne('.ecl-site-header', document);
if (siteHeader) {
const headerRect = siteHeader.getBoundingClientRect();
const headerBottom = headerRect.bottom;
const item = queryOne(this.itemSelector, this.element);
const rect = item.getBoundingClientRect();
const rectHeight = rect.height;
const wrappers = queryAll('.ecl-mega-menu__wrapper', this.element);
if (wrappers) {
wrappers.forEach((wrapper) => {
wrapper.style.top = `${rectHeight}px`;
});
}
if (menuOverlay) {
menuOverlay.style.top = `${headerBottom}px`;
}
} else {
const bottomPosition = this.element.getBoundingClientRect().bottom;
if (menuOverlay) {
menuOverlay.style.top = `${bottomPosition}px`;
}
}
}, 0);
}
}
/**
* Handles keyboard events specific to the menu.
*
* @param {Event} e
*/
handleKeyboard(e) {
const element = e.target;
const cList = element.classList;
const menuExpanded = this.element.getAttribute('aria-expanded');
// Detect press on Escape
if (e.key === 'Escape' || e.key === 'Esc') {
if (document.activeElement === element) {
element.blur();
}
if (menuExpanded === 'false') {
this.closeOpenDropdown();
}
return;
}
// Handle Keyboard on the first panel
if (cList.contains('ecl-mega-menu__info-link')) {
if (e.key === 'ArrowUp') {
if (this.isDesktop) {
// Focus on the expanded nav item
queryOne(
'.ecl-mega-menu__item--expanded button',
this.element,
).focus();
} else if (this.back && !this.isDesktop) {
// focus on the back button
this.back.focus();
}
}
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
// First item in the open dropdown.
element.parentElement.parentElement.nextSibling.firstChild.firstChild.firstChild.focus();
}
}
if (cList.contains('ecl-mega-menu__parent-link')) {
if (e.key === 'ArrowUp') {
const back = queryOne('.ecl-mega-menu__back', this.element);
back.focus();
return;
}
if (e.key === 'ArrowDown') {
const mega = e.target.nextSibling;
mega.firstElementChild.firstElementChild.firstChild.focus();
return;
}
}
// Handle keyboard on the see all links
if (element.parentElement.classList.contains('ecl-mega-menu__see-all')) {
if (e.key === 'ArrowUp') {
// Focus on the last element of the sub-list
element.parentElement.previousSibling.firstChild.focus();
}
if (e.key === 'ArrowDown') {
// Focus on the fi
const featured = element.parentElement.parentElement.nextSibling;
if (featured) {
const focusableSelectors = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
];
const focusableElements = queryAll(
focusableSelectors.join(', '),
featured,
);
if (focusableElements.length > 0) {
focusableElements[0].focus();
}
}
}
}
// Handle keyboard on the back button
if (cList.contains('ecl-mega-menu__back')) {
if (e.key === 'ArrowDown') {
e.preventDefault();
const expanded = queryOne(
'[aria-expanded="true"]',
element.parentElement.nextSibling,
);
// We have an opened list
if (expanded) {
const innerExpanded = queryOne(
'.ecl-mega-menu__subitem--expanded',
expanded.parentElement,
);
// We have an opened sub-list
if (innerExpanded) {
const parentLink = queryOne(
'.ecl-mega-menu__parent-link',
innerExpanded,
);
if (parentLink) {
parentLink.focus();
}
} else {
const infoLink = queryOne(
'.ecl-mega-menu__info-link',
expanded.parentElement,
);
if (infoLink) {
infoLink.focus();
} else {
queryOne(
'.ecl-mega-menu__subitem:first-child .ecl-mega-menu__sublink',
expanded.parentElement,
).focus();
}
}
}
}
if (e.key === 'ArrowUp') {
// Focus on the open button
this.open.focus();
}
}
// Key actions to navigate between first level menu items
if (cList.contains('ecl-mega-menu__link')) {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
let prevItem = element.previousSibling;
if (prevItem && prevItem.classList.contains('ecl-mega-menu__link')) {
prevItem.focus();
return;
}
prevItem = element.parentElement.previousSibling;
if (prevItem) {
const prevLink = queryOne('.ecl-mega-menu__link', prevItem);
if (prevLink) {
prevLink.focus();
return;
}
}
}
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
if (
element.parentElement.getAttribute('aria-expanded') === 'true' &&
e.key === 'ArrowDown'
) {
const infoLink = queryOne(
'.ecl-mega-menu__info-link',
element.parentElement,
);
if (infoLink) {
infoLink.focus();
return;
}
}
const nextItem = element.parentElement.nextSibling;
if (nextItem) {
const nextLink = queryOne('.ecl-mega-menu__link', nextItem);
if (nextLink) {
nextLink.focus();
return;
}
}
}
}
// Key actions to navigate between the sub-links
if (cList.contains('ecl-mega-menu__sublink')) {
if (e.key === 'ArrowDown') {
e.preventDefault();
const nextItem = element.parentElement.nextSibling;
let nextLink = '';
if (nextItem) {
nextLink = queryOne('.ecl-mega-menu__sublink', nextItem);
if (
!nextLink &&
nextItem.classList.contains('ecl-mega-menu__spacer')
) {
nextLink = nextItem.nextSibling.firstElementChild;
}
if (nextLink) {
nextLink.focus();
return;
}
}
}
if (e.key === 'ArrowUp') {
e.preventDefault();
const prevItem = element.parentElement.previousSibling;
if (prevItem) {
const prevLink = queryOne('.ecl-mega-menu__sublink', prevItem);
if (prevLink) {
prevLink.focus();
}
} else {
const moreLink = queryOne(
'.ecl-mega-menu__info-link',
element.parentElement.parentElement.parentElement.previousSibling,
);
if (moreLink) {
moreLink.focus();
} else if (this.openPanel.num === 2) {
const parent = e.target.closest(
'.ecl-mega-menu__mega',
).previousSibling;
if (parent) {
parent.focus();
}
} else if (this.back) {
this.back.focus();
}
}
}
}
if (e.key === 'ArrowRight') {
const expanded =
element.parentElement.getAttribute('aria-expanded') === 'true';
if (expanded) {
e.preventDefault();
// Focus on the first element in the second panel
element.nextSibling.firstElementChild.firstChild.firstChild.focus();
}
}
}
/**
* Handles global keyboard events, triggered outside of the menu.
*
* @param {Event} e
*/
handleKeyboardGlobal(e) {
// Detect press on Escape
if (e.key === 'Escape' || e.key === 'Esc') {
if (this.isOpen) {
this.closeOpenDropdown(true);
}
}
}
/**
* Open menu list.
*
* @param {Event} e
*
* @fires MegaMenu#onOpen
*/
handleClickOnOpen(e) {
if (this.isOpen) {
this.handleClickOnClose(e);
} else {
e.preventDefault();
this.disableScroll();
this.element.setAttribute('aria-expanded', 'true');
this.element.classList.add('ecl-mega-menu--start-panel');
this.element.classList.remove(
'ecl-mega-menu--one-panel',
'ecl-mega-menu--two-panels',
);
this.open.setAttribute('aria-expanded', 'true');
this.inner.setAttribute('aria-hidden', 'false');
this.isOpen = true;
if (this.header) {
this.header.classList.add(
'ecl-site-header--open-menu',
'ecl-site-header--open-menu-start',
);
}
// Update label
const closeLabel = this.element.getAttribute(this.labelCloseAttribute);
if (this.toggleLabel && closeLabel) {
this.toggleLabel.innerHTML = closeLabel;
}
this.positionMenuOverlay();
// Focus first element
if (this.links.length > 0) {
this.links[0].focus();
}
this.trigger('onOpen', e);
}
}
/**
* Close menu list.
*
* @param {Event} e
*
* @fires Menu#onClose
*/
handleClickOnClose(e) {
if (this.element.getAttribute('aria-expanded') === 'true') {
this.focusTrap.deactivate();
this.closeOpenDropdown();
this.trigger('onClose', e);
} else {
this.handleClickOnOpen(e);
}
}
/**
* Toggle menu list.
*
* @param {Event} e
*/
handleClickOnToggle(e) {
e.preventDefault();
if (this.isOpen) {
this.handleClickOnClose(e);
} else {
this.handleClickOnOpen(e);
}
}
/**
* Get back to previous list (on mobile)
*
* @fires MegaMenu#onBack
*/
handleClickOnBack() {
const infoPanels = queryAll('.ecl-mega-menu__info', this.element);
infoPanels.forEach((info) => {
info.style.top = '';
});
const level2 = queryOne('.ecl-mega-menu__subitem--expanded', this.element);
if (level2) {
this.element.classList.remove(
'ecl-mega-menu--two-panels',
'ecl-mega-menu--start-panel',
);
this.element.classList.add('ecl-mega-menu--one-panel');
level2.setAttribute('aria-expanded', 'false');
level2.classList.remove(
'ecl-mega-menu__subitem--expanded',
'ecl-mega-menu__subitem--current',
);
const itemLink = queryOne(this.subLinkSelector, level2);
itemLink.setAttribute('aria-expanded', 'false');
itemLink.classList.remove('ecl-mega-menu__parent-link');
const siblings = level2.parentElement.childNodes;
if (siblings) {
siblings.forEach((sibling) => {
sibling.style.display = '';
});
}
if (this.header) {
this.header.classList.remove('ecl-site-header--open-menu-start');
}
// Move the focus to the previously selected item
if (this.backItemLevel2) {
this.backItemLevel2.firstElementChild.focus();
}
this.openPanel.num = 1;
} else {
if (this.header) {
if (this.headerBanner) {
this.headerBanner.style.display = 'flex';
}
if (this.headerNotification) {
this.headerNotification.style.display = 'flex';
}
}
// Remove expanded class from inner menu
this.inner.classList.remove('ecl-mega-menu__inner--expanded');
this.element.classList.remove('ecl-mega-menu--one-panel');
// Remove css class and attribute from menu items
this.items.forEach((item) => {
item.classList.remove(
'ecl-mega-menu__item--expanded',
'ecl-mega-menu__item--current',
);
const itemLink = queryOne(this.linkSelector, item);
itemLink.setAttribute('aria-expanded', 'false');
});
// Move the focus to the previously selected item
if (this.backItemLevel1) {
this.backItemLevel1.firstElementChild.focus();
} else {
this.items[0].firstElementChild.focus();
}
this.openPanel.num = 0;
if (this.header) {
this.header.classList.add('ecl-site-header--open-menu-start');
}
this.positionMenuOverlay();
}
this.trigger('onBack', { level: level2 ? 2 : 1 });
}
/**
* Show/hide the first panel
*
* @param {Node} menuItem
* @param {string} op (expand or collapse)
*
* @fires MegaMenu#onOpenPanel
*/
handleFirstPanel(menuItem, op) {
switch (op) {
case 'expand': {
this.inner.classList.add('ecl-mega-menu__inner--expanded');
this.positionMenuOverlay();
this.checkDropdownHeight(menuItem);
this.element.setAttribute('data-expanded', true);
this.element.setAttribute('aria-expanded', 'true');
this.element.classList.add('ecl-mega-menu--one-panel');
this.element.classList.remove('ecl-mega-menu--start-panel');
this.open.setAttribute('aria-expanded', 'true');
if (this.header) {
this.header.classList.add('ecl-site-header--open-menu');
this.header.classList.remove('ecl-site-header--open-menu-start');
if (!this.isDesktop) {
if (this.headerBanner) {
this.headerBanner.style.display = 'none';
}
if (this.headerNotification) {
this.headerNotification.style.display = 'none';
}
}
}
this.disableScroll();
this.isOpen = true;
this.items.forEach((item) => {
const itemLink = queryOne(this.linkSelector, item);
if (itemLink.hasAttribute('aria-expanded')) {
if (item === menuItem) {
item.classList.add(
'ecl-mega-menu__item--expanded',
'ecl-mega-menu__item--current',
);
itemLink.setAttribute('aria-expanded', 'true');
this.backItemLevel1 = item;
} else {
itemLink.setAttribute('aria-expanded', 'false');
item.classList.remove(
'ecl-mega-menu__item--current',
'ecl-mega-menu__item--expanded',
);
}
}
});
if (!this.isDesktop && this.back) {
this.back.focus();
}
this.openPanel = {
num: 1,
item: menuItem,
};
const details = { panel: 1, item: menuItem };
this.trigger('OnOpenPanel', details);
if (this.isDesktop) {
const list = queryOne('.ecl-mega-menu__sublist', menuItem);
if (list) {
// Expand the first item in the sublist if it contains children.
const expandedChild = Array.from(
list.children,
)[0].firstElementChild.hasAttribute('aria-expanded')
? Array.from(list.children)[0]
: false;
if (expandedChild) {
this.handleSecondPanel(expandedChild, 'expand');
}
}
}
break;
}
case 'collapse':
this.closeOpenDropdown();
break;
default:
}
}
/**
* Show/hide the second panel
*
* @param {Node} menuItem
* @param {string} op (expand or collapse)
*
* @fires MegaMenu#onOpenPanel
*/
handleSecondPanel(menuItem, op) {
const infoPanel = queryOne(
'.ecl-mega-menu__info',
menuItem.closest('.ecl-container'),
);
let siblings;
switch (op) {
case 'expand': {
this.element.classList.remove(
'ecl-mega-menu--one-panel',
'ecl-mega-menu--start-panel',
);
this.element.classList.add('ecl-mega-menu--two-panels');
this.subItems.forEach((item) => {
const itemLink = queryOne(this.subLinkSelector, item);
if (item === menuItem) {
if (itemLink.hasAttribute('aria-expanded')) {
itemLink.setAttribute('aria-expanded', 'true');
if (!this.isDesktop) {
// We use this class mainly to recover the default behavior of the link.
itemLink.classList.add('ecl-mega-menu__parent-link');
if (this.back) {
this.back.focus();
}
}
item.classList.add('ecl-mega-menu__subitem--expanded');
}
item.classList.add('ecl-mega-menu__subitem--current');
this.backItemLevel2 = item;
} else {
if (itemLink.hasAttribute('aria-expanded')) {
itemLink.setAttribute('aria-expanded', 'false');
itemLink.classList.remove('ecl-mega-menu__parent-link');
item.classList.remove('ecl-mega-menu__subitem--expanded');
}
item.classList.remove('ecl-mega-menu__subitem--current');
}
});
this.openPanel = { num: 2, item: menuItem };
siblings = menuItem.parentNode.childNodes;
if (this.isDesktop) {
// Reset style for the siblings, in case they were hidden
siblings.forEach((sibling) => {
if (sibling !== menuItem) {
sibling.style.display = '';
}
});
} else {
// Hide other items in the sublist
siblings.forEach((sibling) => {
if (sibling !== menuItem) {
sibling.style.display = 'none';
}
});
}
this.positionMenuOverlay();
const details = { panel: 2, item: menuItem };
this.trigger('OnOpenPanel', details);
break;
}
case 'collapse':
this.element.classList.remove('ecl-mega-menu--two-panels');
this.openPanel = { num: 1 };
// eslint-disable-next-line no-case-declarations
const itemLink = queryOne(this.subLinkSelector, menuItem);
itemLink.setAttribute('aria-expanded', 'false');
menuItem.classList.remove(
'ecl-mega-menu__subitem--expanded',
'ecl-mega-menu__subitem--current',
);
if (infoPanel) {
infoPanel.style.top = '';
}
break;
default:
}
}
/**
* Click on a menu item
*
* @param {Event} e
*
* @fires MegaMenu#onItemClick
*/
handleClickOnItem(e) {
let isInTheContainer = false;
const menuItem = e.target.closest('li');
const container = queryOne(
'.ecl-mega-menu__mega-container-scrollable',
menuItem,
);
if (container) {
isInTheContainer = container.contains(e.target);
}
// We need to ensure that the click doesn't come from a parent link
// or from an open container, in that case we do not act.
if (
!e.target.classList.contains(
'ecl-mega-menu__mega-container-scrollable',
) &&
!isInTheContainer
) {
this.trigger('onItemClick', { item: menuItem, event: e });
const hasChildren =
menuItem.firstElementChild.getAttribute('aria-expanded');
if (hasChildren && menuItem.classList.contains('ecl-mega-menu__item')) {
e.preventDefault();
e.stopPropagation();
if (!this.isDesktop) {
this.handleFirstPanel(menuItem, 'expand');
} else {
const isOpen = hasChildren === 'true';
if (isOpen) {
this.handleFirstPanel(menuItem, 'collapse');
} else {
this.closeOpenDropdown();
this.handleFirstPanel(menuItem, 'expand');
}
}
}
}
}
/**
* Click on a subitem
*
* @param {Event} e
*/
handleClickOnSubitem(e) {
const menuItem = e.target.closest(this.subItemSelector);
if (menuItem && menuItem.firstElementChild.hasAttribute('aria-expanded')) {
const parentLink = queryOne('.ecl-mega-menu__parent-link', menuItem);
if (parentLink) {
return;
}
e.preventDefault();
e.stopPropagation();
const isExpanded =
menuItem.firstElementChild.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
this.handleSecondPanel(menuItem, 'collapse');
} else {
this.handleSecondPanel(menuItem, 'expand');
}
}
}
/**
* Deselect any opened menu item
*
* @param {boolean} esc, whether the call was originated by a press on Esc
*
* @fires MegaMenu#onFocusTrapToggle
*/
closeOpenDropdown(esc = false) {
if (this.header) {
this.header.classList.remove(
'ecl-site-header--open-menu',
'ecl-site-header--open-menu-start',
);
if (this.headerBanner) {
this.headerBanner.style.display = 'flex';
}
if (this.headerNotification) {
this.headerNotification.style.display = 'flex';
}
}
this.enableScroll();
this.element.setAttribute('aria-expanded', 'false');
this.element.removeAttribute('data-expanded');
this.element.classList.remove(
'ecl-mega-menu--start-panel',
'ecl-mega-menu--two-panels',
'ecl-mega-menu--one-panel',
);
this.open.setAttribute('aria-expanded', 'false');
// Remove css class and attribute from inner menu
this.inner.classList.remove('ecl-mega-menu__inner--expanded');
// Reset heights
const megaMenus = queryAll(
'.ecl-mega-menu__item > .ecl-mega-menu__wrapper > .ecl-container > [data-ecl-mega-menu-mega]',
this.element,
);
megaMenus.forEach((mega) => {
mega.style.height = '';
mega.style.top = '';
});
let currentItem = false;
// Remove css class and attribute from menu items
this.items.forEach((item) => {
item.classList.remove('ecl-mega-menu__item--current');
const itemLink = queryOne(this.linkSelector, item);
if (itemLink.getAttribute('aria-expanded') === 'true') {
item.classList.remove('ecl-mega-menu__item--expanded');
itemLink.setAttribute('aria-expanded', 'false');
currentItem = itemLink;
}
});
// Remove css class and attribute from menu subitems
this.subItems.forEach((item) => {
item.classList.remove('ecl-mega-menu__subitem--current');
item.style.display = '';
const itemLink = queryOne(this.subLinkSelector, item);
if (itemLink.hasAttribute('aria-expanded')) {
item.classList.remove('ecl-mega-menu__subitem--expanded');
item.style.display = '';
itemLink.setAttribute('aria-expanded', 'false');
itemLink.classList.remove('ecl-mega-menu__parent-link');
}
});
// Remove styles set for the sublists
const sublists = queryAll('.ecl-mega-menu__sublist');
if (sublists) {
sublists.forEach((sublist) => {
sublist.classList.remove(
'ecl-mega-menu__sublist--no-border',
'.ecl-mega-menu__sublist--scrollable',
);
});
}
// Update label
const openLabel = this.element.getAttribute(this.labelOpenAttribute);
if (this.toggleLabel && openLabel) {
this.toggleLabel.innerHTML = openLabel;
}
this.openPanel = {
num: 0,
item: false,
};
// If the focus trap is active, deactivate it
this.focusTrap.deactivate();
// Focus on the open button in mobile or on the formerly expanded item in desktop.
if (!this.isDesktop && this.open && esc) {
this.open.focus();
} else if (this.isDesktop && currentItem && esc) {
currentItem.focus();
}
this.trigger('onFocusTrapToggle', { active: false });
this.isOpen = false;
}
/**
* Focus out of a menu link
*
* @param {Event} e
*
* @fires MegaMenu#onFocusTrapToggle
*/
handleFocusOut(e) {
const element = e.target;
const menuExpanded = this.element.getAttribute('aria-expanded');
// Specific focus action for mobile menu
// Loop through the items and go back to close button
if (menuExpanded === 'true' && !this.isDesktop) {
const nextItem = element.parentElement.nextSibling;
if (!nextItem) {
const nextFocusTarget = e.relatedTarget;
if (!this.element.contains(nextFocusTarget)) {
// This is the last item, go back to close button
this.focusTrap.activate();
this.trigger('onFocusTrapToggle', {
active: true,
lastFocusedEl: element.parentElement,
});
}
}
}
}
/**
* Handles global click events, triggered outside of the menu.
*
* @param {Event} e
*/
handleClickGlobal(e) {
if (
!e.target.classList.contains(
'ecl-mega-menu__mega-container-scrollable',
) &&
(e.target.classList.contains('ecl-mega-menu__overlay') ||
!this.element.contains(e.target)) &&
this.isOpen
) {
this.closeOpenDropdown();
}
}
}
export default MegaMenu;