Created
October 12, 2017 12:34
-
-
Save aFarkas/d4f1e48d90d9fb6a1aa07e43f43af438 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(window, factory) { | |
var lazySizes = factory(window, window.document); | |
window.lazySizes = lazySizes; | |
if(typeof module == 'object' && module.exports){ | |
module.exports = lazySizes; | |
} | |
}(window, function l(window, document) { | |
'use strict'; | |
/*jshint eqnull:true */ | |
if(!document.getElementsByClassName){return;} | |
var lazysizes, 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 regClassCache = {}; | |
var forEach = Array.prototype.forEach; | |
var hasClass = function(ele, cls) { | |
if(!regClassCache[cls]){ | |
regClassCache[cls] = new RegExp('(\\s|^)'+cls+'(\\s|$)'); | |
} | |
return regClassCache[cls].test(ele[_getAttribute]('class') || '') && regClassCache[cls]; | |
}; | |
var addClass = function(ele, cls) { | |
if (!hasClass(ele, cls)){ | |
ele.setAttribute('class', (ele[_getAttribute]('class') || '').trim() + ' ' + cls); | |
} | |
}; | |
var removeClass = function(ele, cls) { | |
var reg; | |
if ((reg = hasClass(ele,cls))) { | |
ele.setAttribute('class', (ele[_getAttribute]('class') || '').replace(reg, ' ')); | |
} | |
}; | |
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'); | |
if(!detail){ | |
detail = {}; | |
} | |
detail.instance = lazysizes; | |
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 getCSS = function (elem, style){ | |
return (getComputedStyle(elem, null) || {})[style]; | |
}; | |
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 firstFns = []; | |
var secondFns = []; | |
var fns = firstFns; | |
var run = function(){ | |
var runFns = fns; | |
fns = firstFns.length ? secondFns : firstFns; | |
running = true; | |
waiting = false; | |
while(runFns.length){ | |
runFns.shift()(); | |
} | |
running = false; | |
}; | |
var rafBatch = function(fn, queue){ | |
if(running && !queue){ | |
fn.apply(this, arguments); | |
} else { | |
fns.push(fn); | |
if(!waiting){ | |
waiting = true; | |
(document.hidden ? setTimeout : requestAnimationFrame)(run); | |
} | |
} | |
}; | |
rafBatch._lsFlush = run; | |
return rafBatch; | |
})(); | |
var rAFIt = function(fn, simple){ | |
return simple ? | |
function() { | |
rAF(fn); | |
} : | |
function(){ | |
var that = this; | |
var args = arguments; | |
rAF(function(){ | |
fn.apply(that, args); | |
}); | |
} | |
; | |
}; | |
var throttle = function(fn){ | |
var running; | |
var lastTime = 0; | |
var gDelay = 125; | |
var RIC_DEFAULT_TIMEOUT = 66; | |
var rICTimeout = RIC_DEFAULT_TIMEOUT; | |
var run = function(){ | |
running = false; | |
lastTime = Date.now(); | |
fn(); | |
}; | |
var idleCallback = requestIdleCallback ? | |
function(){ | |
requestIdleCallback(run, {timeout: rICTimeout}); | |
if(rICTimeout !== RIC_DEFAULT_TIMEOUT){ | |
rICTimeout = RIC_DEFAULT_TIMEOUT; | |
} | |
}: | |
rAFIt(function(){ | |
setTimeout(run); | |
}, true) | |
; | |
return function(isPriority){ | |
var delay; | |
if((isPriority = isPriority === true)){ | |
rICTimeout = 44; | |
} | |
if(running){ | |
return; | |
} | |
running = true; | |
delay = gDelay - (Date.now() - lastTime); | |
if(delay < 0){ | |
delay = 0; | |
} | |
if(isPriority || (delay < 9 && requestIdleCallback)){ | |
idleCallback(); | |
} else { | |
setTimeout(idleCallback, delay); | |
} | |
}; | |
}; | |
//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 preloadElems, isCompleted, resetPreloadingTimer, loadMode, started; | |
var eLvW, elvH, eLtop, eLleft, eLright, eLbottom; | |
var defaultExpand, preloadExpand, hFac; | |
var regImg = /^img$/i; | |
var regIframe = /^iframe$/i; | |
var supportScroll = ('onscroll' in window) && !(/glebot/.test(navigator.userAgent)); | |
var shrinkExpand = 0; | |
var currentExpand = 0; | |
var isLoading = 0; | |
var lowRuns = -1; | |
var resetPreloading = function(e){ | |
isLoading--; | |
if(e && e.target){ | |
addRemoveLoadEvents(e.target, resetPreloading); | |
} | |
if(!e || isLoading < 0 || !e.target){ | |
isLoading = 0; | |
} | |
}; | |
var isNestedVisible = function(elem, elemExpand){ | |
var outerRect; | |
var parent = elem; | |
var visible = getCSS(document.body, 'visibility') == 'hidden' || getCSS(elem, 'visibility') != 'hidden'; | |
eLtop -= elemExpand; | |
eLbottom += elemExpand; | |
eLleft -= elemExpand; | |
eLright += elemExpand; | |
while(visible && (parent = parent.offsetParent) && parent != document.body && parent != docElem){ | |
visible = ((getCSS(parent, 'opacity') || 1) > 0); | |
if(visible && getCSS(parent, 'overflow') != 'visible'){ | |
outerRect = parent.getBoundingClientRect(); | |
visible = eLright > outerRect.left && | |
eLleft < outerRect.right && | |
eLbottom > outerRect.top - 1 && | |
eLtop < outerRect.bottom + 1 | |
; | |
} | |
} | |
return visible; | |
}; | |
var checkElements = function() { | |
var eLlen, i, rect, autoLoadElem, loadedSomething, elemExpand, elemNegativeExpand, elemExpandVal, beforeExpandVal; | |
var lazyloadElems = lazysizes.elements; | |
if((loadMode = lazySizesConfig.loadMode) && isLoading < 8 && (eLlen = lazyloadElems.length)){ | |
i = 0; | |
lowRuns++; | |
if(preloadExpand == null){ | |
if(!('expand' in lazySizesConfig)){ | |
lazySizesConfig.expand = docElem.clientHeight > 500 && docElem.clientWidth > 500 ? 500 : 370; | |
} | |
defaultExpand = lazySizesConfig.expand; | |
preloadExpand = defaultExpand * lazySizesConfig.expFactor; | |
} | |
if(currentExpand < preloadExpand && isLoading < 1 && lowRuns > 2 && loadMode > 2 && !document.hidden){ | |
currentExpand = preloadExpand; | |
lowRuns = 0; | |
} else if(loadMode > 1 && lowRuns > 1 && isLoading < 6){ | |
currentExpand = defaultExpand; | |
} else { | |
currentExpand = shrinkExpand; | |
} | |
for(; i < eLlen; i++){ | |
if(!lazyloadElems[i] || lazyloadElems[i]._lazyRace){continue;} | |
if(!supportScroll){unveilElement(lazyloadElems[i]);continue;} | |
if(!(elemExpandVal = lazyloadElems[i][_getAttribute]('data-expand')) || !(elemExpand = elemExpandVal * 1)){ | |
elemExpand = currentExpand; | |
} | |
if(beforeExpandVal !== elemExpand){ | |
eLvW = innerWidth + (elemExpand * hFac); | |
elvH = innerHeight + elemExpand; | |
elemNegativeExpand = elemExpand * -1; | |
beforeExpandVal = elemExpand; | |
} | |
rect = lazyloadElems[i].getBoundingClientRect(); | |
if ((eLbottom = rect.bottom) >= elemNegativeExpand && | |
(eLtop = rect.top) <= elvH && | |
(eLright = rect.right) >= elemNegativeExpand * hFac && | |
(eLleft = rect.left) <= eLvW && | |
(eLbottom || eLright || eLleft || eLtop) && | |
(lazySizesConfig.loadHidden || getCSS(lazyloadElems[i], 'visibility') != 'hidden') && | |
((isCompleted && isLoading < 3 && !elemExpandVal && (loadMode < 3 || lowRuns < 4)) || isNestedVisible(lazyloadElems[i], elemExpand))){ | |
unveilElement(lazyloadElems[i]); | |
loadedSomething = true; | |
if(isLoading > 9){break;} | |
} else if(!loadedSomething && isCompleted && !autoLoadElem && | |
isLoading < 4 && lowRuns < 4 && loadMode > 2 && | |
(preloadElems[0] || lazySizesConfig.preloadAfterLoad) && | |
(preloadElems[0] || (!elemExpandVal && ((eLbottom || eLright || eLleft || eLtop) || lazyloadElems[i][_getAttribute](lazySizesConfig.sizesAttr) != 'auto')))){ | |
autoLoadElem = preloadElems[0] || lazyloadElems[i]; | |
} | |
} | |
if(autoLoadElem && !loadedSomething){ | |
unveilElement(autoLoadElem); | |
} | |
} | |
}; | |
var throttledCheckElements = throttle(checkElements); | |
var switchLoadingClass = function(e){ | |
addClass(e.target, lazySizesConfig.loadedClass); | |
removeClass(e.target, lazySizesConfig.loadingClass); | |
addRemoveLoadEvents(e.target, rafSwitchLoadingClass); | |
triggerEvent(e.target, 'lazyloaded'); | |
}; | |
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(isImg && (srcset || isPicture)){ | |
updatePolyfill(elem, {src: src}); | |
} | |
} | |
if(elem._lazyRace){ | |
delete elem._lazyRace; | |
} | |
removeClass(elem, lazySizesConfig.lazyClass); | |
rAF(function(){ | |
if( !firesLoad || (elem.complete && elem.naturalWidth > 1)){ | |
if(firesLoad){ | |
resetPreloading(event); | |
} else { | |
isLoading--; | |
} | |
switchLoadingClass(event); | |
} | |
}, true); | |
}); | |
var unveilElement = function (elem){ | |
var detail; | |
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[_getAttribute]('src') || elem.srcset) && !elem.complete && !hasClass(elem, lazySizesConfig.errorClass) && hasClass(elem, lazySizesConfig.lazyClass)){return;} | |
detail = triggerEvent(elem, 'lazyunveilread').detail; | |
if(isAuto){ | |
autoSizer.updateElem(elem, true, elem.offsetWidth); | |
} | |
elem._lazyRace = true; | |
isLoading++; | |
lazyUnveil(elem, detail, isAuto, sizes, isImg); | |
}; | |
var onload = function(){ | |
if(isCompleted){return;} | |
if(Date.now() - started < 999){ | |
setTimeout(onload, 999); | |
return; | |
} | |
var afterScroll = debounce(function(){ | |
lazySizesConfig.loadMode = 3; | |
throttledCheckElements(); | |
}); | |
isCompleted = true; | |
lazySizesConfig.loadMode = 3; | |
throttledCheckElements(); | |
addEventListener('scroll', function(){ | |
if(lazySizesConfig.loadMode == 3){ | |
lazySizesConfig.loadMode = 2; | |
} | |
afterScroll(); | |
}, true); | |
}; | |
return { | |
_: function(){ | |
started = Date.now(); | |
lazysizes.elements = document.getElementsByClassName(lazySizesConfig.lazyClass); | |
preloadElems = document.getElementsByClassName(lazySizesConfig.lazyClass + ' ' + lazySizesConfig.preloadClass); | |
hFac = lazySizesConfig.hFac; | |
addEventListener('scroll', throttledCheckElements, true); | |
addEventListener('resize', throttledCheckElements, true); | |
if(window.MutationObserver){ | |
new MutationObserver( throttledCheckElements ).observe( docElem, {childList: true, subtree: true, attributes: true} ); | |
} else { | |
docElem[_addEventListener]('DOMNodeInserted', throttledCheckElements, true); | |
docElem[_addEventListener]('DOMAttrModified', throttledCheckElements, true); | |
setInterval(throttledCheckElements, 999); | |
} | |
addEventListener('hashchange', throttledCheckElements, true); | |
//, 'fullscreenchange' | |
['focus', 'mouseover', 'click', 'load', 'transitionend', 'animationend', 'webkitAnimationEnd'].forEach(function(name){ | |
document[_addEventListener](name, throttledCheckElements, true); | |
}); | |
if((/d$|^c/.test(document.readyState))){ | |
onload(); | |
} else { | |
addEventListener('load', onload); | |
document[_addEventListener]('DOMContentLoaded', throttledCheckElements); | |
setTimeout(onload, 20000); | |
} | |
if(lazysizes.elements.length){ | |
checkElements(); | |
rAF._lsFlush(); | |
} else { | |
throttledCheckElements(); | |
} | |
}, | |
checkElems: throttledCheckElements, | |
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', | |
loadedClass: 'lazyloaded', | |
loadingClass: 'lazyloading', | |
preloadClass: 'lazypreload', | |
errorClass: 'lazyerror', | |
//strictClass: 'lazystrict', | |
autosizesClass: 'lazyautosizes', | |
srcAttr: 'data-src', | |
srcsetAttr: 'data-srcset', | |
sizesAttr: 'data-sizes', | |
//preloadAfterLoad: false, | |
minSize: 40, | |
customMedia: {}, | |
init: true, | |
expFactor: 1.5, | |
hFac: 0.8, | |
loadMode: 2, | |
loadHidden: true, | |
}; | |
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(); | |
} | |
}); | |
})(); | |
lazysizes = { | |
cfg: lazySizesConfig, | |
autoSizer: autoSizer, | |
loader: loader, | |
init: init, | |
uP: updatePolyfill, | |
aC: addClass, | |
rC: removeClass, | |
hC: hasClass, | |
fire: triggerEvent, | |
gW: getWidth, | |
rAF: rAF, | |
}; | |
return lazysizes; | |
} | |
)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment