UNPKG

9.54 kBJavaScriptView Raw
1/*!
2 * imagesLoaded v5.0.0
3 * JavaScript is all like "You images are done yet or what?"
4 * MIT License
5 */
6
7( function( window, factory ) {
8 // universal module definition
9 if ( typeof module == 'object' && module.exports ) {
10 // CommonJS
11 module.exports = factory( window, require('ev-emitter') );
12 } else {
13 // browser global
14 window.imagesLoaded = factory( window, window.EvEmitter );
15 }
16
17} )( typeof window !== 'undefined' ? window : this,
18 function factory( window, EvEmitter ) {
19
20let $ = window.jQuery;
21let console = window.console;
22
23// -------------------------- helpers -------------------------- //
24
25// turn element or nodeList into an array
26function makeArray( obj ) {
27 // use object if already an array
28 if ( Array.isArray( obj ) ) return obj;
29
30 let isArrayLike = typeof obj == 'object' && typeof obj.length == 'number';
31 // convert nodeList to array
32 if ( isArrayLike ) return [ ...obj ];
33
34 // array of single index
35 return [ obj ];
36}
37
38// -------------------------- imagesLoaded -------------------------- //
39
40/**
41 * @param {[Array, Element, NodeList, String]} elem
42 * @param {[Object, Function]} options - if function, use as callback
43 * @param {Function} onAlways - callback function
44 * @returns {ImagesLoaded}
45 */
46function ImagesLoaded( elem, options, onAlways ) {
47 // coerce ImagesLoaded() without new, to be new ImagesLoaded()
48 if ( !( this instanceof ImagesLoaded ) ) {
49 return new ImagesLoaded( elem, options, onAlways );
50 }
51 // use elem as selector string
52 let queryElem = elem;
53 if ( typeof elem == 'string' ) {
54 queryElem = document.querySelectorAll( elem );
55 }
56 // bail if bad element
57 if ( !queryElem ) {
58 console.error(`Bad element for imagesLoaded ${queryElem || elem}`);
59 return;
60 }
61
62 this.elements = makeArray( queryElem );
63 this.options = {};
64 // shift arguments if no options set
65 if ( typeof options == 'function' ) {
66 onAlways = options;
67 } else {
68 Object.assign( this.options, options );
69 }
70
71 if ( onAlways ) this.on( 'always', onAlways );
72
73 this.getImages();
74 // add jQuery Deferred object
75 if ( $ ) this.jqDeferred = new $.Deferred();
76
77 // HACK check async to allow time to bind listeners
78 setTimeout( this.check.bind( this ) );
79}
80
81ImagesLoaded.prototype = Object.create( EvEmitter.prototype );
82
83ImagesLoaded.prototype.getImages = function() {
84 this.images = [];
85
86 // filter & find items if we have an item selector
87 this.elements.forEach( this.addElementImages, this );
88};
89
90const elementNodeTypes = [ 1, 9, 11 ];
91
92/**
93 * @param {Node} elem
94 */
95ImagesLoaded.prototype.addElementImages = function( elem ) {
96 // filter siblings
97 if ( elem.nodeName === 'IMG' ) {
98 this.addImage( elem );
99 }
100 // get background image on element
101 if ( this.options.background === true ) {
102 this.addElementBackgroundImages( elem );
103 }
104
105 // find children
106 // no non-element nodes, #143
107 let { nodeType } = elem;
108 if ( !nodeType || !elementNodeTypes.includes( nodeType ) ) return;
109
110 let childImgs = elem.querySelectorAll('img');
111 // concat childElems to filterFound array
112 for ( let img of childImgs ) {
113 this.addImage( img );
114 }
115
116 // get child background images
117 if ( typeof this.options.background == 'string' ) {
118 let children = elem.querySelectorAll( this.options.background );
119 for ( let child of children ) {
120 this.addElementBackgroundImages( child );
121 }
122 }
123};
124
125const reURL = /url\((['"])?(.*?)\1\)/gi;
126
127ImagesLoaded.prototype.addElementBackgroundImages = function( elem ) {
128 let style = getComputedStyle( elem );
129 // Firefox returns null if in a hidden iframe https://bugzil.la/548397
130 if ( !style ) return;
131
132 // get url inside url("...")
133 let matches = reURL.exec( style.backgroundImage );
134 while ( matches !== null ) {
135 let url = matches && matches[2];
136 if ( url ) {
137 this.addBackground( url, elem );
138 }
139 matches = reURL.exec( style.backgroundImage );
140 }
141};
142
143/**
144 * @param {Image} img
145 */
146ImagesLoaded.prototype.addImage = function( img ) {
147 let loadingImage = new LoadingImage( img );
148 this.images.push( loadingImage );
149};
150
151ImagesLoaded.prototype.addBackground = function( url, elem ) {
152 let background = new Background( url, elem );
153 this.images.push( background );
154};
155
156ImagesLoaded.prototype.check = function() {
157 this.progressedCount = 0;
158 this.hasAnyBroken = false;
159 // complete if no images
160 if ( !this.images.length ) {
161 this.complete();
162 return;
163 }
164
165 /* eslint-disable-next-line func-style */
166 let onProgress = ( image, elem, message ) => {
167 // HACK - Chrome triggers event before object properties have changed. #83
168 setTimeout( () => {
169 this.progress( image, elem, message );
170 } );
171 };
172
173 this.images.forEach( function( loadingImage ) {
174 loadingImage.once( 'progress', onProgress );
175 loadingImage.check();
176 } );
177};
178
179ImagesLoaded.prototype.progress = function( image, elem, message ) {
180 this.progressedCount++;
181 this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded;
182 // progress event
183 this.emitEvent( 'progress', [ this, image, elem ] );
184 if ( this.jqDeferred && this.jqDeferred.notify ) {
185 this.jqDeferred.notify( this, image );
186 }
187 // check if completed
188 if ( this.progressedCount === this.images.length ) {
189 this.complete();
190 }
191
192 if ( this.options.debug && console ) {
193 console.log( `progress: ${message}`, image, elem );
194 }
195};
196
197ImagesLoaded.prototype.complete = function() {
198 let eventName = this.hasAnyBroken ? 'fail' : 'done';
199 this.isComplete = true;
200 this.emitEvent( eventName, [ this ] );
201 this.emitEvent( 'always', [ this ] );
202 if ( this.jqDeferred ) {
203 let jqMethod = this.hasAnyBroken ? 'reject' : 'resolve';
204 this.jqDeferred[ jqMethod ]( this );
205 }
206};
207
208// -------------------------- -------------------------- //
209
210function LoadingImage( img ) {
211 this.img = img;
212}
213
214LoadingImage.prototype = Object.create( EvEmitter.prototype );
215
216LoadingImage.prototype.check = function() {
217 // If complete is true and browser supports natural sizes,
218 // try to check for image status manually.
219 let isComplete = this.getIsImageComplete();
220 if ( isComplete ) {
221 // report based on naturalWidth
222 this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
223 return;
224 }
225
226 // If none of the checks above matched, simulate loading on detached element.
227 this.proxyImage = new Image();
228 // add crossOrigin attribute. #204
229 if ( this.img.crossOrigin ) {
230 this.proxyImage.crossOrigin = this.img.crossOrigin;
231 }
232 this.proxyImage.addEventListener( 'load', this );
233 this.proxyImage.addEventListener( 'error', this );
234 // bind to image as well for Firefox. #191
235 this.img.addEventListener( 'load', this );
236 this.img.addEventListener( 'error', this );
237 this.proxyImage.src = this.img.currentSrc || this.img.src;
238};
239
240LoadingImage.prototype.getIsImageComplete = function() {
241 // check for non-zero, non-undefined naturalWidth
242 // fixes Safari+InfiniteScroll+Masonry bug infinite-scroll#671
243 return this.img.complete && this.img.naturalWidth;
244};
245
246LoadingImage.prototype.confirm = function( isLoaded, message ) {
247 this.isLoaded = isLoaded;
248 let { parentNode } = this.img;
249 // emit progress with parent <picture> or self <img>
250 let elem = parentNode.nodeName === 'PICTURE' ? parentNode : this.img;
251 this.emitEvent( 'progress', [ this, elem, message ] );
252};
253
254// ----- events ----- //
255
256// trigger specified handler for event type
257LoadingImage.prototype.handleEvent = function( event ) {
258 let method = 'on' + event.type;
259 if ( this[ method ] ) {
260 this[ method ]( event );
261 }
262};
263
264LoadingImage.prototype.onload = function() {
265 this.confirm( true, 'onload' );
266 this.unbindEvents();
267};
268
269LoadingImage.prototype.onerror = function() {
270 this.confirm( false, 'onerror' );
271 this.unbindEvents();
272};
273
274LoadingImage.prototype.unbindEvents = function() {
275 this.proxyImage.removeEventListener( 'load', this );
276 this.proxyImage.removeEventListener( 'error', this );
277 this.img.removeEventListener( 'load', this );
278 this.img.removeEventListener( 'error', this );
279};
280
281// -------------------------- Background -------------------------- //
282
283function Background( url, element ) {
284 this.url = url;
285 this.element = element;
286 this.img = new Image();
287}
288
289// inherit LoadingImage prototype
290Background.prototype = Object.create( LoadingImage.prototype );
291
292Background.prototype.check = function() {
293 this.img.addEventListener( 'load', this );
294 this.img.addEventListener( 'error', this );
295 this.img.src = this.url;
296 // check if image is already complete
297 let isComplete = this.getIsImageComplete();
298 if ( isComplete ) {
299 this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
300 this.unbindEvents();
301 }
302};
303
304Background.prototype.unbindEvents = function() {
305 this.img.removeEventListener( 'load', this );
306 this.img.removeEventListener( 'error', this );
307};
308
309Background.prototype.confirm = function( isLoaded, message ) {
310 this.isLoaded = isLoaded;
311 this.emitEvent( 'progress', [ this, this.element, message ] );
312};
313
314// -------------------------- jQuery -------------------------- //
315
316ImagesLoaded.makeJQueryPlugin = function( jQuery ) {
317 jQuery = jQuery || window.jQuery;
318 if ( !jQuery ) return;
319
320 // set local variable
321 $ = jQuery;
322 // $().imagesLoaded()
323 $.fn.imagesLoaded = function( options, onAlways ) {
324 let instance = new ImagesLoaded( this, options, onAlways );
325 return instance.jqDeferred.promise( $( this ) );
326 };
327};
328// try making plugin
329ImagesLoaded.makeJQueryPlugin();
330
331// -------------------------- -------------------------- //
332
333return ImagesLoaded;
334
335} );