diff options
author | Yury German <blueknight@gentoo.org> | 2022-01-23 18:37:36 -0500 |
---|---|---|
committer | Yury German <blueknight@gentoo.org> | 2022-01-23 18:37:36 -0500 |
commit | f18b23a3a9378fb0a98856d436aa9ebf94e47429 (patch) | |
tree | e418433e22854ebd2d77eaa869d5d0470a973317 /plugins/jetpack/modules/infinite-scroll/infinity.js | |
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/infinity.js')
-rw-r--r-- | plugins/jetpack/modules/infinite-scroll/infinity.js | 967 |
1 files changed, 540 insertions, 427 deletions
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 |