menu.js

import Stickyfill from 'stickyfilljs';
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.closeSelector Selector for the close button
 * @param {String} options.backSelector Selector for the back button
 * @param {String} options.innerSelector Selector for the menu inner
 * @param {String} options.listSelector Selector for the menu items list
 * @param {String} options.itemSelector Selector for the menu item
 * @param {String} options.linkSelector Selector for the menu link
 * @param {String} options.buttonPreviousSelector Selector for the previous items button (for overflow)
 * @param {String} options.buttonNextSelector Selector for the next items button (for overflow)
 * @param {String} options.megaSelector Selector for the mega menu
 * @param {String} options.subItemSelector Selector for the menu sub items
 * @param {Int} options.maxLines Number of lines maximum for each menu item (for overflow). Set it to zero to disable automatic resize.
 * @param {String} options.maxLinesAttribute The data attribute to set the max lines in the markup, if needed
 * @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.attachTouchListener Whether or not to bind touch 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 Menu {
  /**
   * @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, { MENU: defaultOptions = {} } = {}) {
    const menu = new Menu(root, defaultOptions);
    menu.init();
    root.ECLMenu = menu;
    return menu;
  }

  /**
   *   @event Menu#onOpen
   */
  /**
   *   @event Menu#onClose
   */

  /**
   * An array of supported events for this component.
   *
   * @type {Array<string>}
   * @memberof Menu
   */
  supportedEvents = ['onOpen', 'onClose'];

  constructor(
    element,
    {
      openSelector = '[data-ecl-menu-open]',
      closeSelector = '[data-ecl-menu-close]',
      backSelector = '[data-ecl-menu-back]',
      innerSelector = '[data-ecl-menu-inner]',
      listSelector = '[data-ecl-menu-list]',
      itemSelector = '[data-ecl-menu-item]',
      linkSelector = '[data-ecl-menu-link]',
      buttonPreviousSelector = '[data-ecl-menu-items-previous]',
      buttonNextSelector = '[data-ecl-menu-items-next]',
      caretSelector = '[data-ecl-menu-caret]',
      megaSelector = '[data-ecl-menu-mega]',
      subItemSelector = '[data-ecl-menu-subitem]',
      maxLines = 2,
      maxLinesAttribute = 'data-ecl-menu-max-lines',
      labelOpenAttribute = 'data-ecl-menu-label-open',
      labelCloseAttribute = 'data-ecl-menu-label-close',
      attachClickListener = true,
      attachTouchListener = true,
      attachHoverListener = true,
      attachFocusListener = true,
      attachKeyListener = true,
      attachResizeListener = true,
      onCloseCallback = null,
      onOpenCallback = null,
    } = {},
  ) {
    // 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.closeSelector = closeSelector;
    this.backSelector = backSelector;
    this.innerSelector = innerSelector;
    this.listSelector = listSelector;
    this.itemSelector = itemSelector;
    this.linkSelector = linkSelector;
    this.buttonPreviousSelector = buttonPreviousSelector;
    this.buttonNextSelector = buttonNextSelector;
    this.caretSelector = caretSelector;
    this.megaSelector = megaSelector;
    this.subItemSelector = subItemSelector;
    this.maxLines = maxLines;
    this.maxLinesAttribute = maxLinesAttribute;
    this.labelOpenAttribute = labelOpenAttribute;
    this.labelCloseAttribute = labelCloseAttribute;
    this.attachClickListener = attachClickListener;
    this.attachTouchListener = attachTouchListener;
    this.attachHoverListener = attachHoverListener;
    this.attachFocusListener = attachFocusListener;
    this.attachKeyListener = attachKeyListener;
    this.attachResizeListener = attachResizeListener;
    this.onOpenCallback = onOpenCallback;
    this.onCloseCallback = onCloseCallback;

    // Private variables
    this.direction = 'ltr';
    this.open = null;
    this.close = null;
    this.toggleLabel = null;
    this.back = null;
    this.backItem = null;
    this.inner = null;
    this.itemsList = null;
    this.items = null;
    this.links = null;
    this.btnPrevious = null;
    this.btnNext = null;
    this.isOpen = false;
    this.resizeTimer = null;
    this.isKeyEvent = false;
    this.isDesktop = false;
    this.hasOverflow = false;
    this.offsetLeft = 0;
    this.lastVisibleItem = null;
    this.currentItem = null;
    this.totalItemsWidth = 0;
    this.breakpointL = 996;
    this.windowWidth = null;
    this.ignorehover = false;

    // 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.handleClickOnNextItems = this.handleClickOnNextItems.bind(this);
    this.handleClickOnPreviousItems =
      this.handleClickOnPreviousItems.bind(this);
    this.handleClickOnCaret = this.handleClickOnCaret.bind(this);
    this.handleClickGlobal = this.handleClickGlobal.bind(this);
    this.openItem = this.openItem.bind(this);
    this.closeItem = this.closeItem.bind(this);
    this.handleTouchOnCaret = this.handleTouchOnCaret.bind(this);
    this.handleHoverOnItem = this.handleHoverOnItem.bind(this);
    this.handleHoverOffItem = this.handleHoverOffItem.bind(this);
    this.handleFocusIn = this.handleFocusIn.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.checkMenuOverflow = this.checkMenuOverflow.bind(this);
    this.checkMenuItem = this.checkMenuItem.bind(this);
    this.checkMegaMenu = this.checkMegaMenu.bind(this);
    this.closeOpenDropdown = this.closeOpenDropdown.bind(this);
    this.positionMenuOverlay = this.positionMenuOverlay.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();
    // Check display
    this.direction = getComputedStyle(this.element).direction;

    // Query elements
    this.open = queryOne(this.openSelector, this.element);
    this.close = queryOne(this.closeSelector, this.element);
    this.toggleLabel = queryOne('.ecl-button__label', this.open);
    this.back = queryOne(this.backSelector, this.element);
    this.inner = queryOne(this.innerSelector, this.element);
    this.itemsList = queryOne(this.listSelector, 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.carets = queryAll(this.caretSelector, this.element);

    // Get extra parameter from markup
    const maxLinesMarkup = this.element.getAttribute(this.maxLinesAttribute);
    if (maxLinesMarkup) {
      this.maxLines = maxLinesMarkup;
    }

    // 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);
      }

      // Close
      if (this.close) {
        this.close.addEventListener('click', this.handleClickOnClose);
      }

      // Back
      if (this.back) {
        this.back.addEventListener('click', this.handleClickOnBack);
      }

      // Previous items
      if (this.btnPrevious) {
        this.btnPrevious.addEventListener(
          'click',
          this.handleClickOnPreviousItems,
        );
      }

      // Next items
      if (this.btnNext) {
        this.btnNext.addEventListener('click', this.handleClickOnNextItems);
      }

      // 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('focusin', this.closeOpenDropdown);
          link.addEventListener('focusin', this.handleFocusIn);
        }
        if (this.attachKeyListener) {
          link.addEventListener('keyup', this.handleKeyboard);
        }
      });
    }

    // Bind event on caret buttons
    if (this.carets) {
      this.carets.forEach((caret) => {
        if (this.attachFocusListener) {
          caret.addEventListener('focusin', this.handleFocusIn);
        }
        if (this.attachKeyListener) {
          caret.addEventListener('keyup', this.handleKeyboard);
        }
        if (this.attachClickListener) {
          caret.addEventListener('click', this.handleClickOnCaret);
        }

        if (caret.parentElement.hasAttribute('data-ecl-has-children')) {
          // Bind touch events on caret
          if (this.attachTouchListener) {
            caret.addEventListener('touchstart', this.handleTouchOnCaret);
          }
        }
      });
    }

    // Bind event on sub menu links
    if (this.subItems) {
      this.subItems.forEach((subItem) => {
        const subLink = queryOne('.ecl-menu__sublink', subItem);
        if (this.attachKeyListener && subLink) {
          subLink.addEventListener('keyup', this.handleKeyboard);
        }
      });
    }

    // Bind global keyboard events
    if (this.attachKeyListener) {
      document.addEventListener('keyup', this.handleKeyboardGlobal);
    }

    // Bind resize events
    if (this.attachResizeListener) {
      this.windowWidth = window.innerWidth;
      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.checkMenuItem(item);
        this.totalItemsWidth += item.offsetWidth;

        if (item.hasAttribute('data-ecl-has-children')) {
          // Bind hover events on menu items
          if (this.attachHoverListener) {
            item.addEventListener('mouseover', this.handleHoverOnItem);
            item.addEventListener('mouseout', this.handleHoverOffItem);
          }
        }
      });
    }

    this.positionMenuOverlay();

    // Update overflow display
    this.checkMenuOverflow();

    // Check if the current item is hidden (one side or the other)
    if (this.currentItem) {
      if (
        this.currentItem.getAttribute('data-ecl-menu-item-visible') === 'false'
      ) {
        this.btnNext.classList.add('ecl-menu__item--current');
      } else {
        this.btnPrevious.classList.add('ecl-menu__item--current');
      }
    }

    // Init sticky header
    this.stickyInstance = new Stickyfill.Sticky(this.element);
    this.focusTrap = createFocusTrap(this.element, {
      onActivate: () => this.element.classList.add('trap-is-active'),
      onDeactivate: () => this.element.classList.remove('trap-is-active'),
    });

    if (this.direction === 'rtl') {
      this.element.classList.add('ecl-menu--rtl');
    }

    // Hack to prevent css transition to be played on page load on chrome
    setTimeout(() => {
      this.element.classList.add('ecl-menu--transition');
    }, 500);

    // 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 Menu
   * @instance
   *
   * @example
   * // Registering a callback for the 'onOpen' event
   * menu.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 Menu
   */
  trigger(eventName, eventData) {
    this.eventManager.trigger(eventName, eventData);
  }

  /**
   * Destroy component.
   */
  destroy() {
    if (this.stickyInstance) {
      this.stickyInstance.remove();
    }

    if (this.attachClickListener) {
      if (this.open) {
        this.open.removeEventListener('click', this.handleClickOnToggle);
      }

      if (this.close) {
        this.close.removeEventListener('click', this.handleClickOnClose);
      }

      if (this.back) {
        this.back.removeEventListener('click', this.handleClickOnBack);
      }

      if (this.btnPrevious) {
        this.btnPrevious.removeEventListener(
          'click',
          this.handleClickOnPreviousItems,
        );
      }

      if (this.btnNext) {
        this.btnNext.removeEventListener('click', this.handleClickOnNextItems);
      }

      if (this.attachClickListener) {
        document.removeEventListener('click', this.handleClickGlobal);
      }
    }

    if (this.attachKeyListener && this.carets) {
      this.carets.forEach((caret) => {
        caret.removeEventListener('keyup', this.handleKeyboard);
      });
    }

    if (this.items && this.isDesktop) {
      this.items.forEach((item) => {
        if (item.hasAttribute('data-ecl-has-children')) {
          if (this.attachHoverListener) {
            item.removeEventListener('mouseover', this.handleHoverOnItem);
            item.removeEventListener('mouseout', this.handleHoverOffItem);
          }
        }
      });
    }

    if (this.links) {
      this.links.forEach((link) => {
        if (this.attachFocusListener) {
          link.removeEventListener('focusin', this.closeOpenDropdown);
          link.removeEventListener('focusin', this.handleFocusIn);
        }
        if (this.attachKeyListener) {
          link.removeEventListener('keyup', this.handleKeyboard);
        }
      });
    }

    if (this.carets) {
      this.carets.forEach((caret) => {
        if (this.attachFocusListener) {
          caret.removeEventListener('focusin', this.handleFocusIn);
        }
        if (this.attachKeyListener) {
          caret.removeEventListener('keyup', this.handleKeyboard);
        }
        if (this.attachClickListener) {
          caret.removeEventListener('click', this.handleClickOnCaret);
        }
      });
    }

    if (this.subItems) {
      this.subItems.forEach((subItem) => {
        const subLink = queryOne('.ecl-menu__sublink', subItem);
        if (this.attachKeyListener && subLink) {
          subLink.removeEventListener('keyup', this.handleKeyboard);
        }
      });
    }

    if (this.attachKeyListener) {
      document.removeEventListener('keyup', this.handleKeyboardGlobal);
    }

    if (this.attachResizeListener) {
      window.removeEventListener('resize', this.handleResize);
    }

    if (this.element) {
      this.element.removeAttribute('data-ecl-auto-initialized');
      ECL.components.delete(this.element);
    }
  }

  /* eslint-disable class-methods-use-this */
  /**
   * Disable page scrolling
   */
  disableScroll() {
    document.body.classList.add('no-scroll');
  }

  /**
   * Enable page scrolling
   */
  enableScroll() {
    document.body.classList.remove('no-scroll');
  }
  /* eslint-enable class-methods-use-this */

  /**
   * 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-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-menu--forced-mobile');
    return true;
  }

  /**
   * Trigger events on resize
   * Uses a debounce, for performance
   */
  handleResize() {
    // Do not trigger the resize event if not needed (when scrolling on mobile)
    if (window.innerWidth !== this.windowWidth) {
      // Scroll to top to ensure the menu is correctly positioned.
      document.documentElement.scrollTop = 0;
      document.body.scrollTop = 0;

      // Disable transition
      this.element.classList.remove('ecl-menu--transition');
      if (this.direction === 'rtl') {
        this.element.classList.add('ecl-menu--rtl');
      } else {
        this.element.classList.remove('ecl-menu--rtl');
      }

      clearTimeout(this.resizeTimer);
      this.resizeTimer = setTimeout(() => {
        this.element.classList.remove('ecl-menu--forced-mobile');

        // Check global display
        this.isDesktop = this.useDesktopDisplay();
        if (this.isDesktop) {
          this.focusTrap.deactivate();
        }
        // Update items display
        this.totalItemsWidth = 0;
        if (this.items) {
          this.items.forEach((item) => {
            this.checkMenuItem(item);
            this.totalItemsWidth += item.offsetWidth;
          });
        }

        // Update overflow display
        this.checkMenuOverflow();
        this.positionMenuOverlay();

        // Bring transition back
        this.element.classList.add('ecl-menu--transition');

        // Update saved width
        this.windowWidth = window.innerWidth;
      }, 200);
    }
  }

  /**
   * Dinamically set the position of the menu overlay
   */
  positionMenuOverlay() {
    const menuOverlay = queryOne('.ecl-menu__overlay', this.element);
    if (!this.isDesktop) {
      if (this.isOpen) {
        this.disableScroll();
      }
      setTimeout(() => {
        const header = queryOne('.ecl-site-header__header', document);
        if (header) {
          const position = header.getBoundingClientRect();
          const bottomPosition = Math.round(position.bottom);
          if (menuOverlay) {
            menuOverlay.style.top = `${bottomPosition}px`;
          }
          if (this.inner) {
            this.inner.style.top = `${bottomPosition}px`;
          }
        }
      }, 500);
    } else {
      this.enableScroll();
      if (this.inner) {
        this.inner.style.top = '';
      }
      if (menuOverlay) {
        menuOverlay.style.top = '';
      }
    }
  }

  /**
   * Check how to display menu horizontally and manage overflow
   */
  checkMenuOverflow() {
    // Backward compatibility
    if (!this.itemsList) {
      this.itemsList = queryOne('.ecl-menu__list', this.element);
    }

    if (
      !this.itemsList ||
      !this.inner ||
      !this.btnNext ||
      !this.btnPrevious ||
      !this.items
    ) {
      return;
    }

    // Check if the menu is too large
    // We take some margin for safety (same margin as the container's padding)
    this.hasOverflow = this.totalItemsWidth > this.inner.offsetWidth + 16;
    if (!this.hasOverflow || !this.isDesktop) {
      // Reset values related to overflow
      if (this.btnPrevious) {
        this.btnPrevious.style.display = 'none';
      }
      if (this.btnNext) {
        this.btnNext.style.display = 'none';
      }
      if (this.itemsList) {
        this.itemsList.style.left = '0';
      }
      if (this.inner) {
        this.inner.classList.remove('ecl-menu__inner--has-overflow');
      }
      this.offsetLeft = 0;
      this.totalItemsWidth = 0;
      this.lastVisibleItem = null;
      return;
    }

    if (this.inner) {
      this.inner.classList.add('ecl-menu__inner--has-overflow');
    }

    // Reset visibility indicator
    if (this.items) {
      this.items.forEach((item) => {
        item.removeAttribute('data-ecl-menu-item-visible');
      });
    }

    // First case: overflow to the end
    if (this.offsetLeft === 0) {
      this.btnNext.style.display = 'flex';

      // Get visible items
      if (this.direction === 'rtl') {
        this.items.every((item) => {
          if (
            item.getBoundingClientRect().left <
            this.itemsList.getBoundingClientRect().left
          ) {
            this.lastVisibleItem = item;
            return false;
          }
          item.setAttribute('data-ecl-menu-item-visible', true);
          return true;
        });
      } else {
        this.items.every((item) => {
          if (
            item.getBoundingClientRect().right >
            this.itemsList.getBoundingClientRect().right
          ) {
            this.lastVisibleItem = item;
            return false;
          }
          item.setAttribute('data-ecl-menu-item-visible', true);
          return true;
        });
      }
    }
    // Second case: overflow to the begining
    else {
      // Get visible items
      // eslint-disable-next-line no-lonely-if
      if (this.direction === 'rtl') {
        this.items.forEach((item) => {
          if (
            item.getBoundingClientRect().right <=
            this.inner.getBoundingClientRect().right
          ) {
            item.setAttribute('data-ecl-menu-item-visible', true);
          }
        });
      } else {
        this.items.forEach((item) => {
          if (
            item.getBoundingClientRect().left >=
            this.inner.getBoundingClientRect().left
          ) {
            item.setAttribute('data-ecl-menu-item-visible', true);
          }
        });
      }
    }
  }

  /**
   * Check for a specific menu item how to display it:
   * - number of lines
   * - mega menu position
   *
   * @param {Node} menuItem
   */
  checkMenuItem(menuItem) {
    const menuLink = queryOne(this.linkSelector, menuItem);

    // Save current menu item
    if (menuItem.classList.contains('ecl-menu__item--current')) {
      this.currentItem = menuItem;
    }

    if (!this.isDesktop) {
      menuLink.style.width = 'auto';
      return;
    }

    // Check if line management has been disabled by user
    if (this.maxLines < 1) return;

    // Handle menu item height and width (n "lines" max)
    // Max height: n * line-height + padding
    // We need to temporally change item alignments to get the height
    menuItem.style.alignItems = 'flex-start';
    let linkWidth = menuLink.offsetWidth;
    const linkStyle = window.getComputedStyle(menuLink);
    const maxHeight =
      parseInt(linkStyle.lineHeight, 10) * this.maxLines +
      parseInt(linkStyle.paddingTop, 10) +
      parseInt(linkStyle.paddingBottom, 10);

    while (menuLink.offsetHeight > maxHeight) {
      menuLink.style.width = `${(linkWidth += 1)}px`;

      // Safety exit
      if (linkWidth > 1000) break;
    }
    menuItem.style.alignItems = 'unset';
  }

  /**
   * Handle positioning of mega menu
   * @param {Node} menuItem
   */
  checkMegaMenu(menuItem) {
    const menuMega = queryOne(this.megaSelector, menuItem);
    if (menuMega && this.inner) {
      // Check number of items and put them in column
      const subItems = queryAll(this.subItemSelector, menuMega);

      if (subItems.length < 5) {
        menuItem.classList.add('ecl-menu__item--col1');
      } else if (subItems.length < 9) {
        menuItem.classList.add('ecl-menu__item--col2');
      } else if (subItems.length < 13) {
        menuItem.classList.add('ecl-menu__item--col3');
      } else {
        menuItem.classList.add('ecl-menu__item--full');
        if (this.direction === 'rtl') {
          menuMega.style.right = `${this.offsetLeft}px`;
        } else {
          menuMega.style.left = `${this.offsetLeft}px`;
        }
        return;
      }

      // Check if there is enough space on the right to display the menu
      const megaBounding = menuMega.getBoundingClientRect();
      const containerBounding = this.inner.getBoundingClientRect();
      const menuItemBounding = menuItem.getBoundingClientRect();

      const megaWidth = megaBounding.width;
      const containerWidth = containerBounding.width;
      const menuItemPosition = menuItemBounding.left - containerBounding.left;

      if (menuItemPosition + megaWidth > containerWidth) {
        menuMega.classList.add('ecl-menu__mega--rtl');
      } else {
        menuMega.classList.remove('ecl-menu__mega--rtl');
      }
    }
  }

  /**
   * 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');
    const menuItem = element.closest(this.itemSelector);

    // Detect press on Escape
    if (e.key === 'Escape' || e.key === 'Esc') {
      if (document.activeElement === element) {
        element.blur();
      }

      if (menuExpanded === 'false') {
        const buttonCaret = queryOne('.ecl-menu__button-caret', menuItem);
        if (buttonCaret) {
          buttonCaret.focus();
        }
        this.closeOpenDropdown();
      }
      return;
    }

    // Key actions to toggle the caret buttons
    if (cList.contains('ecl-menu__button-caret') && menuExpanded === 'false') {
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        const firstItem = queryOne(
          '.ecl-menu__sublink:first-of-type',
          menuItem,
        );
        if (firstItem) {
          this.handleHoverOnItem(e);
          firstItem.focus();
          return;
        }
      }
    }

    // Key actions to navigate between first level menu items
    if (
      cList.contains('ecl-menu__link') ||
      cList.contains('ecl-menu__button-caret')
    ) {
      if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
        e.preventDefault();
        let prevItem = element.previousSibling;

        if (prevItem && prevItem.classList.contains('ecl-menu__link')) {
          prevItem.focus();
          return;
        }

        prevItem = element.parentElement.previousSibling;
        if (prevItem) {
          const prevClass = prevItem.classList.contains(
            'ecl-menu__item--has-children',
          )
            ? '.ecl-menu__button-caret'
            : '.ecl-menu__link';
          const prevLink = queryOne(prevClass, prevItem);

          if (prevLink) {
            prevLink.focus();
            return;
          }
        }
      }
      if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
        e.preventDefault();
        let nextItem = element.nextSibling;

        if (nextItem && nextItem.classList.contains('ecl-menu__button-caret')) {
          nextItem.focus();
          return;
        }
        nextItem = element.parentElement.nextSibling;
        if (nextItem) {
          const nextLink = queryOne('.ecl-menu__link', nextItem);

          if (nextLink) {
            nextLink.focus();
          }
        }
      }
    }

    // Key actions to navigate between the sub-links
    if (cList.contains('ecl-menu__sublink')) {
      if (e.key === 'ArrowDown') {
        const nextItem = element.parentElement.nextSibling;
        if (nextItem) {
          const nextLink = queryOne('.ecl-menu__sublink', nextItem);

          if (nextLink) {
            nextLink.focus();
            return;
          }
        }
      }
      if (e.key === 'ArrowUp') {
        const prevItem = element.parentElement.previousSibling;
        if (prevItem) {
          const prevLink = queryOne('.ecl-menu__sublink', prevItem);

          if (prevLink) {
            prevLink.focus();
          }
        } else {
          const caretButton = queryOne(
            `${this.itemSelector}[aria-expanded="true"] ${this.caretSelector}`,
            this.element,
          );

          if (caretButton) {
            caretButton.focus();
          }
        }
      }
    }
  }

  /**
   * Handles global keyboard events, triggered outside of the menu.
   *
   * @param {Event} e
   */
  handleKeyboardGlobal(e) {
    const menuExpanded = this.element.getAttribute('aria-expanded');

    // Detect press on Escape
    if (e.key === 'Escape' || e.key === 'Esc') {
      if (menuExpanded === 'true') {
        this.handleClickOnClose();
      }
      this.items.forEach((item) => {
        item.setAttribute('aria-expanded', 'false');
      });
      this.carets.forEach((caret) => {
        caret.setAttribute('aria-expanded', 'false');
      });
    }
  }

  /**
   * Open menu list.
   * @param {Event} e
   *
   * @fires Menu#onOpen
   */
  handleClickOnOpen(e) {
    e.preventDefault();

    this.element.setAttribute('aria-expanded', 'true');
    this.inner.setAttribute('aria-hidden', 'false');
    this.disableScroll();
    this.open.setAttribute('aria-expanded', 'true');
    this.isOpen = true;
    this.focusTrap.activate();

    // Update label
    const closeLabel = this.element.getAttribute(this.labelCloseAttribute);
    if (this.toggleLabel && closeLabel) {
      this.toggleLabel.innerHTML = closeLabel;
    }

    // Focus first element
    if (this.links.length > 0) {
      this.links[0].focus();
    }

    this.trigger('onOpen', e);

    return this;
  }

  /**
   * Close menu list.
   * @param {Event} e
   *
   * @fires Menu#onClose
   */
  handleClickOnClose(e) {
    this.element.setAttribute('aria-expanded', 'false');

    // Remove css class and attribute from inner menu
    this.inner.classList.remove('ecl-menu__inner--expanded');
    this.inner.setAttribute('aria-hidden', 'true');
    this.open.setAttribute('aria-expanded', 'false');

    // Remove css class and attribute from menu items
    this.items.forEach((item) => {
      item.classList.remove('ecl-menu__item--expanded');
      item.setAttribute('aria-expanded', 'false');
    });

    // Update label
    const openLabel = this.element.getAttribute(this.labelOpenAttribute);
    if (this.toggleLabel && openLabel) {
      this.toggleLabel.innerHTML = openLabel;
    }

    // Set focus to hamburger button

    this.enableScroll();
    this.focusTrap.deactivate();
    this.isOpen = false;
    this.trigger('onClose', e);

    return this;
  }

  /**
   * 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)
   */
  handleClickOnBack() {
    // Remove css class from inner menu
    this.inner.classList.remove('ecl-menu__inner--expanded');

    // Remove css class and attribute from menu items
    this.items.forEach((item) => {
      item.classList.remove('ecl-menu__item--expanded');
      item.setAttribute('aria-expanded', 'false');
    });

    // Focus previously selected item
    if (this.backItem) {
      const backItemButton = queryOne(this.caretSelector, this.backItem);
      if (backItemButton) {
        backItemButton.focus();
      }
    }

    return this;
  }

  /**
   * Click on the previous items button
   */
  handleClickOnPreviousItems() {
    if (!this.itemsList || !this.btnNext) return;

    this.offsetLeft = 0;
    if (this.direction === 'rtl') {
      this.itemsList.style.right = '0';
      this.itemsList.style.left = 'auto';
    } else {
      this.itemsList.style.left = '0';
      this.itemsList.style.right = 'auto';
    }

    // Update button display
    this.btnPrevious.style.display = 'none';
    this.btnNext.style.display = 'flex';

    // Refresh display
    if (this.items) {
      this.items.forEach((item) => {
        this.checkMenuItem(item);
        item.toggleAttribute('data-ecl-menu-item-visible');
      });
    }
  }

  /**
   * Click on the next items button
   */
  handleClickOnNextItems() {
    if (
      !this.itemsList ||
      !this.items ||
      !this.btnPrevious ||
      !this.lastVisibleItem
    )
      return;

    // Update button display
    this.btnPrevious.style.display = 'flex';
    this.btnNext.style.display = 'none';

    // Calculate left offset
    if (this.direction === 'rtl') {
      this.offsetLeft =
        this.itemsList.getBoundingClientRect().right -
        this.lastVisibleItem.getBoundingClientRect().right -
        this.btnPrevious.offsetWidth;

      this.itemsList.style.right = `-${this.offsetLeft}px`;
      this.itemsList.style.left = 'auto';
    } else {
      this.offsetLeft =
        this.lastVisibleItem.getBoundingClientRect().left -
        this.itemsList.getBoundingClientRect().left -
        this.btnPrevious.offsetWidth;

      this.itemsList.style.left = `-${this.offsetLeft}px`;
      this.itemsList.style.right = 'auto';
    }

    // Refresh display
    if (this.items) {
      this.items.forEach((item) => {
        this.checkMenuItem(item);
        item.toggleAttribute('data-ecl-menu-item-visible');
      });
    }
  }

  /**
   * Click on a menu item caret
   * @param {Event} e
   */
  handleClickOnCaret(e) {
    const menuExpanded = this.element.getAttribute('aria-expanded');
    const menuItem = e.target.closest(this.itemSelector);

    // Desktop display
    if (menuExpanded === 'false') {
      if (menuItem.getAttribute('aria-expanded') === 'true') {
        this.closeItem(e);
      } else {
        this.openItem(e);
      }
      return;
    }

    // Mobile display
    // Add css class to inner menu
    this.inner.classList.add('ecl-menu__inner--expanded');

    // Add css class and attribute to current item, and remove it from others
    // Also save the current item
    this.items.forEach((item) => {
      if (item === menuItem) {
        item.classList.add('ecl-menu__item--expanded');
        item.setAttribute('aria-expanded', 'true');
        this.backItem = item;
      } else {
        item.classList.remove('ecl-menu__item--expanded');
        item.setAttribute('aria-expanded', 'false');
      }
    });
    this.checkMegaMenu(menuItem);

    // Focus first item
    const firstItem = queryOne(
      '.ecl-menu__subitem:first-of-type .ecl-menu__sublink',
      menuItem,
    );
    if (firstItem) {
      firstItem.focus();
    }

    // Reactivate hover event
    this.ignorehover = false;
  }

  /**
   * Open a menu item
   * @param {Event} e
   */
  openItem(e) {
    const menuItem = e.target.closest(this.itemSelector);

    // Ignore hidden or partially hidden items
    if (
      this.hasOverflow &&
      !menuItem.hasAttribute('data-ecl-menu-item-visible')
    )
      return;

    // Add attribute to current item, and remove it from others
    this.items.forEach((item) => {
      const caretButton = queryOne(this.caretSelector, item);
      if (item === menuItem) {
        item.setAttribute('aria-expanded', 'true');

        if (caretButton) {
          caretButton.setAttribute('aria-expanded', 'true');
        }
      } else {
        item.setAttribute('aria-expanded', 'false');

        // Force remove focus on caret buttons
        if (caretButton) {
          caretButton.setAttribute('aria-expanded', 'false');
          caretButton.blur();
        }
      }
    });

    this.checkMegaMenu(menuItem);
  }

  /**
   * Close a menu item
   * @param {Event} e
   */
  closeItem(e) {
    // Remove attribute to current item
    const menuItem = e.target.closest(this.itemSelector);
    menuItem.setAttribute('aria-expanded', 'false');

    const caretButton = queryOne(this.caretSelector, menuItem);
    if (caretButton) {
      caretButton.setAttribute('aria-expanded', 'false');
    }

    return this;
  }

  /**
   * Touch on a caret
   */
  handleTouchOnCaret() {
    // Disable hover event, as they are triggered also by touch screens
    this.ignorehover = true;
  }

  /**
   * Hover on a menu item
   * @param {Event} e
   */
  handleHoverOnItem(e) {
    // Ignore touch screen
    if (this.ignorehover) return;

    this.openItem(e);
  }

  /**
   * Hover off a menu item
   * @param {Event} e
   */
  handleHoverOffItem(e) {
    // Ignore touch screen
    if (this.ignorehover) return;

    this.closeItem(e);
  }

  /**
   * Deselect any opened menu item
   */
  closeOpenDropdown() {
    const currentItem = queryOne(
      `${this.itemSelector}[aria-expanded='true']`,
      this.element,
    );
    if (currentItem) {
      currentItem.setAttribute('aria-expanded', 'false');

      const caretButton = queryOne(this.caretSelector, currentItem);
      if (caretButton) {
        caretButton.setAttribute('aria-expanded', 'false');
      }
    }
  }

  /**
   * Focus in a menu link
   * @param {Event} e
   */
  handleFocusIn(e) {
    const element = e.target;

    // Specific focus action for desktop menu
    if (this.isDesktop && this.hasOverflow) {
      const parentItem = element.closest('[data-ecl-menu-item]');
      if (!parentItem.hasAttribute('data-ecl-menu-item-visible')) {
        // Trigger scroll button depending on the context
        if (this.offsetLeft === 0) {
          this.handleClickOnNextItems();
        } else {
          this.handleClickOnPreviousItems();
        }
      }
    }
  }

  /**
   * Handles global click events, triggered outside of the menu.
   *
   * @param {Event} e
   */
  handleClickGlobal(e) {
    // Check if the menu is open
    if (this.isOpen) {
      // Check if the click occured in the menu
      if (!this.inner.contains(e.target) && !this.open.contains(e.target)) {
        this.handleClickOnClose(e);
      }
    }
  }
}

export default Menu;