progressive_image_loading-8.x-1.x-dev/js/progressive-image-loading.loader.js

js/progressive-image-loading.loader.js
/**
 * @file
 * Progressive Image Loading loader plugin.
 *
 * Based on Lozad.js.
 * @see https://github.com/ApoorvSaxena/lozad.js
 */

(function (global, window, document, factory) {
  if (typeof exports === 'object' && typeof module !== 'undefined') {
    module.exports = factory(window, document);
  }
  else if (typeof define === 'function' && define.amd) {
    define(function () {
      global.ProgressiveImageLoading = factory(window, document);
      return global.ProgressiveImageLoading;
    });
  }
  else {
    global = global || self, global.ProgressiveImageLoading = factory(window, document);
  }
}(this, window, document, function (window, document) {

  /**
   * Object for public API.
   *
   * @type {object}
   *
   * @private
   */
  var progressiveImageLoading = {};

  /**
   * Default configuration.
   *
   * @type {object}
   *
   * @private
   */
  var config = {

    /**
     * IntersectionObserver.root.
     *
     * A specific ancestor of the target element being observed. If no value was
     * passed, the top-level document's viewport is used.
     *
     * @type {Element|null}
     *
     * @see https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root
     */
    root: null,

    /**
     * IntersectionObserver.rootMargin.
     *
     * An offset rectangle applied to the root's bounding box when calculating
     * intersections.
     *
     * @type {string}
     *
     * @see https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin
     */
    rootMargin: '200px',

    /**
     * IntersectionObserver.threshold.
     *
     * A list of thresholds, sorted in increasing numeric order, where each
     * threshold is a ratio of intersection area to bounding box area of an
     * observed target.
     *
     * @type {int}
     *
     * @see https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/thresholds
     */
    threshold: 0,

    /**
     * The elements selector to observe.
     *
     * @type {string}
     */
    selector: '.progressive-image-loading',

    /**
     * Current theme's breakpoints.
     *
     * @type {object|null}
     */
    breakpoints: null
  };

  /**
   * The intersection observer instance.
   *
   * @type {IntersectionObserver|void}
   *
   * @private
   */
  var observer = void 0;

  /**
   * Current breakpoint.
   *
   * @type {string|false}
   */
  var currentBreakpoint = false;

  /**
   * Breakpoint listeners.
   *
   * @type {object}
   */
  var breakpointListeners = {};

  /**
   * Merge default and custom settings.
   *
   * @return {object}
   *   The merged settings.
   *
   * @private
   */
  var mergeSettings = function (settings) {
    var extended = {};
    var prop;
    for (prop in config) {
      if (Object.prototype.hasOwnProperty.call(config, prop)) {
        extended[prop] = config[prop];
      }
    }
    for (prop in settings) {
      if (Object.prototype.hasOwnProperty.call(settings, prop)) {
        extended[prop] = settings[prop];
      }
    }

    return extended;
  };

  /**
   * Adds media query listeners.
   *
   * @private
   */
  var setMediaQueryListeners = function () {
    if (Object.keys(config.breakpoints).length === 0) {
      return [];
    }

    // We need to update responsive background images, so we need to know when
    // the breakpoint change and update each loaded background element.
    Object.keys(config.breakpoints).forEach(function (key) {
      if (config.breakpoints[key].mediaQuery === '') {
        config.breakpoints[key].mediaQuery = '(min-width: 0)';
      }
      var mql = window.matchMedia(config.breakpoints[key].mediaQuery);
      // This method is deprecated, but MediaQueryList.onchange is not supported
      // by all browsers.
      mql.addListener(breakpointChange);
      breakpointListeners[key] = mql;
      if (mql.matches) {
        currentBreakpoint = key;
      }
    });
  };

  /**
   * Breakpoint listener handler.
   *
   * @private
   */
  var breakpointChange = function () {
    Object.keys(breakpointListeners).forEach(function (key) {
      if (breakpointListeners[key].matches && currentBreakpoint !== key) {
        currentBreakpoint = key;

        // Get all backgrounds witch have an alternative for this breakpoint and update them.
        var elementsToUpdate = getElements('[data-loaded="true"][data-background-image-' + key + ']');

        for (var i = 0; i < elementsToUpdate.length; i++) {
          updateBackgroundElement(elementsToUpdate[i]);
        }

        return false;
      }
    });
  };

  /**
   * Initializes the observation for each element.
   *
   * @private
   */
  var observeElements = function (elements) {
    for (var i = 0; i < elements.length; i++) {
      var isLoaded = progressiveImageLoading.isLoaded(elements[i]);
      // Start to observe unloaded elements.
      if (!isLoaded && observer) {
        observer.observe(elements[i]);
      }
      // Always load the elements if IntersectionObserver is not present.
      else if (!isLoaded) {
        progressiveImageLoading.triggerLoad(elements[i]);
      }
      // Unobserve loaded elements.
      else if (observer) {
        observer.unobserve(elements[i]);
      }
    }
  };

  /**
   * Triggers the element load on intersection.
   *
   * @private
   */
  var onIntersection = function () {
    return function (entries) {
      entries.forEach(function (entry) {
        if (entry.intersectionRatio > 0 || entry.isIntersecting) {
          progressiveImageLoading.triggerLoad(entry.target);
        }
      });
    };
  };

  /**
   * Returns the available elements in the defined root element.
   *
   * @return {Array}
   *   The elements array.
   *
   * @private
   */
  var getElements = function (selector) {
    if (selector instanceof Element) {
      return [selector];
    }

    if (selector instanceof NodeList) {
      return selector;
    }

    return config.root.querySelectorAll(selector);
  };

  /**
   * Loads an element.
   *
   * @param {Element} $element
   *   An element to load.
   *
   * @private
   */
  var loadElement = function ($element) {
    var elementCase = $element.nodeName.toLowerCase();

    // Element: picture.
    if (elementCase === 'picture') {
      if ($element.children) {
        var childs = $element.children;
        var childSrc = void 0;
        for (var i = 0; i <= childs.length - 1; i++) {
          childSrc = childs[i].getAttribute('data-srcset');
          if (childSrc) {
            childs[i].srcset = childSrc;
          }
        }
      }
    }

    // Element with attribute: data-src.
    if ($element.getAttribute('data-src')) {
      $element.src = $element.getAttribute('data-src');
    }

    // Element with attribute: data-srcset.
    if ($element.getAttribute('data-srcset')) {
      $element.setAttribute('srcset', $element.getAttribute('data-srcset'));
    }

    // Element with attribute: data-background-image.
    if ($element.getAttribute('data-background-image')) {
      updateBackgroundElement($element);
    }

    // Element with attribute: data-toggle-class.
    if ($element.getAttribute('data-toggle-class')) {
      var className = $element.getAttribute('data-toggle-class');

      if ($element.getAttribute('class').indexOf(className) > -1) {
        $element.setAttribute('class', $element.getAttribute('class').replace(className, ''));
      }
      else {
        $element.setAttribute('class', $element.getAttribute('class') + ' ' + className);
      }
    }
  };

  /**
   * Update the background image style.
   *
   * @param {Element} $element
   *   An element to update.
   *
   * @private
   */
  var updateBackgroundElement = function ($element) {
    // Element with attribute: data-background-image.
    var src = null;

    // If a breakpoint qualified, then get the attr.
    if (currentBreakpoint && $element.hasAttribute('data-background-image-' + currentBreakpoint)) {
      src = $element.getAttribute('data-background-image-' + currentBreakpoint);
    }
    else if ($element.hasAttribute('data-background-image')) {
      // Get the default data-background-image.
      src = $element.getAttribute('data-background-image');
    }

    if (src !== null) {
      // Avoid white flash between background replacements.
      preloadImageSource('background', $element, src);
    }
  };

  /**
   * Loads an element's source.
   *
   * @param {string} mode
   *   The load mode.
   * @param {Element} $element
   *   The element to add the new src.
   * @param {string} src
   *   The img src to load..
   *
   * @private
   */
  var preloadImageSource = function (mode, $element, src) {
    var newImage = new Image();

    newImage.onload = function () {
      switch (mode) {
        case 'background':
          $element.style.backgroundImage = 'url(' + this.src + ')';
          break;
      }
    };

    newImage.src = src;
  };

  /**
   * Starts observing all the elements for intersection changes.
   *
   * @public
   */
  progressiveImageLoading.observe = function () {
    var elements = getElements(config.selector, config.root);

    observeElements(elements);
  };

  /**
   * Loads an element.
   *
   * @public
   */
  progressiveImageLoading.triggerLoad = function ($element) {
    if (observer) {
      observer.unobserve($element);
    }

    if (progressiveImageLoading.isLoaded($element)) {
      return;
    }

    // Allow other modules to prepare an element before load it.
    if (progressiveImageLoading.hasOwnProperty('beforeLoad') && progressiveImageLoading.beforeLoad instanceof Function) {
      progressiveImageLoading.beforeLoad($element);
    }
    loadElement($element);
    progressiveImageLoading.markAsLoaded($element);
  };

  /**
   * Checks if an element is loaded.
   *
   * @param {Element} $element
   *   An element to evaluate.
   *
   * @return {boolean}
   *   Whether the element is loaded or not.
   *
   * @public
   */
  progressiveImageLoading.isLoaded = function ($element) {
    return $element.getAttribute('data-loaded') === 'true';
  };

  /**
   * Marks an element as loaded.
   *
   * @param {Element} $element
   *   An element to mark.
   *
   * @public
   */
  progressiveImageLoading.markAsLoaded = function ($element) {
    $element.setAttribute('data-loaded', 'true');
  };

  /**
   * ProgressiveImageLoading constructor.
   *
   * @param {HTMLDocument|HTMLElement} context
   *   A context where observe.
   * @param {object} settings
   *   The settings argument.
   */
  return function ProgressiveImageLoading(context, settings) {
    if (typeof (settings.settings) === 'object') {
      config = mergeSettings(settings.settings);
    }
    if (typeof (settings.breakpoints) === 'object') {
      config.breakpoints = settings.breakpoints;
      setMediaQueryListeners();
    }
    if (!config.root && context) {
      config.root = context;
    }

    if (window.IntersectionObserver) {
      observer = new IntersectionObserver(onIntersection(), {
        root: document.querySelector(config.root.nodeName),
        rootMargin: config.rootMargin,
        threshold: config.threshold
      });
    }

    progressiveImageLoading.observe();

    return progressiveImageLoading;
  };

}));

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc