;(function ($) { // Namespace all events. var eventNamespace = 'waitForImages'; // CSS properties which contain references to images. $.waitForImages = { hasImageProperties: [ 'backgroundImage', 'listStyleImage', 'borderImage', 'borderCornerImage', 'cursor' ], hasImageAttributes: ['srcset'] }; // Custom selector to find `img` elements that have a valid `src` // attribute and have not already loaded. $.expr[':'].uncached = function (obj) { // Ensure we are dealing with an `img` element with a valid // `src` attribute. if (!$(obj).is('img[src][src!=""]')) { return false; } // Firefox's `complete` property will always be `true` even if the // image has not been downloaded. Doing it this way works in Firefox. var img = new Image(); img.src = obj.src; return !img.complete; }; $.fn.waitForImages = function () { var allImgsLength = 0; var allImgsLoaded = 0; var deferred = $.Deferred(); var finishedCallback; var eachCallback; var waitForAll; // Handle options object (if passed). if ($.isPlainObject(arguments[0])) { waitForAll = arguments[0].waitForAll; eachCallback = arguments[0].each; finishedCallback = arguments[0].finished; } else { // Handle if using deferred object and only one param was passed in. if (arguments.length === 1 && $.type(arguments[0]) === 'boolean') { waitForAll = arguments[0]; } else { finishedCallback = arguments[0]; eachCallback = arguments[1]; waitForAll = arguments[2]; } } // Handle missing callbacks. finishedCallback = finishedCallback || $.noop; eachCallback = eachCallback || $.noop; // Convert waitForAll to Boolean waitForAll = !! waitForAll; // Ensure callbacks are functions. if (!$.isFunction(finishedCallback) || !$.isFunction(eachCallback)) { throw new TypeError('An invalid callback was supplied.'); } this.each(function () { // Build a list of all imgs, dependent on what images will // be considered. var obj = $(this); var allImgs = []; // CSS properties which may contain an image. var hasImgProperties = $.waitForImages.hasImageProperties || []; // Element attributes which may contain an image. var hasImageAttributes = $.waitForImages.hasImageAttributes || []; // To match `url()` references. // Spec: http://www.w3.org/TR/CSS2/syndata.html#value-def-uri var matchUrl = /url\(\s*(['"]?)(.*?)\1\s*\)/g; if (waitForAll) { // Get all elements (including the original), as any one of // them could have a background image. obj.find('*').addBack().each(function () { var element = $(this); // If an `img` element, add it. But keep iterating in // case it has a background image too. if (element.is('img:uncached')) { allImgs.push({ src: element.attr('src'), element: element[0] }); } $.each(hasImgProperties, function (i, property) { var propertyValue = element.css(property); var match; // If it doesn't contain this property, skip. if (!propertyValue) { return true; } // Get all url() of this element. while (match = matchUrl.exec(propertyValue)) { allImgs.push({ src: match[2], element: element[0] }); } }); $.each(hasImageAttributes, function (i, attribute) { var attributeValue = element.attr(attribute); var attributeValues; // If it doesn't contain this property, skip. if (!attributeValue) { return true; } // Check for multiple comma separated images attributeValues = attributeValue.split(','); $.each(attributeValues, function(i, value) { // Trim value and get string before first // whitespace (for use with srcset). value = $.trim(value).split(' ')[0]; allImgs.push({ src: value, element: element[0] }); }); }); }); } else { // For images only, the task is simpler. obj.find('img:uncached') .each(function () { allImgs.push({ src: this.src, element: this }); }); } allImgsLength = allImgs.length; allImgsLoaded = 0; // If no images found, don't bother. if (allImgsLength === 0) { finishedCallback.call(obj[0]); deferred.resolveWith(obj[0]); } $.each(allImgs, function (i, img) { var image = new Image(); var events = 'load.' + eventNamespace + ' error.' + eventNamespace; // Handle the image loading and error with the same callback. $(image).one(events, function me (event) { // If an error occurred with loading the image, set the // third argument accordingly. var eachArguments = [ allImgsLoaded, allImgsLength, event.type == 'load' ]; allImgsLoaded++; eachCallback.apply(img.element, eachArguments); deferred.notifyWith(img.element, eachArguments); // Unbind the event listeners. I use this in addition to // `one` as one of those events won't be called (either // 'load' or 'error' will be called). $(this).off(events, me); if (allImgsLoaded == allImgsLength) { finishedCallback.call(obj[0]); deferred.resolveWith(obj[0]); return false; } }); image.src = img.src; }); }); return deferred.promise(); }; }(jQuery));