category-filter.js

import { queryAll } from '@ecl/dom-utils';

/**
 * @param {HTMLElement} element DOM element for component instantiation and scope
 * @param {Object} options
 * @param {String} options.itemSelector Selector for the items
 * @param {String} options.parentItemSelector Selector for the parent items
 * @param {String} options.listSelector Selector for the lists
 * @param {Boolean} options.attachClickListener Whether or not to bind click events
 */
export class CategoryFilter {
  /**
   * @static
   * Shorthand for instance creation and initialisation.
   *
   * @param {HTMLElement} root DOM element for component instantiation and scope
   *
   * @return {CategoryFilter} An instance of CategoryFilter.
   */
  static autoInit(root, { CATEGORY_FILTER: defaultOptions = {} } = {}) {
    const categoryFilter = new CategoryFilter(root, defaultOptions);
    categoryFilter.init();
    root.ECLCategoryFilter = categoryFilter;
    return categoryFilter;
  }

  constructor(
    element,
    {
      itemSelector = '.ecl-category-filter__item',
      parentItemSelector = 'ecl-category-filter__item--has-children',
      listSelector = '.ecl-category-filter__list',
      attachClickListener = 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;

    // Options
    this.itemSelector = itemSelector;
    this.parentItemSelector = parentItemSelector;
    this.listSelector = listSelector;
    this.attachClickListener = attachClickListener;

    // Private variables
    this.items = null;

    // Bind `this` for use in callbacks
    this.handleClickExpand = this.handleClickExpand.bind(this);
    this.expandParents = this.expandParents.bind(this);
  }

  /**
   * Initialise component.
   */
  init() {
    if (!ECL) {
      throw new TypeError('Called init but ECL is not present');
    }
    ECL.components = ECL.components || new Map();
    // Query elementslur
    this.items = queryAll(this.itemSelector, this.element);
    const e = { preventDefault: () => null };
    // Bind click event on open
    if (this.attachClickListener && this.items) {
      this.items.forEach((item) => {
        item.addEventListener('click', this.handleClickExpand);
        // Epand the needed items if there is a current item set
        if (item.getAttribute('aria-current')) {
          e.target = item;
          this.handleClickExpand(e);
          this.expandParents.call(this, item);
        }
      });
    }

    // Set ecl initialized attribute
    this.element.setAttribute('data-ecl-auto-initialized', 'true');
    ECL.components.set(this.element, this);
  }

  /**
   * Destroy component.
   */
  destroy() {
    if (this.attachClickListener && this.items) {
      this.items.forEach((item) => {
        item.removeEventListener('click', this.handleClickExpand, false);
      });
    }
    if (this.element) {
      this.element.removeAttribute('data-ecl-auto-initialized');
      ECL.components.delete(this.element);
    }
  }

  /**
   * Expand parents of the given item.
   * @param {Node} item
   */
  expandParents(item) {
    if (!item) return;
    const e = { preventDefault: () => null };
    const parent = item.closest(this.listSelector)?.previousElementSibling;
    if (parent && parent.classList.contains(this.parentItemSelector)) {
      e.target = parent;
      this.handleClickExpand(e);
      this.expandParents.call(this, parent);
    }
  }

  /**
   * Expand tree list item.
   * @param {Event} e
   */
  handleClickExpand(e) {
    // Get item even if we clicked on the icon
    const treeItem = e.target.closest(this.itemSelector);
    const isNotInit = typeof e.stopPropagation === 'function';
    // Toggle current item
    if (isNotInit) {
      this.items.forEach((item) => {
        if (item === treeItem) {
          item.setAttribute('aria-current', true);
        } else {
          item.removeAttribute('aria-current');
        }
      });
    }

    // Toggle expanded
    const isExpanded = treeItem.getAttribute('aria-expanded');
    if (isExpanded && isExpanded === 'true') {
      e.preventDefault();
      treeItem.setAttribute('aria-expanded', 'false');
      treeItem.parentElement.classList.remove(
        'ecl-category-filter__list-item--open',
      );
    } else if (isExpanded && isExpanded === 'false') {
      e.preventDefault();
      treeItem.setAttribute('aria-expanded', 'true');
      treeItem.parentElement.classList.add(
        'ecl-category-filter__list-item--open',
      );
    }

    if (isExpanded && isNotInit) {
      // For first level, keep only one item open
      if (treeItem.classList.contains('ecl-category-filter__item--level-1')) {
        this.items.forEach((item) => {
          if (item !== treeItem) {
            item.parentElement.classList.remove(
              'ecl-category-filter__list-item--open',
            );
            if (item.classList.contains(this.parentItemSelector)) {
              item.setAttribute('aria-expanded', 'false');
            }
          }
        });
      }
    }
  }
}

export default CategoryFilter;