Repository: desandro/imagesloaded Branch: master Commit: 92de29b5a9a4 Files: 32 Total size: 60.0 KB Directory structure: gitextract_8bkypk_n/ ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── LICENSE.md ├── README.md ├── bower.json ├── contributing.md ├── imagesloaded.js ├── imagesloaded.pkgd.js ├── package.json ├── sandbox/ │ ├── background/ │ │ ├── css/ │ │ │ └── background.css │ │ └── index.html │ ├── picture.html │ └── progress/ │ ├── index.html │ └── progress.js ├── tasks/ │ ├── dist.js │ └── version.js └── test/ ├── css/ │ └── tests.css ├── index.html └── unit/ ├── append.js ├── background.js ├── basics.js ├── data-uri.js ├── jquery-fail.js ├── jquery-success.js ├── local-files.js ├── no-images.js ├── non-element.js ├── picture.js ├── selector-string.js ├── single-element.js └── srcset.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ /* eslint-env node */ module.exports = { plugins: [ 'metafizzy' ], extends: 'plugin:metafizzy/browser', env: { browser: true, commonjs: true, }, parserOptions: { ecmaVersion: 2018, }, globals: { imagesLoaded: 'readonly', QUnit: 'readonly', }, rules: { eqeqeq: [ 'error', 'smart' ], 'id-length': [ 'error', { min: 2, max: 30, exceptions: [ 'x', 'y', 'z', 'i', 'j', 'a', 'b', 't', '$' ], } ], 'new-cap': 'off', }, ignorePatterns: [ '*pkgd*.js' ], }; ================================================ FILE: .gitignore ================================================ bower_components/ node_modules/ build/index.html build/*.woff* build/imagesloaded*.js .netlify ================================================ FILE: .nvmrc ================================================ 16 ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2011-2022 [David DeSandro](https://desandro.com) and [contributors](https://github.com/desandro/imagesloaded/graphs/contributors) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # imagesLoaded

JavaScript is all like "You images done yet or what?"

[imagesloaded.desandro.com](https://imagesloaded.desandro.com) Detect when images have been loaded. ## Install ### Download + [imagesloaded.pkgd.min.js](https://unpkg.com/imagesloaded@5/imagesloaded.pkgd.min.js) minified + [imagesloaded.pkgd.js](https://unpkg.com/imagesloaded@5/imagesloaded.pkgd.js) un-minified ### CDN ``` html ``` ### Package managers Install via npm: `npm install imagesloaded` Install via Yarn: `yarn add imagesloaded` ## jQuery You can use imagesLoaded as a jQuery Plugin. ``` js $('#container').imagesLoaded( function() { // images have loaded }); // options $('#container').imagesLoaded( { // options... }, function() { // images have loaded } ); ``` `.imagesLoaded()` returns a [jQuery Deferred object](https://api.jquery.com/category/deferred-object/). This allows you to use `.always()`, `.done()`, `.fail()` and `.progress()`. ``` js $('#container').imagesLoaded() .always( function( instance ) { console.log('all images loaded'); }) .done( function( instance ) { console.log('all images successfully loaded'); }) .fail( function() { console.log('all images loaded, at least one is broken'); }) .progress( function( instance, image ) { var result = image.isLoaded ? 'loaded' : 'broken'; console.log( 'image is ' + result + ' for ' + image.img.src ); }); ``` ## Vanilla JavaScript You can use imagesLoaded with vanilla JS. ``` js imagesLoaded( elem, callback ) // options imagesLoaded( elem, options, callback ) // you can use `new` if you like new imagesLoaded( elem, callback ) ``` + `elem` _Element, NodeList, Array, or Selector String_ + `options` _Object_ + `callback` _Function_ - function triggered after all images have been loaded Using a callback function is the same as binding it to the `always` event (see below). ``` js // element imagesLoaded( document.querySelector('#container'), function( instance ) { console.log('all images are loaded'); }); // selector string imagesLoaded( '#container', function() {...}); // multiple elements var posts = document.querySelectorAll('.post'); imagesLoaded( posts, function() {...}); ``` Bind events with vanilla JS with .on(), .off(), and .once() methods. ``` js var imgLoad = imagesLoaded( elem ); function onAlways( instance ) { console.log('all images are loaded'); } // bind with .on() imgLoad.on( 'always', onAlways ); // unbind with .off() imgLoad.off( 'always', onAlways ); ``` ## Background Detect when background images have loaded, in addition to ``s. Set `{ background: true }` to detect when the element's background image has loaded. ``` js // jQuery $('#container').imagesLoaded( { background: true }, function() { console.log('#container background image loaded'); }); // vanilla JS imagesLoaded( '#container', { background: true }, function() { console.log('#container background image loaded'); }); ``` [See jQuery demo](https://codepen.io/desandro/pen/pjVMPB) or [vanilla JS demo](https://codepen.io/desandro/pen/avKooW) on CodePen. Set to a selector string like `{ background: '.item' }` to detect when the background images of child elements have loaded. ``` js // jQuery $('#container').imagesLoaded( { background: '.item' }, function() { console.log('all .item background images loaded'); }); // vanilla JS imagesLoaded( '#container', { background: '.item' }, function() { console.log('all .item background images loaded'); }); ``` [See jQuery demo](https://codepen.io/desandro/pen/avKoZL) or [vanilla JS demo](https://codepen.io/desandro/pen/vNrBGz) on CodePen. ## Events ### always ``` js // jQuery $('#container').imagesLoaded().always( function( instance ) { console.log('ALWAYS - all images have been loaded'); }); // vanilla JS imgLoad.on( 'always', function( instance ) { console.log('ALWAYS - all images have been loaded'); }); ``` Triggered after all images have been either loaded or confirmed broken. + `instance` _imagesLoaded_ - the imagesLoaded instance ### done ``` js // jQuery $('#container').imagesLoaded().done( function( instance ) { console.log('DONE - all images have been successfully loaded'); }); // vanilla JS imgLoad.on( 'done', function( instance ) { console.log('DONE - all images have been successfully loaded'); }); ``` Triggered after all images have successfully loaded without any broken images. ### fail ``` js $('#container').imagesLoaded().fail( function( instance ) { console.log('FAIL - all images loaded, at least one is broken'); }); // vanilla JS imgLoad.on( 'fail', function( instance ) { console.log('FAIL - all images loaded, at least one is broken'); }); ``` Triggered after all images have been loaded with at least one broken image. ### progress ``` js imgLoad.on( 'progress', function( instance, image ) { var result = image.isLoaded ? 'loaded' : 'broken'; console.log( 'image is ' + result + ' for ' + image.img.src ); }); ``` Triggered after each image has been loaded. + `instance` _imagesLoaded_ - the imagesLoaded instance + `image` _LoadingImage_ - the LoadingImage instance of the loaded image ## Properties ### LoadingImage.img _Image_ - The `img` element ### LoadingImage.isLoaded _Boolean_ - `true` when the image has successfully loaded ### imagesLoaded.images Array of _LoadingImage_ instances for each image detected ``` js var imgLoad = imagesLoaded('#container'); imgLoad.on( 'always', function() { console.log( imgLoad.images.length + ' images loaded' ); // detect which image is broken for ( var i = 0, len = imgLoad.images.length; i < len; i++ ) { var image = imgLoad.images[i]; var result = image.isLoaded ? 'loaded' : 'broken'; console.log( 'image is ' + result + ' for ' + image.img.src ); } }); ``` ## Webpack Install imagesLoaded with npm. ``` bash npm install imagesloaded ``` You can then `require('imagesloaded')`. ``` js // main.js var imagesLoaded = require('imagesloaded'); imagesLoaded( '#container', function() { // images have loaded }); ``` Use `.makeJQueryPlugin` to make `.imagesLoaded()` jQuery plugin. ``` js // main.js var imagesLoaded = require('imagesloaded'); var $ = require('jquery'); // provide jQuery argument imagesLoaded.makeJQueryPlugin( $ ); // now use .imagesLoaded() jQuery plugin $('#container').imagesLoaded( function() {...}); ``` Run webpack. ``` bash webpack main.js bundle.js ``` ## Browserify imagesLoaded works with [Browserify](https://browserify.org/). ``` bash npm install imagesloaded --save ``` ``` js var imagesLoaded = require('imagesloaded'); imagesLoaded( elem, function() {...} ); ``` Use `.makeJQueryPlugin` to make to use `.imagesLoaded()` jQuery plugin. ``` js var $ = require('jquery'); var imagesLoaded = require('imagesloaded'); // provide jQuery argument imagesLoaded.makeJQueryPlugin( $ ); // now use .imagesLoaded() jQuery plugin $('#container').imagesLoaded( function() {...}); ``` ## Browser support + Chrome 49+ + Firefox 41+ + Edge 14+ + iOS Safari 8+ Use [imagesLoaded v4](https://github.com/desandro/imagesloaded/tree/v4.1.4) for Internet Explorer and other older browser support. ## Development Development uses Node.js v16 with npm v8 ``` bash nvm use ``` ## MIT License imagesLoaded is released under the [MIT License](https://desandro.mit-license.org/). Have at it. ================================================ FILE: bower.json ================================================ { "name": "imagesloaded", "description": "JavaScript is all like _You images done yet or what?_", "main": "imagesloaded.js", "dependencies": { "ev-emitter": "^2.1.2" }, "devDependencies": { }, "ignore": [ "**/.*", "test", "package.json", "composer.json", "node_modules", "bower_components", "tests", "sandbox/", "gulpfile.js", "contributing.md" ], "homepage": "https://imagesloaded.desandro.com", "authors": [ "David DeSandro" ], "moduleType": [ "amd", "globals", "node" ], "keywords": [ "images" ], "license": "MIT" } ================================================ FILE: contributing.md ================================================ ## Submitting issues ### Reduced test case required All bug reports and problem issues require a [**reduced test case**](https://css-tricks.com/reduced-test-cases/). + A reduced test case clearly demonstrates the bug or issue. + It contains the bare minimum HTML, CSS, and JavaScript required to demonstrate the bug. + A link to your production site is **not** a reduced test case. Create a test case by forking a [CodePen demos](https://codepen.io/collection/xKRgYx). + [progress with jQuery](https://codepen.io/desandro/pen/podVjYW) + [progress with vanilla JS](https://codepen.io/desandro/pen/XWzqXrP) + [`{ background: true }` with jQuery](https://codepen.io/desandro/pen/QWOrNNY) + [`{ background: true }` with vanilla JS](https://codepen.io/desandro/pen/oNodxYW) + [`{ background: '.selector' }` with jQuery](https://codepen.io/desandro/pen/YzELWdo) + [`{ background: '.selector' }` with vanilla JS](https://codepen.io/desandro/pen/KKyRMEv) Providing a reduced test case is the best way to get your issue addressed. They help you point out the problem. They help me verify and debug the problem. They help others understand the problem. Without a reduced test case, your issue may be closed. ================================================ FILE: imagesloaded.js ================================================ /*! * imagesLoaded v5.0.0 * JavaScript is all like "You images are done yet or what?" * MIT License */ ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( window, require('ev-emitter') ); } else { // browser global window.imagesLoaded = factory( window, window.EvEmitter ); } } )( typeof window !== 'undefined' ? window : this, function factory( window, EvEmitter ) { let $ = window.jQuery; let console = window.console; // -------------------------- helpers -------------------------- // // turn element or nodeList into an array function makeArray( obj ) { // use object if already an array if ( Array.isArray( obj ) ) return obj; let isArrayLike = typeof obj == 'object' && typeof obj.length == 'number'; // convert nodeList to array if ( isArrayLike ) return [ ...obj ]; // array of single index return [ obj ]; } // -------------------------- imagesLoaded -------------------------- // /** * @param {[Array, Element, NodeList, String]} elem * @param {[Object, Function]} options - if function, use as callback * @param {Function} onAlways - callback function * @returns {ImagesLoaded} */ function ImagesLoaded( elem, options, onAlways ) { // coerce ImagesLoaded() without new, to be new ImagesLoaded() if ( !( this instanceof ImagesLoaded ) ) { return new ImagesLoaded( elem, options, onAlways ); } // use elem as selector string let queryElem = elem; if ( typeof elem == 'string' ) { queryElem = document.querySelectorAll( elem ); } // bail if bad element if ( !queryElem ) { console.error(`Bad element for imagesLoaded ${queryElem || elem}`); return; } this.elements = makeArray( queryElem ); this.options = {}; // shift arguments if no options set if ( typeof options == 'function' ) { onAlways = options; } else { Object.assign( this.options, options ); } if ( onAlways ) this.on( 'always', onAlways ); this.getImages(); // add jQuery Deferred object if ( $ ) this.jqDeferred = new $.Deferred(); // HACK check async to allow time to bind listeners setTimeout( this.check.bind( this ) ); } ImagesLoaded.prototype = Object.create( EvEmitter.prototype ); ImagesLoaded.prototype.getImages = function() { this.images = []; // filter & find items if we have an item selector this.elements.forEach( this.addElementImages, this ); }; const elementNodeTypes = [ 1, 9, 11 ]; /** * @param {Node} elem */ ImagesLoaded.prototype.addElementImages = function( elem ) { // filter siblings if ( elem.nodeName === 'IMG' ) { this.addImage( elem ); } // get background image on element if ( this.options.background === true ) { this.addElementBackgroundImages( elem ); } // find children // no non-element nodes, #143 let { nodeType } = elem; if ( !nodeType || !elementNodeTypes.includes( nodeType ) ) return; let childImgs = elem.querySelectorAll('img'); // concat childElems to filterFound array for ( let img of childImgs ) { this.addImage( img ); } // get child background images if ( typeof this.options.background == 'string' ) { let children = elem.querySelectorAll( this.options.background ); for ( let child of children ) { this.addElementBackgroundImages( child ); } } }; const reURL = /url\((['"])?(.*?)\1\)/gi; ImagesLoaded.prototype.addElementBackgroundImages = function( elem ) { let style = getComputedStyle( elem ); // Firefox returns null if in a hidden iframe https://bugzil.la/548397 if ( !style ) return; // get url inside url("...") let matches = reURL.exec( style.backgroundImage ); while ( matches !== null ) { let url = matches && matches[2]; if ( url ) { this.addBackground( url, elem ); } matches = reURL.exec( style.backgroundImage ); } }; /** * @param {Image} img */ ImagesLoaded.prototype.addImage = function( img ) { let loadingImage = new LoadingImage( img ); this.images.push( loadingImage ); }; ImagesLoaded.prototype.addBackground = function( url, elem ) { let background = new Background( url, elem ); this.images.push( background ); }; ImagesLoaded.prototype.check = function() { this.progressedCount = 0; this.hasAnyBroken = false; // complete if no images if ( !this.images.length ) { this.complete(); return; } /* eslint-disable-next-line func-style */ let onProgress = ( image, elem, message ) => { // HACK - Chrome triggers event before object properties have changed. #83 setTimeout( () => { this.progress( image, elem, message ); } ); }; this.images.forEach( function( loadingImage ) { loadingImage.once( 'progress', onProgress ); loadingImage.check(); } ); }; ImagesLoaded.prototype.progress = function( image, elem, message ) { this.progressedCount++; this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded; // progress event this.emitEvent( 'progress', [ this, image, elem ] ); if ( this.jqDeferred && this.jqDeferred.notify ) { this.jqDeferred.notify( this, image ); } // check if completed if ( this.progressedCount === this.images.length ) { this.complete(); } if ( this.options.debug && console ) { console.log( `progress: ${message}`, image, elem ); } }; ImagesLoaded.prototype.complete = function() { let eventName = this.hasAnyBroken ? 'fail' : 'done'; this.isComplete = true; this.emitEvent( eventName, [ this ] ); this.emitEvent( 'always', [ this ] ); if ( this.jqDeferred ) { let jqMethod = this.hasAnyBroken ? 'reject' : 'resolve'; this.jqDeferred[ jqMethod ]( this ); } }; // -------------------------- -------------------------- // function LoadingImage( img ) { this.img = img; } LoadingImage.prototype = Object.create( EvEmitter.prototype ); LoadingImage.prototype.check = function() { // If complete is true and browser supports natural sizes, // try to check for image status manually. let isComplete = this.getIsImageComplete(); if ( isComplete ) { // report based on naturalWidth this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' ); return; } // If none of the checks above matched, simulate loading on detached element. this.proxyImage = new Image(); // add crossOrigin attribute. #204 if ( this.img.crossOrigin ) { this.proxyImage.crossOrigin = this.img.crossOrigin; } this.proxyImage.addEventListener( 'load', this ); this.proxyImage.addEventListener( 'error', this ); // bind to image as well for Firefox. #191 this.img.addEventListener( 'load', this ); this.img.addEventListener( 'error', this ); this.proxyImage.src = this.img.currentSrc || this.img.src; }; LoadingImage.prototype.getIsImageComplete = function() { // check for non-zero, non-undefined naturalWidth // fixes Safari+InfiniteScroll+Masonry bug infinite-scroll#671 return this.img.complete && this.img.naturalWidth; }; LoadingImage.prototype.confirm = function( isLoaded, message ) { this.isLoaded = isLoaded; let { parentNode } = this.img; // emit progress with parent or self let elem = parentNode.nodeName === 'PICTURE' ? parentNode : this.img; this.emitEvent( 'progress', [ this, elem, message ] ); }; // ----- events ----- // // trigger specified handler for event type LoadingImage.prototype.handleEvent = function( event ) { let method = 'on' + event.type; if ( this[ method ] ) { this[ method ]( event ); } }; LoadingImage.prototype.onload = function() { this.confirm( true, 'onload' ); this.unbindEvents(); }; LoadingImage.prototype.onerror = function() { this.confirm( false, 'onerror' ); this.unbindEvents(); }; LoadingImage.prototype.unbindEvents = function() { this.proxyImage.removeEventListener( 'load', this ); this.proxyImage.removeEventListener( 'error', this ); this.img.removeEventListener( 'load', this ); this.img.removeEventListener( 'error', this ); }; // -------------------------- Background -------------------------- // function Background( url, element ) { this.url = url; this.element = element; this.img = new Image(); } // inherit LoadingImage prototype Background.prototype = Object.create( LoadingImage.prototype ); Background.prototype.check = function() { this.img.addEventListener( 'load', this ); this.img.addEventListener( 'error', this ); this.img.src = this.url; // check if image is already complete let isComplete = this.getIsImageComplete(); if ( isComplete ) { this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' ); this.unbindEvents(); } }; Background.prototype.unbindEvents = function() { this.img.removeEventListener( 'load', this ); this.img.removeEventListener( 'error', this ); }; Background.prototype.confirm = function( isLoaded, message ) { this.isLoaded = isLoaded; this.emitEvent( 'progress', [ this, this.element, message ] ); }; // -------------------------- jQuery -------------------------- // ImagesLoaded.makeJQueryPlugin = function( jQuery ) { jQuery = jQuery || window.jQuery; if ( !jQuery ) return; // set local variable $ = jQuery; // $().imagesLoaded() $.fn.imagesLoaded = function( options, onAlways ) { let instance = new ImagesLoaded( this, options, onAlways ); return instance.jqDeferred.promise( $( this ) ); }; }; // try making plugin ImagesLoaded.makeJQueryPlugin(); // -------------------------- -------------------------- // return ImagesLoaded; } ); ================================================ FILE: imagesloaded.pkgd.js ================================================ /*! * imagesLoaded PACKAGED v5.0.0 * JavaScript is all like "You images are done yet or what?" * MIT License */ /** * EvEmitter v2.1.1 * Lil' event emitter * MIT License */ ( function( global, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS - Browserify, Webpack module.exports = factory(); } else { // Browser globals global.EvEmitter = factory(); } }( typeof window != 'undefined' ? window : this, function() { function EvEmitter() {} let proto = EvEmitter.prototype; proto.on = function( eventName, listener ) { if ( !eventName || !listener ) return this; // set events hash let events = this._events = this._events || {}; // set listeners array let listeners = events[ eventName ] = events[ eventName ] || []; // only add once if ( !listeners.includes( listener ) ) { listeners.push( listener ); } return this; }; proto.once = function( eventName, listener ) { if ( !eventName || !listener ) return this; // add event this.on( eventName, listener ); // set once flag // set onceEvents hash let onceEvents = this._onceEvents = this._onceEvents || {}; // set onceListeners object let onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {}; // set flag onceListeners[ listener ] = true; return this; }; proto.off = function( eventName, listener ) { let listeners = this._events && this._events[ eventName ]; if ( !listeners || !listeners.length ) return this; let index = listeners.indexOf( listener ); if ( index != -1 ) { listeners.splice( index, 1 ); } return this; }; proto.emitEvent = function( eventName, args ) { let listeners = this._events && this._events[ eventName ]; if ( !listeners || !listeners.length ) return this; // copy over to avoid interference if .off() in listener listeners = listeners.slice( 0 ); args = args || []; // once stuff let onceListeners = this._onceEvents && this._onceEvents[ eventName ]; for ( let listener of listeners ) { let isOnce = onceListeners && onceListeners[ listener ]; if ( isOnce ) { // remove listener // remove before trigger to prevent recursion this.off( eventName, listener ); // unset once flag delete onceListeners[ listener ]; } // trigger listener listener.apply( this, args ); } return this; }; proto.allOff = function() { delete this._events; delete this._onceEvents; return this; }; return EvEmitter; } ) ); /*! * imagesLoaded v5.0.0 * JavaScript is all like "You images are done yet or what?" * MIT License */ ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( window, require('ev-emitter') ); } else { // browser global window.imagesLoaded = factory( window, window.EvEmitter ); } } )( typeof window !== 'undefined' ? window : this, function factory( window, EvEmitter ) { let $ = window.jQuery; let console = window.console; // -------------------------- helpers -------------------------- // // turn element or nodeList into an array function makeArray( obj ) { // use object if already an array if ( Array.isArray( obj ) ) return obj; let isArrayLike = typeof obj == 'object' && typeof obj.length == 'number'; // convert nodeList to array if ( isArrayLike ) return [ ...obj ]; // array of single index return [ obj ]; } // -------------------------- imagesLoaded -------------------------- // /** * @param {[Array, Element, NodeList, String]} elem * @param {[Object, Function]} options - if function, use as callback * @param {Function} onAlways - callback function * @returns {ImagesLoaded} */ function ImagesLoaded( elem, options, onAlways ) { // coerce ImagesLoaded() without new, to be new ImagesLoaded() if ( !( this instanceof ImagesLoaded ) ) { return new ImagesLoaded( elem, options, onAlways ); } // use elem as selector string let queryElem = elem; if ( typeof elem == 'string' ) { queryElem = document.querySelectorAll( elem ); } // bail if bad element if ( !queryElem ) { console.error(`Bad element for imagesLoaded ${queryElem || elem}`); return; } this.elements = makeArray( queryElem ); this.options = {}; // shift arguments if no options set if ( typeof options == 'function' ) { onAlways = options; } else { Object.assign( this.options, options ); } if ( onAlways ) this.on( 'always', onAlways ); this.getImages(); // add jQuery Deferred object if ( $ ) this.jqDeferred = new $.Deferred(); // HACK check async to allow time to bind listeners setTimeout( this.check.bind( this ) ); } ImagesLoaded.prototype = Object.create( EvEmitter.prototype ); ImagesLoaded.prototype.getImages = function() { this.images = []; // filter & find items if we have an item selector this.elements.forEach( this.addElementImages, this ); }; const elementNodeTypes = [ 1, 9, 11 ]; /** * @param {Node} elem */ ImagesLoaded.prototype.addElementImages = function( elem ) { // filter siblings if ( elem.nodeName === 'IMG' ) { this.addImage( elem ); } // get background image on element if ( this.options.background === true ) { this.addElementBackgroundImages( elem ); } // find children // no non-element nodes, #143 let { nodeType } = elem; if ( !nodeType || !elementNodeTypes.includes( nodeType ) ) return; let childImgs = elem.querySelectorAll('img'); // concat childElems to filterFound array for ( let img of childImgs ) { this.addImage( img ); } // get child background images if ( typeof this.options.background == 'string' ) { let children = elem.querySelectorAll( this.options.background ); for ( let child of children ) { this.addElementBackgroundImages( child ); } } }; const reURL = /url\((['"])?(.*?)\1\)/gi; ImagesLoaded.prototype.addElementBackgroundImages = function( elem ) { let style = getComputedStyle( elem ); // Firefox returns null if in a hidden iframe https://bugzil.la/548397 if ( !style ) return; // get url inside url("...") let matches = reURL.exec( style.backgroundImage ); while ( matches !== null ) { let url = matches && matches[2]; if ( url ) { this.addBackground( url, elem ); } matches = reURL.exec( style.backgroundImage ); } }; /** * @param {Image} img */ ImagesLoaded.prototype.addImage = function( img ) { let loadingImage = new LoadingImage( img ); this.images.push( loadingImage ); }; ImagesLoaded.prototype.addBackground = function( url, elem ) { let background = new Background( url, elem ); this.images.push( background ); }; ImagesLoaded.prototype.check = function() { this.progressedCount = 0; this.hasAnyBroken = false; // complete if no images if ( !this.images.length ) { this.complete(); return; } /* eslint-disable-next-line func-style */ let onProgress = ( image, elem, message ) => { // HACK - Chrome triggers event before object properties have changed. #83 setTimeout( () => { this.progress( image, elem, message ); } ); }; this.images.forEach( function( loadingImage ) { loadingImage.once( 'progress', onProgress ); loadingImage.check(); } ); }; ImagesLoaded.prototype.progress = function( image, elem, message ) { this.progressedCount++; this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded; // progress event this.emitEvent( 'progress', [ this, image, elem ] ); if ( this.jqDeferred && this.jqDeferred.notify ) { this.jqDeferred.notify( this, image ); } // check if completed if ( this.progressedCount === this.images.length ) { this.complete(); } if ( this.options.debug && console ) { console.log( `progress: ${message}`, image, elem ); } }; ImagesLoaded.prototype.complete = function() { let eventName = this.hasAnyBroken ? 'fail' : 'done'; this.isComplete = true; this.emitEvent( eventName, [ this ] ); this.emitEvent( 'always', [ this ] ); if ( this.jqDeferred ) { let jqMethod = this.hasAnyBroken ? 'reject' : 'resolve'; this.jqDeferred[ jqMethod ]( this ); } }; // -------------------------- -------------------------- // function LoadingImage( img ) { this.img = img; } LoadingImage.prototype = Object.create( EvEmitter.prototype ); LoadingImage.prototype.check = function() { // If complete is true and browser supports natural sizes, // try to check for image status manually. let isComplete = this.getIsImageComplete(); if ( isComplete ) { // report based on naturalWidth this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' ); return; } // If none of the checks above matched, simulate loading on detached element. this.proxyImage = new Image(); // add crossOrigin attribute. #204 if ( this.img.crossOrigin ) { this.proxyImage.crossOrigin = this.img.crossOrigin; } this.proxyImage.addEventListener( 'load', this ); this.proxyImage.addEventListener( 'error', this ); // bind to image as well for Firefox. #191 this.img.addEventListener( 'load', this ); this.img.addEventListener( 'error', this ); this.proxyImage.src = this.img.currentSrc || this.img.src; }; LoadingImage.prototype.getIsImageComplete = function() { // check for non-zero, non-undefined naturalWidth // fixes Safari+InfiniteScroll+Masonry bug infinite-scroll#671 return this.img.complete && this.img.naturalWidth; }; LoadingImage.prototype.confirm = function( isLoaded, message ) { this.isLoaded = isLoaded; let { parentNode } = this.img; // emit progress with parent or self let elem = parentNode.nodeName === 'PICTURE' ? parentNode : this.img; this.emitEvent( 'progress', [ this, elem, message ] ); }; // ----- events ----- // // trigger specified handler for event type LoadingImage.prototype.handleEvent = function( event ) { let method = 'on' + event.type; if ( this[ method ] ) { this[ method ]( event ); } }; LoadingImage.prototype.onload = function() { this.confirm( true, 'onload' ); this.unbindEvents(); }; LoadingImage.prototype.onerror = function() { this.confirm( false, 'onerror' ); this.unbindEvents(); }; LoadingImage.prototype.unbindEvents = function() { this.proxyImage.removeEventListener( 'load', this ); this.proxyImage.removeEventListener( 'error', this ); this.img.removeEventListener( 'load', this ); this.img.removeEventListener( 'error', this ); }; // -------------------------- Background -------------------------- // function Background( url, element ) { this.url = url; this.element = element; this.img = new Image(); } // inherit LoadingImage prototype Background.prototype = Object.create( LoadingImage.prototype ); Background.prototype.check = function() { this.img.addEventListener( 'load', this ); this.img.addEventListener( 'error', this ); this.img.src = this.url; // check if image is already complete let isComplete = this.getIsImageComplete(); if ( isComplete ) { this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' ); this.unbindEvents(); } }; Background.prototype.unbindEvents = function() { this.img.removeEventListener( 'load', this ); this.img.removeEventListener( 'error', this ); }; Background.prototype.confirm = function( isLoaded, message ) { this.isLoaded = isLoaded; this.emitEvent( 'progress', [ this, this.element, message ] ); }; // -------------------------- jQuery -------------------------- // ImagesLoaded.makeJQueryPlugin = function( jQuery ) { jQuery = jQuery || window.jQuery; if ( !jQuery ) return; // set local variable $ = jQuery; // $().imagesLoaded() $.fn.imagesLoaded = function( options, onAlways ) { let instance = new ImagesLoaded( this, options, onAlways ); return instance.jqDeferred.promise( $( this ) ); }; }; // try making plugin ImagesLoaded.makeJQueryPlugin(); // -------------------------- -------------------------- // return ImagesLoaded; } ); ================================================ FILE: package.json ================================================ { "name": "imagesloaded", "version": "5.0.0", "description": "JavaScript is all like _You images done yet or what?_", "main": "imagesloaded.js", "files": [ "imagesloaded*.js" ], "dependencies": { "ev-emitter": "^2.1.2" }, "devDependencies": { "eslint": "^7.32.0", "eslint-plugin-metafizzy": "^1.2.1", "jquery": "^3.6.0", "qunit": "^2.17.2", "terser": "^5.10.0" }, "scripts": { "test": "npm run lint", "lint": "npx eslint .", "dist": "node tasks/dist.js", "version": "node tasks/version.js && npm run dist && git add -A ." }, "repository": { "type": "git", "url": "git://github.com/desandro/imagesloaded.git" }, "keywords": [ "images", "loaded", "ui", "dom", "jquery-plugin" ], "license": "MIT", "bugs": { "url": "https://github.com/desandro/imagesloaded/issues" }, "homepage": "https://github.com/desandro/imagesloaded", "directories": { "test": "test" }, "author": "David DeSandro" } ================================================ FILE: sandbox/background/css/background.css ================================================ .box { width: 300px; height: 300px; margin: 0 20px 20px 0; border: 1px solid; display: inline-block; } .orange-tree { background: url('https://i.imgur.com/bwy74ok.jpg'); background-size: cover; } .thunder-cloud { background: url('../../../test/img/thunder-cloud.jpg'); background-size: contain; } .multi1 { background: url("https://i.imgur.com/ZAVN3.png"), url('https://i.imgur.com/6UdOxeB.png') bottom right, url(https://i.imgur.com/LkmcILl.jpg); background-size: cover; } .blue { background: #09F; } ================================================ FILE: sandbox/background/index.html ================================================ background

