Nenhuma Descrição

infinity.js 27KB

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