1(function($){ // Open closure 2// Local vars 3var Scroller, ajaxurl, stats, type, text, totop; 4 5// IE requires special handling 6var isIE = ( -1 != navigator.userAgent.search( 'MSIE' ) ); 7if ( isIE ) { 8 var IEVersion = navigator.userAgent.match(/MSIE\s?(\d+)\.?\d*;/); 9 var IEVersion = parseInt( IEVersion[1] ); 10} 11 12// HTTP ajaxurl when site is HTTPS causes Access-Control-Allow-Origin failure in Desktop and iOS Safari 13if ( "https:" == document.location.protocol ) { 14 infiniteScroll.settings.ajaxurl = infiniteScroll.settings.ajaxurl.replace( "http://", "https://" ); 15} 16 17/** 18 * Loads new posts when users scroll near the bottom of the page. 19 */ 20Scroller = function( settings ) { 21 var self = this; 22 23 // Initialize our variables 24 this.id = settings.id; 25 this.body = $( document.body ); 26 this.window = $( window ); 27 this.element = $( '#' + settings.id ); 28 this.wrapperClass = settings.wrapper_class; 29 this.ready = true; 30 this.disabled = false; 31 this.page = 1; 32 this.offset = settings.offset; 33 this.currentday = settings.currentday; 34 this.order = settings.order; 35 this.throttle = false; 36 this.handle = '<div id="infinite-handle"><span><button>' + text.replace( '\\', '' ) + '</button></span></div>'; 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 this.pageCache = {}; 42 43 // Footer settings 44 this.footer = $( '#infinite-footer' ); 45 this.footer.wrap = settings.footer; 46 47 // Core's native MediaElement.js implementation needs special handling 48 this.wpMediaelement = null; 49 50 // We have two type of infinite scroll 51 // cases 'scroll' and 'click' 52 53 if ( type == 'scroll' ) { 54 // Bind refresh to the scroll event 55 // Throttle to check for such case every 300ms 56 57 // On event the case becomes a fact 58 this.window.bind( 'scroll.infinity', function() { 59 this.throttle = true; 60 }); 61 62 // Go back top method 63 self.gotop(); 64 65 setInterval( function() { 66 if ( this.throttle ) { 67 // Once the case is the case, the action occurs and the fact is no more 68 this.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 77 // Ensure that enough posts are loaded to fill the initial viewport, to compensate for short posts and large displays. 78 self.ensureFilledViewport(); 79 this.body.bind( 'post-load', { self: self }, self.checkViewportOnLoad ); 80 } else if ( type == 'click' ) { 81 if ( this.click_handle ) { 82 this.element.append( this.handle ); 83 } 84 85 this.body.delegate( '#infinite-handle', 'click.infinity', function() { 86 // Handle the handle 87 if ( self.click_handle ) { 88 $( '#infinite-handle' ).remove(); 89 } 90 91 // Fire the refresh 92 self.refresh(); 93 }); 94 } 95 96 // Initialize any Core audio or video players loaded via IS 97 this.body.bind( 'post-load', { self: self }, self.initializeMejs ); 98}; 99 100/** 101 * Check whether we should fetch any additional posts. 102 */ 103Scroller.prototype.check = function() { 104 var container = this.element.offset(); 105 106 // If the container can't be found, stop otherwise errors result 107 if ( 'object' !== typeof container ) { 108 return false; 109 } 110 111 var bottom = this.window.scrollTop() + this.window.height(), 112 threshold = container.top + this.element.outerHeight(false) - (this.window.height() * 2); 113 114 return bottom > threshold; 115}; 116 117/** 118 * Renders the results from a successful response. 119 */ 120Scroller.prototype.render = function( response ) { 121 this.body.addClass( 'infinity-success' ); 122 123 // Check if we can wrap the html 124 this.element.append( response.html ); 125 this.body.trigger( 'post-load', response ); 126 this.ready = true; 127}; 128 129/** 130 * Returns the object used to query for new posts. 131 */ 132Scroller.prototype.query = function() { 133 return { 134 page : this.page, 135 currentday : this.currentday, 136 order : this.order,
137 scripts : window.infiniteScroll.settings.scripts, 138 styles : window.infiniteScroll.settings.styles, 139 query_args : window.infiniteScroll.settings.query_args, 140 last_post_date : window.infiniteScroll.settings.last_post_date, 141 }; 142}; 143 144/** 145 * Scroll back to top. 146 */ 147Scroller.prototype.gotop = function() { 148 var blog = $( '#infinity-blog-title' ); 149 150 blog.attr( 'title', totop ); 151 152 // Scroll to top on blog title 153 blog.bind( 'click', function( e ) { 154 $( 'html, body' ).animate( { scrollTop: 0 }, 'fast' ); 155 e.preventDefault(); 156 }); 157}; 158 159 160/** 161 * The infinite footer. 162 */ 163Scroller.prototype.thefooter = function() { 164 var self = this, 165 width; 166 167 // Check if we have an id for the page wrapper 168 if ( $.type( this.footer.wrap ) === "string" ) { 169 width = $( 'body #' + this.footer.wrap ).outerWidth( false ); 170 171 // Make the footer match the width of the page 172 if ( width > 479 ) 173 this.footer.find( '.container' ).css( 'width', width ); 174 } 175 176 // Reveal footer 177 if ( this.window.scrollTop() >= 350 ) 178 self.footer.animate( { 'bottom': 0 }, 'fast' ); 179 else if ( this.window.scrollTop() < 350 ) 180 self.footer.animate( { 'bottom': '-50px' }, 'fast' ); 181}; 182 183 184/** 185 * Controls the flow of the refresh. Don't mess. 186 */ 187Scroller.prototype.refresh = function() { 188 var self = this, 189 query, jqxhr, load, loader, color; 190 191 // If we're disabled, ready, or don't pass the check, bail. 192 if ( this.disabled || ! this.ready || ! this.check() ) 193 return; 194 195 // Let's get going -- set ready to false to prevent 196 // multiple refreshes from occurring at once. 197 this.ready = false; 198 199 // Create a loader element to show it's working. 200 if ( this.click_handle ) { 201 loader = '<span class="infinite-loader"></span>'; 202 this.element.append( loader ); 203 204 loader = this.element.find( '.infinite-loader' ); 205 color = loader.css( 'color' ); 206 207 try { 208 loader.spin( 'medium-left', color ); 209 } catch ( error ) { } 210 } 211 212 // Generate our query vars. 213 query = $.extend({ 214 action: 'infinite_scroll' 215 }, this.query() ); 216 217 // Fire the ajax request. 218 jqxhr = $.post( infiniteScroll.settings.ajaxurl, query ); 219 220 // Allow refreshes to occur again if an error is triggered. 221 jqxhr.fail( function() { 222 if ( self.click_handle ) { 223 loader.hide(); 224 } 225 226 self.ready = true; 227 }); 228 229 // Success handler 230 jqxhr.done( function( response ) { 231 // On success, let's hide the loader circle. 232 if ( self.click_handle ) { 233 loader.hide(); 234 } 235 236 // Check for and parse our response. 237 if ( ! response ) 238 return; 239 240 response = $.parseJSON( response ); 241 242 if ( ! response || ! response.type ) 243 return; 244 245 // If there are no remaining posts... 246 if ( response.type == 'empty' ) { 247 // Disable the scroller. 248 self.disabled = true; 249 // Update body classes, allowing the footer to return to static positioning 250 self.body.addClass( 'infinity-end' ).removeClass( 'infinity-success' ); 251 252 // If we've succeeded... 253 } else if ( response.type == 'success' ) { 254 // If additional scripts are required by the incoming set of posts, parse them 255 if ( response.scripts ) { 256 $( response.scripts ).each( function() { 257 var elementToAppendTo = this.footer ? 'body' : 'head'; 258 259 // Add script handle to list of those already parsed 260 window.infiniteScroll.settings.scripts.push( this.handle ); 261 262 // Output extra data, if present 263 if ( this.extra_data ) { 264 var data = document.createElement('script'), 265 dataContent = document.createTextNode( "//<![CDATA[ \n" + this.extra_data + "\n//]]>" ); 266 267 data.type = 'text/javascript'; 268 data.appendChild( dataContent ); 269 270 document.getElementsByTagName( elementToAppendTo )[0].appendChild(data); 271 } 272 273 // Build script tag and append to DOM in requested location 274 var script = document.createElement('script'); 275 script.type = 'text/javascript'; 276 script.src = this.src; 277 script.id = this.handle; 278 279 // If MediaElement.js is loaded in by this set of posts, don't initialize the players a second time as it breaks them all 280 if ( 'wp-mediaelement' === this.handle ) { 281 self.body.unbind( 'post-load', self.initializeMejs ); 282 } 283 284 if ( 'wp-mediaelement' === this.handle && 'undefined' === typeof mejs ) { 285 self.wpMediaelement = {}; 286 self.wpMediaelement.tag = script; 287 self.wpMediaelement.element = elementToAppendTo; 288 setTimeout( self.maybeLoadMejs.bind( self ), 250 ); 289 } else { 290 document.getElementsByTagName( elementToAppendTo )[0].appendChild(script); 291 } 292 } ); 293 } 294 295 // If additional stylesheets are required by the incoming set of posts, parse them 296 if ( response.styles ) { 297 $( response.styles ).each( function() { 298 // Add stylesheet handle to list of those already parsed
299 window.infiniteScroll.settings.styles.push( this.handle ); 300 301 // Build link tag 302 var style = document.createElement('link'); 303 style.rel = 'stylesheet'; 304 style.href = this.src; 305 style.id = this.handle + '-css'; 306 307 // Destroy link tag if a conditional statement is present and either the browser isn't IE, or the conditional doesn't evaluate true 308 if ( this.conditional && ( ! isIE || ! eval( this.conditional.replace( /%ver/g, IEVersion ) ) ) ) 309 var style = false; 310 311 // Append link tag if necessary 312 if ( style ) 313 document.getElementsByTagName('head')[0].appendChild(style); 314 } ); 315 } 316 317 // stash the response in the page cache 318 self.pageCache[self.page] = response; 319 320 // Increment the page number 321 self.page++; 322 323 // Record pageview in WP Stats, if available. 324 if ( stats ) 325 new Image().src = document.location.protocol + '../g.gif' + stats + '&post=0&baba=' + Math.random(); 326 327 // Add new posts to the postflair object 328 if ( 'object' == typeof response.postflair && 'object' == typeof WPCOM_sharing_counts ) 329 WPCOM_sharing_counts = $.extend( WPCOM_sharing_counts, response.postflair ); 330 331 // Render the results 332 self.render.apply( self, arguments ); 333 334 // If 'click' type and there are still posts to fetch, add back the handle 335 if ( type == 'click' ) { 336 if ( response.lastbatch ) { 337 if ( self.click_handle ) { 338 $( '#infinite-handle' ).remove(); 339 } else { 340 self.body.trigger( 'infinite-scroll-posts-end' ); 341 } 342 } else { 343 if ( self.click_handle ) { 344 self.element.append( self.handle ); 345 } else { 346 self.body.trigger( 'infinite-scroll-posts-more' ); 347 } 348 } 349 } 350 351 // Update currentday to the latest value returned from the server 352 if ( response.currentday ) { 353 self.currentday = response.currentday; 354 } 355 356 // Fire Google Analytics pageview 357 if ( self.google_analytics ) { 358 var ga_url = self.history.path.replace( /%d/, self.page ); 359 if ( 'object' === typeof _gaq ) { 360 _gaq.push( [ '_trackPageview', ga_url ] ); 361 } 362 if ( 'function' === typeof ga ) { 363 ga( 'send', 'pageview', ga_url ); 364 } 365 } 366 } 367 }); 368 369 return jqxhr; 370}; 371 372/** 373 * Core's native media player uses MediaElement.js 374 * 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. 375 */ 376Scroller.prototype.maybeLoadMejs = function() { 377 if ( null === this.wpMediaelement ) { 378 return; 379 } 380 381 if ( 'undefined' === typeof mejs ) { 382 setTimeout( this.maybeLoadMejs, 250 ); 383 } else { 384 document.getElementsByTagName( this.wpMediaelement.element )[0].appendChild( this.wpMediaelement.tag ); 385 this.wpMediaelement = null; 386 387 // Ensure any subsequent IS loads initialize the players 388 this.body.bind( 'post-load', { self: this }, this.initializeMejs ); 389 } 390} 391 392/** 393 * Initialize the MediaElement.js player for any posts not previously initialized 394 */ 395Scroller.prototype.initializeMejs = function( ev, response ) { 396 // Are there media players in the incoming set of posts? 397 if ( ! response.html || -1 === response.html.indexOf( 'wp-audio-shortcode' ) && -1 === response.html.indexOf( 'wp-video-shortcode' ) ) { 398 return; 399 } 400 401 // Don't bother if mejs isn't loaded for some reason 402 if ( 'undefined' === typeof mejs ) { 403 return; 404 } 405 406 // Adapted from wp-includes/js/mediaelement/wp-mediaelement.js 407 // Modified to not initialize already-initialized players, as Mejs doesn't handle that well 408 $(function () { 409 var settings = {}; 410 411 if ( typeof _wpmejsSettings !== 'undefined' ) { 412 settings.pluginPath = _wpmejsSettings.pluginPath; 413 } 414 415 settings.success = function (mejs) { 416 var autoplay = mejs.attributes.autoplay && 'false' !== mejs.attributes.autoplay; 417 if ( 'flash' === mejs.pluginType && autoplay ) { 418 mejs.addEventListener( 'canplay', function () { 419 mejs.play(); 420 }, false ); 421 } 422 }; 423 424 $('.wp-audio-shortcode, .wp-video-shortcode').not( '.mejs-container' ).mediaelementplayer( settings ); 425 }); 426} 427 428/** 429 * Trigger IS to load additional posts if the initial posts don't fill the window. 430 * 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. 431 */ 432Scroller.prototype.ensureFilledViewport = function() { 433 var self = this, 434 windowHeight = self.window.height(), 435 postsHeight = self.element.height(), 436 aveSetHeight = 0, 437 wrapperQty = 0; 438 439 // Account for situations where postsHeight is 0 because child list elements are floated 440 if ( postsHeight === 0 ) { 441 $( self.element.selector + ' > li' ).each( function() { 442 postsHeight += $( this ).height(); 443 } ); 444 445 if ( postsHeight === 0 ) { 446 self.body.unbind( 'post-load', self.checkViewportOnLoad ); 447 return; 448 } 449 } 450 451 // Calculate average height of a set of posts to prevent more posts than needed from being loaded. 452 $( '.' + self.wrapperClass ).each( function() { 453 aveSetHeight += $( this ).height(); 454 wrapperQty++; 455 } ); 456 457 if ( wrapperQty > 0 ) 458 aveSetHeight = aveSetHeight / wrapperQty; 459 else 460 aveSetHeight = 0; 461 462 // Load more posts if space permits, otherwise stop checking for a full viewport 463 if ( postsHeight < windowHeight && ( postsHeight + aveSetHeight < windowHeight ) ) { 464 self.ready = true; 465 self.refresh(); 466 } 467 else { 468 self.body.unbind( 'post-load', self.checkViewportOnLoad ); 469 } 470} 471 472/** 473 * Event handler for ensureFilledViewport(), tied to the post-load trigger. 474 * 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. 475 */ 476Scroller.prototype.checkViewportOnLoad = function( ev ) { 477 ev.data.self.ensureFilledViewport(); 478} 479 480/** 481 * Identify archive page that corresponds to majority of posts shown in the current browser window. 482 */ 483Scroller.prototype.determineURL = function () { 484 var self = this, 485 windowTop = $( window ).scrollTop(), 486 windowBottom = windowTop + $( window ).height(), 487 windowSize = windowBottom - windowTop, 488 setsInView = [], 489 setsHidden = [], 490 pageNum = false;
491 492 // Find out which sets are in view 493 $( '.' + self.wrapperClass ).each( function() { 494 var id = $( this ).attr( 'id' ), 495 setTop = $( this ).offset().top, 496 setHeight = $( this ).outerHeight( false ), 497 setBottom = 0, 498 setPageNum = $( this ).data( 'page-num' ); 499 500 // Account for containers that have no height because their children are floated elements. 501 if ( 0 === setHeight ) { 502 $( '> *', this ).each( function() { 503 setHeight += $( this ).outerHeight( false ); 504 } ); 505 } 506 507 // Determine position of bottom of set by adding its height to the scroll position of its top. 508 setBottom = setTop + setHeight; 509 510 // Populate setsInView object. While this logic could all be combined into a single conditional statement, this is easier to understand. 511 if ( setTop < windowTop && setBottom > windowBottom ) { // top of set is above window, bottom is below 512 setsInView.push({'id': id, 'top': setTop, 'bottom': setBottom, 'pageNum': setPageNum }); 513 } 514 else if( setTop > windowTop && setTop < windowBottom ) { // top of set is between top (gt) and bottom (lt) 515 setsInView.push({'id': id, 'top': setTop, 'bottom': setBottom, 'pageNum': setPageNum }); 516 } 517 else if( setBottom > windowTop && setBottom < windowBottom ) { // bottom of set is between top (gt) and bottom (lt) 518 setsInView.push({'id': id, 'top': setTop, 'bottom': setBottom, 'pageNum': setPageNum }); 519 } else { 520 setsHidden.push({'id': id, 'top': setTop, 'bottom': setBottom, 'pageNum': setPageNum }); 521 } 522 } ); 523 524 $.each(setsHidden, function() { 525 var $set = $('#' + this.id); 526 if( $set.hasClass( 'is--replaced' ) ) { 527 return; 528 } 529 530 self.pageCache[ this.pageNum].html = $set.html(); 531 532 $set.css('min-height', ( this.bottom - this.top ) + 'px' ) 533 .addClass('is--replaced') 534 .empty(); 535 }); 536 537 $.each(setsInView, function() { 538 var $set = $('#' + this.id); 539 540 if( $set.hasClass('is--replaced') ) { 541 $set.css('min-height', '').removeClass('is--replaced'); 542 if( this.pageNum in self.pageCache ) { 543 $set.html( self.pageCache[this.pageNum].html ); 544 self.body.trigger( 'post-load', self.pageCache[this.pageNum] ); 545 } 546 } 547 548 }); 549 550 // 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. 551 if ( 0 == setsInView.length ) { 552 pageNum = -1; 553 } 554 else if ( 1 == setsInView.length ) { 555 var setData = setsInView.pop(); 556 557 // 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 558 if ( ( ( windowBottom - setData.top ) / windowSize ) < 0.5 ) 559 pageNum = -1; 560 else 561 pageNum = setData.pageNum; 562 } 563 else { 564 var majorityPercentageInView = 0; 565 566 // Identify the IS set that comprises the majority of the current window and set the URL to it. 567 $.each( setsInView, function( i, setData ) { 568 var topInView = 0, 569 bottomInView = 0, 570 percentOfView = 0; 571 572 // Figure percentage of view the current set represents 573 if ( setData.top > windowTop && setData.top < windowBottom ) 574 topInView = ( windowBottom - setData.top ) / windowSize; 575 576 if ( setData.bottom > windowTop && setData.bottom < windowBottom ) 577 bottomInView = ( setData.bottom - windowTop ) / windowSize; 578 579 // Figure out largest percentage of view for current set 580 if ( topInView >= bottomInView ) 581 percentOfView = topInView; 582 else if ( bottomInView >= topInView ) 583 percentOfView = bottomInView; 584 585 // Does current set's percentage of view supplant the largest previously-found set? 586 if ( percentOfView > majorityPercentageInView ) { 587 pageNum = setData.pageNum; 588 majorityPercentageInView = percentOfView; 589 } 590 } ); 591 } 592 593 // If a page number could be determined, update the URL 594 // -1 indicates that the original requested URL should be used. 595 if ( 'number' == typeof pageNum ) { 596 if ( pageNum != -1 ) 597 pageNum++; 598 599 self.updateURL( pageNum ); 600 } 601} 602 603/** 604 * Update address bar to reflect archive page URL for a given page number. 605 * Checks if URL is different to prevent pollution of browser history. 606 */ 607Scroller.prototype.updateURL = function( page ) { 608 // IE only supports pushState() in v10 and above, so don't bother if those conditions aren't met. 609 if ( ! window.history.pushState ) { 610 return; 611 } 612 var self = this, 613 offset = self.offset > 0 ? self.offset - 1 : 0, 614 pageSlug = -1 == page ? self.origURL : window.location.protocol + '//' + self.history.host + self.history.path.replace( /%d/, page + offset ) + self.history.parameters; 615 616 if ( window.location.href != pageSlug ) { 617 history.pushState( null, null, pageSlug ); 618 } 619} 620 621/** 622 * Ready, set, go! 623 */ 624$( document ).ready( function() { 625 // Check for our variables 626 if ( 'object' != typeof infiniteScroll ) 627 return; 628 629 // Set ajaxurl (for brevity)
630 ajaxurl = infiniteScroll.settings.ajaxurl; 631 632 // Set stats, used for tracking stats 633 stats = infiniteScroll.settings.stats; 634 635 // Define what type of infinity we have, grab text for click-handle 636 type = infiniteScroll.settings.type; 637 text = infiniteScroll.settings.text; 638 totop = infiniteScroll.settings.totop; 639 640 // Initialize the scroller (with the ID of the element from the theme) 641 infiniteScroll.scroller = new Scroller( infiniteScroll.settings ); 642 643 /** 644 * Monitor user scroll activity to update URL to correspond to archive page for current set of IS posts 645 */ 646 if( type == 'click' ) { 647 var timer = null; 648 $( window ).bind( 'scroll', function() { 649 // run the real scroll handler once every 250 ms. 650 if ( timer ) { return; } 651 timer = setTimeout( function() { 652 infiniteScroll.scroller.determineURL(); 653 timer = null; 654 } , 250 ); 655 }); 656 } 657}); 658 659 660})(jQuery); // Close closure
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.