src/scrollTrigger.js

import debounce from '../node_modules/lodash.debounce/index.js';
import saKnife from './saKnife.js';
/**
 * Scroll Trigger Options Object
 * @typedef {object} stOptions
 * @property {string} eventName - Custom event name
 * @property {number} offset - Trigger line numerical offset.
 * @property {string} position - 'center' (default), 'top', 'bottom' |
 Position of trigger line.
 * @property {boolean} probe - false (default).
 * @property {string} selector - 'Element' selector. Must be data-*.  
 */

/**
 * Scroll Trigger Elements wrapper
 * @typedef stElement
 * @type {object}
 * @property {HTMLElement} el - HTMLElement
 * @property {offsetObject} offset - HTMLElement's [offsetObject]{@link offsetObject}.
 * @property {boolean} active - True/False if is element is active.
 * @property {number} index - Index. Nuff' said.
*/

/**
 * Yet another scroll trigger js tool. When an element gets to a 
 * certain point in the viewport (crosses a trigger line) run a function.
 * @example
 * // Showing default parameters.
 * const ST = new scrollTrigger({
      eventName: 'scrollTrigger',
      offset: 0,
      position: 'center',
      probe: false,
      selector: '[data-scroll-trigger]'
  });
 */
class scrollTrigger {
  /**
   * @param {stOptions} override - [stOptions]{@link stOptions}
   */
  constructor(override) {
    // Merge overrides and defaults into options object
    this.options = Object.assign(
      {
        selector: '[data-scroll-trigger]',
        eventName: 'scrollTrigger',
        eventNameProbe: 'scrollProbe',
        offset: 0,
        position: 'center',
        probe: false
      },
      override
    );
    // Get window and document size
    this.window = saKnife.winSize();
    this.options.triggerLine = this._getTriggerLine(this.options.position);

    // Find elements and add them to array
    this.elements = scrollTrigger.generateElementsObj(this.options.selector);

    this.onScrollResize();

    // Add event listeners
    if (this.elements.length > 0) {
      window.addEventListener(
        'scroll',
        debounce(() => {
          this.onScrollTrigger();
        }, 100)
      );
    }
    if (this.options.probe !== false) {
      window.addEventListener('scroll', () => {
        this.onScrollProbe();
      });
    }
    window.addEventListener(
      'resize',
      debounce(() => {
        this.onScrollResize();
      }, 200)
    );
  }

  /**
   * Generate elements array. Each element is wrapped in a an stElement object.
   * The idea is to precalculate offset position, so that scrolling performance
   * is not impacted by multiple calls to getBoundingClientRect().
   * @param {string} selector - Elements selector.
   * @returns {stElement[]} An array of [stElements]{@link stElement}
  */
  static generateElementsObj(selector) {
    const elements = document.querySelectorAll(selector);
    let elArray = [];
    elements.forEach((element, index) => {
      let newElement = {
        el: element,
        offset: saKnife.offset(element),
        active: false,
        index
      };
      elArray.push(newElement);
    });
    return elArray;
  }
  /**
   * Get Trigger Line position in pixels.
   * @param {string} position - Position option.
   * @returns {number} - Trigger Line position in pixels
  */
  _getTriggerLine(position) {
    switch (position) {
      case 'top':
        return 0;
      case 'bottom':
        return this.window.height;
      case 'center':
      default:
        return this.window.vCenter;
    }
  }
  /**
   * scrollTrigger default event.
   * @event module:src/scrollTrigger#defaultEvent
   * @type {object}
   * @property {stElement[]} detail - An array of [stElements]{@link stElement}
   */

  /**
   * Method to run on document scroll if elements are found by _getTriggerLine. Dispatches custom event named by 'options.eventName'.
   * @fires module:src/scrollTrigger#defaultEvent
  */
  onScrollTrigger() {
    let changed = this.elements.filter(element => {
      const scrolled = window.scrollY + this.options.triggerLine,
        bottomTrigger = element.offset.top + element.el.offsetHeight,
        checks = {
          afterTop: element.offset.top <= scrolled,
          beforeBottom: scrolled <= bottomTrigger
        };
      // active | inactive
      if (checks.afterTop && checks.beforeBottom && element.active === false) {
        element.active = true;
        return true;
      } else if (element.active === true) {
        element.active = false;
        return true;
      }
    });

    if (this.options.event && changed.length > 0) {
      window.dispatchEvent(
        new CustomEvent(this.options.eventName, {
          detail: changed
        })
      );
    }
  }
  /**
   * scrollTrigger probe event.
   * @event module:src/scrollTrigger#probeEvent
   * @type {object}
   * @property {number} detail - Scrolled percent in decimals
   */
  /**
   * Method to run on document scroll if probe is set to true. Dispatches custom event named by 'options.eventName'.
   * @fires module:src/scrollTrigger#probeEvent
  */
  onScrollProbe() {
    let percentScrolled =
      window.scrollY / (this.window.documentHeight - this.window.height);

    window.dispatchEvent(
      new CustomEvent(this.options.eventNameProbe, {
        detail: percentScrolled
      })
    );
  }
  /**
   * Method to run on document resize if elements are found by _getTriggerLine.
   * It recalculates the window size, trigger line position and each elements offset.
   */
  onScrollResize() {
    this.window = saKnife.winSize();
    this.options.triggerLine = this._getTriggerLine(this.options.position);
    this.elements.forEach(element => {
      element.offset = saKnife.offset(element.el);
    });
  }
}
/**
 * exports class [scrollTrigger]{@link scrollTrigger}
 * @module src/scrollTrigger
 * @requires src/saKnife - saKnife
 * @requires node_modules/lodash.debounce - lodash.debounce
 */
export default scrollTrigger;