PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.0-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.0-beta
16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / infinite-scroll / infinity.js
jetpack / modules / infinite-scroll Last commit date
themes 7 months ago infinity-customizer.js 1 year ago infinity.css 2 months ago infinity.js 2 months ago infinity.php 2 weeks ago
infinity.js
921 lines
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