PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 All 502 releases
jetpack / modules / infinite-scroll / infinity.js

infinity.js in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at modules/infinite-scroll/infinity.js

921 lines 26.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
921