UNPKG

lazysizes

Version:

High performance (jankfree) lazy loader for images (including responsive images), iframes and scripts (widgets).

538 lines (425 loc) 12.6 kB
(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, }; }));