slider-pager.js

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

/**
 * @param {HTMLElement} element DOM element for component instantiation and scope
 * @param {Object} options
 * @param {String} options.slider Slider instance (embla)
 * @param {String} options.accessibility Accessibility plugin instance (embla)
 * @param {String} options.pagerElement Slider pager
 * @param {String} options.prevSelector Css class of the prev button
 * @param {String} options.nextSelector Css class of the next button
 * @param {String} options.dotsSelector Css class of the dots wrapper
 * @param {String} options.dotSelector Css class of the single dot
 * @param {String} options.activeDotSelector Css class to be assigned to the active dot
 * @param {String} options.dotTemplateSelector Dots template selector
 */

export class SliderPager {
  constructor({
    slider,
    accessibility,
    prevSelector,
    nextSelector,
    pagerElement,
    dotsSelector,
    dotTemplateSelector,
    dotSelector,
    activeDotSelector,
    onUpdate,
  } = {}) {
    this.slider = slider;
    this.accessibility = accessibility;
    this.prevSelector = prevSelector;
    this.nextSelector = nextSelector;
    this.pagerElement = pagerElement;
    this.dotsSelector = dotsSelector;
    this.dotTemplateSelector = dotTemplateSelector;
    this.dotSelector = dotSelector;
    this.activeDotSelector = activeDotSelector;
    this.onUpdate = onUpdate;

    this.dotNodes = [];
    this.resizeTimer = null;

    this.toggleButtonsDisabled = this.toggleButtonsDisabled.bind(this);
    this.updateDots = this.updateDots.bind(this);
    this.rebuildDots = this.rebuildDots.bind(this);
    this.handleResize = this.handleResize.bind(this);
    this.handleKeyboard = this.handleKeyboard.bind(this);
    this.escapeSlider = this.escapeSlider.bind(this);
    this.onPrevClick = this.onPrevClick.bind(this);
    this.onNextClick = this.onNextClick.bind(this);
  }

  /**
   * Initialise component.
   *
   * @memberof SliderPager
   */
  init() {
    if (!this.slider) {
      return;
    }

    this.prevButton = queryOne(this.prevSelector, this.pagerElement);
    this.nextButton = queryOne(this.nextSelector, this.pagerElement);
    this.dotsContainer = queryOne(this.dotsSelector, this.pagerElement);
    this.dotTemplate = queryOne(this.dotTemplateSelector, this.pagerElement);

    if (this.prevButton && this.nextButton) {
      this.toggleButtonsDisabled(this.slider);

      this.slider.on('select', this.toggleButtonsDisabled);
      this.slider.on('reInit', this.toggleButtonsDisabled);

      this.prevButton.addEventListener('click', this.onPrevClick);
      this.nextButton.addEventListener('click', this.onNextClick);

      this.accessibility?.setupPrevAndNextButtons(
        this.prevButton,
        this.nextButton,
      );
    }

    if (this.dotsContainer) {
      this.rebuildDots();
      this.slider.on('reInit', this.rebuildDots);
      this.updateDots();
      this.slider.on('select', this.updateDots);
      this.accessibility?.setupDotButtons(this.dotsContainer);
    }

    this.slider.rootNode().addEventListener('keydown', this.handleKeyboard);
    window.addEventListener('resize', this.handleResize);
  }

  /**
   * Destroy component.
   *
   * @memberof SliderPager
   */
  destroy() {
    this.slider.rootNode().removeEventListener('keydown', this.handleKeyboard);
    this.slider.off('select', this.toggleButtonsDisabled);
    this.slider.off('reInit', this.toggleButtonsDisabled);

    this.slider.off('select', this.updateDots);
    this.slider.off('reInit', this.rebuildDots);

    if (this.prevButton && this.nextButton) {
      this.prevButton.removeEventListener('click', this.onPrevClick);
      this.nextButton.removeEventListener('click', this.onNextClick);
    }

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

  /**
   * Prev button click handler
   *
   * @memberof SliderPager
   */
  onPrevClick() {
    this.slider.goToPrev();
  }

  /**
   * Next button click handler.
   *
   * @memberof SliderPager
   */
  onNextClick() {
    this.slider.goToNext();
  }

  /**
   * Toggle disable status of the prev and next buttons.
   *
   * @memberof SliderPager
   */
  toggleButtonsDisabled(emblaApi) {
    if (this.prevButton) {
      this.prevButton.toggleAttribute('disabled', !emblaApi.canGoToPrev());
    }

    if (this.nextButton) {
      this.nextButton.toggleAttribute('disabled', !emblaApi.canGoToNext());
    }
  }

  /**
   * Generate dots.
   *
   * @memberof SliderPager
   */
  rebuildDots() {
    const canScroll = this.slider.canGoToPrev() || this.slider.canGoToNext();

    if (this.pager) {
      this.pager.style.display = canScroll ? '' : 'none';
    }

    if (!canScroll) {
      this.dotsContainer.innerHTML = '';
      this.dotNodes = [];
      return;
    }

    this.dotsContainer.innerHTML = this.slider
      .snapList()
      .map(() => this.dotTemplate.innerHTML)
      .join('');

    this.dotNodes = Array.from(queryAll(this.dotSelector, this.dotsContainer));

    this.dotNodes.forEach((dot, index) => {
      dot.addEventListener('click', () => this.slider.goTo(index));
    });

    this.accessibility?.setupDotButtons(this.dotsContainer);

    this.updateDots();
  }

  /**
   * Update dots status.
   *
   * @memberof SliderPager
   */
  updateDots() {
    const index = this.slider.selectedSnap();

    this.dotNodes.forEach((dot, i) => {
      dot.classList.toggle(this.activeDotSelector, i === index);

      dot.classList.toggle('is-prev', i === index - 1);
    });

    if (this.onUpdate) {
      this.onUpdate(index);
    }
  }

  /**
   * Trigger events on resize
   * Uses a debounce, for performance
   *
   * @memberof SliderPager
   */
  handleResize() {
    clearTimeout(this.resizeTimer);
    this.resizeTimer = setTimeout(() => {
      if (this.slider) {
        this.rebuildDots();
        if (this.accessibility && this.dotsNode) {
          this.accessibility.setupDotButtons(this.dotsNode);
        }
        this.updateDots();
        if (this.toggleButtonsDisabled) {
          this.toggleButtonsDisabled(this.slider);
        }
      }
    }, 100);
  }

  /**
   * Handle keyboard events for accessibility.
   *
   * @memberof SliderPager
   */
  handleKeyboard(e) {
    if (e.key === 'Escape') {
      this.escapeSlider();
    }
  }

  /**
   * Moves the focus out of the slider when Escape is pressed.
   *
   * @memberof SliderPager
   */
  escapeSlider() {
    if (this.slider.canGoToNext()) {
      this.nextButton.focus();
      return;
    }

    // Move focus on the disabled next button in case there are no dots.
    if (
      this.dotNodes.length === 0 &&
      !this.slider.canGoToNext() &&
      this.nextButton
    ) {
      this.nextButton.disabled = false;
      this.nextButton.style.display = 'flex';
      this.nextButton.style.visibility = 'visible';
      this.nextButton.classList.add('.ecl-slider-pager__next--escape');
      this.nextButton.focus();
      return;
    }

    if (this.dotNodes.length > 0) {
      const lastDot = this.dotNodes[this.dotNodes.length - 1];
      lastDot.focus();
    }
  }
}

export default SliderPager;