imagesloaded.js (12283B)
1 /*! 2 * imagesLoaded PACKAGED v4.1.0 3 * JavaScript is all like "You images are done yet or what?" 4 * MIT License 5 */ 6 7 /** 8 * EvEmitter v1.0.1 9 * Lil' event emitter 10 * MIT License 11 */ 12 13 /* jshint unused: true, undef: true, strict: true */ 14 15 ( function( global, factory ) { 16 // universal module definition 17 /* jshint strict: false */ /* globals define, module */ 18 if ( typeof define == 'function' && define.amd ) { 19 // AMD - RequireJS 20 define( 'ev-emitter/ev-emitter',factory ); 21 } else if ( typeof module == 'object' && module.exports ) { 22 // CommonJS - Browserify, Webpack 23 module.exports = factory(); 24 } else { 25 // Browser globals 26 global.EvEmitter = factory(); 27 } 28 29 }( this, function() { 30 31 32 33 function EvEmitter() {} 34 35 var proto = EvEmitter.prototype; 36 37 proto.on = function( eventName, listener ) { 38 if ( !eventName || !listener ) { 39 return; 40 } 41 // set events hash 42 var events = this._events = this._events || {}; 43 // set listeners array 44 var listeners = events[ eventName ] = events[ eventName ] || []; 45 // only add once 46 if ( listeners.indexOf( listener ) == -1 ) { 47 listeners.push( listener ); 48 } 49 50 return this; 51 }; 52 53 proto.once = function( eventName, listener ) { 54 if ( !eventName || !listener ) { 55 return; 56 } 57 // add event 58 this.on( eventName, listener ); 59 // set once flag 60 // set onceEvents hash 61 var onceEvents = this._onceEvents = this._onceEvents || {}; 62 // set onceListeners array 63 var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || []; 64 // set flag 65 onceListeners[ listener ] = true; 66 67 return this; 68 }; 69 70 proto.off = function( eventName, listener ) { 71 var listeners = this._events && this._events[ eventName ]; 72 if ( !listeners || !listeners.length ) { 73 return; 74 } 75 var index = listeners.indexOf( listener ); 76 if ( index != -1 ) { 77 listeners.splice( index, 1 ); 78 } 79 80 return this; 81 }; 82 83 proto.emitEvent = function( eventName, args ) { 84 var listeners = this._events && this._events[ eventName ]; 85 if ( !listeners || !listeners.length ) { 86 return; 87 } 88 var i = 0; 89 var listener = listeners[i]; 90 args = args || []; 91 // once stuff 92 var onceListeners = this._onceEvents && this._onceEvents[ eventName ]; 93 94 while ( listener ) { 95 var isOnce = onceListeners && onceListeners[ listener ]; 96 if ( isOnce ) { 97 // remove listener 98 // remove before trigger to prevent recursion 99 this.off( eventName, listener ); 100 // unset once flag 101 delete onceListeners[ listener ]; 102 } 103 // trigger listener 104 listener.apply( this, args ); 105 // get next listener 106 i += isOnce ? 0 : 1; 107 listener = listeners[i]; 108 } 109 110 return this; 111 }; 112 113 return EvEmitter; 114 115 })); 116 117 /*! 118 * imagesLoaded v4.1.0 119 * JavaScript is all like "You images are done yet or what?" 120 * MIT License 121 */ 122 123 ( function( window, factory ) { 'use strict'; 124 // universal module definition 125 126 /*global define: false, module: false, require: false */ 127 128 if ( typeof define == 'function' && define.amd ) { 129 // AMD 130 define( [ 131 'ev-emitter/ev-emitter' 132 ], function( EvEmitter ) { 133 return factory( window, EvEmitter ); 134 }); 135 } else if ( typeof module == 'object' && module.exports ) { 136 // CommonJS 137 module.exports = factory( 138 window, 139 require('ev-emitter') 140 ); 141 } else { 142 // browser global 143 window.imagesLoaded = factory( 144 window, 145 window.EvEmitter 146 ); 147 } 148 149 })( window, 150 151 // -------------------------- factory -------------------------- // 152 153 function factory( window, EvEmitter ) { 154 155 156 157 var $ = window.jQuery; 158 var console = window.console; 159 160 // -------------------------- helpers -------------------------- // 161 162 // extend objects 163 function extend( a, b ) { 164 for ( var prop in b ) { 165 a[ prop ] = b[ prop ]; 166 } 167 return a; 168 } 169 170 // turn element or nodeList into an array 171 function makeArray( obj ) { 172 var ary = []; 173 if ( Array.isArray( obj ) ) { 174 // use object if already an array 175 ary = obj; 176 } else if ( typeof obj.length == 'number' ) { 177 // convert nodeList to array 178 for ( var i=0; i < obj.length; i++ ) { 179 ary.push( obj[i] ); 180 } 181 } else { 182 // array of single index 183 ary.push( obj ); 184 } 185 return ary; 186 } 187 188 // -------------------------- imagesLoaded -------------------------- // 189 190 /** 191 * @param {Array, Element, NodeList, String} elem 192 * @param {Object or Function} options - if function, use as callback 193 * @param {Function} onAlways - callback function 194 */ 195 function ImagesLoaded( elem, options, onAlways ) { 196 // coerce ImagesLoaded() without new, to be new ImagesLoaded() 197 if ( !( this instanceof ImagesLoaded ) ) { 198 return new ImagesLoaded( elem, options, onAlways ); 199 } 200 // use elem as selector string 201 if ( typeof elem == 'string' ) { 202 elem = document.querySelectorAll( elem ); 203 } 204 205 this.elements = makeArray( elem ); 206 this.options = extend( {}, this.options ); 207 208 if ( typeof options == 'function' ) { 209 onAlways = options; 210 } else { 211 extend( this.options, options ); 212 } 213 214 if ( onAlways ) { 215 this.on( 'always', onAlways ); 216 } 217 218 this.getImages(); 219 220 if ( $ ) { 221 // add jQuery Deferred object 222 this.jqDeferred = new $.Deferred(); 223 } 224 225 // HACK check async to allow time to bind listeners 226 setTimeout( function() { 227 this.check(); 228 }.bind( this )); 229 } 230 231 ImagesLoaded.prototype = Object.create( EvEmitter.prototype ); 232 233 ImagesLoaded.prototype.options = {}; 234 235 ImagesLoaded.prototype.getImages = function() { 236 this.images = []; 237 238 // filter & find items if we have an item selector 239 this.elements.forEach( this.addElementImages, this ); 240 }; 241 242 /** 243 * @param {Node} element 244 */ 245 ImagesLoaded.prototype.addElementImages = function( elem ) { 246 // filter siblings 247 if ( elem.nodeName == 'IMG' ) { 248 this.addImage( elem ); 249 } 250 // get background image on element 251 if ( this.options.background === true ) { 252 this.addElementBackgroundImages( elem ); 253 } 254 255 // find children 256 // no non-element nodes, #143 257 var nodeType = elem.nodeType; 258 if ( !nodeType || !elementNodeTypes[ nodeType ] ) { 259 return; 260 } 261 var childImgs = elem.querySelectorAll('img'); 262 // concat childElems to filterFound array 263 for ( var i=0; i < childImgs.length; i++ ) { 264 var img = childImgs[i]; 265 this.addImage( img ); 266 } 267 268 // get child background images 269 if ( typeof this.options.background == 'string' ) { 270 var children = elem.querySelectorAll( this.options.background ); 271 for ( i=0; i < children.length; i++ ) { 272 var child = children[i]; 273 this.addElementBackgroundImages( child ); 274 } 275 } 276 }; 277 278 var elementNodeTypes = { 279 1: true, 280 9: true, 281 11: true 282 }; 283 284 ImagesLoaded.prototype.addElementBackgroundImages = function( elem ) { 285 var style = getComputedStyle( elem ); 286 if ( !style ) { 287 // Firefox returns null if in a hidden iframe https://bugzil.la/548397 288 return; 289 } 290 // get url inside url("...") 291 var reURL = /url\((['"])?(.*?)\1\)/gi; 292 var matches = reURL.exec( style.backgroundImage ); 293 while ( matches !== null ) { 294 var url = matches && matches[2]; 295 if ( url ) { 296 this.addBackground( url, elem ); 297 } 298 matches = reURL.exec( style.backgroundImage ); 299 } 300 }; 301 302 /** 303 * @param {Image} img 304 */ 305 ImagesLoaded.prototype.addImage = function( img ) { 306 var loadingImage = new LoadingImage( img ); 307 this.images.push( loadingImage ); 308 }; 309 310 ImagesLoaded.prototype.addBackground = function( url, elem ) { 311 var background = new Background( url, elem ); 312 this.images.push( background ); 313 }; 314 315 ImagesLoaded.prototype.check = function() { 316 var _this = this; 317 this.progressedCount = 0; 318 this.hasAnyBroken = false; 319 // complete if no images 320 if ( !this.images.length ) { 321 this.complete(); 322 return; 323 } 324 325 function onProgress( image, elem, message ) { 326 // HACK - Chrome triggers event before object properties have changed. #83 327 setTimeout( function() { 328 _this.progress( image, elem, message ); 329 }); 330 } 331 332 this.images.forEach( function( loadingImage ) { 333 loadingImage.once( 'progress', onProgress ); 334 loadingImage.check(); 335 }); 336 }; 337 338 ImagesLoaded.prototype.progress = function( image, elem, message ) { 339 this.progressedCount++; 340 this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded; 341 // progress event 342 this.emitEvent( 'progress', [ this, image, elem ] ); 343 if ( this.jqDeferred && this.jqDeferred.notify ) { 344 this.jqDeferred.notify( this, image ); 345 } 346 // check if completed 347 if ( this.progressedCount == this.images.length ) { 348 this.complete(); 349 } 350 351 if ( this.options.debug && console ) { 352 console.log( 'progress: ' + message, image, elem ); 353 } 354 }; 355 356 ImagesLoaded.prototype.complete = function() { 357 var eventName = this.hasAnyBroken ? 'fail' : 'done'; 358 this.isComplete = true; 359 this.emitEvent( eventName, [ this ] ); 360 this.emitEvent( 'always', [ this ] ); 361 if ( this.jqDeferred ) { 362 var jqMethod = this.hasAnyBroken ? 'reject' : 'resolve'; 363 this.jqDeferred[ jqMethod ]( this ); 364 } 365 }; 366 367 // -------------------------- -------------------------- // 368 369 function LoadingImage( img ) { 370 this.img = img; 371 } 372 373 LoadingImage.prototype = Object.create( EvEmitter.prototype ); 374 375 LoadingImage.prototype.check = function() { 376 // If complete is true and browser supports natural sizes, 377 // try to check for image status manually. 378 var isComplete = this.getIsImageComplete(); 379 if ( isComplete ) { 380 // report based on naturalWidth 381 this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' ); 382 return; 383 } 384 385 // If none of the checks above matched, simulate loading on detached element. 386 this.proxyImage = new Image(); 387 this.proxyImage.addEventListener( 'load', this ); 388 this.proxyImage.addEventListener( 'error', this ); 389 // bind to image as well for Firefox. #191 390 this.img.addEventListener( 'load', this ); 391 this.img.addEventListener( 'error', this ); 392 this.proxyImage.src = this.img.src; 393 }; 394 395 LoadingImage.prototype.getIsImageComplete = function() { 396 return this.img.complete && this.img.naturalWidth !== undefined; 397 }; 398 399 LoadingImage.prototype.confirm = function( isLoaded, message ) { 400 this.isLoaded = isLoaded; 401 this.emitEvent( 'progress', [ this, this.img, message ] ); 402 }; 403 404 // ----- events ----- // 405 406 // trigger specified handler for event type 407 LoadingImage.prototype.handleEvent = function( event ) { 408 var method = 'on' + event.type; 409 if ( this[ method ] ) { 410 this[ method ]( event ); 411 } 412 }; 413 414 LoadingImage.prototype.onload = function() { 415 this.confirm( true, 'onload' ); 416 this.unbindEvents(); 417 }; 418 419 LoadingImage.prototype.onerror = function() { 420 this.confirm( false, 'onerror' ); 421 this.unbindEvents(); 422 }; 423 424 LoadingImage.prototype.unbindEvents = function() { 425 this.proxyImage.removeEventListener( 'load', this ); 426 this.proxyImage.removeEventListener( 'error', this ); 427 this.img.removeEventListener( 'load', this ); 428 this.img.removeEventListener( 'error', this ); 429 }; 430 431 // -------------------------- Background -------------------------- // 432 433 function Background( url, element ) { 434 this.url = url; 435 this.element = element; 436 this.img = new Image(); 437 } 438 439 // inherit LoadingImage prototype 440 Background.prototype = Object.create( LoadingImage.prototype ); 441 442 Background.prototype.check = function() { 443 this.img.addEventListener( 'load', this ); 444 this.img.addEventListener( 'error', this ); 445 this.img.src = this.url; 446 // check if image is already complete 447 var isComplete = this.getIsImageComplete(); 448 if ( isComplete ) { 449 this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' ); 450 this.unbindEvents(); 451 } 452 }; 453 454 Background.prototype.unbindEvents = function() { 455 this.img.removeEventListener( 'load', this ); 456 this.img.removeEventListener( 'error', this ); 457 }; 458 459 Background.prototype.confirm = function( isLoaded, message ) { 460 this.isLoaded = isLoaded; 461 this.emitEvent( 'progress', [ this, this.element, message ] ); 462 }; 463 464 // -------------------------- jQuery -------------------------- // 465 466 ImagesLoaded.makeJQueryPlugin = function( jQuery ) { 467 jQuery = jQuery || window.jQuery; 468 if ( !jQuery ) { 469 return; 470 } 471 // set local variable 472 $ = jQuery; 473 // $().imagesLoaded() 474 $.fn.imagesLoaded = function( options, callback ) { 475 var instance = new ImagesLoaded( this, options, callback ); 476 return instance.jqDeferred.promise( $(this) ); 477 }; 478 }; 479 // try making plugin 480 ImagesLoaded.makeJQueryPlugin(); 481 482 // -------------------------- -------------------------- // 483 484 return ImagesLoaded; 485 486 }); 487