PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | modules/infinite-scroll/infinity.js +920 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,920 @@
1 +/* globals infiniteScroll, _wpmejsSettings, ga, _gaq, WPCOM_sharing_counts, MediaElementPlayer */
2 +( function () {
3 + // Open closure.
4 + // Local vars.
5 + var Scroller, stats, type, text, totop, loading_text;
6 +
7 + // IE requires special handling
8 + var isIE = -1 !== navigator.userAgent.search( 'MSIE' );
9 + if ( isIE ) {
10 + var IEVersion = navigator.userAgent.match( /MSIE\s?(\d+)\.?\d*;/ );
11 + IEVersion = parseInt( IEVersion[ 1 ] );
12 + }
13 +
14 + // HTTP ajaxurl when site is HTTPS causes Access-Control-Allow-Origin failure in Desktop and iOS Safari
15 + if ( 'https:' === document.location.protocol ) {
16 + infiniteScroll.settings.ajaxurl = infiniteScroll.settings.ajaxurl.replace(
17 + 'http://',
18 + 'https://'
19 + );
20 + }
21 +
22 + /**
23 + * Loads new posts when users scroll near the bottom of the page.
24 + */
25 + Scroller = function ( settings ) {
26 + var self = this;
27 +
28 + // Initialize our variables
29 + this.id = settings.id;
30 + this.body = document.body;
31 + this.window = window;
32 + this.element = document.getElementById( settings.id );
33 + this.wrapperClass = settings.wrapper_class;
34 + this.ready = true;
35 + this.disabled = false;
36 + this.page = 1;
37 + this.offset = settings.offset;
38 + this.currentday = settings.currentday;
39 + this.order = settings.order;
40 + this.throttle = false;
41 + this.click_handle = settings.click_handle;
42 + this.google_analytics = settings.google_analytics;
43 + this.history = settings.history;
44 + this.origURL = window.location.href;
45 +
46 + // Handle element
47 + this.handle = document.createElement( 'div' );
48 + this.handle.setAttribute( 'id', 'infinite-handle' );
49 + var span = document.createElement( 'span' );
50 + var button = document.createElement( 'button' );
51 + button.textContent = text;
52 + span.appendChild( button );
53 + this.handle.appendChild( span );
54 +
55 + // Footer settings
56 + this.footer = {
57 + el: document.getElementById( 'infinite-footer' ),
58 + wrap: settings.footer,
59 + };
60 +
61 + // Bind methods used as callbacks
62 + this.checkViewportOnLoadBound = self.checkViewportOnLoad.bind( this );
63 +
64 + // Core's native MediaElement.js implementation needs special handling
65 + this.wpMediaelement = null;
66 +
67 + // We have two type of infinite scroll
68 + // cases 'scroll' and 'click'
69 +
70 + if ( type === 'scroll' ) {
71 + // Bind refresh to the scroll event
72 + // Throttle to check for such case every 300ms
73 +
74 + // On event the case becomes a fact
75 + this.window.addEventListener( 'scroll', function () {
76 + self.throttle = true;
77 + } );
78 +
79 + // Go back top method
80 + self.gotop();
81 +
82 + setInterval( function () {
83 + if ( self.throttle ) {
84 + // Once the case is the case, the action occurs and the fact is no more
85 + self.throttle = false;
86 + // Reveal or hide footer
87 + self.thefooter();
88 + // Fire the refresh
89 + self.refresh();
90 + self.determineURL(); // determine the url
91 + }
92 + }, 250 );
93 +
94 + // Ensure that enough posts are loaded to fill the initial viewport, to compensate for short posts and large displays.
95 + self.ensureFilledViewport();
96 + this.body.addEventListener( 'is.post-load', self.checkViewportOnLoadBound );
97 + } else if ( type === 'click' ) {
98 + if ( this.click_handle ) {
99 + this.element.appendChild( this.handle );
100 + }
101 +
102 + this.handle.addEventListener( 'click', function () {
103 + // Handle the handle
104 + if ( self.click_handle ) {
105 + self.handle.parentNode.removeChild( self.handle );
106 + }
107 +
108 + // Fire the refresh
109 + self.refresh();
110 + } );
111 + }
112 +
113 + // Initialize any Core audio or video players loaded via IS
114 + this.body.addEventListener( 'is.post-load', self.initializeMejs );
115 + };
116 +
117 + /**
118 + * Normalize the access to the document scrollTop value.
119 + */
120 + Scroller.prototype.getScrollTop = function () {
121 + return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
122 + };
123 +
124 + /**
125 + * Polyfill jQuery.extend.
126 + */
127 + Scroller.prototype.extend = function ( out ) {
128 + out = out || {};
129 +
130 + for ( var i = 1; i < arguments.length; i++ ) {
131 + if ( ! arguments[ i ] ) {
132 + continue;
133 + }
134 +
135 + for ( var key in arguments[ i ] ) {
136 + if ( Object.hasOwn( arguments[ i ], key ) ) {
137 + out[ key ] = arguments[ i ][ key ];
138 + }
139 + }
140 + }
141 + return out;
142 + };
143 +
144 + /**
145 + * Check whether we should fetch any additional posts.
146 + */
147 + Scroller.prototype.check = function () {
148 + var wrapperMeasurements = this.measure( this.element, [ this.wrapperClass ] );
149 +
150 + // Fetch more posts when we're less than 2 screens away from the bottom.
151 + return wrapperMeasurements.bottom < 2 * this.window.innerHeight;
152 + };
153 +
154 + /**
155 + * Renders the results from a successful response.
156 + */
157 + Scroller.prototype.render = function ( response ) {
158 + var childrenToAppend = Array.prototype.slice.call( response.fragment.childNodes );
159 + this.body.classList.add( 'infinity-success' );
160 +
161 + // Render the retrieved nodes.
162 + while ( childrenToAppend.length > 0 ) {
163 + var currentNode = childrenToAppend.shift();
164 + this.element.appendChild( currentNode );
165 + }
166 +
167 + this.trigger( this.body, 'is.post-load', {
168 + jqueryEventName: 'post-load',
169 + data: response,
170 + } );
171 +
172 + this.ready = true;
173 + };
174 +
175 + /**
176 + * Returns the object used to query for new posts.
177 + */
178 + Scroller.prototype.query = function () {
179 + return {
180 + page: this.page + this.offset, // Load the next page.
181 + currentday: this.currentday,
182 + order: this.order,
183 + scripts: window.infiniteScroll.settings.scripts,
184 + styles: window.infiniteScroll.settings.styles,
185 + query_args: window.infiniteScroll.settings.query_args,
186 + query_before: window.infiniteScroll.settings.query_before,
187 + last_post_date: window.infiniteScroll.settings.last_post_date,
188 + };
189 + };
190 +
191 + Scroller.prototype.animate = function ( cb, duration ) {
192 + var start = performance.now();
193 +
194 + requestAnimationFrame( function animate( time ) {
195 + var timeFraction = Math.min( 1, ( time - start ) / duration );
196 + cb( timeFraction );
197 +
198 + if ( timeFraction < 1 ) {
199 + requestAnimationFrame( animate );
200 + }
201 + } );
202 + };
203 +
204 + /**
205 + * Scroll back to top.
206 + */
207 + Scroller.prototype.gotop = function () {
208 + var blog = document.getElementById( 'infinity-blog-title' );
209 + var self = this;
210 +
211 + if ( ! blog ) {
212 + return;
213 + }
214 +
215 + blog.setAttribute( 'title', totop );
216 + blog.addEventListener( 'click', function ( e ) {
217 + var sourceScroll = self.window.pageYOffset;
218 + e.preventDefault();
219 +
220 + self.animate( function ( progress ) {
221 + var currentScroll = sourceScroll - sourceScroll * progress;
222 + document.documentElement.scrollTop = document.body.scrollTop = currentScroll;
223 + }, 200 );
224 + } );
225 + };
226 +
227 + /**
228 + * The infinite footer.
229 + */
230 + Scroller.prototype.thefooter = function () {
231 + var self = this,
232 + pageWrapper,
233 + footerContainer,
234 + width,
235 + sourceBottom,
236 + targetBottom,
237 + footerEnabled = this.footer && this.footer.el;
238 +
239 + if ( ! footerEnabled ) {
240 + return;
241 + }
242 +
243 + // Check if we have an id for the page wrapper
244 + if ( 'string' === typeof this.footer.wrap ) {
245 + try {
246 + pageWrapper = document.getElementById( this.footer.wrap );
247 + width = pageWrapper.getBoundingClientRect();
248 + width = width.width;
249 + } catch {
250 + width = 0;
251 + }
252 +
253 + // Make the footer match the width of the page
254 + if ( width > 479 ) {
255 + footerContainer = this.footer.el.querySelector( '.container' );
256 + if ( footerContainer ) {
257 + footerContainer.style.width = width + 'px';
258 + }
259 + }
260 + }
261 +
262 + // Reveal footer
263 + sourceBottom = parseInt( self.footer.el.style.bottom || -50, 10 );
264 + targetBottom = this.window.pageYOffset >= 350 ? 0 : -50;
265 +
266 + if ( sourceBottom !== targetBottom ) {
267 + self.animate( function ( progress ) {
268 + var currentBottom = sourceBottom + ( targetBottom - sourceBottom ) * progress;
269 + self.footer.el.style.bottom = currentBottom + 'px';
270 +
271 + if ( 1 === progress ) {
272 + sourceBottom = targetBottom;
273 + }
274 + }, 200 );
275 + }
276 + };
277 +
278 + /**
279 + * Recursively convert a JS object into URL encoded data.
280 + */
281 + Scroller.prototype.urlEncodeJSON = function ( obj, prefix ) {
282 + var params = [],
283 + encodedKey,
284 + newPrefix;
285 +
286 + for ( var key in obj ) {
287 + encodedKey = encodeURIComponent( key );
288 + newPrefix = prefix ? prefix + '[' + encodedKey + ']' : encodedKey;
289 +
290 + if ( 'object' === typeof obj[ key ] ) {
291 + if ( ! Array.isArray( obj[ key ] ) || obj[ key ].length > 0 ) {
292 + params.push( this.urlEncodeJSON( obj[ key ], newPrefix ) );
293 + } else {
294 + // Explicitly expose empty arrays with no values
295 + params.push( newPrefix + '[]=' );
296 + }
297 + } else {
298 + params.push( newPrefix + '=' + encodeURIComponent( obj[ key ] ) );
299 + }
300 + }
301 + return params.join( '&' );
302 + };
303 +
304 + /**
305 + * Controls the flow of the refresh. Don't mess.
306 + */
307 + Scroller.prototype.refresh = function () {
308 + var self = this,
309 + query,
310 + xhr,
311 + loader,
312 + customized;
313 +
314 + // If we're disabled, ready, or don't pass the check, bail.
315 + if ( this.disabled || ! this.ready || ! this.check() ) {
316 + return;
317 + }
318 +
319 + // Let's get going -- set ready to false to prevent
320 + // multiple refreshes from occurring at once.
321 + this.ready = false;
322 +
323 + // Create a loader element to show it's working.
324 + if ( this.click_handle ) {
325 + if ( ! loader ) {
326 + document.getElementById( 'infinite-aria' ).textContent = loading_text;
327 + loader = document.createElement( 'div' );
328 + loader.classList.add( 'infinite-loader' );
329 + loader.setAttribute( 'role', 'progress' );
330 + loader.innerHTML =
331 + '<svg class="jetpack-spinner infinite-loader-spinner spinner" width="28" height="28" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><circle cx="50" cy="50" r="46" fill="none" stroke="#ddd" stroke-width="8"/><path d="M 50 4 A 46 46 0 0 1 96 50" fill="none" stroke="currentColor" stroke-width="8" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" dur="1.4s" from="0 50 50" to="360 50 50" repeatCount="indefinite"/></path></svg>';
332 + }
333 + this.element.appendChild( loader );
334 + }
335 +
336 + // Generate our query vars.
337 + query = self.extend(
338 + {
339 + action: 'infinite_scroll',
340 + },
341 + this.query()
342 + );
343 +
344 + // Inject Customizer state.
345 + if ( 'undefined' !== typeof wp && wp.customize && wp.customize.settings.theme ) {
346 + customized = {};
347 + query.wp_customize = 'on';
348 + query.theme = wp.customize.settings.theme.stylesheet;
349 + wp.customize.each( function ( setting ) {
350 + if ( setting._dirty ) {
351 + customized[ setting.id ] = setting();
352 + }
353 + } );
354 + query.customized = JSON.stringify( customized );
355 + query.nonce = wp.customize.settings.nonce.preview;
356 + }
357 +
358 + // Fire the ajax request.
359 + xhr = new XMLHttpRequest();
360 + xhr.open( 'POST', infiniteScroll.settings.ajaxurl, true );
361 + xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
362 + xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' );
363 + xhr.send( self.urlEncodeJSON( query ) );
364 +
365 + // Allow refreshes to occur again if an error is triggered.
366 + xhr.onerror = function () {
367 + if ( self.click_handle && loader.parentNode ) {
368 + loader.parentNode.removeChild( loader );
369 + }
370 +
371 + self.ready = true;
372 + };
373 +
374 + // Success handler
375 + xhr.onload = function () {
376 + var response = JSON.parse( xhr.responseText ),
377 + httpCheck = xhr.status >= 200 && xhr.status < 300,
378 + responseCheck = 'undefined' !== typeof response.html;
379 +
380 + if ( ! response || ! httpCheck || ! responseCheck ) {
381 + if ( self.click_handle && loader.parentNode ) {
382 + loader.parentNode.removeChild( loader );
383 + }
384 + return;
385 + }
386 +
387 + // On success, let's hide the loader circle.
388 + if ( self.click_handle && loader.parentNode ) {
389 + loader.parentNode.removeChild( loader );
390 + }
391 +
392 + // If additional scripts are required by the incoming set of posts, parse them
393 + if ( response.scripts && Array.isArray( response.scripts ) ) {
394 + response.scripts.forEach( function ( item ) {
395 + var elementToAppendTo = item.footer ? 'body' : 'head';
396 +
397 + // Add script handle to list of those already parsed
398 + window.infiniteScroll.settings.scripts.push( item.handle );
399 +
400 + // Output extra data, if present
401 + if ( item.extra_data ) {
402 + self.appendInlineScript( item.extra_data, elementToAppendTo );
403 + }
404 +
405 + if ( item.before_handle ) {
406 + self.appendInlineScript( item.before_handle, elementToAppendTo );
407 + }
408 +
409 + // Build script tag and append to DOM in requested location
410 + var script = document.createElement( 'script' );
411 + script.type = 'text/javascript';
412 + script.src = item.src;
413 + script.id = item.handle;
414 +
415 + // Dynamically loaded scripts are async by default.
416 + // We don't want that, it breaks stuff, e.g. wp-mediaelement init.
417 + script.async = false;
418 +
419 + if ( item.after_handle ) {
420 + script.onload = function () {
421 + self.appendInlineScript( item.after_handle, elementToAppendTo );
422 + };
423 + }
424 +
425 + // If MediaElement.js is loaded in by item set of posts, don't initialize the players a second time as it breaks them all
426 + if ( 'wp-mediaelement' === item.handle ) {
427 + self.body.removeEventListener( 'is.post-load', self.initializeMejs );
428 + }
429 +
430 + if ( 'wp-mediaelement' === item.handle && 'undefined' === typeof mejs ) {
431 + self.wpMediaelement = {};
432 + self.wpMediaelement.tag = script;
433 + self.wpMediaelement.element = elementToAppendTo;
434 + setTimeout( self.maybeLoadMejs.bind( self ), 250 );
435 + } else {
436 + document.getElementsByTagName( elementToAppendTo )[ 0 ].appendChild( script );
437 + }
438 + } );
439 + }
440 +
441 + // If additional stylesheets are required by the incoming set of posts, parse them
442 + if ( response.styles && Array.isArray( response.styles ) ) {
443 + response.styles.forEach( function ( item ) {
444 + // Add stylesheet handle to list of those already parsed
445 + window.infiniteScroll.settings.styles.push( item.handle );
446 +
447 + // Build link tag
448 + var style = document.createElement( 'link' );
449 + style.rel = 'stylesheet';
450 + style.href = item.src;
451 + style.id = item.handle + '-css';
452 +
453 + // Destroy link tag if a conditional statement is present and either the browser isn't IE, or the conditional doesn't evaluate true
454 + if (
455 + item.conditional &&
456 + ( ! isIE || ! eval( item.conditional.replace( /%ver/g, IEVersion ) ) )
457 + ) {
458 + style = false;
459 + }
460 +
461 + // Append link tag if necessary
462 + if ( style ) {
463 + document.getElementsByTagName( 'head' )[ 0 ].appendChild( style );
464 + }
465 + } );
466 + }
467 +
468 + // Convert the response.html to a fragment element.
469 + // Using a div instead of DocumentFragment, because the latter doesn't support innerHTML.
470 + response.fragment = document.createElement( 'div' );
471 + response.fragment.innerHTML = response.html;
472 +
473 + // Increment the page number
474 + self.page++;
475 +
476 + // Record pageview in WP Stats, if available.
477 + if ( stats ) {
478 + new Image().src =
479 + document.location.protocol +
480 + '//pixel.wp.com/g.gif?' +
481 + stats +
482 + '&post=0&baba=' +
483 + Math.random();
484 + }
485 +
486 + // Add new posts to the postflair object
487 + if ( 'object' === typeof response.postflair && 'object' === typeof WPCOM_sharing_counts ) {
488 + WPCOM_sharing_counts = self.extend( WPCOM_sharing_counts, response.postflair ); // eslint-disable-line no-global-assign
489 + }
490 +
491 + // Render the results
492 + self.render.call( self, response );
493 +
494 + // If 'click' type and there are still posts to fetch, add back the handle
495 + if ( type === 'click' ) {
496 + // add focus to new posts, only in button mode as we know where page focus currently is and only if we have a wrapper
497 + if ( infiniteScroll.settings.wrapper ) {
498 + document
499 + .querySelector(
500 + '#infinite-view-' + ( self.page + self.offset - 1 ) + ' a:first-of-type'
501 + )
502 + .focus( {
503 + preventScroll: true,
504 + } );
505 + }
506 +
507 + if ( response.lastbatch ) {
508 + if ( self.click_handle ) {
509 + // Update body classes
510 + self.body.classList.add( 'infinity-end' );
511 + self.body.classList.remove( 'infinity-success' );
512 + } else {
513 + self.trigger( this.body, 'infinite-scroll-posts-end' );
514 + }
515 + } else if ( self.click_handle ) {
516 + self.element.appendChild( self.handle );
517 + } else {
518 + self.trigger( this.body, 'infinite-scroll-posts-more' );
519 + }
520 + } else if ( response.lastbatch ) {
521 + self.disabled = true;
522 +
523 + self.body.classList.add( 'infinity-end' );
524 + self.body.classList.remove( 'infinity-success' );
525 + }
526 +
527 + // Update currentday to the latest value returned from the server
528 + if ( response.currentday ) {
529 + self.currentday = response.currentday;
530 + }
531 +
532 + // Fire Google Analytics pageview
533 + if ( self.google_analytics ) {
534 + var ga_url = self.history.path.replace( /%d/, self.page );
535 + if ( 'object' === typeof _gaq ) {
536 + _gaq.push( [ '_trackPageview', ga_url ] );
537 + }
538 + if ( 'function' === typeof ga ) {
539 + ga( 'send', 'pageview', ga_url );
540 + }
541 + }
542 + };
543 +
544 + return xhr;
545 + };
546 +
547 + /**
548 + * Given JavaScript blob and the name of a parent tag, this helper function will
549 + * generate a script tag, insert the JavaScript blob, and append it to the parent.
550 + *
551 + * It's important to note that the JavaScript blob will be evaluated immediately. If
552 + * you need a parent script to load first, use that script element's onload handler.
553 + *
554 + * @param {string} script The blob of JavaScript to run.
555 + * @param {string} parentTag The tag name of the parent element.
556 + */
557 + Scroller.prototype.appendInlineScript = function ( script, parentTag ) {
558 + var element = document.createElement( 'script' ),
559 + scriptContent = document.createTextNode( '//<![CDATA[ \n' + script + '\n//]]>' );
560 +
561 + element.type = 'text/javascript';
562 + element.appendChild( scriptContent );
563 +
564 + document.getElementsByTagName( parentTag )[ 0 ].appendChild( element );
565 + };
566 +
567 + /**
568 + * Core's native media player uses MediaElement.js
569 + * The library's size is sufficient that it may not be loaded in time for Core's helper to invoke it, so we need to delay until `mejs` exists.
570 + */
571 + Scroller.prototype.maybeLoadMejs = function () {
572 + if ( null === this.wpMediaelement ) {
573 + return;
574 + }
575 +
576 + if ( 'undefined' === typeof mejs ) {
577 + setTimeout( this.maybeLoadMejs.bind( this ), 250 );
578 + } else {
579 + document
580 + .getElementsByTagName( this.wpMediaelement.element )[ 0 ]
581 + .appendChild( this.wpMediaelement.tag );
582 + this.wpMediaelement = null;
583 +
584 + // Ensure any subsequent IS loads initialize the players
585 + this.body.addEventListener( 'is.post-load', this.initializeMejs );
586 + }
587 + };
588 +
589 + /**
590 + * Initialize the MediaElement.js player for any posts not previously initialized
591 + */
592 + Scroller.prototype.initializeMejs = function ( e ) {
593 + // Are there media players in the incoming set of posts?
594 + if (
595 + ! e.detail ||
596 + ! e.detail.html ||
597 + ( -1 === e.detail.html.indexOf( 'wp-audio-shortcode' ) &&
598 + -1 === e.detail.html.indexOf( 'wp-video-shortcode' ) )
599 + ) {
600 + return;
601 + }
602 +
603 + // Don't bother if mejs isn't loaded for some reason
604 + if ( 'undefined' === typeof mejs ) {
605 + return;
606 + }
607 +
608 + // Adapted from wp-includes/js/mediaelement/wp-mediaelement.js
609 + // Modified to not initialize already-initialized players, as Mejs doesn't handle that well
610 + var settings = {};
611 + var audioVideoElements;
612 +
613 + if ( typeof _wpmejsSettings !== 'undefined' ) {
614 + settings.pluginPath = _wpmejsSettings.pluginPath;
615 + }
616 +
617 + settings.success = function ( mejs ) {
618 + var autoplay = mejs.attributes.autoplay && 'false' !== mejs.attributes.autoplay;
619 + if ( 'flash' === mejs.pluginType && autoplay ) {
620 + mejs.addEventListener(
621 + 'canplay',
622 + function () {
623 + mejs.play();
624 + },
625 + false
626 + );
627 + }
628 + };
629 +
630 + audioVideoElements = document.querySelectorAll( '.wp-audio-shortcode, .wp-video-shortcode' );
631 + audioVideoElements = Array.prototype.slice.call( audioVideoElements );
632 +
633 + // Only process already unprocessed shortcodes.
634 + audioVideoElements = audioVideoElements.filter( function ( el ) {
635 + while ( el.parentNode ) {
636 + if ( el.classList.contains( 'mejs-container' ) ) {
637 + return false;
638 + }
639 + el = el.parentNode;
640 + }
641 + return true;
642 + } );
643 +
644 + for ( var i = 0; i < audioVideoElements.length; i++ ) {
645 + new MediaElementPlayer( audioVideoElements[ i ], settings );
646 + }
647 + };
648 +
649 + /**
650 + * Get element measurements relative to the viewport.
651 + *
652 + * @return {object}
653 + */
654 + Scroller.prototype.measure = function ( element, expandClasses ) {
655 + expandClasses = expandClasses || [];
656 +
657 + var childrenToTest = Array.prototype.slice.call( element.children );
658 + var currentChild,
659 + minTop = Number.MAX_VALUE,
660 + maxBottom = 0,
661 + currentChildRect,
662 + i;
663 +
664 + while ( childrenToTest.length > 0 ) {
665 + currentChild = childrenToTest.shift();
666 +
667 + for ( i = 0; i < expandClasses.length; i++ ) {
668 + // Expand (= measure) child elements of nodes with class names from expandClasses.
669 + if ( currentChild.classList.contains( expandClasses[ i ] ) ) {
670 + childrenToTest = childrenToTest.concat(
671 + Array.prototype.slice.call( currentChild.children )
672 + );
673 + break;
674 + }
675 + }
676 + currentChildRect = currentChild.getBoundingClientRect();
677 +
678 + minTop = Math.min( minTop, currentChildRect.top );
679 + maxBottom = Math.max( maxBottom, currentChildRect.bottom );
680 + }
681 +
682 + var viewportMiddle = Math.round( window.innerHeight / 2 );
683 +
684 + // isActive = does the middle of the viewport cross the element?
685 + var isActive = minTop <= viewportMiddle && maxBottom >= viewportMiddle;
686 +
687 + /**
688 + * Factor = percentage of viewport above the middle line occupied by the element.
689 + *
690 + * Negative factors are assigned for elements below the middle line. That's on purpose
691 + * to only allow "page 2" to change the URL once it's in the middle of the viewport.
692 + */
693 + var factor = ( Math.min( maxBottom, viewportMiddle ) - Math.max( minTop, 0 ) ) / viewportMiddle;
694 +
695 + return {
696 + top: minTop,
697 + bottom: maxBottom,
698 + height: maxBottom - minTop,
699 + factor: factor,
700 + isActive: isActive,
701 + };
702 + };
703 +
704 + /**
705 + * Trigger IS to load additional posts if the initial posts don't fill the window.
706 + *
707 + * On large displays, or when posts are very short, the viewport may not be filled with posts,
708 + * so we overcome this by loading additional posts when IS initializes.
709 + */
710 + Scroller.prototype.ensureFilledViewport = function () {
711 + var self = this,
712 + windowHeight = self.window.innerHeight,
713 + wrapperMeasurements = self.measure( self.element, [ self.wrapperClass ] );
714 +
715 + // Only load more posts once. This prevents infinite loops when there are no more posts.
716 + self.body.removeEventListener( 'is.post-load', self.checkViewportOnLoadBound );
717 +
718 + // Load more posts if space permits, otherwise stop checking for a full viewport.
719 + if ( wrapperMeasurements.bottom !== 0 && wrapperMeasurements.bottom < windowHeight ) {
720 + self.ready = true;
721 + self.refresh();
722 + }
723 + };
724 +
725 + /**
726 + * Event handler for ensureFilledViewport(), tied to the post-load trigger.
727 + * Necessary to ensure that the variable `this` contains the scroller when used in ensureFilledViewport(). Since this function is tied to an event, `this` becomes the DOM element the event is tied to.
728 + */
729 + Scroller.prototype.checkViewportOnLoad = function () {
730 + this.ensureFilledViewport();
731 + };
732 +
733 + function fullscreenState() {
734 + return document.fullscreenElement ||
735 + document.mozFullScreenElement ||
736 + document.webkitFullscreenElement ||
737 + document.msFullscreenElement
738 + ? 1
739 + : 0;
740 + }
741 +
742 + var previousFullScrenState = fullscreenState();
743 +
744 + /**
745 + * Identify archive page that corresponds to majority of posts shown in the current browser window.
746 + */
747 + Scroller.prototype.determineURL = function () {
748 + var self = this,
749 + pageNum = -1,
750 + currentFullScreenState = fullscreenState(),
751 + wrapperEls,
752 + maxFactor = 0;
753 +
754 + // xor - check if the state has changed
755 + // eslint-disable-next-line no-bitwise
756 + if ( previousFullScrenState ^ currentFullScreenState ) {
757 + // If we just switched to/from fullscreen,
758 + // don't do the div clearing/caching or the
759 + // URL setting. Doing so can break video playback
760 + // if the video goes to fullscreen.
761 +
762 + previousFullScrenState = currentFullScreenState;
763 + return;
764 + }
765 + previousFullScrenState = currentFullScreenState;
766 + wrapperEls = document.querySelectorAll( '.' + self.wrapperClass );
767 +
768 + for ( var i = 0; i < wrapperEls.length; i++ ) {
769 + var setMeasurements = self.measure( wrapperEls[ i ] );
770 +
771 + // If it exists, pick a set that is crossed by the middle of the viewport.
772 + if ( setMeasurements.isActive ) {
773 + pageNum = parseInt( wrapperEls[ i ].dataset.pageNum, 10 );
774 + break;
775 + }
776 +
777 + // If there is such a set, pick the one that occupies the most space
778 + // above the middle of the viewport.
779 + if ( setMeasurements.factor > maxFactor ) {
780 + pageNum = parseInt( wrapperEls[ i ].dataset.pageNum, 10 );
781 + maxFactor = setMeasurements.factor;
782 + }
783 +
784 + // Otherwise default to -1
785 + }
786 +
787 + self.updateURL( pageNum );
788 + };
789 +
790 + /**
791 + * Update address bar to reflect archive page URL for a given page number.
792 + * Checks if URL is different to prevent pollution of browser history.
793 + */
794 + Scroller.prototype.updateURL = function ( page ) {
795 + // IE only supports pushState() in v10 and above, so don't bother if those conditions aren't met.
796 + if ( ! window.history.pushState ) {
797 + return;
798 + }
799 + var self = this,
800 + pageSlug = self.origURL;
801 +
802 + if ( -1 !== page ) {
803 + pageSlug =
804 + window.location.protocol +
805 + '//' +
806 + self.history.host +
807 + self.history.path.replace( /%d/, page ) +
808 + self.history.parameters;
809 + }
810 +
811 + if ( window.location.href !== pageSlug ) {
812 + history.pushState( null, null, pageSlug );
813 + }
814 + };
815 +
816 + /**
817 + * Pause scrolling.
818 + */
819 + Scroller.prototype.pause = function () {
820 + this.disabled = true;
821 + };
822 +
823 + /**
824 + * Resume scrolling.
825 + */
826 + Scroller.prototype.resume = function () {
827 + this.disabled = false;
828 + };
829 +
830 + /**
831 + * Emits custom JS events.
832 + *
833 + * @param {Node} el
834 + * @param {string} eventName
835 + * @param {*} data
836 + */
837 + Scroller.prototype.trigger = function ( el, eventName, opts ) {
838 + opts = opts || {};
839 +
840 + /**
841 + * Emit the event in a jQuery way for backwards compatibility where necessary.
842 + */
843 + if ( opts.jqueryEventName && 'undefined' !== typeof jQuery ) {
844 + jQuery( el ).trigger( opts.jqueryEventName, opts.data || null );
845 + }
846 +
847 + /**
848 + * Emit the event in a standard way.
849 + */
850 + var e;
851 + try {
852 + e = new CustomEvent( eventName, {
853 + bubbles: true,
854 + cancelable: true,
855 + detail: opts.data || null,
856 + } );
857 + } catch {
858 + e = document.createEvent( 'CustomEvent' );
859 + e.initCustomEvent( eventName, true, true, opts.data || null );
860 + }
861 + el.dispatchEvent( e );
862 + };
863 +
864 + /**
865 + * Ready, set, go!
866 + */
867 + var jetpackInfinityModule = function () {
868 + // Check for our variables
869 + if ( 'object' !== typeof infiniteScroll ) {
870 + return;
871 + }
872 +
873 + var bodyClasses = infiniteScroll.settings.body_class.split( ' ' );
874 + bodyClasses.forEach( function ( className ) {
875 + if ( className ) {
876 + document.body.classList.add( className );
877 + }
878 + } );
879 +
880 + // Set stats, used for tracking stats
881 + stats = infiniteScroll.settings.stats;
882 +
883 + // Define what type of infinity we have, grab text for click-handle
884 + type = infiniteScroll.settings.type;
885 + text = infiniteScroll.settings.text;
886 + totop = infiniteScroll.settings.totop;
887 +
888 + // aria text
889 + loading_text = infiniteScroll.settings.loading_text;
890 +
891 + // Initialize the scroller (with the ID of the element from the theme)
892 + infiniteScroll.scroller = new Scroller( infiniteScroll.settings );
893 +
894 + /**
895 + * Monitor user scroll activity to update URL to correspond to archive page for current set of IS posts
896 + */
897 + if ( type === 'click' ) {
898 + var timer = null;
899 + window.addEventListener( 'scroll', function () {
900 + // run the real scroll handler once every 250 ms.
901 + if ( timer ) {
902 + return;
903 + }
904 + timer = setTimeout( function () {
905 + infiniteScroll.scroller.determineURL();
906 + timer = null;
907 + }, 250 );
908 + } );
909 + }
910 + };
911 +
912 + /**
913 + * Ready, set, go!
914 + */
915 + if ( document.readyState === 'interactive' || document.readyState === 'complete' ) {
916 + jetpackInfinityModule();
917 + } else {
918 + document.addEventListener( 'DOMContentLoaded', jetpackInfinityModule );
919 + }
920 +} )(); // Close closure