/**
* @preserve SlothyLoader - v2.0.0 - 2019-03-06
* @preserve LaziestLoader - v0.7.3 - 2016-01-03
* An lazy loader based on Josh Williams’s
* LaziestLoader, but without jQuery dependency
* v2.0, now with IntersectionObserver
* See: http://sjwilliams.github.io/laziestloader/
* Copyright (c) 2016 Josh Williams; Licensed MIT
*/
var slothyLoader = function(options, callback) {
var slothyloaderEv = new CustomEvent('slothyloader');
var $elements = null,
$loaded = [], // elements with the correct source set
retina = window.devicePixelRatio > 1,
retina3x = window.devicePixelRatio > 2,
didScroll = false,
observers = [];
var defaultOptions = {
selector: '.dvz-lazy',
threshold: 0,
sizePattern: /{{SIZE}}/ig,
getSource: getSource,
event: 'scroll', // DEPRECATED
scrollThrottle: 250, // DEPRECATED // time in ms to throttle scroll. Increase for better performance.
sizeOffsetPercent: 0, // Subtract this % of width from the containing element, and fit images as though the element were that size. 0 = fit to element size; 40 = size - 40%; -30 = size + 30%
setSourceMode: true // plugin sets source attribute of the element. Set to false if you would like to, instead, use the callback to completely manage the element on trigger.
}
const intersectionObserverOpts = { root: null, threshold: [0, 0.25, 0.5, 0.75], rootMargin: (typeof options !== 'undefined' && typeof options.threshold !== 'undefined' ? options.threshold : defaultOptions.threshold) + 'px' };
if (typeof options === 'undefined') options = {}
for (var x in defaultOptions) {
if (typeof defaultOptions[x] !== 'undefined' && typeof options[x] == 'undefined') {
options[x] = defaultOptions[x]
}
}
$elements = document.querySelectorAll(options["selector"])
var useNativeScroll = (typeof options.event === 'string') && (options.event.indexOf('scroll') === 0); // Only concerns legacy way
function supportsIntersectionObserver() {
if (typeof window !== 'undefined') {
return 'IntersectionObserver' in window && 'IntersectionObserverEntry' in window
}
return false;
}
/**
* Generate source path of image to load. Take into account
* type of data supplied and whether or not a retina
* image is available.
*
* Basic option: data attributes specifing a single image to load,
* regardless of viewport.
* Eg:
*
*
*
*
* Range of sizes: specify a string path with a {{size}} that
* will be replaced by an integer from a list of available sizes.
* Eg:
*
*
*
*
*
* Range of sizes, with slugs: specify a string path with a {{size}} that
* will be replaced by a slug representing an image size.
* Eg:
*
*
*
* @param {Element} $el
* @return {String}
*/
function getSource($el) {
var source, slug;
var data = $el.dataset;
if (data.pattern && data.widths && Array.isArray(JSON.parse(data.widths))) {
source = retina3x ? data.patternRetina3x : retina ? data.patternRetina : data.pattern;
source = source || data.patternRetina || data.pattern;
var parsedWidths = JSON.parse(data.widths)
// width or slug version?
if (typeof parsedWidths[0] === 'object') {
slug = (function() {
var widths = parsedWidths.map(function(val) {
return val.size;
});
var bestFitWidth = bestFit($el.offsetWidth, widths);
// match best width back to its corresponding slug
for (var i = parsedWidths.length - 1; i >= 0; i--) {
if (parsedWidths[i].size === bestFitWidth) {
return parsedWidths[i].slug;
}
}
})();
source = source.replace(options.sizePattern, slug);
} else {
if (data.patternRetina || data.patternRetina3x) {
source = source.replace(options.sizePattern, bestFit($el.offsetWidth, parsedWidths))
} else {
var bestFitWidth = bestFit($el.offsetWidth, parsedWidths)
if (retina3x) bestFitWidth *= 3
else if (retina) bestFitWidth *= 2
source = source.replace(options.sizePattern, bestFitWidth)
}
}
} else {
source = retina3x ? data.srcRetina3x : retina ? data.srcRetina : data.src;
source = source || data.src;
}
return source;
}
/**
* Reflect loaded state in class names
* and fire event.
*
* @param {Element} $el
*/
function onLoad($el) {
addClass($el, 'll-loaded')
removeClass($el, 'll-notloaded');
if (typeof callback === 'function') {
callback.call($el);
}
}
/**
* Handler that sets up and loads image
*
* @param {Element} $el
*/
function slHandler($el) {
var source;
// set height?
if ($el.dataset.ratio) {
setHeight.call($el);
}
// set content. default: set element source
if (options.setSourceMode) {
source = options.getSource($el);
if (source && $el.getAttribute('src') !== source) {
$el.setAttribute('src', source);
}
}
// applied immediately to reflect that media has started but,
// perhaps, hasn't finished downloading.
addClass($el, 'll-loadstarted');
// Determine when to fire `loaded` event. Wait until
// media is truly loaded if possible, otherwise immediately.
if (options.setSourceMode && ($el.nodeName === 'IMG' || $el.nodeName === 'VIDEO' || $el.nodeName === 'AUDIO') ) {
if ($el.nodeName === 'IMG') {
$el.onload = function() {
onLoad($el);
};
} else {
$el.onloadstart = function() {
onLoad($el);
};
}
} else {
onLoad($el);
}
}
/**
* Handler for Intersection Observer
*
* @param {IntersectionObserverEntry[]} $entries
*/
function slHandleIntersection(entries) {
var entry = entries[0];
var $el = entry.target;
if (entry.intersectionRatio > 0 || entry.intersectionRatio <= 0 && (entry.boundingClientRect.top < 0 || entry.boundingClientRect.y < 0)) {
slHandler($el);
}
}
/**
* Event listener
*
* @param {Event} $e
*/
function slLegacyEvListener(e) { // Used on scroll
var $el = this;
slHandler($el);
}
/**
* Attach observer on each matching element
*/
function bindObservers() {
for (var i = 0; i < $elements.length; ++i) {
var el = $elements[i];
var observer = new IntersectionObserver(slHandleIntersection, intersectionObserverOpts);
observer.observe(el);
observers.push(observer);
}
}
/**
* Attach event handler that sets correct
* media source for the elements' width, or
* allows callback to manipulate element
* exclusively.
*/
function bindLegacyLoader() {
for (var i = 0; i < $elements.length; ++i) {
var el = $elements[i];
el.addEventListener('slothyloader', slLegacyEvListener)
}
}
/**
* Remove event handler from elements
*/
function unbindLegacyLoader() {
for (var i = 0; i < $elements.length; ++i) {
var el = $elements[i];
el.removeEventListener('slothyloader', slLegacyEvListener);
}
}
/**
* Find the best sized image, opting for larger over smaller
*
* @param {Number} targetWidth element width
* @param {Array} widths array of numbers
* @return {Number}
*/
var bestFit = slothyLoader.bestFit = function(targetWidth, widths) {
var selectedWidth = widths[widths.length - 1],
i = widths.length,
offset = targetWidth * (options.sizeOffsetPercent / 100);
// sort smallest to largest
widths.sort(function(a, b) {
return a - b;
});
while (i--) {
if ((targetWidth - offset) <= widths[i]) {
selectedWidth = widths[i];
}
}
return selectedWidth;
};
/**
* Cycle through elements that haven't had their
* source set and, if they're in the viewport within
* the threshold, load their media
*/
function slothyloader() {
var docEl = document.documentElement;
var wHeight = window.innerHeight || docEl.clientHeight;
var wWidth = window.innerWidth || docEl.clientWidth;
var threshold = options.threshold;
var $inview = [];
for (var i = 0; i < $elements.length; ++i) {
var el = $elements[i];
if (includesElement($loaded,el)) continue // If already loaded, ignore
var rect = el.getBoundingClientRect();
if (
rect.bottom + threshold > 0 &&
rect.right + threshold > 0 &&
rect.left < wWidth + threshold &&
rect.top < wHeight + threshold
) $inview.push(el);
}
for (var i = 0; i < $inview.length; ++i) {
var inviewEl = $inview[i];
inviewEl.dispatchEvent(slothyloaderEv)
$loaded.push(inviewEl)
}
}
/**
* Given a lazy element, check if it should have
* its height set based on a data-ratio multiplier.
*/
function setHeight() {
var $el = this,
data = $el.dataset;
data.ratio = data.ratio || data.heightMultiplier; // backwards compatible for old data-height-multiplier code.
if (data.ratio && !isNaN(+data.ratio)) {
var newHeight = Math.round($el.offsetWidth * +data.ratio)
if (newHeight > 0) $el.style.height = newHeight + "px" // avoid 0 offsetWidth when element is display none (different from jQuery)
}
}
// add inital state classes, and check if
// element dimensions need to be set.
for (var i = 0; i < $elements.length; ++i) {
var el = $elements[i];
addClass(el, 'll-init ll-notloaded')
setHeight.call(el);
}
// initial binding
if (supportsIntersectionObserver()) {
bindObservers();
} else {
bindLegacyLoader();
// Watch either native scroll events, throttled by
// options.scrollThrottle, or a custom event that
// implements its own throttling.
if (useNativeScroll) {
window.addEventListener("scroll", function(){
didScroll = true;
});
setInterval(function() {
if (didScroll) {
didScroll = false;
slothyloader();
}
}, options.scrollThrottle);
} else {
if (typeof options.event === 'function') {
options.event(slothyloader);
} else {
window.addEventListener(options.event, function(){
slothyloader();
});
}
}
}
// Helpful native HTML functions for class manipulation to replace jQuery equivalents
function hasClass(el, className) {
var classList = className.split(" ")
for (var i=0; i>> 0;
// 3. If len is 0, return false.
if (len === 0) {
return false;
}
// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;
// 5. If n ≥ 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
// 7. Repeat, while k < len
while (k < len) {
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
// b. If SameValueZero(searchElement, elementK) is true, return true.
// c. Increase k by 1.
// NOTE: === provides the correct "SameValueZero" comparison needed here.
if (o[k] === searchElement) {
return true;
}
k++;
}
// 8. Return false
return false;
}
// reset state on resize
window.addEventListener('resize', function() {
$loaded = [];
unbindLegacyLoader();
bindLegacyLoader();
slothyloader();
});
// initial check for lazy images
window.addEventListener('DOMContentLoaded', function() {
slothyloader();
});
return this;
};
// CustomEvent is to provide support for Event in Internet Explorer
(function () {
if ( typeof window.CustomEvent === "function" ) return false;
function CustomEvent ( event, params ) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent( 'CustomEvent' );
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
}
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
})();
if (typeof module !== 'undefined') module.exports = slothyLoader