diff options
author | 2022-01-23 18:37:36 -0500 | |
---|---|---|
committer | 2022-01-23 18:37:36 -0500 | |
commit | f18b23a3a9378fb0a98856d436aa9ebf94e47429 (patch) | |
tree | e418433e22854ebd2d77eaa869d5d0470a973317 /plugins/jetpack/modules/infinite-scroll | |
parent | Add classic-editor 1.5 (diff) | |
download | blogs-gentoo-f18b23a3a9378fb0a98856d436aa9ebf94e47429.tar.gz blogs-gentoo-f18b23a3a9378fb0a98856d436aa9ebf94e47429.tar.bz2 blogs-gentoo-f18b23a3a9378fb0a98856d436aa9ebf94e47429.zip |
Updating Classic Editor, Google Authenticatior, Jetpack, Public Post Preview, Table of Contents, Wordpress Importer
Signed-off-by: Yury German <blueknight@gentoo.org>
Diffstat (limited to 'plugins/jetpack/modules/infinite-scroll')
6 files changed, 1180 insertions, 499 deletions
diff --git a/plugins/jetpack/modules/infinite-scroll/infinity-customizer.js b/plugins/jetpack/modules/infinite-scroll/infinity-customizer.js new file mode 100644 index 00000000..9d5937b1 --- /dev/null +++ b/plugins/jetpack/modules/infinite-scroll/infinity-customizer.js @@ -0,0 +1,54 @@ +/* globals wp */ +( function ( $ ) { + /** + * Ready, set, go! + */ + $( document ).ready( function () { + // Integrate with Selective Refresh in the Customizer. + if ( 'undefined' !== typeof wp && wp.customize && wp.customize.selectiveRefresh ) { + /** + * Handle rendering of selective refresh partials. + * + * Make sure that when a partial is rendered, the Jetpack post-load event + * will be triggered so that any dynamic elements will be re-constructed, + * such as ME.js elements, Photon replacements, social sharing, and more. + * Note that this is applying here not strictly to posts being loaded. + * If a widget contains a ME.js element and it is previewed via selective + * refresh, the post-load would get triggered allowing any dynamic elements + * therein to also be re-constructed. + * + * @param {wp.customize.selectiveRefresh.Placement} placement + */ + wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function ( placement ) { + var content; + if ( 'string' === typeof placement.addedContent ) { + content = placement.addedContent; + } else if ( placement.container ) { + content = $( placement.container ).html(); + } + + if ( content ) { + $( document.body ).trigger( 'post-load', { html: content } ); + } + } ); + + /* + * Add partials for posts added via infinite scroll. + * + * This is unnecessary when MutationObserver is supported by the browser + * since then this will be handled by Selective Refresh in core. + */ + if ( 'undefined' === typeof MutationObserver ) { + $( document.body ).on( 'post-load', function ( e, response ) { + var rootElement = null; + if ( response.html && -1 !== response.html.indexOf( 'data-customize-partial' ) ) { + if ( window.infiniteScroll.settings.id ) { + rootElement = $( '#' + window.infiniteScroll.settings.id ); + } + wp.customize.selectiveRefresh.addPartials( rootElement ); + } + } ); + } + } + } ); +} )( jQuery ); // Close closure diff --git a/plugins/jetpack/modules/infinite-scroll/infinity.css b/plugins/jetpack/modules/infinite-scroll/infinity.css index 4c84e294..8b4dec2e 100644 --- a/plugins/jetpack/modules/infinite-scroll/infinity.css +++ b/plugins/jetpack/modules/infinite-scroll/infinity.css @@ -1,24 +1,120 @@ /* =Infinity Styles -------------------------------------------------------------- */ -.infinite-wrap { -} .infinite-loader { color: #000; display: block; height: 28px; - text-indent: -9999px; + text-align: center; } #infinite-handle span { background: #333; border-radius: 1px; - color: #eee; + color: #f0f0f1; cursor: pointer; font-size: 13px; padding: 6px 16px; } /** + * CSS Spinner Styles + */ +@keyframes spinner-inner { + 0% { opacity: 1 } + 100% { opacity: 0 } +} +.infinite-loader .spinner-inner div { + left: 47px; + top: 24px; + position: absolute; + animation: spinner-inner linear 1s infinite; + background: #000000; + outline: 1px solid white; + width: 6px; + height: 12px; + border-radius: 3px / 6px; + transform-origin: 3px 26px; +} +.infinite-loader .spinner-inner div:nth-child(1) { + transform: rotate(0deg); + animation-delay: -0.9166666666666666s; + background: #000000; +} +.infinite-loader .spinner-inner div:nth-child(2) { + transform: rotate(30deg); + animation-delay: -0.8333333333333334s; + background: #000000; +} +.infinite-loader .spinner-inner div:nth-child(3) { + transform: rotate(60deg); + animation-delay: -0.75s; + background: #000000; +} +.infinite-loader .spinner-inner div:nth-child(4) { + transform: rotate(90deg); + animation-delay: -0.6666666666666666s; + background: #000000; +} +.infinite-loader .spinner-inner div:nth-child(5) { + transform: rotate(120deg); + animation-delay: -0.5833333333333334s; + background: #000000; +} +.infinite-loader .spinner-inner div:nth-child(6) { + transform: rotate(150deg); + animation-delay: -0.5s; + background: #000000; +} +.infinite-loader .spinner-inner div:nth-child(7) { + transform: rotate(180deg); + animation-delay: -0.4166666666666667s; + background: #000000; +} +.infinite-loader .spinner-inner div:nth-child(8) { + transform: rotate(210deg); + animation-delay: -0.3333333333333333s; + background: #000000; +} +.infinite-loader .spinner-inner div:nth-child(9) { + transform: rotate(240deg); + animation-delay: -0.25s; + background: #000000; +} +.infinite-loader .spinner-inner div:nth-child(10) { + transform: rotate(270deg); + animation-delay: -0.16666666666666666s; + background: #000000; +} +.infinite-loader .spinner-inner div:nth-child(11) { + transform: rotate(300deg); + animation-delay: -0.08333333333333333s; + background: #000000; +} +.infinite-loader .spinner-inner div:nth-child(12) { + transform: rotate(330deg); + animation-delay: 0s; + background: #000000; +} +.infinite-loader .spinner { + width: 28px; + height: 28px; + display: inline-block; + overflow: hidden; + background: none; +} +.infinite-loader .spinner-inner { + width: 100%; + height: 100%; + position: relative; + transform: translateZ(0) scale(0.28); + backface-visibility: hidden; + transform-origin: 0 0; /* see note above */ +} +.infinite-loader .spinner-inner div { + box-sizing: content-box; +} + +/** * Using a highly-specific rule to make sure that all button styles * will be reset */ @@ -122,7 +218,7 @@ text-align: right; } #infinite-footer .blog-credits a { - color: #666; + color: #646970; } /** @@ -161,4 +257,22 @@ #infinite-footer { position: static; } -}
\ No newline at end of file +} + +/** + * Hide infinite aria feedback visually + */ +#infinite-aria { + position: absolute; + overflow: hidden; + clip: rect(0 0 0 0); + height: 1px; width: 1px; + margin: -1px; padding: 0; border: 0; +} + +/** + * Hide focus on infinite wrappers + */ +.infinite-wrap:focus { + outline: 0 !important; +} diff --git a/plugins/jetpack/modules/infinite-scroll/infinity.js b/plugins/jetpack/modules/infinite-scroll/infinity.js index 24dd413e..01d83d07 100644 --- a/plugins/jetpack/modules/infinite-scroll/infinity.js +++ b/plugins/jetpack/modules/infinite-scroll/infinity.js @@ -1,8 +1,8 @@ -/* globals infiniteScroll, _wpmejsSettings, ga, _gaq, WPCOM_sharing_counts */ -( function( $ ) { - // Open closure - // Local vars - var Scroller, ajaxurl, stats, type, text, totop; +/* globals infiniteScroll, _wpmejsSettings, ga, _gaq, WPCOM_sharing_counts, MediaElementPlayer */ +( function () { + // Open closure. + // Local vars. + var Scroller, ajaxurl, stats, type, text, totop, loading_text; // IE requires special handling var isIE = -1 != navigator.userAgent.search( 'MSIE' ); @@ -22,14 +22,14 @@ /** * Loads new posts when users scroll near the bottom of the page. */ - Scroller = function( settings ) { + Scroller = function ( settings ) { var self = this; // Initialize our variables this.id = settings.id; - this.body = $( document.body ); - this.window = $( window ); - this.element = $( '#' + settings.id ); + this.body = document.body; + this.window = window; + this.element = document.getElementById( settings.id ); this.wrapperClass = settings.wrapper_class; this.ready = true; this.disabled = false; @@ -38,19 +38,24 @@ this.currentday = settings.currentday; this.order = settings.order; this.throttle = false; - this.handle = - '<div id="infinite-handle"><span><button>' + - text.replace( '\\', '' ) + - '</button></span></div>'; this.click_handle = settings.click_handle; this.google_analytics = settings.google_analytics; this.history = settings.history; this.origURL = window.location.href; - this.pageCache = {}; + + // Handle element + this.handle = document.createElement( 'div' ); + this.handle.setAttribute( 'id', 'infinite-handle' ); + this.handle.innerHTML = '<span><button>' + text.replace( '\\', '' ) + '</button></span>'; // Footer settings - this.footer = $( '#infinite-footer' ); - this.footer.wrap = settings.footer; + this.footer = { + el: document.getElementById( 'infinite-footer' ), + wrap: settings.footer, + }; + + // Bind methods used as callbacks + this.checkViewportOnLoadBound = self.checkViewportOnLoad.bind( this ); // Core's native MediaElement.js implementation needs special handling this.wpMediaelement = null; @@ -63,17 +68,17 @@ // Throttle to check for such case every 300ms // On event the case becomes a fact - this.window.bind( 'scroll.infinity', function() { - this.throttle = true; + this.window.addEventListener( 'scroll', function () { + self.throttle = true; } ); // Go back top method self.gotop(); - setInterval( function() { - if ( this.throttle ) { + setInterval( function () { + if ( self.throttle ) { // Once the case is the case, the action occurs and the fact is no more - this.throttle = false; + self.throttle = false; // Reveal or hide footer self.thefooter(); // Fire the refresh @@ -84,16 +89,16 @@ // Ensure that enough posts are loaded to fill the initial viewport, to compensate for short posts and large displays. self.ensureFilledViewport(); - this.body.bind( 'post-load', { self: self }, self.checkViewportOnLoad ); + this.body.addEventListener( 'is.post-load', self.checkViewportOnLoadBound ); } else if ( type == 'click' ) { if ( this.click_handle ) { - this.element.append( this.handle ); + this.element.appendChild( this.handle ); } - this.body.delegate( '#infinite-handle', 'click.infinity', function() { + this.handle.addEventListener( 'click', function () { // Handle the handle if ( self.click_handle ) { - $( '#infinite-handle' ).remove(); + self.handle.parentNode.removeChild( self.handle ); } // Fire the refresh @@ -102,42 +107,71 @@ } // Initialize any Core audio or video players loaded via IS - this.body.bind( 'post-load', { self: self }, self.initializeMejs ); + this.body.addEventListener( 'is.post-load', self.initializeMejs ); }; /** - * Check whether we should fetch any additional posts. + * Normalize the access to the document scrollTop value. + */ + Scroller.prototype.getScrollTop = function () { + return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; + }; + + /** + * Polyfill jQuery.extend. */ - Scroller.prototype.check = function() { - var container = this.element.offset(); + Scroller.prototype.extend = function ( out ) { + out = out || {}; + + for ( var i = 1; i < arguments.length; i++ ) { + if ( ! arguments[ i ] ) { + continue; + } - // If the container can't be found, stop otherwise errors result - if ( 'object' !== typeof container ) { - return false; + for ( var key in arguments[ i ] ) { + if ( arguments[ i ].hasOwnProperty( key ) ) { + out[ key ] = arguments[ i ][ key ]; + } + } } + return out; + }; - var bottom = this.window.scrollTop() + this.window.height(), - threshold = container.top + this.element.outerHeight( false ) - this.window.height() * 2; + /** + * Check whether we should fetch any additional posts. + */ + Scroller.prototype.check = function () { + var wrapperMeasurements = this.measure( this.element, [ this.wrapperClass ] ); - return bottom > threshold; + // Fetch more posts when we're less than 2 screens away from the bottom. + return wrapperMeasurements.bottom < 2 * this.window.innerHeight; }; /** * Renders the results from a successful response. */ - Scroller.prototype.render = function( response ) { - this.body.addClass( 'infinity-success' ); + Scroller.prototype.render = function ( response ) { + var childrenToAppend = Array.prototype.slice.call( response.fragment.childNodes ); + this.body.classList.add( 'infinity-success' ); + + // Render the retrieved nodes. + while ( childrenToAppend.length > 0 ) { + var currentNode = childrenToAppend.shift(); + this.element.appendChild( currentNode ); + } + + this.trigger( this.body, 'is.post-load', { + jqueryEventName: 'post-load', + data: response, + } ); - // Check if we can wrap the html - this.element.append( response.html ); - this.body.trigger( 'post-load', response ); this.ready = true; }; /** * Returns the object used to query for new posts. */ - Scroller.prototype.query = function() { + Scroller.prototype.query = function () { return { page: this.page + this.offset, // Load the next page. currentday: this.currentday, @@ -150,56 +184,127 @@ }; }; + Scroller.prototype.animate = function ( cb, duration ) { + var start = performance.now(); + + requestAnimationFrame( function animate( time ) { + var timeFraction = Math.min( 1, ( time - start ) / duration ); + cb( timeFraction ); + + if ( timeFraction < 1 ) { + requestAnimationFrame( animate ); + } + } ); + }; + /** * Scroll back to top. */ - Scroller.prototype.gotop = function() { - var blog = $( '#infinity-blog-title' ); + Scroller.prototype.gotop = function () { + var blog = document.getElementById( 'infinity-blog-title' ); + var self = this; - blog.attr( 'title', totop ); + if ( ! blog ) { + return; + } - // Scroll to top on blog title - blog.bind( 'click', function( e ) { - $( 'html, body' ).animate( { scrollTop: 0 }, 'fast' ); + blog.setAttribute( 'title', totop ); + blog.addEventListener( 'click', function ( e ) { + var sourceScroll = self.window.pageYOffset; e.preventDefault(); + + self.animate( function ( progress ) { + var currentScroll = sourceScroll - sourceScroll * progress; + document.documentElement.scrollTop = document.body.scrollTop = currentScroll; + }, 200 ); } ); }; /** * The infinite footer. */ - Scroller.prototype.thefooter = function() { + Scroller.prototype.thefooter = function () { var self = this, - width; + pageWrapper, + footerContainer, + width, + sourceBottom, + targetBottom, + footerEnabled = this.footer && this.footer.el; + + if ( ! footerEnabled ) { + return; + } // Check if we have an id for the page wrapper - if ( $.type( this.footer.wrap ) === 'string' ) { - width = $( 'body #' + this.footer.wrap ).outerWidth( false ); + if ( 'string' === typeof this.footer.wrap ) { + try { + pageWrapper = document.getElementById( this.footer.wrap ); + width = pageWrapper.getBoundingClientRect(); + width = width.width; + } catch ( err ) { + width = 0; + } // Make the footer match the width of the page if ( width > 479 ) { - this.footer.find( '.container' ).css( 'width', width ); + footerContainer = this.footer.el.querySelector( '.container' ); + if ( footerContainer ) { + footerContainer.style.width = width + 'px'; + } } } // Reveal footer - if ( this.window.scrollTop() >= 350 ) { - self.footer.animate( { bottom: 0 }, 'fast' ); - } else if ( this.window.scrollTop() < 350 ) { - self.footer.animate( { bottom: '-50px' }, 'fast' ); + sourceBottom = parseInt( self.footer.el.style.bottom || -50, 10 ); + targetBottom = this.window.pageYOffset >= 350 ? 0 : -50; + + if ( sourceBottom !== targetBottom ) { + self.animate( function ( progress ) { + var currentBottom = sourceBottom + ( targetBottom - sourceBottom ) * progress; + self.footer.el.style.bottom = currentBottom + 'px'; + + if ( 1 === progress ) { + sourceBottom = targetBottom; + } + }, 200 ); + } + }; + + /** + * Recursively convert a JS object into URL encoded data. + */ + Scroller.prototype.urlEncodeJSON = function ( obj, prefix ) { + var params = [], + encodedKey, + newPrefix; + + for ( var key in obj ) { + encodedKey = encodeURIComponent( key ); + newPrefix = prefix ? prefix + '[' + encodedKey + ']' : encodedKey; + + if ( 'object' === typeof obj[ key ] ) { + if ( ! Array.isArray( obj[ key ] ) || obj[ key ].length > 0 ) { + params.push( this.urlEncodeJSON( obj[ key ], newPrefix ) ); + } else { + // Explicitly expose empty arrays with no values + params.push( newPrefix + '[]=' ); + } + } else { + params.push( newPrefix + '=' + encodeURIComponent( obj[ key ] ) ); + } } + return params.join( '&' ); }; /** * Controls the flow of the refresh. Don't mess. */ - Scroller.prototype.refresh = function() { + Scroller.prototype.refresh = function () { var self = this, query, - jqxhr, - load, + xhr, loader, - color, customized; // If we're disabled, ready, or don't pass the check, bail. @@ -213,19 +318,19 @@ // Create a loader element to show it's working. if ( this.click_handle ) { - loader = '<span class="infinite-loader"></span>'; - this.element.append( loader ); - - loader = this.element.find( '.infinite-loader' ); - color = loader.css( 'color' ); - - try { - loader.spin( 'medium-left', color ); - } catch ( error ) {} + if ( ! loader ) { + document.getElementById( 'infinite-aria' ).textContent = loading_text; + loader = document.createElement( 'div' ); + loader.classList.add( 'infinite-loader' ); + loader.setAttribute( 'role', 'progress' ); + loader.innerHTML = + '<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>'; + } + this.element.appendChild( loader ); } // Generate our query vars. - query = $.extend( + query = self.extend( { action: 'infinite_scroll', }, @@ -237,7 +342,7 @@ customized = {}; query.wp_customize = 'on'; query.theme = wp.customize.settings.theme.stylesheet; - wp.customize.each( function( setting ) { + wp.customize.each( function ( setting ) { if ( setting._dirty ) { customized[ setting.id ] = setting(); } @@ -247,179 +352,227 @@ } // Fire the ajax request. - jqxhr = $.post( infiniteScroll.settings.ajaxurl, query ); + xhr = new XMLHttpRequest(); + xhr.open( 'POST', infiniteScroll.settings.ajaxurl, true ); + xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' ); + xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' ); + xhr.send( self.urlEncodeJSON( query ) ); // Allow refreshes to occur again if an error is triggered. - jqxhr.fail( function() { + xhr.onerror = function () { if ( self.click_handle ) { - loader.hide(); + loader.parentNode.removeChild( loader ); } self.ready = true; - } ); + }; // Success handler - jqxhr.done( function( response ) { + xhr.onload = function () { + var response = JSON.parse( xhr.responseText ), + httpCheck = xhr.status >= 200 && xhr.status < 300, + responseCheck = 'undefined' !== typeof response.html; + + if ( ! response || ! httpCheck || ! responseCheck ) { + if ( self.click_handle ) { + loader.parentNode.removeChild( loader ); + } + return; + } + // On success, let's hide the loader circle. if ( self.click_handle ) { - loader.hide(); + loader.parentNode.removeChild( loader ); } - // Check for and parse our response. - if ( ! response || ! response.type ) { - return; - } + // If additional scripts are required by the incoming set of posts, parse them + if ( response.scripts && Array.isArray( response.scripts ) ) { + response.scripts.forEach( function ( item ) { + var elementToAppendTo = item.footer ? 'body' : 'head'; - // If we've succeeded... - if ( response.type == 'success' ) { - // If additional scripts are required by the incoming set of posts, parse them - if ( response.scripts ) { - $( response.scripts ).each( function() { - var elementToAppendTo = this.footer ? 'body' : 'head'; - - // Add script handle to list of those already parsed - window.infiniteScroll.settings.scripts.push( this.handle ); - - // Output extra data, if present - if ( this.extra_data ) { - var data = document.createElement( 'script' ), - dataContent = document.createTextNode( - '//<![CDATA[ \n' + this.extra_data + '\n//]]>' - ); - - data.type = 'text/javascript'; - data.appendChild( dataContent ); - - document.getElementsByTagName( elementToAppendTo )[ 0 ].appendChild( data ); - } - - // Build script tag and append to DOM in requested location - var script = document.createElement( 'script' ); - script.type = 'text/javascript'; - script.src = this.src; - script.id = this.handle; - - // If MediaElement.js is loaded in by this set of posts, don't initialize the players a second time as it breaks them all - if ( 'wp-mediaelement' === this.handle ) { - self.body.unbind( 'post-load', self.initializeMejs ); - } - - if ( 'wp-mediaelement' === this.handle && 'undefined' === typeof mejs ) { - self.wpMediaelement = {}; - self.wpMediaelement.tag = script; - self.wpMediaelement.element = elementToAppendTo; - setTimeout( self.maybeLoadMejs.bind( self ), 250 ); - } else { - document.getElementsByTagName( elementToAppendTo )[ 0 ].appendChild( script ); - } - } ); - } + // Add script handle to list of those already parsed + window.infiniteScroll.settings.scripts.push( item.handle ); - // If additional stylesheets are required by the incoming set of posts, parse them - if ( response.styles ) { - $( response.styles ).each( function() { - // Add stylesheet handle to list of those already parsed - window.infiniteScroll.settings.styles.push( this.handle ); - - // Build link tag - var style = document.createElement( 'link' ); - style.rel = 'stylesheet'; - style.href = this.src; - style.id = this.handle + '-css'; - - // Destroy link tag if a conditional statement is present and either the browser isn't IE, or the conditional doesn't evaluate true - if ( - this.conditional && - ( ! isIE || ! eval( this.conditional.replace( /%ver/g, IEVersion ) ) ) - ) { - style = false; - } - - // Append link tag if necessary - if ( style ) { - document.getElementsByTagName( 'head' )[ 0 ].appendChild( style ); - } - } ); - } + // Output extra data, if present + if ( item.extra_data ) { + self.appendInlineScript( item.extra_data, elementToAppendTo ); + } - // stash the response in the page cache - self.pageCache[ self.page + self.offset ] = response; + if ( item.before_handle ) { + self.appendInlineScript( item.before_handle, elementToAppendTo ); + } - // Increment the page number - self.page++; + // Build script tag and append to DOM in requested location + var script = document.createElement( 'script' ); + script.type = 'text/javascript'; + script.src = item.src; + script.id = item.handle; - // Record pageview in WP Stats, if available. - if ( stats ) { - new Image().src = - document.location.protocol + - '//pixel.wp.com/g.gif?' + - stats + - '&post=0&baba=' + - Math.random(); - } + // Dynamically loaded scripts are async by default. + // We don't want that, it breaks stuff, e.g. wp-mediaelement init. + script.async = false; - // Add new posts to the postflair object - if ( 'object' === typeof response.postflair && 'object' === typeof WPCOM_sharing_counts ) { - WPCOM_sharing_counts = $.extend( WPCOM_sharing_counts, response.postflair ); // eslint-disable-line no-global-assign - } + if ( item.after_handle ) { + script.onload = function () { + self.appendInlineScript( item.after_handle, elementToAppendTo ); + }; + } - // Render the results - self.render.apply( self, arguments ); - - // If 'click' type and there are still posts to fetch, add back the handle - if ( type == 'click' ) { - if ( response.lastbatch ) { - if ( self.click_handle ) { - $( '#infinite-handle' ).remove(); - // Update body classes - self.body.addClass( 'infinity-end' ).removeClass( 'infinity-success' ); - } else { - self.body.trigger( 'infinite-scroll-posts-end' ); - } + // If MediaElement.js is loaded in by item set of posts, don't initialize the players a second time as it breaks them all + if ( 'wp-mediaelement' === item.handle ) { + self.body.removeEventListener( 'is.post-load', self.initializeMejs ); + } + + if ( 'wp-mediaelement' === item.handle && 'undefined' === typeof mejs ) { + self.wpMediaelement = {}; + self.wpMediaelement.tag = script; + self.wpMediaelement.element = elementToAppendTo; + setTimeout( self.maybeLoadMejs.bind( self ), 250 ); } else { - if ( self.click_handle ) { - self.element.append( self.handle ); - } else { - self.body.trigger( 'infinite-scroll-posts-more' ); - } + document.getElementsByTagName( elementToAppendTo )[ 0 ].appendChild( script ); } - } else if ( response.lastbatch ) { - self.disabled = true; - self.body.addClass( 'infinity-end' ).removeClass( 'infinity-success' ); - } + } ); + } + + // If additional stylesheets are required by the incoming set of posts, parse them + if ( response.styles && Array.isArray( response.styles ) ) { + response.styles.forEach( function ( item ) { + // Add stylesheet handle to list of those already parsed + window.infiniteScroll.settings.styles.push( item.handle ); + + // Build link tag + var style = document.createElement( 'link' ); + style.rel = 'stylesheet'; + style.href = item.src; + style.id = item.handle + '-css'; + + // Destroy link tag if a conditional statement is present and either the browser isn't IE, or the conditional doesn't evaluate true + if ( + item.conditional && + ( ! isIE || ! eval( item.conditional.replace( /%ver/g, IEVersion ) ) ) + ) { + style = false; + } + + // Append link tag if necessary + if ( style ) { + document.getElementsByTagName( 'head' )[ 0 ].appendChild( style ); + } + } ); + } + + // Convert the response.html to a fragment element. + // Using a div instead of DocumentFragment, because the latter doesn't support innerHTML. + response.fragment = document.createElement( 'div' ); + response.fragment.innerHTML = response.html; + + // Increment the page number + self.page++; + + // Record pageview in WP Stats, if available. + if ( stats ) { + new Image().src = + document.location.protocol + + '//pixel.wp.com/g.gif?' + + stats + + '&post=0&baba=' + + Math.random(); + } + + // Add new posts to the postflair object + if ( 'object' === typeof response.postflair && 'object' === typeof WPCOM_sharing_counts ) { + WPCOM_sharing_counts = self.extend( WPCOM_sharing_counts, response.postflair ); // eslint-disable-line no-global-assign + } - // Update currentday to the latest value returned from the server - if ( response.currentday ) { - self.currentday = response.currentday; + // Render the results + self.render.call( self, response ); + + // If 'click' type and there are still posts to fetch, add back the handle + if ( type == 'click' ) { + // add focus to new posts, only in button mode as we know where page focus currently is and only if we have a wrapper + if ( infiniteScroll.settings.wrapper ) { + document + .querySelector( + '#infinite-view-' + ( self.page + self.offset - 1 ) + ' a:first-of-type' + ) + .focus( { + preventScroll: true, + } ); } - // Fire Google Analytics pageview - if ( self.google_analytics ) { - var ga_url = self.history.path.replace( /%d/, self.page ); - if ( 'object' === typeof _gaq ) { - _gaq.push( [ '_trackPageview', ga_url ] ); + if ( response.lastbatch ) { + if ( self.click_handle ) { + // Update body classes + self.body.classList.add( 'infinity-end' ); + self.body.classList.remove( 'infinity-success' ); + } else { + self.trigger( this.body, 'infinite-scroll-posts-end' ); } - if ( 'function' === typeof ga ) { - ga( 'send', 'pageview', ga_url ); + } else { + if ( self.click_handle ) { + self.element.appendChild( self.handle ); + } else { + self.trigger( this.body, 'infinite-scroll-posts-more' ); } } + } else if ( response.lastbatch ) { + self.disabled = true; + + self.body.classList.add( 'infinity-end' ); + self.body.classList.remove( 'infinity-success' ); + } + + // Update currentday to the latest value returned from the server + if ( response.currentday ) { + self.currentday = response.currentday; + } + + // Fire Google Analytics pageview + if ( self.google_analytics ) { + var ga_url = self.history.path.replace( /%d/, self.page ); + if ( 'object' === typeof _gaq ) { + _gaq.push( [ '_trackPageview', ga_url ] ); + } + if ( 'function' === typeof ga ) { + ga( 'send', 'pageview', ga_url ); + } } - } ); + }; - return jqxhr; + return xhr; + }; + + /** + * Given JavaScript blob and the name of a parent tag, this helper function will + * generate a script tag, insert the JavaScript blob, and append it to the parent. + * + * It's important to note that the JavaScript blob will be evaluated immediately. If + * you need a parent script to load first, use that script element's onload handler. + * + * @param {string} script The blob of JavaScript to run. + * @param {string} parentTag The tag name of the parent element. + */ + Scroller.prototype.appendInlineScript = function ( script, parentTag ) { + var element = document.createElement( 'script' ), + scriptContent = document.createTextNode( '//<![CDATA[ \n' + script + '\n//]]>' ); + + element.type = 'text/javascript'; + element.appendChild( scriptContent ); + + document.getElementsByTagName( parentTag )[ 0 ].appendChild( element ); }; /** * Core's native media player uses MediaElement.js * 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. */ - Scroller.prototype.maybeLoadMejs = function() { + Scroller.prototype.maybeLoadMejs = function () { if ( null === this.wpMediaelement ) { return; } if ( 'undefined' === typeof mejs ) { - setTimeout( this.maybeLoadMejs, 250 ); + setTimeout( this.maybeLoadMejs.bind( this ), 250 ); } else { document .getElementsByTagName( this.wpMediaelement.element )[ 0 ] @@ -427,19 +580,20 @@ this.wpMediaelement = null; // Ensure any subsequent IS loads initialize the players - this.body.bind( 'post-load', { self: this }, this.initializeMejs ); + this.body.addEventListener( 'is.post-load', this.initializeMejs ); } }; /** * Initialize the MediaElement.js player for any posts not previously initialized */ - Scroller.prototype.initializeMejs = function( ev, response ) { + Scroller.prototype.initializeMejs = function ( e ) { // Are there media players in the incoming set of posts? if ( - ! response.html || - ( -1 === response.html.indexOf( 'wp-audio-shortcode' ) && - -1 === response.html.indexOf( 'wp-video-shortcode' ) ) + ! e.detail || + ! e.detail.html || + ( -1 === e.detail.html.indexOf( 'wp-audio-shortcode' ) && + -1 === e.detail.html.indexOf( 'wp-video-shortcode' ) ) ) { return; } @@ -451,73 +605,118 @@ // Adapted from wp-includes/js/mediaelement/wp-mediaelement.js // Modified to not initialize already-initialized players, as Mejs doesn't handle that well - $( function() { - var settings = {}; + var settings = {}; + var audioVideoElements; + + if ( typeof _wpmejsSettings !== 'undefined' ) { + settings.pluginPath = _wpmejsSettings.pluginPath; + } - if ( typeof _wpmejsSettings !== 'undefined' ) { - settings.pluginPath = _wpmejsSettings.pluginPath; + settings.success = function ( mejs ) { + var autoplay = mejs.attributes.autoplay && 'false' !== mejs.attributes.autoplay; + if ( 'flash' === mejs.pluginType && autoplay ) { + mejs.addEventListener( + 'canplay', + function () { + mejs.play(); + }, + false + ); } + }; - settings.success = function( mejs ) { - var autoplay = mejs.attributes.autoplay && 'false' !== mejs.attributes.autoplay; - if ( 'flash' === mejs.pluginType && autoplay ) { - mejs.addEventListener( - 'canplay', - function() { - mejs.play(); - }, - false - ); - } - }; + audioVideoElements = document.querySelectorAll( '.wp-audio-shortcode, .wp-video-shortcode' ); + audioVideoElements = Array.prototype.slice.call( audioVideoElements ); - $( '.wp-audio-shortcode, .wp-video-shortcode' ) - .not( '.mejs-container' ) - .mediaelementplayer( settings ); + // Only process already unprocessed shortcodes. + audioVideoElements = audioVideoElements.filter( function ( el ) { + while ( el.parentNode ) { + if ( el.classList.contains( 'mejs-container' ) ) { + return false; + } + el = el.parentNode; + } + return true; } ); + + for ( var i = 0; i < audioVideoElements.length; i++ ) { + new MediaElementPlayer( audioVideoElements[ i ], settings ); + } }; /** - * Trigger IS to load additional posts if the initial posts don't fill the window. - * On large displays, or when posts are very short, the viewport may not be filled with posts, so we overcome this by loading additional posts when IS initializes. + * Get element measurements relative to the viewport. + * + * @returns {object} */ - Scroller.prototype.ensureFilledViewport = function() { - var self = this, - windowHeight = self.window.height(), - postsHeight = self.element.height(), - aveSetHeight = 0, - wrapperQty = 0; - - // Account for situations where postsHeight is 0 because child list elements are floated - if ( postsHeight === 0 ) { - $( self.element.selector + ' > li' ).each( function() { - postsHeight += $( this ).height(); - } ); - - if ( postsHeight === 0 ) { - self.body.unbind( 'post-load', self.checkViewportOnLoad ); - return; + Scroller.prototype.measure = function ( element, expandClasses ) { + expandClasses = expandClasses || []; + + var childrenToTest = Array.prototype.slice.call( element.children ); + var currentChild, + minTop = Number.MAX_VALUE, + maxBottom = 0, + currentChildRect, + i; + + while ( childrenToTest.length > 0 ) { + currentChild = childrenToTest.shift(); + + for ( i = 0; i < expandClasses.length; i++ ) { + // Expand (= measure) child elements of nodes with class names from expandClasses. + if ( currentChild.classList.contains( expandClasses[ i ] ) ) { + childrenToTest = childrenToTest.concat( + Array.prototype.slice.call( currentChild.children ) + ); + break; + } } + currentChildRect = currentChild.getBoundingClientRect(); + + minTop = Math.min( minTop, currentChildRect.top ); + maxBottom = Math.max( maxBottom, currentChildRect.bottom ); } - // Calculate average height of a set of posts to prevent more posts than needed from being loaded. - $( '.' + self.wrapperClass ).each( function() { - aveSetHeight += $( this ).height(); - wrapperQty++; - } ); + var viewportMiddle = Math.round( window.innerHeight / 2 ); - if ( wrapperQty > 0 ) { - aveSetHeight = aveSetHeight / wrapperQty; - } else { - aveSetHeight = 0; - } + // isActive = does the middle of the viewport cross the element? + var isActive = minTop <= viewportMiddle && maxBottom >= viewportMiddle; + + /** + * Factor = percentage of viewport above the middle line occupied by the element. + * + * Negative factors are assigned for elements below the middle line. That's on purpose + * to only allow "page 2" to change the URL once it's in the middle of the viewport. + */ + var factor = ( Math.min( maxBottom, viewportMiddle ) - Math.max( minTop, 0 ) ) / viewportMiddle; + + return { + top: minTop, + bottom: maxBottom, + height: maxBottom - minTop, + factor: factor, + isActive: isActive, + }; + }; + + /** + * Trigger IS to load additional posts if the initial posts don't fill the window. + * + * On large displays, or when posts are very short, the viewport may not be filled with posts, + * so we overcome this by loading additional posts when IS initializes. + */ + Scroller.prototype.ensureFilledViewport = function () { + var self = this, + windowHeight = self.window.innerHeight, + wrapperMeasurements = self.measure( self.element, [ self.wrapperClass ] ); + + // Only load more posts once. This prevents infinite loops when there are no more posts. + self.body.removeEventListener( 'is.post-load', self.checkViewportOnLoadBound ); - // Load more posts if space permits, otherwise stop checking for a full viewport - if ( postsHeight < windowHeight && postsHeight + aveSetHeight < windowHeight ) { + // Load more posts if space permits, otherwise stop checking for a full viewport. + if ( wrapperMeasurements.bottom < windowHeight ) { self.ready = true; self.refresh(); - } else { - self.body.unbind( 'post-load', self.checkViewportOnLoad ); } }; @@ -525,8 +724,8 @@ * Event handler for ensureFilledViewport(), tied to the post-load trigger. * 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. */ - Scroller.prototype.checkViewportOnLoad = function( ev ) { - ev.data.self.ensureFilledViewport(); + Scroller.prototype.checkViewportOnLoad = function () { + this.ensureFilledViewport(); }; function fullscreenState() { @@ -543,15 +742,12 @@ /** * Identify archive page that corresponds to majority of posts shown in the current browser window. */ - Scroller.prototype.determineURL = function() { + Scroller.prototype.determineURL = function () { var self = this, - windowTop = $( window ).scrollTop(), - windowBottom = windowTop + $( window ).height(), - windowSize = windowBottom - windowTop, - setsInView = [], - setsHidden = [], - pageNum = false, - currentFullScreenState = fullscreenState(); + pageNum = -1, + currentFullScreenState = fullscreenState(), + wrapperEls, + maxFactor = 0; // xor - check if the state has changed if ( previousFullScrenState ^ currentFullScreenState ) { @@ -564,123 +760,35 @@ return; } previousFullScrenState = currentFullScreenState; + wrapperEls = document.querySelectorAll( '.' + self.wrapperClass ); - // Find out which sets are in view - $( '.' + self.wrapperClass ).each( function() { - var id = $( this ).attr( 'id' ), - setTop = $( this ).offset().top, - setHeight = $( this ).outerHeight( false ), - setBottom = 0, - setPageNum = $( this ).data( 'page-num' ); - - // Account for containers that have no height because their children are floated elements. - if ( 0 === setHeight ) { - $( '> *', this ).each( function() { - setHeight += $( this ).outerHeight( false ); - } ); - } + for ( var i = 0; i < wrapperEls.length; i++ ) { + var setMeasurements = self.measure( wrapperEls[ i ] ); - // Determine position of bottom of set by adding its height to the scroll position of its top. - setBottom = setTop + setHeight; - - // Populate setsInView object. While this logic could all be combined into a single conditional statement, this is easier to understand. - if ( setTop < windowTop && setBottom > windowBottom ) { - // top of set is above window, bottom is below - setsInView.push( { id: id, top: setTop, bottom: setBottom, pageNum: setPageNum } ); - } else if ( setTop > windowTop && setTop < windowBottom ) { - // top of set is between top (gt) and bottom (lt) - setsInView.push( { id: id, top: setTop, bottom: setBottom, pageNum: setPageNum } ); - } else if ( setBottom > windowTop && setBottom < windowBottom ) { - // bottom of set is between top (gt) and bottom (lt) - setsInView.push( { id: id, top: setTop, bottom: setBottom, pageNum: setPageNum } ); - } else { - setsHidden.push( { id: id, top: setTop, bottom: setBottom, pageNum: setPageNum } ); + // If it exists, pick a set that is crossed by the middle of the viewport. + if ( setMeasurements.isActive ) { + pageNum = parseInt( wrapperEls[ i ].dataset.pageNum, 10 ); + break; } - } ); - $.each( setsHidden, function() { - var $set = $( '#' + this.id ); - if ( $set.hasClass( 'is--replaced' ) ) { - return; + // If there is such a set, pick the one that occupies the most space + // above the middle of the viewport. + if ( setMeasurements.factor > maxFactor ) { + pageNum = parseInt( wrapperEls[ i ].dataset.pageNum, 10 ); + maxFactor = setMeasurements.factor; } - self.pageCache[ this.pageNum ].html = $set.html(); - - $set - .css( 'min-height', this.bottom - this.top + 'px' ) - .addClass( 'is--replaced' ) - .empty(); - } ); - - $.each( setsInView, function() { - var $set = $( '#' + this.id ); - - if ( $set.hasClass( 'is--replaced' ) ) { - $set.css( 'min-height', '' ).removeClass( 'is--replaced' ); - if ( this.pageNum in self.pageCache ) { - $set.html( self.pageCache[ this.pageNum ].html ); - self.body.trigger( 'post-load', self.pageCache[ this.pageNum ] ); - } - } - } ); - - // Parse number of sets found in view in an attempt to update the URL to match the set that comprises the majority of the window. - if ( 0 == setsInView.length ) { - pageNum = -1; - } else if ( 1 == setsInView.length ) { - var setData = setsInView.pop(); - - // If the first set of IS posts is in the same view as the posts loaded in the template by WordPress, determine how much of the view is comprised of IS-loaded posts - if ( ( windowBottom - setData.top ) / windowSize < 0.5 ) { - pageNum = -1; - } else { - pageNum = setData.pageNum; - } - } else { - var majorityPercentageInView = 0; - - // Identify the IS set that comprises the majority of the current window and set the URL to it. - $.each( setsInView, function( i, setData ) { - var topInView = 0, - bottomInView = 0, - percentOfView = 0; - - // Figure percentage of view the current set represents - if ( setData.top > windowTop && setData.top < windowBottom ) { - topInView = ( windowBottom - setData.top ) / windowSize; - } - - if ( setData.bottom > windowTop && setData.bottom < windowBottom ) { - bottomInView = ( setData.bottom - windowTop ) / windowSize; - } - - // Figure out largest percentage of view for current set - if ( topInView >= bottomInView ) { - percentOfView = topInView; - } else if ( bottomInView >= topInView ) { - percentOfView = bottomInView; - } - - // Does current set's percentage of view supplant the largest previously-found set? - if ( percentOfView > majorityPercentageInView ) { - pageNum = setData.pageNum; - majorityPercentageInView = percentOfView; - } - } ); + // Otherwise default to -1 } - // If a page number could be determined, update the URL - // -1 indicates that the original requested URL should be used. - if ( 'number' === typeof pageNum ) { - self.updateURL( pageNum ); - } + self.updateURL( pageNum ); }; /** * Update address bar to reflect archive page URL for a given page number. * Checks if URL is different to prevent pollution of browser history. */ - Scroller.prototype.updateURL = function( page ) { + Scroller.prototype.updateURL = function ( page ) { // IE only supports pushState() in v10 and above, so don't bother if those conditions aren't met. if ( ! window.history.pushState ) { return; @@ -705,27 +813,67 @@ /** * Pause scrolling. */ - Scroller.prototype.pause = function() { + Scroller.prototype.pause = function () { this.disabled = true; }; /** * Resume scrolling. */ - Scroller.prototype.resume = function() { + Scroller.prototype.resume = function () { this.disabled = false; }; /** + * Emits custom JS events. + * + * @param {Node} el + * @param {string} eventName + * @param {*} data + */ + Scroller.prototype.trigger = function ( el, eventName, opts ) { + opts = opts || {}; + + /** + * Emit the event in a jQuery way for backwards compatibility where necessary. + */ + if ( opts.jqueryEventName && 'undefined' !== typeof jQuery ) { + jQuery( el ).trigger( opts.jqueryEventName, opts.data || null ); + } + + /** + * Emit the event in a standard way. + */ + var e; + try { + e = new CustomEvent( eventName, { + bubbles: true, + cancelable: true, + detail: opts.data || null, + } ); + } catch ( err ) { + e = document.createEvent( 'CustomEvent' ); + e.initCustomEvent( eventName, true, true, opts.data || null ); + } + el.dispatchEvent( e ); + }; + + /** * Ready, set, go! */ - $( document ).ready( function() { + var jetpackInfinityModule = function () { + var bodyClasses = infiniteScroll.settings.body_class.split( ' ' ); + // Check for our variables if ( 'object' !== typeof infiniteScroll ) { return; } - $( document.body ).addClass( infiniteScroll.settings.body_class ); + bodyClasses.forEach( function ( className ) { + if ( className ) { + document.body.classList.add( className ); + } + } ); // Set ajaxurl (for brevity) ajaxurl = infiniteScroll.settings.ajaxurl; @@ -738,6 +886,9 @@ text = infiniteScroll.settings.text; totop = infiniteScroll.settings.totop; + // aria text + loading_text = infiniteScroll.settings.loading_text; + // Initialize the scroller (with the ID of the element from the theme) infiniteScroll.scroller = new Scroller( infiniteScroll.settings ); @@ -746,63 +897,25 @@ */ if ( type == 'click' ) { var timer = null; - $( window ).bind( 'scroll', function() { + window.addEventListener( 'scroll', function () { // run the real scroll handler once every 250 ms. if ( timer ) { return; } - timer = setTimeout( function() { + timer = setTimeout( function () { infiniteScroll.scroller.determineURL(); timer = null; }, 250 ); } ); } + }; - // Integrate with Selective Refresh in the Customizer. - if ( 'undefined' !== typeof wp && wp.customize && wp.customize.selectiveRefresh ) { - /** - * Handle rendering of selective refresh partials. - * - * Make sure that when a partial is rendered, the Jetpack post-load event - * will be triggered so that any dynamic elements will be re-constructed, - * such as ME.js elements, Photon replacements, social sharing, and more. - * Note that this is applying here not strictly to posts being loaded. - * If a widget contains a ME.js element and it is previewed via selective - * refresh, the post-load would get triggered allowing any dynamic elements - * therein to also be re-constructed. - * - * @param {wp.customize.selectiveRefresh.Placement} placement - */ - wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) { - var content; - if ( 'string' === typeof placement.addedContent ) { - content = placement.addedContent; - } else if ( placement.container ) { - content = $( placement.container ).html(); - } - - if ( content ) { - $( document.body ).trigger( 'post-load', { html: content } ); - } - } ); - - /* - * Add partials for posts added via infinite scroll. - * - * This is unnecessary when MutationObserver is supported by the browser - * since then this will be handled by Selective Refresh in core. - */ - if ( 'undefined' === typeof MutationObserver ) { - $( document.body ).on( 'post-load', function( e, response ) { - var rootElement = null; - if ( response.html && -1 !== response.html.indexOf( 'data-customize-partial' ) ) { - if ( infiniteScroll.settings.id ) { - rootElement = $( '#' + infiniteScroll.settings.id ); - } - wp.customize.selectiveRefresh.addPartials( rootElement ); - } - } ); - } - } - } ); -} )( jQuery ); // Close closure + /** + * Ready, set, go! + */ + if ( document.readyState === 'interactive' || document.readyState === 'complete' ) { + jetpackInfinityModule(); + } else { + document.addEventListener( 'DOMContentLoaded', jetpackInfinityModule ); + } +} )(); // Close closure diff --git a/plugins/jetpack/modules/infinite-scroll/infinity.php b/plugins/jetpack/modules/infinite-scroll/infinity.php index 6b8d3775..a26419d7 100644 --- a/plugins/jetpack/modules/infinite-scroll/infinity.php +++ b/plugins/jetpack/modules/infinite-scroll/infinity.php @@ -1,6 +1,7 @@ <?php use Automattic\Jetpack\Assets; +use Automattic\Jetpack\Redirect; /* Plugin Name: The Neverending Home Page. @@ -18,6 +19,10 @@ License URI: https://www.gnu.org/licenses/gpl-2.0.html * styling from each theme; including fixed footer. */ class The_Neverending_Home_Page { + /** + * Maximum allowed number of posts per page in $_REQUEST. + */ + const MAX_ALLOWED_POSTS_PER_PAGE_ΙΝ_REQUEST = 5000; /** * Register actions and filters, plus parse IS settings @@ -26,20 +31,25 @@ class The_Neverending_Home_Page { * @return null */ function __construct() { - add_action( 'pre_get_posts', array( $this, 'posts_per_page_query' ) ); - - add_action( 'admin_init', array( $this, 'settings_api_init' ) ); - add_action( 'template_redirect', array( $this, 'action_template_redirect' ) ); - add_action( 'template_redirect', array( $this, 'ajax_response' ) ); - add_action( 'custom_ajax_infinite_scroll', array( $this, 'query' ) ); - add_filter( 'infinite_scroll_query_args', array( $this, 'inject_query_args' ) ); - add_filter( 'infinite_scroll_allowed_vars', array( $this, 'allowed_query_vars' ) ); - add_action( 'the_post', array( $this, 'preserve_more_tag' ) ); - add_action( 'wp_footer', array( $this, 'footer' ) ); + add_action( 'pre_get_posts', array( $this, 'posts_per_page_query' ) ); + add_action( 'admin_init', array( $this, 'settings_api_init' ) ); + add_action( 'template_redirect', array( $this, 'action_template_redirect' ) ); + add_action( 'customize_preview_init', array( $this, 'init_customizer_assets' ) ); + add_action( 'template_redirect', array( $this, 'ajax_response' ) ); + add_action( 'custom_ajax_infinite_scroll', array( $this, 'query' ) ); + add_filter( 'infinite_scroll_query_args', array( $this, 'inject_query_args' ) ); + add_filter( 'infinite_scroll_allowed_vars', array( $this, 'allowed_query_vars' ) ); + add_action( 'the_post', array( $this, 'preserve_more_tag' ) ); + add_action( 'wp_footer', array( $this, 'footer' ) ); + add_filter( 'infinite_scroll_additional_scripts', array( $this, 'add_mejs_config' ) ); // Plugin compatibility add_filter( 'grunion_contact_form_redirect_url', array( $this, 'filter_grunion_redirect_url' ) ); + // AMP compatibility + // needs to happen after parse_query so that Jetpack_AMP_Support::is_amp_request() is ready. + add_action( 'wp', array( $this, 'amp_load_hooks' ) ); + // Parse IS settings from theme self::get_settings(); } @@ -247,11 +257,23 @@ class The_Neverending_Home_Page { * @return int */ static function posts_per_page() { - $posts_per_page = self::get_settings()->posts_per_page ? self::get_settings()->posts_per_page : self::wp_query()->get( 'posts_per_page' ); + $posts_per_page = self::get_settings()->posts_per_page ? self::get_settings()->posts_per_page : self::wp_query()->get( 'posts_per_page' ); + $posts_per_page_core_option = get_option( 'posts_per_page' ); + + // If Infinite Scroll is set to click, and if the site owner changed posts_per_page, let's use that. + if ( + 'click' === self::get_settings()->type + && ( '10' !== $posts_per_page_core_option ) + ) { + $posts_per_page = $posts_per_page_core_option; + } - // Take JS query into consideration here - if ( true === isset( $_REQUEST['query_args']['posts_per_page'] ) ) { - $posts_per_page = $_REQUEST['query_args']['posts_per_page']; + // Take JS query into consideration here. + $posts_per_page_in_request = isset( $_REQUEST['query_args']['posts_per_page'] ) ? (int) $_REQUEST['query_args']['posts_per_page'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( $posts_per_page_in_request > 0 && + self::MAX_ALLOWED_POSTS_PER_PAGE_ΙΝ_REQUEST >= $posts_per_page_in_request + ) { + $posts_per_page = $posts_per_page_in_request; } /** @@ -389,11 +411,12 @@ class The_Neverending_Home_Page { } function infinite_setting_html_calypso_placeholder() { - $details = get_blog_details(); + $details = get_blog_details(); + $writing_url = Redirect::get_url( 'calypso-settings-writing', array( 'site' => $details->domain ) ); echo '<span>' . sprintf( /* translators: Variables are the enclosing link to the settings page */ - esc_html__( 'This option has moved. You can now manage it %1$shere%2$s.' ), - '<a href="' . esc_url( 'https://wordpress.com/settings/writing/' . $details->domain ) . '">', + esc_html__( 'This option has moved. You can now manage it %1$shere%2$s.', 'jetpack' ), + '<a href="' . esc_url( $writing_url ) . '">', '</a>' ) . '</span>'; } @@ -432,6 +455,11 @@ class The_Neverending_Home_Page { if ( empty( $id ) ) return; + // AMP infinite scroll functionality will start on amp_load_hooks(). + if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) { + return; + } + // Add our scripts. wp_register_script( 'the-neverending-homepage', @@ -439,8 +467,8 @@ class The_Neverending_Home_Page { '_inc/build/infinite-scroll/infinity.min.js', 'modules/infinite-scroll/infinity.js' ), - array( 'jquery' ), - '4.0.0', + array(), + JETPACK__VERSION . '-is5.0.1', // Added for ability to cachebust on WP.com. true ); @@ -458,8 +486,6 @@ class The_Neverending_Home_Page { // Add our default styles. wp_enqueue_style( 'the-neverending-homepage' ); - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_spinner_scripts' ) ); - add_action( 'wp_footer', array( $this, 'action_wp_footer_settings' ), 2 ); add_action( 'wp_footer', array( $this, 'action_wp_footer' ), 21 ); // Core prints footer scripts at priority 20, so we just need to be one later than that @@ -468,10 +494,24 @@ class The_Neverending_Home_Page { } /** - * Enqueue spinner scripts. + * Initialize the Customizer logic separately from the main JS. + * + * @since 8.4.0 */ - function enqueue_spinner_scripts() { - wp_enqueue_script( 'jquery.spin' ); + public function init_customizer_assets() { + // Add our scripts. + wp_register_script( + 'the-neverending-homepage-customizer', + Assets::get_file_url_for_environment( + '_inc/build/infinite-scroll/infinity-customizer.min.js', + 'modules/infinite-scroll/infinity-customizer.js' + ), + array( 'customize-base' ), + JETPACK__VERSION . '-is5.0.0', // Added for ability to cachebust on WP.com. + true + ); + + wp_enqueue_script( 'the-neverending-homepage-customizer' ); } /** @@ -814,7 +854,11 @@ class The_Neverending_Home_Page { // Check if the taxonomy is attached to one post type only and use its plural name. // If not, use "Posts" without confusing the users. - if ( count( $taxonomy->object_type ) < 2 ) { + if ( + is_a( $taxonomy, 'WP_Taxonomy' ) + && is_countable( $taxonomy->object_type ) + && count( $taxonomy->object_type ) < 2 + ) { $post_type = $taxonomy->object_type[0]; } } @@ -866,6 +910,7 @@ class The_Neverending_Home_Page { 'query_before' => current_time( 'mysql' ), 'last_post_date' => self::get_last_post_date(), 'body_class' => self::body_class(), + 'loading_text' => esc_js( __( 'Loading new page', 'jetpack' ) ), ); // Optional order param @@ -999,9 +1044,34 @@ class The_Neverending_Home_Page { $styles = apply_filters( 'infinite_scroll_existing_stylesheets', $styles ); ?><script type="text/javascript"> - jQuery.extend( infiniteScroll.settings.scripts, <?php echo json_encode( $scripts ); ?> ); - jQuery.extend( infiniteScroll.settings.styles, <?php echo json_encode( $styles ); ?> ); - </script><?php + (function() { + var extend = function(out) { + out = out || {}; + + for (var i = 1; i < arguments.length; i++) { + if (!arguments[i]) + continue; + + for (var key in arguments[i]) { + if (arguments[i].hasOwnProperty(key)) + out[key] = arguments[i][key]; + } + } + + return out; + }; + extend( window.infiniteScroll.settings.scripts, <?php echo wp_json_encode( $scripts ); ?> ); + extend( window.infiniteScroll.settings.styles, <?php echo wp_json_encode( $styles ); ?> ); + })(); + </script> + <?php + $aria_live = 'assertive'; + if ( 'scroll' === self::get_settings()->type ) { + $aria_live = 'polite'; + } + ?> + <span id="infinite-aria" aria-live="<?php echo esc_attr( $aria_live ); ?>"></span> + <?php } /** @@ -1024,7 +1094,19 @@ class The_Neverending_Home_Page { global $wp_scripts; // Identify new scripts needed by the latest set of IS posts - $new_scripts = array_diff( $wp_scripts->done, $initial_scripts ); + $new_scripts = array_filter( + $wp_scripts->done, + function ( $script_name ) use ( $initial_scripts ) { + // Jetpack block scripts should always be sent, even if they've been + // sent before. These scripts only run once on when loaded, they don't + // watch for new blocks being added. + if ( 0 === strpos( $script_name, 'jetpack-block-' ) ) { + return true; + } + + return ! in_array( $script_name, $initial_scripts, true ); + } + ); // If new scripts are needed, extract relevant data from $wp_scripts if ( ! empty( $new_scripts ) ) { @@ -1032,14 +1114,20 @@ class The_Neverending_Home_Page { foreach ( $new_scripts as $handle ) { // Abort if somehow the handle doesn't correspond to a registered script - if ( ! isset( $wp_scripts->registered[ $handle ] ) ) + // or if the script doesn't have `src` set. + $script_not_registered = ! isset( $wp_scripts->registered[ $handle ] ); + $empty_src = empty( $wp_scripts->registered[ $handle ]->src ); + if ( $script_not_registered || $empty_src ) { continue; + } // Provide basic script data $script_data = array( - 'handle' => $handle, - 'footer' => ( is_array( $wp_scripts->in_footer ) && in_array( $handle, $wp_scripts->in_footer ) ), - 'extra_data' => $wp_scripts->print_extra_script( $handle, false ) + 'handle' => $handle, + 'footer' => ( is_array( $wp_scripts->in_footer ) && in_array( $handle, $wp_scripts->in_footer, true ) ), + 'extra_data' => $wp_scripts->print_extra_script( $handle, false ), + 'before_handle' => $wp_scripts->print_inline_script( $handle, 'before', false ), + 'after_handle' => $wp_scripts->print_inline_script( $handle, 'after', false ), ); // Base source @@ -1280,39 +1368,49 @@ class The_Neverending_Home_Page { $results['type'] = 'success'; /** - * Gather renderer callbacks. These will be called in order and allow multiple callbacks to be queued. Once content is found, no futher callbacks will run. + * Fires when rendering Infinite Scroll posts. * * @module infinite-scroll * - * @since 6.0.0 + * @since 2.0.0 */ - $callbacks = apply_filters( 'infinite_scroll_render_callbacks', array( - self::get_settings()->render, // This is the setting callback e.g. from add theme support. - ) ); - - // Append fallback callback. That rhymes. - $callbacks[] = array( $this, 'render' ); - - foreach ( $callbacks as $callback ) { - if ( false !== $callback && is_callable( $callback ) ) { - rewind_posts(); - ob_start(); - add_action( 'infinite_scroll_render', $callback ); - - /** - * Fires when rendering Infinite Scroll posts. - * - * @module infinite-scroll - * - * @since 2.0.0 - */ - do_action( 'infinite_scroll_render' ); - - $results['html'] = ob_get_clean(); - remove_action( 'infinite_scroll_render', $callback ); - } - if ( ! empty( $results['html'] ) ) { - break; + do_action( 'infinite_scroll_render' ); + $results['html'] = ob_get_clean(); + if ( empty( $results['html'] ) ) { + /** + * Gather renderer callbacks. These will be called in order and allow multiple callbacks to be queued. Once content is found, no futher callbacks will run. + * + * @module infinite-scroll + * + * @since 6.0.0 + */ + $callbacks = apply_filters( + 'infinite_scroll_render_callbacks', + array( self::get_settings()->render ) // This is the setting callback e.g. from add theme support. + ); + + // Append fallback callback. That rhymes. + $callbacks[] = array( $this, 'render' ); + + foreach ( $callbacks as $callback ) { + if ( false !== $callback && is_callable( $callback ) ) { + rewind_posts(); + ob_start(); + add_action( 'infinite_scroll_render', $callback ); + + /** + * This action is already documented above. + * See https://github.com/Automattic/jetpack/pull/16317/ + * for more details as to why it was introduced. + */ + do_action( 'infinite_scroll_render' ); + + $results['html'] = ob_get_clean(); + remove_action( 'infinite_scroll_render', $callback ); + } + if ( ! empty( $results['html'] ) ) { + break; + } } } @@ -1332,8 +1430,13 @@ class The_Neverending_Home_Page { $wrapper_classes = is_string( self::get_settings()->wrapper ) ? self::get_settings()->wrapper : 'infinite-wrap'; $wrapper_classes .= ' infinite-view-' . $page; $wrapper_classes = trim( $wrapper_classes ); + $aria_label = sprintf( + /* translators: %1$s is the page count */ + __( 'Page: %1$d.', 'jetpack' ), + $page + ); - $results['html'] = '<div class="' . esc_attr( $wrapper_classes ) . '" id="infinite-view-' . $page . '" data-page-num="' . $page . '">' . $results['html'] . '</div>'; + $results['html'] = '<div class="' . esc_attr( $wrapper_classes ) . '" id="infinite-view-' . $page . '" data-page-num="' . $page . '" role="region" aria-label="' . esc_attr( $aria_label ) . '">' . $results['html'] . '</div>'; } // Fire wp_footer to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer. @@ -1510,6 +1613,10 @@ class The_Neverending_Home_Page { * @return string or null */ function footer() { + if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) { + return; + } + // Bail if theme requested footer not show if ( false == self::get_settings()->footer ) return; @@ -1602,6 +1709,297 @@ class The_Neverending_Home_Page { return $url; } + + /** + * When the MediaElement is loaded in dynamically, we need to enforce that + * its settings are added to the page as well. + * + * @param array $scripts_data New scripts exposed to the infinite scroll. + * + * @since 8.4.0 + */ + public function add_mejs_config( $scripts_data ) { + foreach ( $scripts_data as $key => $data ) { + if ( 'mediaelement-core' === $data['handle'] ) { + $mejs_settings = array( + 'pluginPath' => includes_url( 'js/mediaelement/', 'relative' ), + 'classPrefix' => 'mejs-', + 'stretching' => 'responsive', + ); + + $scripts_data[ $key ]['extra_data'] = sprintf( + 'window.%s = %s', + '_wpmejsSettings', + wp_json_encode( apply_filters( 'mejs_settings', $mejs_settings ) ) + ); + } + } + return $scripts_data; + } + + /** + * Determines whether the legacy AMP Reader post templates are being used. + * + * @return bool + */ + private function is_exempted_amp_page() { + if ( is_singular( 'web-story' ) ) { + // Ensure that <amp-next-page> is not injected after <amp-story> as generated by the Web Stories plugin. + return true; + } + if ( function_exists( 'amp_is_legacy' ) ) { + // Available since AMP v2.0, this will return false if a theme like Twenty Twenty is selected as the Reader theme. + return amp_is_legacy(); + } + if ( method_exists( 'AMP_Options_Manager', 'get_option' ) ) { + // In versions prior to v2.0, checking the template mode as being 'reader' is sufficient. + return 'reader' === AMP_Options_Manager::get_option( 'theme_support' ); + } + return false; + } + + /** + * Load AMP specific hooks. + * + * @return void + */ + public function amp_load_hooks() { + if ( $this->is_exempted_amp_page() ) { + return; + } + + if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) { + $template = self::get_settings()->render; + + add_filter( 'jetpack_infinite_scroll_load_scripts_and_styles', '__return_false' ); + + add_action( 'template_redirect', array( $this, 'amp_start_output_buffering' ), 0 ); + add_action( 'shutdown', array( $this, 'amp_output_buffer' ), 1 ); + + if ( is_callable( "amp_{$template}_hooks" ) ) { + call_user_func( "amp_{$template}_hooks" ); + } + + // Warms up the amp next page markup. + // This should be done outside the output buffering callback started in the template_redirect. + $this->amp_get_footer_template(); + } + } + + /** + * Start the AMP output buffering. + * + * @return void + */ + public function amp_start_output_buffering() { + ob_start( array( $this, 'amp_finish_output_buffering' ) ); + } + + /** + * Flush the AMP output buffer. + * + * @return void + */ + public function amp_output_buffer() { + if ( ob_get_contents() ) { + ob_end_flush(); + } + } + + /** + * Filter the AMP output buffer contents. + * + * @param string $buffer Contents of the output buffer. + * + * @return string|false + */ + public function amp_finish_output_buffering( $buffer ) { + // Hide WordPress admin bar on next page load. + $buffer = preg_replace( + '/id="wpadminbar"/', + '$0 next-page-hide', + $buffer + ); + + /** + * Get the theme footers. + * + * @module infinite-scroll + * + * @since 9.0.0 + * + * @param array array() An array to store multiple markup entries to be added to the footer. + * @param string $buffer The contents of the output buffer. + */ + $footers = apply_filters( 'jetpack_amp_infinite_footers', array(), $buffer ); + + /** + * Filter the output buffer. + * Themes can leverage this hook to add custom markup on next page load. + * + * @module infinite-scroll + * + * @since 9.0.0 + * + * @param string $buffer The contents of the output buffer. + */ + $buffer = apply_filters( 'jetpack_amp_infinite_output', $buffer ); + + // Add the amp next page markup. + $buffer = preg_replace( + '~</body>~', + $this->amp_get_footer_template( $footers ) . '$0', + $buffer + ); + + return $buffer; + } + + /** + * Get AMP next page markup with the custom footers. + * + * @param string[] $footers The theme footers. + * + * @return string + */ + protected function amp_get_footer_template( $footers = array() ) { + static $template = null; + + if ( null === $template ) { + $template = $this->amp_footer_template(); + } + + if ( empty( $footers ) ) { + return $template; + } + + return preg_replace( + '/%%footer%%/', + implode( '', $footers ), + $template + ); + } + + /** + * AMP Next Page markup. + * + * @return string + */ + protected function amp_footer_template() { + ob_start(); + ?> +<amp-next-page max-pages="<?php echo esc_attr( $this->amp_get_max_pages() ); ?>"> + <script type="application/json"> + [ + <?php echo wp_json_encode( $this->amp_next_page() ); ?> + ] + </script> + <div separator> + <?php + echo wp_kses_post( + /** + * AMP infinite scroll separator. + * + * @module infinite-scroll + * + * @since 9.0.0 + * + * @param string '' The markup for the next page separator. + */ + apply_filters( 'jetpack_amp_infinite_separator', '' ) + ); + ?> + </div> + <div recommendation-box class="recommendation-box"> + <template type="amp-mustache"> + {{#pages}} + <?php + echo wp_kses_post( + /** + * AMP infinite scroll older posts markup. + * + * @module infinite-scroll + * + * @since 9.0.0 + * + * @param string '' The markup for the older posts/next page. + */ + apply_filters( 'jetpack_amp_infinite_older_posts', '' ) + ); + ?> + {{/pages}} + </template> + </div> + <div footer> + %%footer%% + </div> +</amp-next-page> + <?php + return ob_get_clean(); + } + + /** + * Get the AMP next page information. + * + * @return array + */ + protected function amp_next_page() { + $title = ''; + $url = ''; + $image = ''; + + if ( ! static::amp_is_last_page() ) { + $title = sprintf( + '%s - %s %d - %s', + wp_title( '', false ), + __( 'Page', 'jetpack' ), + max( get_query_var( 'paged', 1 ), 1 ) + 1, + get_bloginfo( 'name' ) + ); + $url = get_next_posts_page_link(); + } + + $next_page = array( + 'title' => $title, + 'url' => $url, + 'image' => $image, + ); + + /** + * The next page settings. + * An array containing: + * - title => The title to be featured on the browser tab. + * - url => The URL of next page. + * - image => The image URL. A required AMP setting, not in use currently. Themes are welcome to leverage. + * + * @module infinite-scroll + * + * @since 9.0.0 + * + * @param array $next_page The contents of the output buffer. + */ + return apply_filters( 'jetpack_amp_infinite_next_page_data', $next_page ); + } + + /** + * Get the number of pages left. + * + * @return int + */ + protected static function amp_get_max_pages() { + global $wp_query; + + return (int) $wp_query->max_num_pages - $wp_query->query_vars['paged']; + } + + /** + * Is the last page. + * + * @return bool + */ + protected static function amp_is_last_page() { + return 0 === static::amp_get_max_pages(); + } }; /** diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.css b/plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.css index cc232785..9f2612e5 100644 --- a/plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.css +++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyeleven.css @@ -16,7 +16,7 @@ padding-top: 0; } .infinite-scroll .infinite-wrap .hentry:last-child { - border-bottom: 1px solid #ddd; + border-bottom: 1px solid #dcdcde; } .infinite-scroll .infinite-wrap:last-of-type .hentry:last-child { border-bottom: none; @@ -42,4 +42,4 @@ .infinite-scroll #infinite-handle { padding-bottom: 40px; } -}
\ No newline at end of file +} diff --git a/plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.php b/plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.php index 54a1fbc8..c9710abd 100644 --- a/plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.php +++ b/plugins/jetpack/modules/infinite-scroll/themes/twentyfourteen.php @@ -5,6 +5,8 @@ * Register support for Twenty Fourteen. */ +use Automattic\Jetpack\Device_Detection\User_Agent_Info; + /** * Add theme support for infinite scroll */ @@ -27,7 +29,7 @@ add_action( 'after_setup_theme', 'jetpack_twentyfourteen_infinite_scroll_init' ) */ function jetpack_twentyfourteen_has_footer_widgets() { if ( function_exists( 'jetpack_is_mobile' ) ) { - if ( ( Jetpack_User_Agent_Info::is_ipad() && is_active_sidebar( 'sidebar-1' ) ) + if ( ( User_Agent_Info::is_ipad() && is_active_sidebar( 'sidebar-1' ) ) || ( jetpack_is_mobile( '', true ) && ( is_active_sidebar( 'sidebar-1' ) || is_active_sidebar( 'sidebar-2' ) ) ) || is_active_sidebar( 'sidebar-3' ) ) |