lazysizes
Version:
High performance (jankfree) lazy loader for images (including responsive images), iframes and scripts (widgets).
538 lines (425 loc) • 12.6 kB
JavaScript
(function(window, factory) {
if(typeof module == 'object' && module.exports){
module.exports = lazySizes;
} else {
window.lazySizes = factory(window, window.document);
}
}(window, function l(window, document) {
'use strict';
/*jshint eqnull:true */
if(!window.IntersectionObserver || !document.getElementsByClassName || !window.MutationObserver){return;}
var lazySizesConfig;
var docElem = document.documentElement;
var Date = window.Date;
var supportPicture = window.HTMLPictureElement;
var _addEventListener = 'addEventListener';
var _getAttribute = 'getAttribute';
var addEventListener = window[_addEventListener];
var setTimeout = window.setTimeout;
var requestAnimationFrame = window.requestAnimationFrame || setTimeout;
var requestIdleCallback = window.requestIdleCallback;
var regPicture = /^picture$/i;
var loadEvents = ['load', 'error', 'lazyincluded', '_lazyloaded'];
var forEach = Array.prototype.forEach;
var hasClass = function(ele, cls) {
return ele.classList.contains(cls);
};
var addClass = function(ele, cls) {
ele.classList.add(cls);
};
var removeClass = function(ele, cls) {
ele.classList.remove(cls);
};
var addRemoveLoadEvents = function(dom, fn, add){
var action = add ? _addEventListener : 'removeEventListener';
if(add){
addRemoveLoadEvents(dom, fn);
}
loadEvents.forEach(function(evt){
dom[action](evt, fn);
});
};
var triggerEvent = function(elem, name, detail, noBubbles, noCancelable){
var event = document.createEvent('CustomEvent');
event.initCustomEvent(name, !noBubbles, !noCancelable, detail || {});
elem.dispatchEvent(event);
return event;
};
var updatePolyfill = function (el, full){
var polyfill;
if( !supportPicture && ( polyfill = (window.picturefill || lazySizesConfig.pf) ) ){
polyfill({reevaluate: true, elements: [el]});
} else if(full && full.src){
el.src = full.src;
}
};
var getWidth = function(elem, parent, width){
width = width || elem.offsetWidth;
while(width < lazySizesConfig.minSize && parent && !elem._lazysizesWidth){
width = parent.offsetWidth;
parent = parent.parentNode;
}
return width;
};
var rAF = (function(){
var running, waiting;
var fns = [];
var run = function(){
var fn;
running = true;
waiting = false;
while(fns.length){
fn = fns.shift();
fn[0].apply(fn[1], fn[2]);
}
running = false;
};
return function(fn){
if(running){
fn.apply(this, arguments);
} else {
fns.push([fn, this, arguments]);
if(!waiting){
waiting = true;
(document.hidden ? setTimeout : requestAnimationFrame)(run);
}
}
};
})();
var rAFIt = function(fn, simple){
return simple ?
function() {
rAF(fn);
} :
function(){
var that = this;
var args = arguments;
rAF(function(){
fn.apply(that, args);
});
}
;
};
//based on http://modernjavascript.blogspot.de/2013/08/building-better-debounce.html
var debounce = function(func) {
var timeout, timestamp;
var wait = 99;
var run = function(){
timeout = null;
func();
};
var later = function() {
var last = Date.now() - timestamp;
if (last < wait) {
setTimeout(later, wait - last);
} else {
(requestIdleCallback || run)(run);
}
};
return function() {
timestamp = Date.now();
if (!timeout) {
timeout = setTimeout(later, wait);
}
};
};
var loader = (function(){
var inviewObserver, preloadObserver;
var lazyloadElems, isCompleted, resetPreloadingTimer, started;
var regImg = /^img$/i;
var regIframe = /^iframe$/i;
var supportScroll = ('onscroll' in window) && !(/glebot/.test(navigator.userAgent));
var isLoading = 0;
var isPreloadLoading = 0;
var resetPreloading = function(e){
isLoading--;
if(isPreloadLoading){
isPreloadLoading--;
}
if(e && e.target){
addRemoveLoadEvents(e.target, resetPreloading);
}
if(!e || isLoading < 0 || !e.target){
isLoading = 0;
isPreloadLoading = 0;
}
if(lazyQuedElements.length && (isLoading - isPreloadLoading) < 1 && isLoading < 3){
setTimeout(function(){
while(lazyQuedElements.length && (isLoading - isPreloadLoading) < 1 && isLoading < 4){
lazyUnveilElement({target: lazyQuedElements.shift()});
}
});
}
};
var switchLoadingClass = function(e){
addClass(e.target, lazySizesConfig.loadedClass);
removeClass(e.target, lazySizesConfig.loadingClass);
addRemoveLoadEvents(e.target, rafSwitchLoadingClass);
};
var rafedSwitchLoadingClass = rAFIt(switchLoadingClass);
var rafSwitchLoadingClass = function(e){
rafedSwitchLoadingClass({target: e.target});
};
var changeIframeSrc = function(elem, src){
try {
elem.contentWindow.location.replace(src);
} catch(e){
elem.src = src;
}
};
var handleSources = function(source){
var customMedia;
var sourceSrcset = source[_getAttribute](lazySizesConfig.srcsetAttr);
if( (customMedia = lazySizesConfig.customMedia[source[_getAttribute]('data-media') || source[_getAttribute]('media')]) ){
source.setAttribute('media', customMedia);
}
if(sourceSrcset){
source.setAttribute('srcset', sourceSrcset);
}
};
var lazyUnveil = rAFIt(function (elem, detail, isAuto, sizes, isImg){
var src, srcset, parent, isPicture, event, firesLoad;
if(!(event = triggerEvent(elem, 'lazybeforeunveil', detail)).defaultPrevented){
if(sizes){
if(isAuto){
addClass(elem, lazySizesConfig.autosizesClass);
} else {
elem.setAttribute('sizes', sizes);
}
}
srcset = elem[_getAttribute](lazySizesConfig.srcsetAttr);
src = elem[_getAttribute](lazySizesConfig.srcAttr);
if(isImg) {
parent = elem.parentNode;
isPicture = parent && regPicture.test(parent.nodeName || '');
}
firesLoad = detail.firesLoad || (('src' in elem) && (srcset || src || isPicture));
event = {target: elem};
if(firesLoad){
addRemoveLoadEvents(elem, resetPreloading, true);
clearTimeout(resetPreloadingTimer);
resetPreloadingTimer = setTimeout(resetPreloading, 2500);
addClass(elem, lazySizesConfig.loadingClass);
addRemoveLoadEvents(elem, rafSwitchLoadingClass, true);
}
if(isPicture){
forEach.call(parent.getElementsByTagName('source'), handleSources);
}
if(srcset){
elem.setAttribute('srcset', srcset);
} else if(src && !isPicture){
if(regIframe.test(elem.nodeName)){
changeIframeSrc(elem, src);
} else {
elem.src = src;
}
}
if(srcset || isPicture){
updatePolyfill(elem, {src: src});
}
}
rAF(function(){
if(elem._lazyRace){
delete elem._lazyRace;
}
removeClass(elem, lazySizesConfig.lazyWaitClass);
if( !firesLoad || elem.complete ){
if(firesLoad){
resetPreloading(event);
} else {
isLoading--;
}
switchLoadingClass(event);
}
});
});
var unveilElement = function (elem){
var detail, index;
var isImg = regImg.test(elem.nodeName);
//allow using sizes="auto", but don't use. it's invalid. Use data-sizes="auto" or a valid value for sizes instead (i.e.: sizes="80vw")
var sizes = isImg && (elem[_getAttribute](lazySizesConfig.sizesAttr) || elem[_getAttribute]('sizes'));
var isAuto = sizes == 'auto';
if( (isAuto || !isCompleted) && isImg && (elem.src || elem.srcset) && !elem.complete && !hasClass(elem, lazySizesConfig.errorClass)){return;}
detail = triggerEvent(elem, 'lazyunveilread').detail;
if(isAuto){
autoSizer.updateElem(elem, true, elem.offsetWidth);
}
isLoading++;
if((index = lazyQuedElements.indexOf(elem)) != -1){
lazyQuedElements.splice(index, 1);
}
inviewObserver.unobserve(elem);
preloadObserver.unobserve(elem);
lazyUnveil(elem, detail, isAuto, sizes, isImg);
};
var unveilElements = function(change){
var i, len;
for(i = 0, len = change.length; i < len; i++){
unveilElement(change[i].target);
}
};
var lazyQuedElements = [];
var lazyUnveilElement = function(change){
var index, i, len, element;
for(i = 0, len = change.length; i < len; i++){
element = change[i].target;
if((isLoading - isPreloadLoading) < 1 && isLoading < 4){
isPreloadLoading++;
unveilElement(element);
} else if((index = lazyQuedElements.indexOf(element)) == -1){
lazyQuedElements.push(element);
} else {
lazyQuedElements.splice(index, 1);
}
}
};
var removeLazyClassElements = [];
var removeLazyClass = rAFIt(function(){
var element;
while(removeLazyClassElements.length){
element = removeLazyClassElements.shift();
addClass(element, lazySizesConfig.lazyWaitClass);
removeClass(element, lazySizesConfig.lazyClass);
if(element._lazyAdd){
delete element._lazyAdd;
}
}
}, true);
var addElements = function(){
var i, len, runLazyRemove;
for(i = 0, len = lazyloadElems.length; i < len; i++){
if(!lazyloadElems[i]._lazyAdd){
lazyloadElems[i]._lazyAdd = true;
inviewObserver.observe(lazyloadElems[i]);
preloadObserver.observe(lazyloadElems[i]);
removeLazyClassElements.push(lazyloadElems[i]);
runLazyRemove = true;
if(!supportScroll){
unveilElement(lazyloadElems[i]);
}
}
}
if(runLazyRemove){
removeLazyClass();
}
};
return {
_: function(){
started = Date.now();
lazyloadElems = document.getElementsByClassName(lazySizesConfig.lazyClass);
inviewObserver = new IntersectionObserver(unveilElements);
preloadObserver = new IntersectionObserver(lazyUnveilElement, {
rootMargin: lazySizesConfig.expand + 'px ' + (lazySizesConfig.expand * lazySizesConfig.hFac) + 'px',
});
new MutationObserver( addElements ).observe( docElem, {childList: true, subtree: true, attributes: true} );
addElements();
},
unveil: unveilElement
};
})();
var autoSizer = (function(){
var autosizesElems;
var sizeElement = rAFIt(function(elem, parent, event, width){
var sources, i, len;
elem._lazysizesWidth = width;
width += 'px';
elem.setAttribute('sizes', width);
if(regPicture.test(parent.nodeName || '')){
sources = parent.getElementsByTagName('source');
for(i = 0, len = sources.length; i < len; i++){
sources[i].setAttribute('sizes', width);
}
}
if(!event.detail.dataAttr){
updatePolyfill(elem, event.detail);
}
});
var getSizeElement = function (elem, dataAttr, width){
var event;
var parent = elem.parentNode;
if(parent){
width = getWidth(elem, parent, width);
event = triggerEvent(elem, 'lazybeforesizes', {width: width, dataAttr: !!dataAttr});
if(!event.defaultPrevented){
width = event.detail.width;
if(width && width !== elem._lazysizesWidth){
sizeElement(elem, parent, event, width);
}
}
}
};
var updateElementsSizes = function(){
var i;
var len = autosizesElems.length;
if(len){
i = 0;
for(; i < len; i++){
getSizeElement(autosizesElems[i]);
}
}
};
var debouncedUpdateElementsSizes = debounce(updateElementsSizes);
return {
_: function(){
autosizesElems = document.getElementsByClassName(lazySizesConfig.autosizesClass);
addEventListener('resize', debouncedUpdateElementsSizes);
},
checkElems: debouncedUpdateElementsSizes,
updateElem: getSizeElement
};
})();
var init = function(){
if(!init.i){
init.i = true;
autoSizer._();
loader._();
}
};
(function(){
var prop;
var lazySizesDefaults = {
lazyClass: 'lazyload',
lazyWaitClass: 'lazyloadwait',
loadedClass: 'lazyloaded',
loadingClass: 'lazyloading',
preloadClass: 'lazypreload',
errorClass: 'lazyerror',
//strictClass: 'lazystrict',
autosizesClass: 'lazyautosizes',
srcAttr: 'data-src',
srcsetAttr: 'data-srcset',
sizesAttr: 'data-sizes',
minSize: 40,
customMedia: {},
init: true,
hFac: 0.8,
loadMode: 2,
expand: 400,
};
lazySizesConfig = window.lazySizesConfig || window.lazysizesConfig || {};
for(prop in lazySizesDefaults){
if(!(prop in lazySizesConfig)){
lazySizesConfig[prop] = lazySizesDefaults[prop];
}
}
window.lazySizesConfig = lazySizesConfig;
setTimeout(function(){
if(lazySizesConfig.init){
init();
}
});
})();
return {
cfg: lazySizesConfig,
autoSizer: autoSizer,
loader: loader,
init: init,
uP: updatePolyfill,
aC: addClass,
rC: removeClass,
hC: hasClass,
fire: triggerEvent,
gW: getWidth,
rAF: rAF,
};
}));