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;
};
}));
