/**
* @preserve LaziestLoader - v0.7.2 - 2015-11-17
* A responsive lazy loader for jQuery.
* http://sjwilliams.github.io/laziestloader/
* Copyright (c) 2015 Josh Williams; Licensed MIT
*/
(function(factory) {
if (typeof define === 'function' && define.amd) {
define(['jquery'], factory);
} else {
factory(jQuery);
}
}(function($) {
var laziestLoader = function(options, callback) {
var $w = $(window),
$elements = this,
$loaded = $(), // elements with the correct source set
retina = window.devicePixelRatio > 1,
didScroll = false;
options = $.extend(true, {
threshold: 0,
sizePattern: /{{SIZE}}/ig,
getSource: getSource,
event: 'scroll',
scrollThrottle: 250, // time in ms to throttle scroll. Increase for better performance.
sizeOffsetPercent: 0, // prefer smaller images
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.
}, options);
var useNativeScroll = (typeof options.event === 'string') && (options.event.indexOf('scroll') === 0);
/**
* 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 {jQuery object} $el
* @return {String}
*/
function getSource($el) {
var source, slug;
var data = $el.data();
if (data.pattern && data.widths && $.isArray(data.widths)) {
source = retina ? data.patternRetina : data.pattern;
source = source || data.pattern;
// width or slug version?
if (typeof data.widths[0] === 'object') {
slug = (function() {
var widths = $.map(data.widths, function(val) {
return val.size;
});
var bestFitWidth = bestFit($el.width(), widths);
// match best width back to its corresponding slug
for (var i = data.widths.length - 1; i >= 0; i--) {
if (data.widths[i].size === bestFitWidth) {
return data.widths[i].slug;
}
}
})();
source = source.replace(options.sizePattern, slug);
} else {
source = source.replace(options.sizePattern, bestFit($el.width(), data.widths));
}
} else {
source = retina ? data.srcRetina : data.src;
source = source || data.src;
}
return source;
}
/**
* Reflect loaded state in class names
* and fire event.
*
* @param {jQuery Object} $el
*/
function onLoad($el) {
$el.addClass('ll-loaded').removeClass('ll-notloaded');
$el.trigger('loaded');
if (typeof callback === 'function') {
callback.call($el);
}
}
/**
* Attach event handler that sets correct
* media source for the elements' width, or
* allows callback to manipulate element
* exclusively.
*/
function bindLoader() {
$elements.one('laziestloader', function() {
var $el = $(this);
var source;
// set height?
if ($el.data().ratio) {
setHeight.call(this);
}
// set content. default: set element source
if (options.setSourceMode) {
source = options.getSource($el);
if (source && this.getAttribute('src') !== source) {
this.setAttribute('src', source);
}
}
// applied immediately to reflect that media has started but,
// perhaps, hasn't finished downloading.
$el.addClass('ll-loadstarted');
// Determine when to fire `loaded` event. Wait until
// media is truly loaded if possible, otherwise immediately.
if (options.setSourceMode && (this.nodeName === 'IMG' || this.nodeName === 'VIDEO' || this.nodeName === 'AUDIO') ) {
if (this.nodeName === 'IMG') {
this.onload = function() {
onLoad($el);
};
} else {
this.onloadstart = function() {
onLoad($el);
};
}
} else {
onLoad($el);
}
});
}
/**
* Remove even handler from elements
*/
function unbindLoader() {
$elements.off('laziestloader');
}
/**
* 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 = laziestLoader.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 laziestloader() {
var docEl = document.documentElement;
var wHeight = window.innerHeight || docEl.clientHeight;
var wWidth = window.innerWidth || docEl.clientWidth;
var threshold = options.threshold;
var $inview = $elements.not($loaded).filter(function() {
if ($(this).is(':hidden')) return;
var rect = $(this)[0].getBoundingClientRect();
return (
rect.bottom + threshold > 0 &&
rect.right + threshold > 0 &&
rect.left < wWidth + threshold &&
rect.top < wHeight + threshold
);
});
$inview.trigger('laziestloader');
$loaded.add($inview);
}
/**
* 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.data();
data.ratio = data.ratio || data.heightMultiplier; // backwards compatible for old data-height-multiplier code.
if (data.ratio) {
$el.css({
height: Math.round($el.width() * data.ratio)
});
}
}
// add inital state classes, and check if
// element dimensions need to be set.
$elements.addClass('ll-init ll-notloaded').each(setHeight);
// initial binding
bindLoader();
// Watch either native scroll events, throttled by
// options.scrollThrottle, or a custom event that
// implements its own throttling.
if (useNativeScroll) {
$w.scroll(function(){
didScroll = true;
});
setInterval(function() {
if (didScroll) {
didScroll = false;
laziestloader();
}
}, options.scrollThrottle);
} else {
// if custom event is a function, it'll need
// to call laziestloader() manually, like so:
//
// $('.g-lazy').laziestloader({
// event: function(cb){
// // custom scroll event on nytimes.com
// PageManager.on('nyt:page-scroll', function(){
// // do something interesting if you like
// // and then call the passed in laziestloader();
// cb();
// });
// }
// });
//
//
// Otherwise, it's a string representing an event on the
// window to subscribe to, like so:
//
// // some code dispatching throttled events
// $window.trigger('nytg-scroll');
//
// $('.g-lazy').laziestloader({
// event: 'nytg-scroll'
// });
//
if (typeof options.event === 'function') {
options.event(laziestloader);
} else {
$w.on(options.event, function(){
laziestloader();
});
}
}
// reset state on resize
$w.resize(function() {
$loaded = $();
unbindLoader();
bindLoader();
laziestloader();
});
// initial check for lazy images
$(document).ready(function() {
laziestloader();
});
return this;
};
$.fn.laziestloader = laziestLoader;
}));