background

================================================ FILE: sandbox/picture.html ================================================ picture ================================================ FILE: sandbox/progress/index.html ================================================ progress

progress

================================================ FILE: sandbox/progress/progress.js ================================================ let progressElem, statusElem, supportsProgress, loadedImageCount, imageCount; let container = document.querySelector('#image-container'); statusElem = document.querySelector('#status'); progressElem = document.querySelector('progress'); supportsProgress = progressElem && // IE does not support progress progressElem.toString().indexOf('Unknown') === -1; document.querySelector('#add').onclick = function() { // add new images let fragment = getItemsFragment(); container.insertBefore( fragment, container.firstChild ); // use ImagesLoaded let imgLoad = imagesLoaded( container ); imgLoad.on( 'progress', onProgress ); imgLoad.on( 'always', onAlways ); // reset progress counter imageCount = imgLoad.images.length; resetProgress(); updateProgress( 0 ); }; // reset container document.querySelector('#reset').onclick = function() { empty( container ); }; // ----- set text helper ----- // let docElem = document.documentElement; let textSetter = docElem.textContent !== undefined ? 'textContent' : 'innerText'; function setText( elem, value ) { elem[ textSetter ] = value; } function empty( elem ) { while ( elem.firstChild ) { elem.removeChild( elem.firstChild ); } } // ----- ----- // // return doc fragment with function getItemsFragment() { let fragment = document.createDocumentFragment(); for ( let i = 0; i < 7; i++ ) { let item = getImageItem(); fragment.appendChild( item ); } return fragment; } // return an
  • with a in it function getImageItem() { let item = document.createElement('li'); item.className = 'is-loading'; let img = document.createElement('img'); let size = Math.random() * 3 + 1; let width = Math.random() * 110 + 100; width = Math.round( width * size ); let height = Math.round( 140 * size ); let rando = Math.ceil( Math.random() * 1000 ); // 10% chance of broken image src // random parameter to prevent cached images img.src = rando < 100 ? `//foo/broken-${rando}.jpg` : // use picsum for great random images `https://picsum.photos/${width}/${height}/?random`; item.appendChild( img ); return item; } // ----- ----- // function resetProgress() { statusElem.style.opacity = 1; loadedImageCount = 0; if ( supportsProgress ) { progressElem.setAttribute( 'max', imageCount ); } } function updateProgress( value ) { if ( supportsProgress ) { progressElem.setAttribute( 'value', value ); } else { // if you don't support progress elem setText( statusElem, value + ' / ' + imageCount ); } } // triggered after each item is loaded function onProgress( imgLoad, image ) { // change class if the image is loaded or broken image.img.parentNode.className = image.isLoaded ? '' : 'is-broken'; // update progress element loadedImageCount++; updateProgress( loadedImageCount ); } // hide status when done function onAlways() { statusElem.style.opacity = 0; } ================================================ FILE: tasks/dist.js ================================================ const fs = require('fs'); const { execSync } = require('child_process'); const { minify } = require('terser'); const indexPath = 'imagesloaded.js'; const distPath = 'imagesloaded.pkgd.js'; const distMinPath = 'imagesloaded.pkgd.min.js'; let indexContent = fs.readFileSync( `./${indexPath}`, 'utf8' ); let paths = [ 'node_modules/ev-emitter/ev-emitter.js', 'imagesloaded.js', ]; // concatenate files execSync(`cat ${paths.join(' ')} > ${distPath}`); // add banner let banner = indexContent.split(' */')[0] + ' */\n\n'; banner = banner.replace( 'imagesLoaded', 'imagesLoaded PACKAGED' ); let distJsContent = fs.readFileSync( distPath, 'utf8' ); distJsContent = banner + distJsContent; fs.writeFileSync( distPath, distJsContent ); // minify ( async function() { let { code } = await minify( distJsContent, { mangle: true } ); fs.writeFileSync( distMinPath, code ); } )(); ================================================ FILE: tasks/version.js ================================================ const fs = require('fs'); const version = require('../package.json').version; const file = 'imagesloaded.js'; let src = fs.readFileSync( file, 'utf8' ); src = src.replace( /imagesLoaded v\d+\.\d+\.\d+/, `imagesLoaded v${version}` ); fs.writeFileSync( file, src, 'utf8' ); ================================================ FILE: test/css/tests.css ================================================ body { font-family: sans-serif; } img { display: inline-block; max-width: 240px; } #qunit { max-width: 600px; right: 0; left: auto; } /* ---- backgrounds ---- */ .bg-box { width: 240px; height: 240px; margin: 0 20px 20px 0; border: 1px solid; display: inline-block; } .bg-box.tulip { background: url('https://i.imgur.com/9xYjgCk.jpg'); background-size: cover; } .bg-box.thunder-cloud { background: url('../img/thunder-cloud.jpg'); background-size: contain; } .bg-box.multi { background: url("https://i.imgur.com/ZAVN3.png"), url('https://i.imgur.com/6UdOxeB.png') bottom right, url(https://picsum.photos/601/401/?random); background-size: cover; } .bg-box.blue { background: #09F; } .bg-box.gulls { background-image: url('https://i.imgur.com/qKhkOKC.jpg'); background-size: cover; } ================================================ FILE: test/index.html ================================================ imagesLoaded tests

    imagesLoaded tests

    Basics

    Locals

    Data URI

    append

    no images

    jQuery success

    jQuery fail

    background

    picture

    srcset

    ================================================ FILE: test/unit/append.js ================================================ QUnit.test( 'append', function( assert ) { let imgUrls = [ 'https://i.imgur.com/bwy74ok.jpg', 'https://i.imgur.com/bAZWoqx.jpg', 'https://i.imgur.com/PgmEBSB.jpg', 'https://i.imgur.com/aboaFoB.jpg', 'https://i.imgur.com/LkmcILl.jpg', 'https://i.imgur.com/q9zO6tw.jpg', ]; // create images let fragment = document.createDocumentFragment(); for ( let i = 0, len = imgUrls.length; i < len; i++ ) { let img = document.createElement('img'); img.src = imgUrls[i]; fragment.appendChild( img ); } let elem = document.querySelector('#append'); elem.appendChild( fragment ); let done = assert.async(); imagesLoaded( elem, { debug: false } ).on( 'always', function() { assert.ok('appended images loaded'); done(); } ); } ); ================================================ FILE: test/unit/background.js ================================================ QUnit.test( 'background', function( assert ) { // from Modernizr let supportsMultiBGs = ( function() { let style = document.createElement('a').style; style.cssText = 'background:url(https://),url(https://),red url(https://)'; return ( /(url\s*\(.*?){3}/ ).test( style.background ); } )(); let multiBGCount = supportsMultiBGs ? 3 : 0; let done = assert.async( 14 + multiBGCount ); let imgLoad0 = imagesLoaded( '#background .tulip', { background: true }, function() { assert.ok( true, 'callback triggered on .orange-tree' ); done(); } ); assert.equal( imgLoad0.images.length, 1, '1 image on .images' ); imgLoad0.on( 'progress', function( instance, image, element ) { assert.ok( element.nodeName === 'DIV', 'progress; element is div' ); assert.ok( image.isLoaded, 'progress; image.isLoaded' ); done(); } ); let imgLoad1 = imagesLoaded( '#background .thunder-cloud', { background: true }, function() { assert.ok( true, 'callback triggered on .thunder-cloud' ); done(); } ); assert.equal( imgLoad1.images.length, 1, '1 image on .images' ); // multiple backgrounds let imgLoad2 = imagesLoaded( '#background .multi', { background: true }, function() { assert.ok( true, 'callback triggered on .multi' ); done(); } ); assert.equal( imgLoad2.images.length, multiBGCount, 'correct multiple BG count on .images' ); // multiple elements let imgLoad3 = imagesLoaded( '#background .bg-box', { background: true }, function() { assert.ok( true, 'callback triggered on .bg-box' ); let count = 5 + multiBGCount; assert.equal( imgLoad3.images.length, count, count + ' images on .bg-box' ); done(); } ); imgLoad3.on( 'progress', function( instance, image /* , element */) { assert.ok( true, 'progress on .bg-box; ' + image.img.src ); assert.equal( image.isLoaded, true, 'image.isLoaded == true' ); done(); } ); // background and children let imgLoad4 = imagesLoaded( '#background .gulls', { background: true } ); assert.equal( imgLoad4.images.length, 3, '3 images: 1 background and 2 ' ); imgLoad4.on( 'progress', function( instance, image ) { assert.equal( image.isLoaded, true, 'image is loaded' ); done(); } ); // child background selector let imgLoad5 = imagesLoaded( '#background', { background: '.bg-box' }, function() { let count = 5 + multiBGCount; assert.equal( imgLoad5.images.length, count, count + ' images on .bg-box, with {background: .bg-box}' ); done(); } ); } ); ================================================ FILE: test/unit/basics.js ================================================ QUnit.test( 'basics', function( assert ) { let elem = document.querySelector('#basics'); let images = elem.querySelectorAll('img'); let done = assert.async( 3 + images.length ); let imgLoader = new imagesLoaded( elem, function( obj ) { assert.ok( true, 'callback function triggered' ); assert.equal( imgLoader, obj, 'callback argument and instance match' ); done(); } ); imgLoader.on( 'done', function() { assert.ok( true, 'done event triggered' ); done(); } ); imgLoader.on( 'always', function() { assert.ok( true, 'always event triggered' ); done(); } ); imgLoader.on( 'progress', function( loader, image ) { assert.ok( image.isLoaded, 'image is loaded' ); done(); } ); } ); ================================================ FILE: test/unit/data-uri.js ================================================ QUnit.test( 'data-uri', function( assert ) { let done = assert.async(); imagesLoaded( '#data-uri', { debug: false } ).on( 'done', function( obj ) { assert.ok( true, 'data-uri images loaded' ); assert.equal( obj.images.length, 2, 'instance has 2 images' ); done(); } ); } ); ================================================ FILE: test/unit/jquery-fail.js ================================================ QUnit.test( 'jquery fail', function( assert ) { let $ = window.jQuery; let $images = $('#jquery-fail img'); let done = assert.async( 3 + $images.length ); $('#jquery-fail').imagesLoaded( function( instance ) { assert.ok( true, 'callback triggered' ); assert.ok( instance instanceof imagesLoaded, 'instance instanceof imagesLoaded' ); done(); } ) .fail( function( instance ) { assert.ok( true, 'fail triggered' ); assert.ok( instance instanceof imagesLoaded, 'instance instanceof imagesLoaded' ); done(); } ) .always( function( instance ) { assert.ok( true, 'always triggered' ); assert.ok( instance instanceof imagesLoaded, 'instance instanceof imagesLoaded' ); done(); } ) .progress( function( /* instance, image */) { assert.ok( true, 'progress trigged' ); done(); } ); } ); ================================================ FILE: test/unit/jquery-success.js ================================================ QUnit.test( 'jquery success', function( assert ) { let $ = window.jQuery; let done = assert.async( 6 ); $('#jquery-success').imagesLoaded( function( instance ) { assert.ok( true, 'callback triggered' ); assert.ok( instance instanceof imagesLoaded, 'instance instanceof imagesLoaded' ); done(); } ) .done( function( instance ) { assert.ok( true, 'done triggered' ); assert.ok( instance instanceof imagesLoaded, 'instance instanceof imagesLoaded' ); done(); } ) .always( function( instance ) { assert.ok( true, 'always triggered' ); assert.ok( instance instanceof imagesLoaded, 'instance instanceof imagesLoaded' ); done(); } ) .progress( function( instance, image ) { assert.ok( image.isLoaded, 'progress trigged, image is loaded' ); done(); } ); } ); ================================================ FILE: test/unit/local-files.js ================================================ QUnit.test( 'local files', function( assert ) { let elem = document.querySelector('#locals'); let done = assert.async( 6 ); let imgLoader = new imagesLoaded( elem, function( obj ) { assert.ok( true, 'callback function triggered' ); assert.equal( imgLoader, obj, 'callback argument and instance match' ); done(); } ); imgLoader.on( 'fail', function() { assert.ok( true, 'fail event triggered' ); done(); } ); imgLoader.on( 'always', function() { assert.ok( true, 'always event triggered' ); done(); } ); imgLoader.on( 'progress', function( loader, image ) { assert.ok( true, 'image progressed' ); if ( image.img.src.indexOf('img/not-there.jpg') !== -1 ) { assert.ok( !image.isLoaded, 'thunder cloud is not loaded' ); } else { assert.ok( image.isLoaded, 'image is loaded' ); } done(); } ); } ); ================================================ FILE: test/unit/no-images.js ================================================ QUnit.test( 'no images', function( assert ) { let elem = document.querySelector('#no-images'); let done = assert.async(); imagesLoaded( elem, function() { assert.ok( true, 'triggered with no images' ); done(); } ); } ); ================================================ FILE: test/unit/non-element.js ================================================ QUnit.test( 'dismiss non-element nodes', function( assert ) { let $ = window.jQuery; let done = assert.async( 2 ); $( '' + ' ' ) .imagesLoaded( function() { assert.ok( true, 'elements from jQuery string ok' ); done(); } ); // test fragment let frag = document.createDocumentFragment(); let img = new Image(); img.src = 'https://picsum.photos/403/303/?random'; frag.appendChild( img ); let imgLoad = imagesLoaded( frag, function() { assert.ok( true, 'document fragment ok' ); assert.equal( imgLoad.images.length, 1, '1 image found' ); done(); } ); } ); ================================================ FILE: test/unit/picture.js ================================================ QUnit.test( 'picture', function( assert ) { let done = assert.async( 4 ); let imgLoad0 = imagesLoaded( '#picture-list', function() { assert.ok( true, 'callback triggered on #picture-list' ); done(); } ); assert.equal( imgLoad0.images.length, 3, '3 images on #picture-list' ); imgLoad0.on( 'progress', function( instance, image, element ) { assert.ok( element.nodeName === 'PICTURE', 'progress; element is picture' ); assert.ok( image.isLoaded, 'progress; image.isLoaded' ); done(); } ); } ); ================================================ FILE: test/unit/selector-string.js ================================================ QUnit.test( 'selector string', function( assert ) { let images = document.querySelectorAll('#basics img'); let done = assert.async(); let imgLoad = imagesLoaded( '#basics', { debug: true } ) .on( 'done', function( obj ) { assert.ok( true, 'selector string worked' ); assert.ok( obj.images, 'argument has images' ); assert.equal( obj.images.length, images.length, 'images.length matches' ); done(); } ); assert.ok( imgLoad.options.debug, 'debug option set' ); } ); ================================================ FILE: test/unit/single-element.js ================================================ QUnit.test( 'single element', function( assert ) { let elem = document.querySelector('#mario-with-shell'); let done = assert.async(); imagesLoaded( elem ).on( 'done', function( obj ) { assert.ok( true, 'single element worked' ); assert.ok( obj.images, 'argument has images' ); assert.equal( obj.images.length, 1, 'images.length = 1' ); done(); } ); } ); ================================================ FILE: test/unit/srcset.js ================================================ QUnit.test( 'srcset', function( assert ) { let done = assert.async( 4 ); let imgLoad0 = imagesLoaded( '#srcset', function() { assert.ok( true, 'callback triggered on #srcset' ); done(); } ); assert.equal( imgLoad0.images.length, 3, '3 images on #srcset' ); imgLoad0.on( 'progress', function( instance, image, element ) { assert.ok( element.nodeName === 'IMG', 'progress; element is img' ); assert.ok( image.isLoaded, 'progress; image.isLoaded' ); done(); } ); } );