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