PluginProbe ʕ •ᴥ•ʔ
Responsive Lightbox & Gallery / 2.5.4
Responsive Lightbox & Gallery v2.5.4
2.7.8 trunk 1.0.0 1.0.1 1.0.1.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.4.0 1.4.0.1 1.4.1 1.4.11 1.4.12 1.4.13 1.4.14 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 2.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3.1 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.4.8 2.4.9 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7
responsive-lightbox / assets / infinitescroll / infinite-scroll.pkgd.js
responsive-lightbox / assets / infinitescroll Last commit date
LICENSE 4 years ago README.md 4 years ago infinite-scroll.pkgd.js 4 years ago infinite-scroll.pkgd.min.js 4 years ago
infinite-scroll.pkgd.js
1910 lines
1 /*!
2 * Infinite Scroll PACKAGED v4.0.1
3 * Automatically add next page
4 *
5 * Licensed GPLv3 for open source use
6 * or Infinite Scroll Commercial License for commercial use
7 *
8 * https://infinite-scroll.com
9 * Copyright 2018-2020 Metafizzy
10 */
11
12 /**
13 * Bridget makes jQuery widgets
14 * v3.0.0
15 * MIT license
16 */
17
18 ( function( window, factory ) {
19 // module definition
20 if ( typeof module == 'object' && module.exports ) {
21 // CommonJS
22 module.exports = factory(
23 window,
24 require('jquery'),
25 );
26 } else {
27 // browser global
28 window.jQueryBridget = factory(
29 window,
30 window.jQuery,
31 );
32 }
33
34 }( window, function factory( window, jQuery ) {
35
36 // ----- utils ----- //
37
38 // helper function for logging errors
39 // $.error breaks jQuery chaining
40 let console = window.console;
41 let logError = typeof console == 'undefined' ? function() {} :
42 function( message ) {
43 console.error( message );
44 };
45
46 // ----- jQueryBridget ----- //
47
48 function jQueryBridget( namespace, PluginClass, $ ) {
49 $ = $ || jQuery || window.jQuery;
50 if ( !$ ) {
51 return;
52 }
53
54 // add option method -> $().plugin('option', {...})
55 if ( !PluginClass.prototype.option ) {
56 // option setter
57 PluginClass.prototype.option = function( opts ) {
58 if ( !opts ) return;
59
60 this.options = Object.assign( this.options || {}, opts );
61 };
62 }
63
64 // make jQuery plugin
65 $.fn[ namespace ] = function( arg0, ...args ) {
66 if ( typeof arg0 == 'string' ) {
67 // method call $().plugin( 'methodName', { options } )
68 return methodCall( this, arg0, args );
69 }
70 // just $().plugin({ options })
71 plainCall( this, arg0 );
72 return this;
73 };
74
75 // $().plugin('methodName')
76 function methodCall( $elems, methodName, args ) {
77 let returnValue;
78 let pluginMethodStr = `$().${namespace}("${methodName}")`;
79
80 $elems.each( function( i, elem ) {
81 // get instance
82 let instance = $.data( elem, namespace );
83 if ( !instance ) {
84 logError( `${namespace} not initialized.` +
85 ` Cannot call method ${pluginMethodStr}` );
86 return;
87 }
88
89 let method = instance[ methodName ];
90 if ( !method || methodName.charAt( 0 ) == '_' ) {
91 logError(`${pluginMethodStr} is not a valid method`);
92 return;
93 }
94
95 // apply method, get return value
96 let value = method.apply( instance, args );
97 // set return value if value is returned, use only first value
98 returnValue = returnValue === undefined ? value : returnValue;
99 } );
100
101 return returnValue !== undefined ? returnValue : $elems;
102 }
103
104 function plainCall( $elems, options ) {
105 $elems.each( function( i, elem ) {
106 let instance = $.data( elem, namespace );
107 if ( instance ) {
108 // set options & init
109 instance.option( options );
110 instance._init();
111 } else {
112 // initialize new instance
113 instance = new PluginClass( elem, options );
114 $.data( elem, namespace, instance );
115 }
116 } );
117 }
118
119 }
120
121 // ----- ----- //
122
123 return jQueryBridget;
124
125 } ) );
126 /**
127 * EvEmitter v2.0.0
128 * Lil' event emitter
129 * MIT License
130 */
131
132 ( function( global, factory ) {
133 // universal module definition
134 if ( typeof module == 'object' && module.exports ) {
135 // CommonJS - Browserify, Webpack
136 module.exports = factory();
137 } else {
138 // Browser globals
139 global.EvEmitter = factory();
140 }
141
142 }( typeof window != 'undefined' ? window : this, function() {
143
144 function EvEmitter() {}
145
146 let proto = EvEmitter.prototype;
147
148 proto.on = function( eventName, listener ) {
149 if ( !eventName || !listener ) return this;
150
151 // set events hash
152 let events = this._events = this._events || {};
153 // set listeners array
154 let listeners = events[ eventName ] = events[ eventName ] || [];
155 // only add once
156 if ( !listeners.includes( listener ) ) {
157 listeners.push( listener );
158 }
159
160 return this;
161 };
162
163 proto.once = function( eventName, listener ) {
164 if ( !eventName || !listener ) return this;
165
166 // add event
167 this.on( eventName, listener );
168 // set once flag
169 // set onceEvents hash
170 let onceEvents = this._onceEvents = this._onceEvents || {};
171 // set onceListeners object
172 let onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {};
173 // set flag
174 onceListeners[ listener ] = true;
175
176 return this;
177 };
178
179 proto.off = function( eventName, listener ) {
180 let listeners = this._events && this._events[ eventName ];
181 if ( !listeners || !listeners.length ) return this;
182
183 let index = listeners.indexOf( listener );
184 if ( index != -1 ) {
185 listeners.splice( index, 1 );
186 }
187
188 return this;
189 };
190
191 proto.emitEvent = function( eventName, args ) {
192 let listeners = this._events && this._events[ eventName ];
193 if ( !listeners || !listeners.length ) return this;
194
195 // copy over to avoid interference if .off() in listener
196 listeners = listeners.slice( 0 );
197 args = args || [];
198 // once stuff
199 let onceListeners = this._onceEvents && this._onceEvents[ eventName ];
200
201 for ( let listener of listeners ) {
202 let isOnce = onceListeners && onceListeners[ listener ];
203 if ( isOnce ) {
204 // remove listener
205 // remove before trigger to prevent recursion
206 this.off( eventName, listener );
207 // unset once flag
208 delete onceListeners[ listener ];
209 }
210 // trigger listener
211 listener.apply( this, args );
212 }
213
214 return this;
215 };
216
217 proto.allOff = function() {
218 delete this._events;
219 delete this._onceEvents;
220 return this;
221 };
222
223 return EvEmitter;
224
225 } ) );
226 /**
227 * Fizzy UI utils v3.0.0
228 * MIT license
229 */
230
231 ( function( global, factory ) {
232 // universal module definition
233 if ( typeof module == 'object' && module.exports ) {
234 // CommonJS
235 module.exports = factory( global );
236 } else {
237 // browser global
238 global.fizzyUIUtils = factory( global );
239 }
240
241 }( this, function factory( global ) {
242
243 let utils = {};
244
245 // ----- extend ----- //
246
247 // extends objects
248 utils.extend = function( a, b ) {
249 return Object.assign( a, b );
250 };
251
252 // ----- modulo ----- //
253
254 utils.modulo = function( num, div ) {
255 return ( ( num % div ) + div ) % div;
256 };
257
258 // ----- makeArray ----- //
259
260 // turn element or nodeList into an array
261 utils.makeArray = function( obj ) {
262 // use object if already an array
263 if ( Array.isArray( obj ) ) return obj;
264
265 // return empty array if undefined or null. #6
266 if ( obj === null || obj === undefined ) return [];
267
268 let isArrayLike = typeof obj == 'object' && typeof obj.length == 'number';
269 // convert nodeList to array
270 if ( isArrayLike ) return [ ...obj ];
271
272 // array of single index
273 return [ obj ];
274 };
275
276 // ----- removeFrom ----- //
277
278 utils.removeFrom = function( ary, obj ) {
279 let index = ary.indexOf( obj );
280 if ( index != -1 ) {
281 ary.splice( index, 1 );
282 }
283 };
284
285 // ----- getParent ----- //
286
287 utils.getParent = function( elem, selector ) {
288 while ( elem.parentNode && elem != document.body ) {
289 elem = elem.parentNode;
290 if ( elem.matches( selector ) ) return elem;
291 }
292 };
293
294 // ----- getQueryElement ----- //
295
296 // use element as selector string
297 utils.getQueryElement = function( elem ) {
298 if ( typeof elem == 'string' ) {
299 return document.querySelector( elem );
300 }
301 return elem;
302 };
303
304 // ----- handleEvent ----- //
305
306 // enable .ontype to trigger from .addEventListener( elem, 'type' )
307 utils.handleEvent = function( event ) {
308 let method = 'on' + event.type;
309 if ( this[ method ] ) {
310 this[ method ]( event );
311 }
312 };
313
314 // ----- filterFindElements ----- //
315
316 utils.filterFindElements = function( elems, selector ) {
317 // make array of elems
318 elems = utils.makeArray( elems );
319
320 return elems
321 // check that elem is an actual element
322 .filter( ( elem ) => elem instanceof HTMLElement )
323 .reduce( ( ffElems, elem ) => {
324 // add elem if no selector
325 if ( !selector ) {
326 ffElems.push( elem );
327 return ffElems;
328 }
329 // filter & find items if we have a selector
330 // filter
331 if ( elem.matches( selector ) ) {
332 ffElems.push( elem );
333 }
334 // find children
335 let childElems = elem.querySelectorAll( selector );
336 // concat childElems to filterFound array
337 ffElems = ffElems.concat( ...childElems );
338 return ffElems;
339 }, [] );
340 };
341
342 // ----- debounceMethod ----- //
343
344 utils.debounceMethod = function( _class, methodName, threshold ) {
345 threshold = threshold || 100;
346 // original method
347 let method = _class.prototype[ methodName ];
348 let timeoutName = methodName + 'Timeout';
349
350 _class.prototype[ methodName ] = function() {
351 clearTimeout( this[ timeoutName ] );
352
353 let args = arguments;
354 this[ timeoutName ] = setTimeout( () => {
355 method.apply( this, args );
356 delete this[ timeoutName ];
357 }, threshold );
358 };
359 };
360
361 // ----- docReady ----- //
362
363 utils.docReady = function( onDocReady ) {
364 let readyState = document.readyState;
365 if ( readyState == 'complete' || readyState == 'interactive' ) {
366 // do async to allow for other scripts to run. metafizzy/flickity#441
367 setTimeout( onDocReady );
368 } else {
369 document.addEventListener( 'DOMContentLoaded', onDocReady );
370 }
371 };
372
373 // ----- htmlInit ----- //
374
375 // http://bit.ly/3oYLusc
376 utils.toDashed = function( str ) {
377 return str.replace( /(.)([A-Z])/g, function( match, $1, $2 ) {
378 return $1 + '-' + $2;
379 } ).toLowerCase();
380 };
381
382 let console = global.console;
383
384 // allow user to initialize classes via [data-namespace] or .js-namespace class
385 // htmlInit( Widget, 'widgetName' )
386 // options are parsed from data-namespace-options
387 utils.htmlInit = function( WidgetClass, namespace ) {
388 utils.docReady( function() {
389 let dashedNamespace = utils.toDashed( namespace );
390 let dataAttr = 'data-' + dashedNamespace;
391 let dataAttrElems = document.querySelectorAll( `[${dataAttr}]` );
392 let jQuery = global.jQuery;
393
394 [ ...dataAttrElems ].forEach( ( elem ) => {
395 let attr = elem.getAttribute( dataAttr );
396 let options;
397 try {
398 options = attr && JSON.parse( attr );
399 } catch ( error ) {
400 // log error, do not initialize
401 if ( console ) {
402 console.error( `Error parsing ${dataAttr} on ${elem.className}: ${error}` );
403 }
404 return;
405 }
406 // initialize
407 let instance = new WidgetClass( elem, options );
408 // make available via $().data('namespace')
409 if ( jQuery ) {
410 jQuery.data( elem, namespace, instance );
411 }
412 } );
413
414 } );
415 };
416
417 // ----- ----- //
418
419 return utils;
420
421 } ) );
422 // core
423 ( function( window, factory ) {
424 // universal module definition
425 if ( typeof module == 'object' && module.exports ) {
426 // CommonJS
427 module.exports = factory(
428 window,
429 require('ev-emitter'),
430 require('fizzy-ui-utils'),
431 );
432 } else {
433 // browser global
434 window.InfiniteScroll = factory(
435 window,
436 window.EvEmitter,
437 window.fizzyUIUtils,
438 );
439 }
440
441 }( window, function factory( window, EvEmitter, utils ) {
442
443 let jQuery = window.jQuery;
444 // internal store of all InfiniteScroll intances
445 let instances = {};
446
447 function InfiniteScroll( element, options ) {
448 let queryElem = utils.getQueryElement( element );
449
450 if ( !queryElem ) {
451 console.error( 'Bad element for InfiniteScroll: ' + ( queryElem || element ) );
452 return;
453 }
454 element = queryElem;
455 // do not initialize twice on same element
456 if ( element.infiniteScrollGUID ) {
457 let instance = instances[ element.infiniteScrollGUID ];
458 instance.option( options );
459 return instance;
460 }
461
462 this.element = element;
463 // options
464 this.options = { ...InfiniteScroll.defaults };
465 this.option( options );
466 // add jQuery
467 if ( jQuery ) {
468 this.$element = jQuery( this.element );
469 }
470
471 this.create();
472 }
473
474 // defaults
475 InfiniteScroll.defaults = {
476 // path: null,
477 // hideNav: null,
478 // debug: false,
479 };
480
481 // create & destroy methods
482 InfiniteScroll.create = {};
483 InfiniteScroll.destroy = {};
484
485 let proto = InfiniteScroll.prototype;
486 // inherit EvEmitter
487 Object.assign( proto, EvEmitter.prototype );
488
489 // -------------------------- -------------------------- //
490
491 // globally unique identifiers
492 let GUID = 0;
493
494 proto.create = function() {
495 // create core
496 // add id for InfiniteScroll.data
497 let id = this.guid = ++GUID;
498 this.element.infiniteScrollGUID = id; // expando
499 instances[ id ] = this; // associate via id
500 // properties
501 this.pageIndex = 1; // default to first page
502 this.loadCount = 0;
503 this.updateGetPath();
504 // bail if getPath not set, or returns falsey #776
505 let hasPath = this.getPath && this.getPath();
506 if ( !hasPath ) {
507 console.error('Disabling InfiniteScroll');
508 return;
509 }
510 this.updateGetAbsolutePath();
511 this.log( 'initialized', [ this.element.className ] );
512 this.callOnInit();
513 // create features
514 for ( let method in InfiniteScroll.create ) {
515 InfiniteScroll.create[ method ].call( this );
516 }
517 };
518
519 proto.option = function( opts ) {
520 Object.assign( this.options, opts );
521 };
522
523 // call onInit option, used for binding events on init
524 proto.callOnInit = function() {
525 let onInit = this.options.onInit;
526 if ( onInit ) {
527 onInit.call( this, this );
528 }
529 };
530
531 // ----- events ----- //
532
533 proto.dispatchEvent = function( type, event, args ) {
534 this.log( type, args );
535 let emitArgs = event ? [ event ].concat( args ) : args;
536 this.emitEvent( type, emitArgs );
537 // trigger jQuery event
538 if ( !jQuery || !this.$element ) {
539 return;
540 }
541 // namespace jQuery event
542 type += '.infiniteScroll';
543 let $event = type;
544 if ( event ) {
545 // create jQuery event
546 /* eslint-disable-next-line new-cap */
547 let jQEvent = jQuery.Event( event );
548 jQEvent.type = type;
549 $event = jQEvent;
550 }
551 this.$element.trigger( $event, args );
552 };
553
554 let loggers = {
555 initialized: ( className ) => `on ${className}`,
556 request: ( path ) => `URL: ${path}`,
557 load: ( response, path ) => `${response.title || ''}. URL: ${path}`,
558 error: ( error, path ) => `${error}. URL: ${path}`,
559 append: ( response, path, items ) => `${items.length} items. URL: ${path}`,
560 last: ( response, path ) => `URL: ${path}`,
561 history: ( title, path ) => `URL: ${path}`,
562 pageIndex: function( index, origin ) {
563 return `current page determined to be: ${index} from ${origin}`;
564 },
565 };
566
567 // log events
568 proto.log = function( type, args ) {
569 if ( !this.options.debug ) return;
570
571 let message = `[InfiniteScroll] ${type}`;
572 let logger = loggers[ type ];
573 if ( logger ) message += '. ' + logger.apply( this, args );
574 console.log( message );
575 };
576
577 // -------------------------- methods used amoung features -------------------------- //
578
579 proto.updateMeasurements = function() {
580 this.windowHeight = window.innerHeight;
581 let rect = this.element.getBoundingClientRect();
582 this.top = rect.top + window.scrollY;
583 };
584
585 proto.updateScroller = function() {
586 let elementScroll = this.options.elementScroll;
587 if ( !elementScroll ) {
588 // default, use window
589 this.scroller = window;
590 return;
591 }
592 // if true, set to element, otherwise use option
593 this.scroller = elementScroll === true ? this.element :
594 utils.getQueryElement( elementScroll );
595 if ( !this.scroller ) {
596 throw new Error(`Unable to find elementScroll: ${elementScroll}`);
597 }
598 };
599
600 // -------------------------- page path -------------------------- //
601
602 proto.updateGetPath = function() {
603 let optPath = this.options.path;
604 if ( !optPath ) {
605 console.error(`InfiniteScroll path option required. Set as: ${optPath}`);
606 return;
607 }
608 // function
609 let type = typeof optPath;
610 if ( type == 'function' ) {
611 this.getPath = optPath;
612 return;
613 }
614 // template string: '/pages/{{#}}.html'
615 let templateMatch = type == 'string' && optPath.match('{{#}}');
616 if ( templateMatch ) {
617 this.updateGetPathTemplate( optPath );
618 return;
619 }
620 // selector: '.next-page-selector'
621 this.updateGetPathSelector( optPath );
622 };
623
624 proto.updateGetPathTemplate = function( optPath ) {
625 // set getPath with template string
626 this.getPath = () => {
627 let nextIndex = this.pageIndex + 1;
628 return optPath.replace( '{{#}}', nextIndex );
629 };
630 // get pageIndex from location
631 // convert path option into regex to look for pattern in location
632 // escape query (?) in url, allows for parsing GET parameters
633 let regexString = optPath
634 .replace( /(\\\?|\?)/, '\\?' )
635 .replace( '{{#}}', '(\\d\\d?\\d?)' );
636 let templateRe = new RegExp( regexString );
637 let match = location.href.match( templateRe );
638
639 if ( match ) {
640 this.pageIndex = parseInt( match[1], 10 );
641 this.log( 'pageIndex', [ this.pageIndex, 'template string' ] );
642 }
643 };
644
645 let pathRegexes = [
646 // WordPress & Tumblr - example.com/page/2
647 // Jekyll - example.com/page2
648 /^(.*?\/?page\/?)(\d\d?\d?)(.*?$)/,
649 // Drupal - example.com/?page=1
650 /^(.*?\/?\?page=)(\d\d?\d?)(.*?$)/,
651 // catch all, last occurence of a number
652 /(.*?)(\d\d?\d?)(?!.*\d)(.*?$)/,
653 ];
654
655 // try matching href to pathRegexes patterns
656 let getPathParts = InfiniteScroll.getPathParts = function( href ) {
657 if ( !href ) return;
658 for ( let regex of pathRegexes ) {
659 let match = href.match( regex );
660 if ( match ) {
661 let [ , begin, index, end ] = match;
662 return { begin, index, end };
663 }
664 }
665 };
666
667 proto.updateGetPathSelector = function( optPath ) {
668 // parse href of link: '.next-page-link'
669 let hrefElem = document.querySelector( optPath );
670 if ( !hrefElem ) {
671 console.error(`Bad InfiniteScroll path option. Next link not found: ${optPath}`);
672 return;
673 }
674
675 let href = hrefElem.getAttribute('href');
676 let pathParts = getPathParts( href );
677 if ( !pathParts ) {
678 console.error(`InfiniteScroll unable to parse next link href: ${href}`);
679 return;
680 }
681
682 let { begin, index, end } = pathParts;
683 this.isPathSelector = true; // flag for checkLastPage()
684 this.getPath = () => begin + ( this.pageIndex + 1 ) + end;
685 // get pageIndex from href
686 this.pageIndex = parseInt( index, 10 ) - 1;
687 this.log( 'pageIndex', [ this.pageIndex, 'next link' ] );
688 };
689
690 proto.updateGetAbsolutePath = function() {
691 let path = this.getPath();
692 // path doesn't start with http or /
693 let isAbsolute = path.match( /^http/ ) || path.match( /^\// );
694 if ( isAbsolute ) {
695 this.getAbsolutePath = this.getPath;
696 return;
697 }
698
699 let { pathname } = location;
700 // query parameter #829. example.com/?pg=2
701 let isQuery = path.match( /^\?/ );
702 // /foo/bar/index.html => /foo/bar
703 let directory = pathname.substring( 0, pathname.lastIndexOf('/') );
704 let pathStart = isQuery ? pathname : directory + '/';
705
706 this.getAbsolutePath = () => pathStart + this.getPath();
707 };
708
709 // -------------------------- nav -------------------------- //
710
711 // hide navigation
712 InfiniteScroll.create.hideNav = function() {
713 let nav = utils.getQueryElement( this.options.hideNav );
714 if ( !nav ) return;
715
716 nav.style.display = 'none';
717 this.nav = nav;
718 };
719
720 InfiniteScroll.destroy.hideNav = function() {
721 if ( this.nav ) this.nav.style.display = '';
722 };
723
724 // -------------------------- destroy -------------------------- //
725
726 proto.destroy = function() {
727 this.allOff(); // remove all event listeners
728 // call destroy methods
729 for ( let method in InfiniteScroll.destroy ) {
730 InfiniteScroll.destroy[ method ].call( this );
731 }
732
733 delete this.element.infiniteScrollGUID;
734 delete instances[ this.guid ];
735 // remove jQuery data. #807
736 if ( jQuery && this.$element ) {
737 jQuery.removeData( this.element, 'infiniteScroll' );
738 }
739 };
740
741 // -------------------------- utilities -------------------------- //
742
743 // https://remysharp.com/2010/07/21/throttling-function-calls
744 InfiniteScroll.throttle = function( fn, threshold ) {
745 threshold = threshold || 200;
746 let last, timeout;
747
748 return function() {
749 let now = +new Date();
750 let args = arguments;
751 let trigger = () => {
752 last = now;
753 fn.apply( this, args );
754 };
755 if ( last && now < last + threshold ) {
756 // hold on to it
757 clearTimeout( timeout );
758 timeout = setTimeout( trigger, threshold );
759 } else {
760 trigger();
761 }
762 };
763 };
764
765 InfiniteScroll.data = function( elem ) {
766 elem = utils.getQueryElement( elem );
767 let id = elem && elem.infiniteScrollGUID;
768 return id && instances[ id ];
769 };
770
771 // set internal jQuery, for Webpack + jQuery v3
772 InfiniteScroll.setJQuery = function( jqry ) {
773 jQuery = jqry;
774 };
775
776 // -------------------------- setup -------------------------- //
777
778 utils.htmlInit( InfiniteScroll, 'infinite-scroll' );
779
780 // add noop _init method for jQuery Bridget. #768
781 proto._init = function() {};
782
783 let { jQueryBridget } = window;
784 if ( jQuery && jQueryBridget ) {
785 jQueryBridget( 'infiniteScroll', InfiniteScroll, jQuery );
786 }
787
788 // -------------------------- -------------------------- //
789
790 return InfiniteScroll;
791
792 } ) );
793 // page-load
794 ( function( window, factory ) {
795 // universal module definition
796 if ( typeof module == 'object' && module.exports ) {
797 // CommonJS
798 module.exports = factory(
799 window,
800 require('./core'),
801 );
802 } else {
803 // browser global
804 factory(
805 window,
806 window.InfiniteScroll,
807 );
808 }
809
810 }( window, function factory( window, InfiniteScroll ) {
811
812 let proto = InfiniteScroll.prototype;
813
814 Object.assign( InfiniteScroll.defaults, {
815 // append: false,
816 loadOnScroll: true,
817 checkLastPage: true,
818 responseBody: 'text',
819 domParseResponse: true,
820 // prefill: false,
821 // outlayer: null,
822 } );
823
824 InfiniteScroll.create.pageLoad = function() {
825 this.canLoad = true;
826 this.on( 'scrollThreshold', this.onScrollThresholdLoad );
827 this.on( 'load', this.checkLastPage );
828 if ( this.options.outlayer ) {
829 this.on( 'append', this.onAppendOutlayer );
830 }
831 };
832
833 proto.onScrollThresholdLoad = function() {
834 if ( this.options.loadOnScroll ) this.loadNextPage();
835 };
836
837 let domParser = new DOMParser();
838
839 proto.loadNextPage = function() {
840 if ( this.isLoading || !this.canLoad ) return;
841
842 let { responseBody, domParseResponse, fetchOptions } = this.options;
843 let path = this.getAbsolutePath();
844 this.isLoading = true;
845 if ( typeof fetchOptions == 'function' ) fetchOptions = fetchOptions();
846
847 let fetchPromise = fetch( path, fetchOptions )
848 .then( ( response ) => {
849 if ( !response.ok ) {
850 let error = new Error( response.statusText );
851 this.onPageError( error, path, response );
852 return { response };
853 }
854
855 return response[ responseBody ]().then( ( body ) => {
856 let canDomParse = responseBody == 'text' && domParseResponse;
857 if ( canDomParse ) {
858 body = domParser.parseFromString( body, 'text/html' );
859 }
860 if ( response.status == 204 ) {
861 this.lastPageReached( body, path );
862 return { body, response };
863 } else {
864 return this.onPageLoad( body, path, response );
865 }
866 } );
867 } )
868 .catch( ( error ) => {
869 this.onPageError( error, path );
870 } );
871
872 this.dispatchEvent( 'request', null, [ path, fetchPromise ] );
873
874 return fetchPromise;
875 };
876
877 proto.onPageLoad = function( body, path, response ) {
878 // done loading if not appending
879 if ( !this.options.append ) {
880 this.isLoading = false;
881 }
882 this.pageIndex++;
883 this.loadCount++;
884 this.dispatchEvent( 'load', null, [ body, path, response ] );
885 return this.appendNextPage( body, path, response );
886 };
887
888 proto.appendNextPage = function( body, path, response ) {
889 let { append, responseBody, domParseResponse } = this.options;
890 // do not append json
891 let isDocument = responseBody == 'text' && domParseResponse;
892 if ( !isDocument || !append ) return { body, response };
893
894 let items = body.querySelectorAll( append );
895 let promiseValue = { body, response, items };
896 // last page hit if no items. #840
897 if ( !items || !items.length ) {
898 this.lastPageReached( body, path );
899 return promiseValue;
900 }
901
902 let fragment = getItemsFragment( items );
903 let appendReady = () => {
904 this.appendItems( items, fragment );
905 this.isLoading = false;
906 this.dispatchEvent( 'append', null, [ body, path, items, response ] );
907 return promiseValue;
908 };
909
910 // TODO add hook for option to trigger appendReady
911 if ( this.options.outlayer ) {
912 return this.appendOutlayerItems( fragment, appendReady );
913 } else {
914 return appendReady();
915 }
916 };
917
918 proto.appendItems = function( items, fragment ) {
919 if ( !items || !items.length ) return;
920
921 // get fragment if not provided
922 fragment = fragment || getItemsFragment( items );
923 refreshScripts( fragment );
924 this.element.appendChild( fragment );
925 };
926
927 function getItemsFragment( items ) {
928 // add items to fragment
929 let fragment = document.createDocumentFragment();
930 if ( items ) fragment.append( ...items );
931 return fragment;
932 }
933
934 // replace <script>s with copies so they load
935 // <script>s added by InfiniteScroll will not load
936 // similar to https://stackoverflow.com/questions/610995
937 function refreshScripts( fragment ) {
938 let scripts = fragment.querySelectorAll('script');
939 for ( let script of scripts ) {
940 let freshScript = document.createElement('script');
941 // copy attributes
942 let attrs = script.attributes;
943 for ( let attr of attrs ) {
944 freshScript.setAttribute( attr.name, attr.value );
945 }
946 // copy inner script code. #718, #782
947 freshScript.innerHTML = script.innerHTML;
948 script.parentNode.replaceChild( freshScript, script );
949 }
950 }
951
952 // ----- outlayer ----- //
953
954 proto.appendOutlayerItems = function( fragment, appendReady ) {
955 let imagesLoaded = InfiniteScroll.imagesLoaded || window.imagesLoaded;
956 if ( !imagesLoaded ) {
957 console.error('[InfiniteScroll] imagesLoaded required for outlayer option');
958 this.isLoading = false;
959 return;
960 }
961 // append once images loaded
962 return new Promise( function( resolve ) {
963 imagesLoaded( fragment, function() {
964 let bodyResponse = appendReady();
965 resolve( bodyResponse );
966 } );
967 } );
968 };
969
970 proto.onAppendOutlayer = function( response, path, items ) {
971 this.options.outlayer.appended( items );
972 };
973
974 // ----- checkLastPage ----- //
975
976 // check response for next element
977 proto.checkLastPage = function( body, path ) {
978 let { checkLastPage, path: pathOpt } = this.options;
979 if ( !checkLastPage ) return;
980
981 // if path is function, check if next path is truthy
982 if ( typeof pathOpt == 'function' ) {
983 let nextPath = this.getPath();
984 if ( !nextPath ) {
985 this.lastPageReached( body, path );
986 return;
987 }
988 }
989 // get selector from checkLastPage or path option
990 let selector;
991 if ( typeof checkLastPage == 'string' ) {
992 selector = checkLastPage;
993 } else if ( this.isPathSelector ) {
994 // path option is selector string
995 selector = pathOpt;
996 }
997 // check last page for selector
998 // bail if no selector or not document response
999 if ( !selector || !body.querySelector ) return;
1000
1001 // check if response has selector
1002 let nextElem = body.querySelector( selector );
1003 if ( !nextElem ) this.lastPageReached( body, path );
1004 };
1005
1006 proto.lastPageReached = function( body, path ) {
1007 this.canLoad = false;
1008 this.dispatchEvent( 'last', null, [ body, path ] );
1009 };
1010
1011 // ----- error ----- //
1012
1013 proto.onPageError = function( error, path, response ) {
1014 this.isLoading = false;
1015 this.canLoad = false;
1016 this.dispatchEvent( 'error', null, [ error, path, response ] );
1017 return error;
1018 };
1019
1020 // -------------------------- prefill -------------------------- //
1021
1022 InfiniteScroll.create.prefill = function() {
1023 if ( !this.options.prefill ) return;
1024
1025 let append = this.options.append;
1026 if ( !append ) {
1027 console.error(`append option required for prefill. Set as :${append}`);
1028 return;
1029 }
1030 this.updateMeasurements();
1031 this.updateScroller();
1032 this.isPrefilling = true;
1033 this.on( 'append', this.prefill );
1034 this.once( 'error', this.stopPrefill );
1035 this.once( 'last', this.stopPrefill );
1036 this.prefill();
1037 };
1038
1039 proto.prefill = function() {
1040 let distance = this.getPrefillDistance();
1041 this.isPrefilling = distance >= 0;
1042 if ( this.isPrefilling ) {
1043 this.log('prefill');
1044 this.loadNextPage();
1045 } else {
1046 this.stopPrefill();
1047 }
1048 };
1049
1050 proto.getPrefillDistance = function() {
1051 // element scroll
1052 if ( this.options.elementScroll ) {
1053 return this.scroller.clientHeight - this.scroller.scrollHeight;
1054 }
1055 // window
1056 return this.windowHeight - this.element.clientHeight;
1057 };
1058
1059 proto.stopPrefill = function() {
1060 this.log('stopPrefill');
1061 this.off( 'append', this.prefill );
1062 };
1063
1064 // -------------------------- -------------------------- //
1065
1066 return InfiniteScroll;
1067
1068 } ) );
1069 // scroll-watch
1070 ( function( window, factory ) {
1071 // universal module definition
1072 if ( typeof module == 'object' && module.exports ) {
1073 // CommonJS
1074 module.exports = factory(
1075 window,
1076 require('./core'),
1077 require('fizzy-ui-utils'),
1078 );
1079 } else {
1080 // browser global
1081 factory(
1082 window,
1083 window.InfiniteScroll,
1084 window.fizzyUIUtils,
1085 );
1086 }
1087
1088 }( window, function factory( window, InfiniteScroll, utils ) {
1089
1090 let proto = InfiniteScroll.prototype;
1091
1092 // default options
1093 Object.assign( InfiniteScroll.defaults, {
1094 scrollThreshold: 400,
1095 // elementScroll: null,
1096 } );
1097
1098 InfiniteScroll.create.scrollWatch = function() {
1099 // events
1100 this.pageScrollHandler = this.onPageScroll.bind( this );
1101 this.resizeHandler = this.onResize.bind( this );
1102
1103 let scrollThreshold = this.options.scrollThreshold;
1104 let isEnable = scrollThreshold || scrollThreshold === 0;
1105 if ( isEnable ) this.enableScrollWatch();
1106 };
1107
1108 InfiniteScroll.destroy.scrollWatch = function() {
1109 this.disableScrollWatch();
1110 };
1111
1112 proto.enableScrollWatch = function() {
1113 if ( this.isScrollWatching ) return;
1114
1115 this.isScrollWatching = true;
1116 this.updateMeasurements();
1117 this.updateScroller();
1118 // TODO disable after error?
1119 this.on( 'last', this.disableScrollWatch );
1120 this.bindScrollWatchEvents( true );
1121 };
1122
1123 proto.disableScrollWatch = function() {
1124 if ( !this.isScrollWatching ) return;
1125
1126 this.bindScrollWatchEvents( false );
1127 delete this.isScrollWatching;
1128 };
1129
1130 proto.bindScrollWatchEvents = function( isBind ) {
1131 let addRemove = isBind ? 'addEventListener' : 'removeEventListener';
1132 this.scroller[ addRemove ]( 'scroll', this.pageScrollHandler );
1133 window[ addRemove ]( 'resize', this.resizeHandler );
1134 };
1135
1136 proto.onPageScroll = InfiniteScroll.throttle( function() {
1137 let distance = this.getBottomDistance();
1138 if ( distance <= this.options.scrollThreshold ) {
1139 this.dispatchEvent('scrollThreshold');
1140 }
1141 } );
1142
1143 proto.getBottomDistance = function() {
1144 let bottom, scrollY;
1145 if ( this.options.elementScroll ) {
1146 bottom = this.scroller.scrollHeight;
1147 scrollY = this.scroller.scrollTop + this.scroller.clientHeight;
1148 } else {
1149 bottom = this.top + this.element.clientHeight;
1150 scrollY = window.scrollY + this.windowHeight;
1151 }
1152 return bottom - scrollY;
1153 };
1154
1155 proto.onResize = function() {
1156 this.updateMeasurements();
1157 };
1158
1159 utils.debounceMethod( InfiniteScroll, 'onResize', 150 );
1160
1161 // -------------------------- -------------------------- //
1162
1163 return InfiniteScroll;
1164
1165 } ) );
1166 // history
1167 ( function( window, factory ) {
1168 // universal module definition
1169 if ( typeof module == 'object' && module.exports ) {
1170 // CommonJS
1171 module.exports = factory(
1172 window,
1173 require('./core'),
1174 require('fizzy-ui-utils'),
1175 );
1176 } else {
1177 // browser global
1178 factory(
1179 window,
1180 window.InfiniteScroll,
1181 window.fizzyUIUtils,
1182 );
1183 }
1184
1185 }( window, function factory( window, InfiniteScroll, utils ) {
1186
1187 let proto = InfiniteScroll.prototype;
1188
1189 Object.assign( InfiniteScroll.defaults, {
1190 history: 'replace',
1191 // historyTitle: false,
1192 } );
1193
1194 let link = document.createElement('a');
1195
1196 // ----- create/destroy ----- //
1197
1198 InfiniteScroll.create.history = function() {
1199 if ( !this.options.history ) return;
1200
1201 // check for same origin
1202 link.href = this.getAbsolutePath();
1203 // MS Edge does not have origin on link
1204 // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12236493/
1205 let linkOrigin = link.origin || link.protocol + '//' + link.host;
1206 let isSameOrigin = linkOrigin == location.origin;
1207 if ( !isSameOrigin ) {
1208 console.error( '[InfiniteScroll] cannot set history with different origin: ' +
1209 `${link.origin} on ${location.origin} . History behavior disabled.` );
1210 return;
1211 }
1212
1213 // two ways to handle changing history
1214 if ( this.options.append ) {
1215 this.createHistoryAppend();
1216 } else {
1217 this.createHistoryPageLoad();
1218 }
1219 };
1220
1221 proto.createHistoryAppend = function() {
1222 this.updateMeasurements();
1223 this.updateScroller();
1224 // array of scroll positions of appended pages
1225 this.scrollPages = [
1226 // first page
1227 {
1228 top: 0,
1229 path: location.href,
1230 title: document.title,
1231 },
1232 ];
1233 this.scrollPage = this.scrollPages[0];
1234 // events
1235 this.scrollHistoryHandler = this.onScrollHistory.bind( this );
1236 this.unloadHandler = this.onUnload.bind( this );
1237 this.scroller.addEventListener( 'scroll', this.scrollHistoryHandler );
1238 this.on( 'append', this.onAppendHistory );
1239 this.bindHistoryAppendEvents( true );
1240 };
1241
1242 proto.bindHistoryAppendEvents = function( isBind ) {
1243 let addRemove = isBind ? 'addEventListener' : 'removeEventListener';
1244 this.scroller[ addRemove ]( 'scroll', this.scrollHistoryHandler );
1245 window[ addRemove ]( 'unload', this.unloadHandler );
1246 };
1247
1248 proto.createHistoryPageLoad = function() {
1249 this.on( 'load', this.onPageLoadHistory );
1250 };
1251
1252 InfiniteScroll.destroy.history =
1253 proto.destroyHistory = function() {
1254 let isHistoryAppend = this.options.history && this.options.append;
1255 if ( isHistoryAppend ) {
1256 this.bindHistoryAppendEvents( false );
1257 }
1258 };
1259
1260 // ----- append history ----- //
1261
1262 proto.onAppendHistory = function( response, path, items ) {
1263 // do not proceed if no items. #779
1264 if ( !items || !items.length ) return;
1265
1266 let firstItem = items[0];
1267 let elemScrollY = this.getElementScrollY( firstItem );
1268 // resolve path
1269 link.href = path;
1270 // add page data to hash
1271 this.scrollPages.push({
1272 top: elemScrollY,
1273 path: link.href,
1274 title: response.title,
1275 });
1276 };
1277
1278 proto.getElementScrollY = function( elem ) {
1279 if ( this.options.elementScroll ) {
1280 return elem.offsetTop - this.top;
1281 } else {
1282 let rect = elem.getBoundingClientRect();
1283 return rect.top + window.scrollY;
1284 }
1285 };
1286
1287 proto.onScrollHistory = function() {
1288 // cycle through positions, find biggest without going over
1289 let scrollPage = this.getClosestScrollPage();
1290 // set history if changed
1291 if ( scrollPage != this.scrollPage ) {
1292 this.scrollPage = scrollPage;
1293 this.setHistory( scrollPage.title, scrollPage.path );
1294 }
1295 };
1296
1297 utils.debounceMethod( InfiniteScroll, 'onScrollHistory', 150 );
1298
1299 proto.getClosestScrollPage = function() {
1300 let scrollViewY;
1301 if ( this.options.elementScroll ) {
1302 scrollViewY = this.scroller.scrollTop + this.scroller.clientHeight / 2;
1303 } else {
1304 scrollViewY = window.scrollY + this.windowHeight / 2;
1305 }
1306
1307 let scrollPage;
1308 for ( let page of this.scrollPages ) {
1309 if ( page.top >= scrollViewY ) break;
1310
1311 scrollPage = page;
1312 }
1313 return scrollPage;
1314 };
1315
1316 proto.setHistory = function( title, path ) {
1317 let optHistory = this.options.history;
1318 let historyMethod = optHistory && history[ optHistory + 'State' ];
1319 if ( !historyMethod ) return;
1320
1321 history[ optHistory + 'State' ]( null, title, path );
1322 if ( this.options.historyTitle ) document.title = title;
1323 this.dispatchEvent( 'history', null, [ title, path ] );
1324 };
1325
1326 // scroll to top to prevent initial scroll-reset after page refresh
1327 // https://stackoverflow.com/a/18633915/182183
1328 proto.onUnload = function() {
1329 if ( this.scrollPage.top === 0 ) return;
1330
1331 // calculate where scroll position would be on refresh
1332 let scrollY = window.scrollY - this.scrollPage.top + this.top;
1333 // disable scroll event before setting scroll #679
1334 this.destroyHistory();
1335 scrollTo( 0, scrollY );
1336 };
1337
1338 // ----- load history ----- //
1339
1340 // update URL
1341 proto.onPageLoadHistory = function( response, path ) {
1342 this.setHistory( response.title, path );
1343 };
1344
1345 // -------------------------- -------------------------- //
1346
1347 return InfiniteScroll;
1348
1349 } ) );
1350 // button
1351 ( function( window, factory ) {
1352 // universal module definition
1353 if ( typeof module == 'object' && module.exports ) {
1354 // CommonJS
1355 module.exports = factory(
1356 window,
1357 require('./core'),
1358 require('fizzy-ui-utils'),
1359 );
1360 } else {
1361 // browser global
1362 factory(
1363 window,
1364 window.InfiniteScroll,
1365 window.fizzyUIUtils,
1366 );
1367 }
1368
1369 }( window, function factory( window, InfiniteScroll, utils ) {
1370
1371 // -------------------------- InfiniteScrollButton -------------------------- //
1372
1373 class InfiniteScrollButton {
1374 constructor( element, infScroll ) {
1375 this.element = element;
1376 this.infScroll = infScroll;
1377 // events
1378 this.clickHandler = this.onClick.bind( this );
1379 this.element.addEventListener( 'click', this.clickHandler );
1380 infScroll.on( 'request', this.disable.bind( this ) );
1381 infScroll.on( 'load', this.enable.bind( this ) );
1382 infScroll.on( 'error', this.hide.bind( this ) );
1383 infScroll.on( 'last', this.hide.bind( this ) );
1384 }
1385
1386 onClick( event ) {
1387 event.preventDefault();
1388 this.infScroll.loadNextPage();
1389 }
1390
1391 enable() {
1392 this.element.removeAttribute('disabled');
1393 }
1394
1395 disable() {
1396 this.element.disabled = 'disabled';
1397 }
1398
1399 hide() {
1400 this.element.style.display = 'none';
1401 }
1402
1403 destroy() {
1404 this.element.removeEventListener( 'click', this.clickHandler );
1405 }
1406
1407 }
1408
1409 // -------------------------- InfiniteScroll methods -------------------------- //
1410
1411 // InfiniteScroll.defaults.button = null;
1412
1413 InfiniteScroll.create.button = function() {
1414 let buttonElem = utils.getQueryElement( this.options.button );
1415 if ( buttonElem ) {
1416 this.button = new InfiniteScrollButton( buttonElem, this );
1417 }
1418 };
1419
1420 InfiniteScroll.destroy.button = function() {
1421 if ( this.button ) this.button.destroy();
1422 };
1423
1424 // -------------------------- -------------------------- //
1425
1426 InfiniteScroll.Button = InfiniteScrollButton;
1427
1428 return InfiniteScroll;
1429
1430 } ) );
1431 // status
1432 ( function( window, factory ) {
1433 // universal module definition
1434 if ( typeof module == 'object' && module.exports ) {
1435 // CommonJS
1436 module.exports = factory(
1437 window,
1438 require('./core'),
1439 require('fizzy-ui-utils'),
1440 );
1441 } else {
1442 // browser global
1443 factory(
1444 window,
1445 window.InfiniteScroll,
1446 window.fizzyUIUtils,
1447 );
1448 }
1449
1450 }( window, function factory( window, InfiniteScroll, utils ) {
1451
1452 let proto = InfiniteScroll.prototype;
1453
1454 // InfiniteScroll.defaults.status = null;
1455
1456 InfiniteScroll.create.status = function() {
1457 let statusElem = utils.getQueryElement( this.options.status );
1458 if ( !statusElem ) return;
1459
1460 // elements
1461 this.statusElement = statusElem;
1462 this.statusEventElements = {
1463 request: statusElem.querySelector('.infinite-scroll-request'),
1464 error: statusElem.querySelector('.infinite-scroll-error'),
1465 last: statusElem.querySelector('.infinite-scroll-last'),
1466 };
1467 // events
1468 this.on( 'request', this.showRequestStatus );
1469 this.on( 'error', this.showErrorStatus );
1470 this.on( 'last', this.showLastStatus );
1471 this.bindHideStatus('on');
1472 };
1473
1474 proto.bindHideStatus = function( bindMethod ) {
1475 let hideEvent = this.options.append ? 'append' : 'load';
1476 this[ bindMethod ]( hideEvent, this.hideAllStatus );
1477 };
1478
1479 proto.showRequestStatus = function() {
1480 this.showStatus('request');
1481 };
1482
1483 proto.showErrorStatus = function() {
1484 this.showStatus('error');
1485 };
1486
1487 proto.showLastStatus = function() {
1488 this.showStatus('last');
1489 // prevent last then append event race condition from showing last status #706
1490 this.bindHideStatus('off');
1491 };
1492
1493 proto.showStatus = function( eventName ) {
1494 show( this.statusElement );
1495 this.hideStatusEventElements();
1496 let eventElem = this.statusEventElements[ eventName ];
1497 show( eventElem );
1498 };
1499
1500 proto.hideAllStatus = function() {
1501 hide( this.statusElement );
1502 this.hideStatusEventElements();
1503 };
1504
1505 proto.hideStatusEventElements = function() {
1506 for ( let type in this.statusEventElements ) {
1507 let eventElem = this.statusEventElements[ type ];
1508 hide( eventElem );
1509 }
1510 };
1511
1512 // -------------------------- -------------------------- //
1513
1514 function hide( elem ) {
1515 setDisplay( elem, 'none' );
1516 }
1517
1518 function show( elem ) {
1519 setDisplay( elem, 'block' );
1520 }
1521
1522 function setDisplay( elem, value ) {
1523 if ( elem ) {
1524 elem.style.display = value;
1525 }
1526 }
1527
1528 // -------------------------- -------------------------- //
1529
1530 return InfiniteScroll;
1531
1532 } ) );
1533 /*!
1534 * imagesLoaded v4.1.4
1535 * JavaScript is all like "You images are done yet or what?"
1536 * MIT License
1537 */
1538
1539 ( function( window, factory ) { 'use strict';
1540 // universal module definition
1541
1542 /*global define: false, module: false, require: false */
1543
1544 if ( typeof define == 'function' && define.amd ) {
1545 // AMD
1546 define( [
1547 'ev-emitter/ev-emitter'
1548 ], function( EvEmitter ) {
1549 return factory( window, EvEmitter );
1550 });
1551 } else if ( typeof module == 'object' && module.exports ) {
1552 // CommonJS
1553 module.exports = factory(
1554 window,
1555 require('ev-emitter')
1556 );
1557 } else {
1558 // browser global
1559 window.imagesLoaded = factory(
1560 window,
1561 window.EvEmitter
1562 );
1563 }
1564
1565 })( typeof window !== 'undefined' ? window : this,
1566
1567 // -------------------------- factory -------------------------- //
1568
1569 function factory( window, EvEmitter ) {
1570
1571 'use strict';
1572
1573 var $ = window.jQuery;
1574 var console = window.console;
1575
1576 // -------------------------- helpers -------------------------- //
1577
1578 // extend objects
1579 function extend( a, b ) {
1580 for ( var prop in b ) {
1581 a[ prop ] = b[ prop ];
1582 }
1583 return a;
1584 }
1585
1586 var arraySlice = Array.prototype.slice;
1587
1588 // turn element or nodeList into an array
1589 function makeArray( obj ) {
1590 if ( Array.isArray( obj ) ) {
1591 // use object if already an array
1592 return obj;
1593 }
1594
1595 var isArrayLike = typeof obj == 'object' && typeof obj.length == 'number';
1596 if ( isArrayLike ) {
1597 // convert nodeList to array
1598 return arraySlice.call( obj );
1599 }
1600
1601 // array of single index
1602 return [ obj ];
1603 }
1604
1605 // -------------------------- imagesLoaded -------------------------- //
1606
1607 /**
1608 * @param {Array, Element, NodeList, String} elem
1609 * @param {Object or Function} options - if function, use as callback
1610 * @param {Function} onAlways - callback function
1611 */
1612 function ImagesLoaded( elem, options, onAlways ) {
1613 // coerce ImagesLoaded() without new, to be new ImagesLoaded()
1614 if ( !( this instanceof ImagesLoaded ) ) {
1615 return new ImagesLoaded( elem, options, onAlways );
1616 }
1617 // use elem as selector string
1618 var queryElem = elem;
1619 if ( typeof elem == 'string' ) {
1620 queryElem = document.querySelectorAll( elem );
1621 }
1622 // bail if bad element
1623 if ( !queryElem ) {
1624 console.error( 'Bad element for imagesLoaded ' + ( queryElem || elem ) );
1625 return;
1626 }
1627
1628 this.elements = makeArray( queryElem );
1629 this.options = extend( {}, this.options );
1630 // shift arguments if no options set
1631 if ( typeof options == 'function' ) {
1632 onAlways = options;
1633 } else {
1634 extend( this.options, options );
1635 }
1636
1637 if ( onAlways ) {
1638 this.on( 'always', onAlways );
1639 }
1640
1641 this.getImages();
1642
1643 if ( $ ) {
1644 // add jQuery Deferred object
1645 this.jqDeferred = new $.Deferred();
1646 }
1647
1648 // HACK check async to allow time to bind listeners
1649 setTimeout( this.check.bind( this ) );
1650 }
1651
1652 ImagesLoaded.prototype = Object.create( EvEmitter.prototype );
1653
1654 ImagesLoaded.prototype.options = {};
1655
1656 ImagesLoaded.prototype.getImages = function() {
1657 this.images = [];
1658
1659 // filter & find items if we have an item selector
1660 this.elements.forEach( this.addElementImages, this );
1661 };
1662
1663 /**
1664 * @param {Node} element
1665 */
1666 ImagesLoaded.prototype.addElementImages = function( elem ) {
1667 // filter siblings
1668 if ( elem.nodeName == 'IMG' ) {
1669 this.addImage( elem );
1670 }
1671 // get background image on element
1672 if ( this.options.background === true ) {
1673 this.addElementBackgroundImages( elem );
1674 }
1675
1676 // find children
1677 // no non-element nodes, #143
1678 var nodeType = elem.nodeType;
1679 if ( !nodeType || !elementNodeTypes[ nodeType ] ) {
1680 return;
1681 }
1682 var childImgs = elem.querySelectorAll('img');
1683 // concat childElems to filterFound array
1684 for ( var i=0; i < childImgs.length; i++ ) {
1685 var img = childImgs[i];
1686 this.addImage( img );
1687 }
1688
1689 // get child background images
1690 if ( typeof this.options.background == 'string' ) {
1691 var children = elem.querySelectorAll( this.options.background );
1692 for ( i=0; i < children.length; i++ ) {
1693 var child = children[i];
1694 this.addElementBackgroundImages( child );
1695 }
1696 }
1697 };
1698
1699 var elementNodeTypes = {
1700 1: true,
1701 9: true,
1702 11: true
1703 };
1704
1705 ImagesLoaded.prototype.addElementBackgroundImages = function( elem ) {
1706 var style = getComputedStyle( elem );
1707 if ( !style ) {
1708 // Firefox returns null if in a hidden iframe https://bugzil.la/548397
1709 return;
1710 }
1711 // get url inside url("...")
1712 var reURL = /url\((['"])?(.*?)\1\)/gi;
1713 var matches = reURL.exec( style.backgroundImage );
1714 while ( matches !== null ) {
1715 var url = matches && matches[2];
1716 if ( url ) {
1717 this.addBackground( url, elem );
1718 }
1719 matches = reURL.exec( style.backgroundImage );
1720 }
1721 };
1722
1723 /**
1724 * @param {Image} img
1725 */
1726 ImagesLoaded.prototype.addImage = function( img ) {
1727 var loadingImage = new LoadingImage( img );
1728 this.images.push( loadingImage );
1729 };
1730
1731 ImagesLoaded.prototype.addBackground = function( url, elem ) {
1732 var background = new Background( url, elem );
1733 this.images.push( background );
1734 };
1735
1736 ImagesLoaded.prototype.check = function() {
1737 var _this = this;
1738 this.progressedCount = 0;
1739 this.hasAnyBroken = false;
1740 // complete if no images
1741 if ( !this.images.length ) {
1742 this.complete();
1743 return;
1744 }
1745
1746 function onProgress( image, elem, message ) {
1747 // HACK - Chrome triggers event before object properties have changed. #83
1748 setTimeout( function() {
1749 _this.progress( image, elem, message );
1750 });
1751 }
1752
1753 this.images.forEach( function( loadingImage ) {
1754 loadingImage.once( 'progress', onProgress );
1755 loadingImage.check();
1756 });
1757 };
1758
1759 ImagesLoaded.prototype.progress = function( image, elem, message ) {
1760 this.progressedCount++;
1761 this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded;
1762 // progress event
1763 this.emitEvent( 'progress', [ this, image, elem ] );
1764 if ( this.jqDeferred && this.jqDeferred.notify ) {
1765 this.jqDeferred.notify( this, image );
1766 }
1767 // check if completed
1768 if ( this.progressedCount == this.images.length ) {
1769 this.complete();
1770 }
1771
1772 if ( this.options.debug && console ) {
1773 console.log( 'progress: ' + message, image, elem );
1774 }
1775 };
1776
1777 ImagesLoaded.prototype.complete = function() {
1778 var eventName = this.hasAnyBroken ? 'fail' : 'done';
1779 this.isComplete = true;
1780 this.emitEvent( eventName, [ this ] );
1781 this.emitEvent( 'always', [ this ] );
1782 if ( this.jqDeferred ) {
1783 var jqMethod = this.hasAnyBroken ? 'reject' : 'resolve';
1784 this.jqDeferred[ jqMethod ]( this );
1785 }
1786 };
1787
1788 // -------------------------- -------------------------- //
1789
1790 function LoadingImage( img ) {
1791 this.img = img;
1792 }
1793
1794 LoadingImage.prototype = Object.create( EvEmitter.prototype );
1795
1796 LoadingImage.prototype.check = function() {
1797 // If complete is true and browser supports natural sizes,
1798 // try to check for image status manually.
1799 var isComplete = this.getIsImageComplete();
1800 if ( isComplete ) {
1801 // report based on naturalWidth
1802 this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
1803 return;
1804 }
1805
1806 // If none of the checks above matched, simulate loading on detached element.
1807 this.proxyImage = new Image();
1808 this.proxyImage.addEventListener( 'load', this );
1809 this.proxyImage.addEventListener( 'error', this );
1810 // bind to image as well for Firefox. #191
1811 this.img.addEventListener( 'load', this );
1812 this.img.addEventListener( 'error', this );
1813 this.proxyImage.src = this.img.src;
1814 };
1815
1816 LoadingImage.prototype.getIsImageComplete = function() {
1817 // check for non-zero, non-undefined naturalWidth
1818 // fixes Safari+InfiniteScroll+Masonry bug infinite-scroll#671
1819 return this.img.complete && this.img.naturalWidth;
1820 };
1821
1822 LoadingImage.prototype.confirm = function( isLoaded, message ) {
1823 this.isLoaded = isLoaded;
1824 this.emitEvent( 'progress', [ this, this.img, message ] );
1825 };
1826
1827 // ----- events ----- //
1828
1829 // trigger specified handler for event type
1830 LoadingImage.prototype.handleEvent = function( event ) {
1831 var method = 'on' + event.type;
1832 if ( this[ method ] ) {
1833 this[ method ]( event );
1834 }
1835 };
1836
1837 LoadingImage.prototype.onload = function() {
1838 this.confirm( true, 'onload' );
1839 this.unbindEvents();
1840 };
1841
1842 LoadingImage.prototype.onerror = function() {
1843 this.confirm( false, 'onerror' );
1844 this.unbindEvents();
1845 };
1846
1847 LoadingImage.prototype.unbindEvents = function() {
1848 this.proxyImage.removeEventListener( 'load', this );
1849 this.proxyImage.removeEventListener( 'error', this );
1850 this.img.removeEventListener( 'load', this );
1851 this.img.removeEventListener( 'error', this );
1852 };
1853
1854 // -------------------------- Background -------------------------- //
1855
1856 function Background( url, element ) {
1857 this.url = url;
1858 this.element = element;
1859 this.img = new Image();
1860 }
1861
1862 // inherit LoadingImage prototype
1863 Background.prototype = Object.create( LoadingImage.prototype );
1864
1865 Background.prototype.check = function() {
1866 this.img.addEventListener( 'load', this );
1867 this.img.addEventListener( 'error', this );
1868 this.img.src = this.url;
1869 // check if image is already complete
1870 var isComplete = this.getIsImageComplete();
1871 if ( isComplete ) {
1872 this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
1873 this.unbindEvents();
1874 }
1875 };
1876
1877 Background.prototype.unbindEvents = function() {
1878 this.img.removeEventListener( 'load', this );
1879 this.img.removeEventListener( 'error', this );
1880 };
1881
1882 Background.prototype.confirm = function( isLoaded, message ) {
1883 this.isLoaded = isLoaded;
1884 this.emitEvent( 'progress', [ this, this.element, message ] );
1885 };
1886
1887 // -------------------------- jQuery -------------------------- //
1888
1889 ImagesLoaded.makeJQueryPlugin = function( jQuery ) {
1890 jQuery = jQuery || window.jQuery;
1891 if ( !jQuery ) {
1892 return;
1893 }
1894 // set local variable
1895 $ = jQuery;
1896 // $().imagesLoaded()
1897 $.fn.imagesLoaded = function( options, callback ) {
1898 var instance = new ImagesLoaded( this, options, callback );
1899 return instance.jqDeferred.promise( $(this) );
1900 };
1901 };
1902 // try making plugin
1903 ImagesLoaded.makeJQueryPlugin();
1904
1905 // -------------------------- -------------------------- //
1906
1907 return ImagesLoaded;
1908
1909 });
1910