/* * JAIL: jQuery Asynchronous Image Loader * * Copyright (c) 2011-12 Sebastiano Armeli-Battana (http://www.sebastianoarmelibattana.com) * * By Sebastiano Armeli-Battana (@sebarmeli) * Licensed under the MIT license. * https://github.com/sebarmeli/JAIL/blob/master/MIT-LICENSE.txt * * Tested with jQuery 1.3.2+ on FF 2+, Opera 10+, Safari 4+, Chrome 8+ on Win/Mac/Linux * and IE 6/7/8 on Win. * * Contributor : Derek Lindahl - @dlindahl * * @link http://github.com/sebarmeli/JAIL * @author Sebastiano Armeli-Battana * @date 14/10/2012 * @version 1.0.0 * */ ; ( function( name, definition ) { var theModule = definition( jQuery ), hasDefine = typeof define === 'function' && define.amd; if ( hasDefine ) { // AMD module define( name, ['jquery'], theModule ); } else { // assign 'jail' to global objects ( this.jQuery || this.$ || this )[name] = theModule; } }( 'jail', function( $ ) { var $window = $( window ), // Defaults parameters defaults = { id: 'jail', timeout: 1, effect: false, speed: 400, triggerElement: null, offset: 0, event: 'load', callback: null, callbackAfterEachImage: null, placeholder: false, loadHiddenImages: false, loadAllHorizontal: false }, // current stack of images currentStack = [], // true if 'callback' fn is called isCallbackDone = false; /* * Public function defining 'jail' * * @module jail * @param elems : images to load - jQuery elements * @param opts : configurations object */ $.jail = function( elems, opts ) { var elements = elems || {}, options = $.extend( {}, defaults, opts ); // Initialize plugin $.jail.prototype.init( elements, options ); // When the event is not specified the images will be loaded with a delay if ( /^(load|scroll)/.test( options.event ) ) { // 'load' event $.jail.prototype.later.call( elements, options ); } else { $.jail.prototype.onEvent.call( elements, options ); } }; /* * Method in charge of initializing the plugin, storing * the 'element triggering the image to load' in a data attribute * for each image and displaying the placeholder image (if existing) * * @method init * @param {Array} elems Images to load - jQuery elements * @param {Object} opts Configurations object */ $.jail.prototype.init = function( elements, options ) { // Store the selector triggering jail into 'triggerElem' data for the images selected elements.data( "triggerElem", ( options.triggerElement ) ? $( options.triggerElement ) : $window ); // Use a placeholder in case it is specified if ( !!options.placeholder ) { elements.each( function() { $( this ).attr( "src", options.placeholder ); } ); } }; /* * Function called when 'event' is different from "load" or "scroll". Two scenarios: * a) Element triggering the images to be loaded (events available on the element: "click", "mouseover", "scroll") * b) Event on the image itself triggering the image to be loaded * * @param options : configurations object */ $.jail.prototype.onEvent = function( options ) { var images = this; if ( !!options.triggerElement ) { // Event on the 'triggerElement' obj _bindEvent( options, images ); } else { // Event on the image itself images.on( options.event + '.' + options.id, {options: options, images: images}, function( e ) { var $img = $( this ), options = e.data.options, images = e.data.images; currentStack = $.extend( {}, images ); // Load the image _loadImage( options, $img ); // Image has been loaded so there is no need to listen anymore $( e.currentTarget ).unbind( e.type + '.' + options.id ); } ); } }; /* * Method called when "event" is equals to "load" (default) or "scroll". The visible images will be * loaded after a specified timeout (or after 1 ms). The scroll method will be bound to the window * to load the images not visible onload. * * @param options : configurations object */ $.jail.prototype.later = function( options ) { var images = this; // After [timeout] has elapsed, load the visible images setTimeout( function() { currentStack = $.extend( {}, images ); //Load the visible ones images.each( function() { _loadImageIfVisible( options, this, images ); } ); // When images become available (scrolling or resizing), they will be loaded options.event = "scroll"; _bindEvent( options, images ); }, options.timeout ); }; /* * Bind _bufferedEventListener() to the event on window/triggerElement. The handler is bound to * resizing the window as well * * @param options : configurations object * @param images : images in the current stack */ function _bindEvent( options, images ) { var triggerElem = false; if ( !!images ) { triggerElem = images.data( "triggerElem" ); } // Check if there are images to load if ( !!triggerElem && typeof triggerElem.on === "function" ) { triggerElem.on( options.event + '.' + options.id, {options: options, images: images}, _bufferedEventListener ); $window.on( 'resize.' + options.id, {options: options, images: images}, _bufferedEventListener ); } } /* * Remove any elements that have been loaded from the jQuery stack. * This should speed up subsequent calls by not having to iterate over the loaded elements. * * @param stack : current images stack */ function _purgeStack( stack ) { // number of images not loaded var i = 0; if ( stack.length === 0 ) { return; } // Check on existence of 'data-src' attribute to verify if the image has been loaded while ( true ) { if ( i === stack.length ) { break; } else { if ( $( stack[i] ).attr( 'data-src' ) ) { i++; } else { stack.splice( i, 1 ); } } } } /* * Event handler for the images to be loaded. Function called when * there is a triggerElement or when there are images to be loaded after scrolling * or resizing window/container * * @param e : event */ function _bufferedEventListener( e ) { var images = e.data.images, options = e.data.options; // Don't set a timer if one is set. if ( images.data( "poller" ) ) { return; } images.data( 'poller', setTimeout( function() { currentStack = $.extend( {}, images ); _purgeStack( currentStack ); // Load only the images left $( currentStack ).each( function() { if ( this === window ) { return; } _loadImageIfVisible( options, this, currentStack ); } ); //Unbind when there are no images if ( _isAllImagesLoaded( currentStack ) ) { $( e.currentTarget ).unbind( e.type + '.' + options.id ); return; } // When images are not in the viewport, let's load them when they become available else if ( options.event !== "scroll" ) { // When images become available (scrolling or resizing), they will be loaded var container = ( /scroll/i.test( options.event ) ) ? images.data( "triggerElem" ) : $window; options.event = "scroll"; images.data( "triggerElem", container ); _bindEvent( options, $( currentStack ) ); } images.data( "poller", null ); }, options.timeout ) ); } /* * Check if all the images are loaded * * @param images : images under analysis * @return boolean */ function _isAllImagesLoaded( images ) { var bool = true; $( images ).each( function() { if ( !!$( this ).attr( "data-src" ) ) { bool = false; } } ); return bool; } /* * Load the image if visible in the viewport * * @param options : configurations object * @param image : image under analysis * @param images : list of images to load */ function _loadImageIfVisible( options, image, images ) { var $img = $( image ), container = ( /scroll/i.test( options.event ) ) ? images.data( "triggerElem" ) : $window, isVisible = true; // If don't you want to load hidden images (default beahviour) if ( !options.loadHiddenImages ) { isVisible = _isVisibleInContainer( $img, container, options ) && $img.is( ":visible" ); } // Load the image if it is not hidden and visible in the screen if ( isVisible && _isInTheScreen( container, $img, options.offset, options.loadAllHorizontal ) ) { _loadImage( options, $img ); } } /* * Function that returns true if the image is visible inside the "window" (or specified container element) * * @param $ct : container - jQuery obj * @param $img : image selected - jQuery obj * @param optionOffset : offset */ function _isInTheScreen( $ct, $img, optionOffset, loadAllHorizontal ) { if ( loadAllHorizontal ) { return true; } else { var is_ct_window = $ct[0] === window, ct_offset = ( is_ct_window ? {top: 0, left: 0} : $ct.offset() ), ct_top = ct_offset.top + ( is_ct_window ? $ct.scrollTop() : 0 ), ct_left = ct_offset.left + ( is_ct_window ? $ct.scrollLeft() : 0 ), ct_right = ct_left + $ct.width(), ct_bottom = ct_top + $ct.height(), img_offset = $img.offset(), img_width = $img.width(), img_height = $img.height(); return ( ct_top - optionOffset ) <= ( img_offset.top + img_height ) && ( ct_bottom + optionOffset ) >= img_offset.top && ( ct_left - optionOffset ) <= ( img_offset.left + img_width ) && ( ct_right + optionOffset ) >= img_offset.left; } } /* * Main function --> Load the images copying the "data-href" attribute into the "src" attribute * * @param options : configurations object * @param $img : image selected - jQuery obj */ function _loadImage( options, $img ) { // Use cache Image object to show images only when ready var cache = new Image(); cache.onload = function() { $img.hide().attr( "src", cache.src ); $img.removeAttr( 'data-src' ); // Images loaded with some effect if existing if ( options.effect ) { if ( options.speed ) { $img[options.effect]( options.speed ); } else { $img[options.effect](); } $img.css( "opacity", 1 ); $img.show(); } else { $img.show(); } _purgeStack( currentStack ); // Callback after each image is loaded if ( !!options.callbackAfterEachImage ) { options.callbackAfterEachImage.call( this, $img, options ); } if ( _isAllImagesLoaded( currentStack ) && !!options.callback && !isCallbackDone ) { options.callback.call( $.jail, options ); isCallbackDone = true; } }; /* * Basic on error handler. For now we limit * the plugin to exectute any callback passed * in the options object. Let the developer * decide what to do with it. */ cache.onerror = function() { if ( !( 'error' in options ) ) { return; } var args = Array.prototype.slice.call( arguments, 0 ); args = [$img, options].concat( args ); options.error.apply( $.jail, args ); }; cache.src = $img.attr( "data-src" ); } /* * Return if the image is visible inside a "container" / window. There are checks around * "visibility" CSS property and around "overflow" property of the "container" * * @param $img : image selected - jQuery obj * @param container : container object * @param options : configurations object */ function _isVisibleInContainer( $img, container, options ) { var parent = $img.parent(), isVisible = true; while ( parent.length && parent.get( 0 ).nodeName.toUpperCase() !== "BODY" ) { // Consider the 'overflow' property if ( parent.css( "overflow" ) === "hidden" ) { if ( !_isInTheScreen( parent, $img, options.offset, options.loadAllHorizontal ) ) { isVisible = false; break; } } else if ( parent.css( "overflow" ) === "scroll" ) { if ( !_isInTheScreen( parent, $img, options.offset, options.loadAllHorizontal ) ) { isVisible = false; $( currentStack ).data( "triggerElem", parent ); options.event = "scroll"; _bindEvent( options, $( currentStack ) ); break; } } if ( parent.css( "visibility" ) === "hidden" || $img.css( "visibility" ) === "hidden" ) { isVisible = false; break; } // If container is not the window, and the parent is the container, exit from the loop if ( container !== $window && parent === container ) { break; } parent = parent.parent(); } return isVisible; } // Small wrapper $.fn.jail = function( options ) { new $.jail( this, options ); // Empty current stack currentStack = []; return this; }; return $.jail; } ) );