Skip to content

Instantly share code, notes, and snippets.

@vgrish
Created September 5, 2013 08:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vgrish/6447311 to your computer and use it in GitHub Desktop.
Save vgrish/6447311 to your computer and use it in GitHub Desktop.
/*!
// Infinite Scroll jQuery plugin
// copyright Paul Irish, licensed GPL & MIT
// version 1.5.110124
// home and docs: http://www.infinite-scroll.com
// support for MODX with Ditto
// Usage
// [!Ditto? &parents=`2` &depth=`1` &tpl=`@FILE:assets/templates/site/chunks/article.html` &paginate=`1` &display=`5`!]
// [+next+]
*/
;(function($){
$.fn.infinitescroll = function(options,callback){
// console log wrapper.
function debug(){
if (opts.debug) { window.console && console.log.call(console,arguments)}
}
// grab each selector option and see if any fail.
function areSelectorsValid(opts){
for (var key in opts){
if (key.indexOf && key.indexOf('Selector') > -1 && $(opts[key]).length === 0){
debug('Your ' + key + ' found no elements.');
return false;
}
return true;
}
}
// find the number to increment in the path.
function determinePath(path){
debug('determinePath page: ' + path);
if ( path.match(/^(.*?)\b2\b(.*?$)/) ){
debug('0 step) ' + path);
// path = path.match(/^(.*?)\b2\b(.*?$)/).slice(1);
debug('1 step) ' + path);
// if there is any 2 in the url at all.
} else if (path.match(/^(.*?)2(.*?$)/)){
debug('2 step) ');
// page= is used in django:
// http://www.infinite-scroll.com/changelog/comment-page-1/#comment-127
if ( path.match(/^(.*?page=)2(\/.*|$)/) ){
path = path.match(/^(.*?page=)2(\/.*|$)/).slice(1);
debug('3 step) ');
return path;
}
debug('Trying backup next selector parse technique. Treacherous waters here, matey.');
path = path.match(/^(.*?)2(.*?$)/).slice(1);
// if var is page - for MODX + Ditto(uses page in the url)
// the default path is returned
} else if (path.match(/^(.*?)page(.*?$)/)){
debug('4 step) ');
return path;
} else {
debug('5 step) ');
// page= is used in drupal too but second page is page=1 not page=2:
// thx Jerod Fritz, vladikoff
if (path.match(/^(.*?page=)1(\/.*|$)/)) {
debug('6 step) ');
path = path.match(/^(.*?page=)1(\/.*|$)/).slice(1);
return path;
}
if ($.isFunction(opts.pathParse)){
debug('7 step) ');
return [path];
} else {
debug('Sorry, we couldn\'t parse your Next (Previous Posts) URL. Verify your the css selector points to the correct A tag. If you still get this error: yell, scream, and kindly ask for help at infinite-scroll.com.');
props.isInvalidPage = true; //prevent it from running on this page.
}
}
return path;
}
// determine filtering nav for multiple instances
function filterNav() {
opts.isFiltered = true;
return binder.trigger( "error.infscr."+opts.infid, [302] );
}
// Calculate internal height (used for local scroll)
function hiddenHeight(element)
{
var height = 0;
$(element).children().each(function() {
height = height + $(this).outerHeight(false);
});
return height;
}
//Generate InstanceID based on random data (to give consistent but different ID's)
function generateInstanceID(element)
{
var number = $(element).length + $(element).html().length + $(element).attr("class").length
+ $(element).attr("id").length;
opts.infid = number;
}
function isNearBottom(){
// distance remaining in the scroll
// computed as: document height - distance already scroll - viewport height - buffer
if(opts.container.nodeName=="HTML")
{
var pixelsFromWindowBottomToBottom = 0
+ $(document).height()
// have to do this bs because safari doesnt report a scrollTop on the html element
- ($(opts.container).scrollTop() || $(opts.container.ownerDocument.body).scrollTop())
- $(window).height();
}
else
{
var pixelsFromWindowBottomToBottom = 0
+ hiddenHeight(opts.container) - $(opts.container).scrollTop() - $(opts.container).height();
}
debug('math:', pixelsFromWindowBottomToBottom, opts.pixelsFromNavToBottom);
// if distance remaining in the scroll (including buffer) is less than the orignal nav to bottom....
return (pixelsFromWindowBottomToBottom - opts.bufferPx < opts.pixelsFromNavToBottom);
}
function showDoneMsg(){
props.loadingMsg
.find('img').hide()
.parent()
.find('div').html(opts.donetext).animate({opacity: 1},2000, function() {
$(this).parent().fadeOut('normal');
});
// user provided callback when done
opts.errorCallback();
}
function infscrSetup(){
if (opts.isDuringAjax || opts.isInvalidPage || opts.isDone || opts.isFiltered || opts.isPaused) return;
if ( !isNearBottom(opts,props) ) return ;
$(document).trigger('retrieve.infscr.'+opts.infid);
} // end of infscrSetup()
function kickOffAjax(){
// we dont want to fire the ajax multiple times
opts.isDuringAjax = true;
// show the loading message quickly
// then hide the previous/next links after we're
// sure the loading message was visible
props.loadingMsg.appendTo( opts.loadMsgSelector ).show(opts.loadingMsgRevealSpeed, function(){
$( opts.navSelector ).hide();
// increment the URL bit. e.g. /page/3/
opts.currPage++;
debug('heading into ajax',path);
// if we're dealing with a table we can't use DIVs
box = $(opts.contentSelector).is('table') ? $('<tbody/>') : $('<div/>');
frag = document.createDocumentFragment();
//detect MODX with Ditto
debug(path);
if (path.match(/^(.*?)page(.*?$)/)) {
if((opts.currPage - 1) > 1){
var splitPath = path.split("page="); //split by page
//put page number * current page - 1
desturl = splitPath[0] + 'page=' + (parseInt(splitPath[1])*(opts.currPage - 1));
}else{
//first ditto call
desturl = path;
}
debug('desturl = '+desturl);
}else if ($.isFunction(opts.pathParse)){
// if we got a custom path parsing function, pass in our path guess and page iteration
desturl = opts.pathParse(path.join('2'), opts.currPage);
debug('desturl join');
} else {
desturl = path.join( opts.currPage );
}
debug('desturl2 = '+desturl);
box.load( desturl + ' ' + opts.itemSelector,null,loadCallback);
});
}
function loadCallback(){
// if we've hit the last page..
if (opts.isDone){
showDoneMsg();
return false;
} else {
var children = box.children();
// if it didn't return anything
if (children.length == 0 || children.hasClass('error404')){
// trigger a 404 error so we can quit.
return infscrError([404]);
}
// use a documentFragment because it works when content is going into a table or UL
while (box[0].firstChild){
frag.appendChild( box[0].firstChild );
}
$(opts.contentSelector)[0].appendChild(frag);
// fadeout currently makes the <em>'d text ugly in IE6
props.loadingMsg.fadeOut('normal' );
// smooth scroll to ease in the new content
if (opts.animate){
var scrollTo = $(window).scrollTop() + $('#infscr-loading').height() + opts.extraScrollPx + 'px';
$('html,body').animate({scrollTop: scrollTo}, 800,function(){ opts.isDuringAjax = false; });
}
// previously, we would pass in the new DOM element as context for the callback
// however we're now using a documentfragment, which doesnt havent parents or children,
// so the context is the contentContainer guy, and we pass in an array
// of the elements collected as the first argument.
callback.call( $(opts.contentSelector)[0], children.get() );
if (!opts.animate) opts.isDuringAjax = false; // once the call is done, we can allow it again.
}
}
function initPause(pauseValue) {
if (pauseValue == "pause") {
opts.isPaused = true;
} else if (pauseValue == "resume") {
opts.isPaused = false;
} else {
opts.isPaused = !opts.isPaused;
}
debug('Paused: ' + opts.isPaused);
return false;
}
function infscrError(xhr){
if (!opts.isDone && xhr == 404) {
// die if we're out of pages.
debug('Page not found. Self-destructing...');
showDoneMsg();
opts.isDone = true;
opts.currPage = 1; // if you need to go back to this instance
binder.unbind('scroll.infscr.'+opts.infid);
$(document).unbind('retrieve.infscr.'+opts.infid);
}
if (opts.isFiltered && xhr == 302) {
// die if filtered.
debug('Filtered. Going to next instance...');
opts.isDone = true;
opts.currPage = 1; // if you need to go back to this instance
opts.isPaused = false;
binder.unbind('scroll.infscr.'+opts.infid, infscrSetup)
.unbind('pause.infscr.'+opts.infid)
.unbind('filter.infscr.'+opts.infid)
.unbind('error.infscr.'+opts.infid);
$(document).unbind('retrieve.infscr.'+opts.infid,kickOffAjax);
}
}
// smartscroll = debounced scroll event
// http://paulirish.com/2009/throttled-smartresize-jquery-event-handler/
// lets get pageed.
$.browser.ie6 = $.browser.msie && $.browser.version < 7;
var opts = $.extend({}, $.infinitescroll.defaults, options),
props = $.infinitescroll, // shorthand
box, frag, desturl, thisPause, errorStatus;
callback = callback || function(){};
if (!areSelectorsValid(opts)){ return false; }
opts.container = opts.container || document.documentElement;
// contentSelector we'll use for our .load()
opts.contentSelector = opts.contentSelector || this;
// Generate unique instance ID
if(opts.infid==0)
generateInstanceID(opts.contentSelector);
// loadMsgSelector - if we want to place the load message in a specific selector, defaulted to the contentSelector
opts.loadMsgSelector = opts.loadMsgSelector || opts.contentSelector;
// get the relative URL - everything past the domain name.
var relurl = /(.*?\/\/).*?(\/.*)/,
path = $(opts.nextSelector).attr('href');
debug ('next link '+path);
if (!path) { debug('Navigation selector not found'); return; }
// set the path to be a relative URL from root.
path = determinePath(path);
// define loading msg
props.loadingMsg = $('<div id="infscr-loading" style="text-align: center;"><img alt="Loading..." src="'+
opts.loadingImg+'" /><div>'+opts.loadingText+'</div></div>');
// preload the image
(new Image()).src = opts.loadingImg;
//Check if its HTML (window scroll)
if(opts.container.nodeName=="HTML")
{
debug("Window Scroll");
var innerContainerHeight = $(document).height();
var binder = $(window);
}
else
{
debug("Local Scroll");
var innerContainerHeight = hiddenHeight(opts.container);
var binder = $(opts.container);
}
// distance from nav links to bottom
// computed as: height of the document + top offset of container - top offset of nav link
opts.pixelsFromNavToBottom = innerContainerHeight +
(opts.container == document.documentElement ? 0 : $(opts.container).offset().top )-
$(opts.navSelector).offset().top;
// set up our bindings
// bind scroll handler to element (if its a local scroll) or window
binder
.bind('scroll.infscr.'+opts.infid, infscrSetup)
.bind('filter.infscr.'+opts.infid, filterNav)
.bind('error.infscr.'+opts.infid, function(event,errorStatus) { infscrError(errorStatus); })
.bind('pause.infscr.'+opts.infid, function(event,thisPause) { initPause(thisPause); })
.trigger('scroll.infscr.'+opts.infid); // trigger the event, in case it's a short page
$(document).bind('retrieve.infscr.'+opts.infid,kickOffAjax);
return this;
} // end of $.fn.infinitescroll()
// options and read-only properties object
$.infinitescroll = {
defaults : {
debug : false,
preload : false,
nextSelector : "div.navigation a:first",
loadingImg : "http://www.infinite-scroll.com/loading.gif",
loadingText : "<em>Loading the next set of posts...</em>",
donetext : "<em>Congratulations, you've reached the end of the internet.</em>",
navSelector : "div.navigation",
contentSelector : null, // not really a selector. :) it's whatever the method was called on..
loadMsgSelector : null,
loadingMsgRevealSpeed : 'fast', // controls how fast you want the loading message to come in, ex: 'fast', 'slow', 200 (milliseconds)
extraScrollPx : 150,
itemSelector : "div.post",
animate : false,
pathParse : undefined,
bufferPx : 40,
errorCallback : function(){},
currPage : 1,
infid : 0, //Instance ID (Generated at setup)
isDuringAjax : false,
isInvalidPage : false,
isFiltered : false,
isDone : false, // for when it goes all the way through the archive.
isPaused : false,
container : undefined, //If left undefined uses window scroll, set as container for local scroll
pixelsFromNavToBottom : undefined
},
loadingImg : undefined,
loadingMsg : undefined,
currDOMChunk : null // defined in setup()'s load()
};
})(jQuery);;;;;;;;;;;;;;;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment