Home Reference Source Repository

src/dom-observer.js

/**
 * dom-observer v1.0.0
 * https://github.com/jstoolkit/dom-observer
 * @license MIT LICENSE
 * @author Matheus R. Kautzmann
 */

/**
 * The MutationObserver object
 * @typedef {Object} MutationObserver
 * @see https://developer.mozilla.org/docs/Web/API/MutationObserver
 */

/**
 * The MutationObserverInit object to specify the observer config
 * @typedef {Object} MutationObserverInit
 * @see https://developer.mozilla.org/docs/Web/API/MutationObserver
 */

/**
 * The MutationRecord object that states the changes made in the DOM
 * @typedef {Object} MutationRecord
 * @see https://developer.mozilla.org/docs/Web/API/MutationObserver
 */

 /**
  * The callback used to report mutations
  * @callback changeCallback
  * @param {MutationRecord[]|MutationRecord} mutations - Report array of changes
  * @see https://developer.mozilla.org/docs/Web/API/MutationObserver
  */

/**
 * Instantiate dom-observer
 * @param {HTMLElement} target - The element to observe
 * @param {changeCallback} callback - The function that will receive the reports
 * @param {MutationObserverInit} [options] - The object with the observer config
 * @param {Object} [options={}] - Object containing onlyFirstChange and onlyLastChange
 * @access public
 * @exports dom-observer
 * @example <caption>Instantiates an observer for all elements in body</caption>
 * var observer = require('dom-observer');
 * @returns {DomObserver} self - The newly created instance of DomObserver
 * var myObserver = observer(document.body, myCallback, { subtree: true });
 * @since 0.1.0
 */
const makeObserver = (target, callback, options = {}) => {
  const { onlyLastChange = false, onlyFirstChange = false } = options;
  // Bring prefixed MutationObserver for older Chrome/Safari and Firefox
  // TODO: REMOVE THIS VARIABLE WHEN POSSIBLE
  const MutationObserver = window.MutationObserver ||
                           window.WebKitMutationObserver ||
                           window.MozMutationObserver;

  let self;
  let currentCallback = callback;

  /**
   * Handle MutationObserver mutations
   * @function
   * @param {MutationRecord[]} _mutations - The mutations
   * @access private
   * @since 0.1.0
   */
  const mutationHandler = (_mutations) => {
    if (onlyFirstChange) {
      self.wipe();
      self.disconnect();
      return currentCallback(_mutations);
    }
    if (onlyLastChange) return currentCallback(_mutations.pop());
    return currentCallback(_mutations);
  };

  /**
   * The inner MutationObserver used to watch for mutations
   * @access private
   * @type MutationObserver
   * @const
   * @since 0.1.0
   */
  const observer = new MutationObserver(mutationHandler);

  /**
   * Spawn a new observer with the specified config
   * @function
   * @param {HTMLElement} _target - The element to observe
   * @param {MutationObserverInit} [options] - The config to respect
   * @access private
   * @since 0.1.0
   */
  function observe(_target, _options) {
    const config = _options;
    const { attributes, childList, characterData } = config;
    if (!(attributes || childList || characterData)) {
      config.attributes = true;
      config.childList = true;
      config.characterData = true;
    }
    if (!(_target instanceof HTMLElement)) {
      throw new Error('You must set a target element!');
    }
    if (currentCallback) {
      observer.observe(_target, config);
    }
  }

  /**
   * The instance of DomObserver with the public API
   * @const
   * @access public
   * @since 0.1.0
   */
  self = (() => {
    observe(target, options);
    return {
      /**
       * Add a target to the current observer
       * @function
       * @param {HTMLElement} _target - The element to observe
       * @example <caption>Add a new element to an existent observer</caption>
       * var myElement = document.querySelector('#awesomeElement');
       * myObserver.addTarget(myElement);
       * @returns {DomObserver} self - The current instance of dom-observer
       * @access public
       * @since 0.1.0
       */
      addTarget: (_target) => {
        observe(_target, options);
        return self;
      },
      /**
       * Add a new target and config to the current observer
       * @function
       * @param {HTMLElement} _target - The element to observe
       * @param {MutationObserverInit} _options - The config to respect
       * @example <caption>Add a new element and config to an observer</caption>
       * var myElement = document.querySelector('#awesomeElement');
       * myObserver.andObserve(myElement, { childList: true });
       * @returns {DomObserver} self - The current instance of dom-observer
       * @access public
       * @since 0.1.0
       */
      andObserve: (_target, _options) => {
        observe(_target, _options);
        return self;
      },
      /**
       * Change the function to be called when reporting changes
       * @function
       * @param {Function} fn - The new callback to use
       * @returns {DomObserver} self - The current instance of dom-observer
       * @example <caption>Change the function that handle the changes</caption>
       * var myNewFunc = function(mutations) { console.log('YAY', mutations); }
       * myObserver.callback = myNewFunc;
       * @returns {DomObserver} self - The current instance of dom-observer
       * @access public
       * @since 1.0.0
       */
      set callback(_fn) {
        currentCallback = _fn;
        return self;
      },
      get callback() {
        return currentCallback;
      },
      /**
       * Expose MutationObserver's takeRecords method
       * @function
       * @example <caption>Taking records</caption>
       * myObserver.takeRecords(); // Now do something with the info.
       * @returns {MutationRecord[]} The array of mutations
       * @access public
       * @since 0.1.0
       */
      takeRecords: () => observer.takeRecords(),
      /**
       * Clean the MutationObserver record pool and return this instance
       * @function
       * @example <caption>Wiping the reports</caption>
       * myObserver.wipe(); // OK, clean.
       * @returns {DomObserver} self - The current instance of dom-observer
       * @access public
       * @since 0.1.0
       */
      wipe: () => {
        observer.takeRecords();
        return self;
      },
      /**
       * Remove all previous observer configuration
       * @function
       * @example <caption>Stopping all reporters</caption>
       * myObserver.disconnect(); // No more change reports
       * @returns {DomObserver} self - The current instance of dom-observer
       * @access public
       * @since 0.1.0
       */
      disconnect: () => {
        observer.disconnect();
        return self;
      },
    };
  })();

  return self;
};

export default makeObserver;