<!DOCTYPE HTML> <html> <head> <title>Bidirectional Infinite Scroll With jQuery And AJAX</title> <style type="text/css"> div.list-chunk { padding-bottom: 1px ; } div.list-item { border: 4px solid #E0E0E0 ; margin: 0px 0px 13px 0px ; padding: 10px 10px 10px 10px ; } div.list-item a.thumbnail { float: left ; height: 150px ; margin: 0px 15px 10px 0px ; width: 100px ; } div.list-item a.thumbnail img { border: 1px solid #999999 ; display: block ; } div.list-item h4 { font-size: 18px ; margin: 0px 0px 12px 0px ; } div.list-item div.offset { border-top: 1px solid #CCCCCC ; clear: both ; font-size: 11px ; padding-top: 4px ; } </style> <script type="text/javascript" src="../jquery-1.4a2.js"></script> <script type="text/javascript"> // I get more list items and either prepend them or append // them to the list depending on the target area. function getMoreListItems( container, targetArea, onComplete ){ // Check to see if there is any existing AJAX call // for the list data items. If there is, we want to // return out of this method - no reason to overload // the server with extraneous requests (more so than // an infinite scroll effect already does!!). if (container.data( "xhr" )){ // Let the active AJAX request complete. return; } // Get the min and max offsets of the current // container. var minOffset = (container.data( "minOffset" ) || 0); var maxOffset = (container.data( "maxOffset" ) || 0); // The count of list items to load per AJAX request. // We are calling it a "chunk" size because each // list chunk will be stored in its own sub-container // to make DOM manipulation easier. var chunkSize = 3; // Check our target area to see what our next offset // for loading should be. if (targetArea == "top"){ // We are prepending list items. var nextOffset = (minOffset - 1 - chunkSize); } else { // We are appending list items. var nextOffset = (maxOffset + 1); } // Launch AJAX request for next set of results and // store the resultant XHR request with the container. container.data( "xhr", $.ajax({ type: "get", url: "./bidirectional.cfm", data: { offset: nextOffset, count: chunkSize }, dataType: "json", success: function( response ){ // Apply the response to the container // for the given target area. applyListItems( container, targetArea, response ); }, complete: function(){ // Remove the stored AJAX request. This // will allow subsequent AJAX requests // to execute. container.removeData( "xhr" ); // Call the onComplete callback. onComplete(); } }) ); } // I apply the given AJAX response to the container. function applyListItems( container, targetArea, items ){ // Get a reference to our HTML template for a new // list item. var template = $( "#list-item-template" ); // Create an array to hold our HTML buffer - this will // be faster than creating individual DOM elements and // appending them piece-wise. var htmlBuffer = []; // Loop over the array to create each list element. $.each( items, function( index, item ){ // Modify the template and append the result // to the HTML buffer. htmlBuffer.push( template.html().replace( new RegExp( "\\$\\{(src|offset)\\}", "g" ), function( $0, $1 ){ // Return property. return( item[ $1.toUpperCase() ] ); } ) ); } ); // Create a list chunk which will hold our data. var chunk = $( "<div class='list-chunk'></div>" ); // Append the list item html buffer to the chunk. chunk.append( htmlBuffer.join( "" ) ); // Create the min and max offset of the chunk. chunk.data( "minOffset", items[ 0 ].OFFSET ); chunk.data( "maxOffset", items[ items.length - 1 ].OFFSET ); // Check to see which target area we are adding the // list items to (top vs. bottom). if (targetArea == "top"){ // Get the current window scroll before we update // the list contente. var viewTop = $( window ).scrollTop(); // Prepend list items. container.prepend( chunk ); // Now that the chunk has been added to the page, // it should have a height that can be calculated. var chunkHeight = chunk.height(); // Re-adjust the scroll of the window to make sure // the user doesn't suddenly jump to a crazy place. $( window ).scrollTop( viewTop + chunkHeight ); // Now that we moved the list up, let's remove // the last chunk from the list. container.find( "> div.list-chunk:last" ).remove(); } else { // Append list items. container.append( chunk ); // Check to see if we have more chunks than we // want (an arbitrary number, but enough to make // sure we can comfortable fill the page). if (container.children().size() > 3){ // We want to remove the first chunk in the // list to free up some browser memory. // Get the current window scroll before we // remove a chunk. var viewTop = $( window ).scrollTop(); // Get the chunk that we are going to remove. var oldChunk = container.children( ":first" ); // Get the height of the chunk we are about // to remove. var oldChunkHeight = oldChunk.height(); // Remove the hunk. oldChunk.remove(); // Now, we need to ajust the scroll offset // of the window so the user is not jumped // around to a crazy place. $( window ).scrollTop( viewTop - oldChunkHeight ); } } // Now that we have updated the chunks in the // container, let's update the min / max offsets of // the container (which will be used on subsequent // AJAX requests). container.data( "minOffset", container.children( ":first" ).data( "minOffset" ) ); container.data( "maxOffset", container.children( ":last" ).data( "maxOffset" ) ); } // I check to see if more list items are needed based on // the scroll offset of the window and the position of // the container. I return a complex result that not only // determines IF more list items are needed, but on what // end of the list. // // NOTE: These calculate are based ONLY on the offset of // the list container in the context of the view frame. // This does not take anything else into account (more // business logic might be required to see if loading // truly needs to take place). function isMoreListItemsNeeded( container ){ // Create a default return. This return value contains // requirements for both the top and bottom of the // content list. var result = { top: false, bottom: false }; // Get the view frame for the window - this is the // top and bottom coordinates of the visible slice of // the document. var viewTop = $( window ).scrollTop(); var viewBottom = (viewTop + $( window ).height()); // Get the offset of the top of the list container. var containerTop = container.offset().top; // Get the offset of the bottom of the list container. var containerBottom = Math.floor( containerTop + container.height() ); // I am the scroll buffers for the top and the bottom; // this is the amount of pre-top and pre-bottom space // we want to take into account before we start // loading the next items. // // NOTE: The top buffer is a bit bigger only to make // the transition feel a bit *safer*. var topScrollBuffer = 500; var bottomScrollBuffer = 200; // Check to see if the container top is close enough // (with buffer) to the top scroll of the view frame // to trigger loading more items (at the top). if ((containerTop + topScrollBuffer) >= viewTop){ // Flag requirement at top. result.top = true; } // Check to see if the container bottom is close // enought (with buffer) to the scroll of the view // frame to trigger loading more items (at the // bottom). if ((containerBottom - bottomScrollBuffer) <= viewBottom){ // Flag requirement at bottom. result.bottom = true; } // Return the requirments for the loading. return( result ); } // I check to see if more list items are needed, and, if // they are, I load them. function checkListItemContents( container ){ // Check to see if more items need to be loaded at // the top or the bottom (based purely on position). // Returns: { top: boolean, bottom: boolean }. var isMoreLoading = isMoreListItemsNeeded( container ); // Define an onComplete method for the AJAX load that // will call this method again to make sure there is // always enough data loaded on the page. var onComplete = function(){ checkListItemContents( container ); }; // Check to see if more list items are needed at the // top. If so, we will check to offsets to see if the // load needs to take place. // // NOTE: Position is only *part* of how we determine // if additional content is needed at the top. if ( isMoreLoading.top && container.data( "minOffset" ) && (container.data( "minOffset" ) > 1) ){ // Load and prepend more list items. getMoreListItems( container, "top", onComplete ); // Check to see if more list items are needed at the // bottom. For this, all we are going to rely on is // the offset of the container (since we can load // ad-infinitum in the bottom direction). } else if (isMoreLoading.bottom){ // Load and append more list items. getMoreListItems( container, "bottom", onComplete ); } } // -------------------------------------------------- // // -------------------------------------------------- // // When the DOM is ready, initialize document. jQuery(function( $ ){ // Get a reference to the list container. var container = $( "#container" ); // Bind the scroll and resize events to the window. // Whenever the user scrolls or resizes the window, // we will need to check to see if more list items // need to be loaded. $( window ).bind( "scroll resize", function( event ){ // Hand the control-flow off to the method // that worries about the list content. checkListItemContents( container ); } ); // Now that the page is loaded, trigger the "Get" // method to populate the list with data. checkListItemContents( container ); }); </script> </head> <body> <h1> Bidirectional Infinite Scroll With jQuery And AJAX </h1> <div id="container"> <!-- Content will be loaded here dynamically. --> </div> <!-- This is the tempalte that will be used when adding the list items to the DOM. It contains two different variable place-holders: ${src} : The source of the image. ${offset} : The offset of the record. --> <script id="list-item-template" type="application/template"> <div class="list-item"> <a href="./${src}" target="_blank" class="thumbnail"> <img src="./thumbs/${src}" width="100" height="150" /> </a> <div class="details"> <h4> ${src} </h4> <p> Pauline Nordin is a fitness figure professional athlete, AST sports science spokesperson, Fitness journalist, fitness model, TV personality, Trainer and Nutrition coach, Swedish Bodybuilding champion three years in a row, Cover model Ironman magazine, Cover model Body Magazine, fitness profile etc etc. </p> </div> <div class="offset"> Offset: ${offset} </div> </div> </script> </body> </html>