/*
--------------------------------
Infinite Scroll
--------------------------------
+ https://github.com/paulirish/infinite-scroll
+ version 2.0b2.120519
+ Copyright 2011/12 Paul Irish & Luke Shumard
+ Licensed under the MIT license
+ Documentation: http://infinite-scroll.com/
*/
(function (window, $, undefined) {
$.infinitescroll = function infscr(options, callback, element) {
this.element = $(element);
// Flag the object in the event of a failed creation
if (!this._create(options, callback)) {
this.failed = true;
}
};
$.infinitescroll.defaults = {
loading: {
finished: undefined,
finishedMsg: "Congratulations, you've reached the end of the internet.",
img: "http://www.infinite-scroll.com/loading.gif",
msg: null,
msgText: "Loading the next set of posts...",
selector: null,
speed: 'fast',
start: undefined
},
state: {
isDuringAjax: false,
isInvalidPage: false,
isDestroyed: false,
isDone: false, // For when it goes all the way through the archive.
isPaused: false,
currPage: 1
},
callback: undefined,
debug: false,
behavior: undefined,
binder: $(window), // used to cache the selector
nextSelector: "div.navigation a:first",
navSelector: "div.navigation",
contentSelector: null, // rename to pageFragment
extraScrollPx: 150,
itemSelector: "div.post",
animate: false,
pathParse: undefined,
dataType: 'html',
appendCallback: true,
bufferPx: 40,
errorCallback: function () { },
infid: 0, //Instance ID
pixelsFromNavToBottom: undefined,
path: undefined
};
$.infinitescroll.prototype = {
/*
----------------------------
Private methods
----------------------------
*/
// Bind or unbind from scroll
_binding: function infscr_binding(binding) {
var instance = this,
opts = instance.options;
opts.v = '2.0b2.111027';
// if behavior is defined and this function is extended, call that instead of default
if (!!opts.behavior && this['_binding_'+opts.behavior] !== undefined) {
this['_binding_'+opts.behavior].call(this);
return;
}
if (binding !== 'bind' && binding !== 'unbind') {
this._debug('Binding value ' + binding + ' not valid')
return false;
}
if (binding == 'unbind') {
(this.options.binder).unbind('smartscroll.infscr.' + instance.options.infid);
} else {
(this.options.binder)[binding]('smartscroll.infscr.' + instance.options.infid, function () {
instance.scroll();
});
};
this._debug('Binding', binding);
},
// Fundamental aspects of the plugin are initialized
_create: function infscr_create(options, callback) {
// Add custom options to defaults
var opts = $.extend(true, {}, $.infinitescroll.defaults, options);
// Validate selectors
if (!this._validate(options)) { return false; }
this.options = opts;
// Validate page fragment path
var path = $(opts.nextSelector).attr('href');
if (!path) {
this._debug('Navigation selector not found');
return false;
}
// Set the path to be a relative URL from root.
opts.path = this._determinepath(path);
// contentSelector is 'page fragment' option for .load() / .ajax() calls
opts.contentSelector = opts.contentSelector || this.element;
// loading.selector - if we want to place the load message in a specific selector, defaulted to the contentSelector
opts.loading.selector = opts.loading.selector || opts.contentSelector;
// Define loading.msg
opts.loading.msg = $('
' + opts.loading.msgText + '
');
// Preload loading.img
(new Image()).src = opts.loading.img;
// distance from nav links to bottom
// computed as: height of the document + top offset of container - top offset of nav link
opts.pixelsFromNavToBottom = $(document).height() - $(opts.navSelector).offset().top;
// determine loading.start actions
opts.loading.start = opts.loading.start || function() {
$(opts.navSelector).hide();
opts.loading.msg
.appendTo(opts.loading.selector)
.show(opts.loading.speed, function () {
beginAjax(opts);
});
};
// determine loading.finished actions
opts.loading.finished = opts.loading.finished || function() {
opts.loading.msg.fadeOut('normal');
};
// callback loading
opts.callback = function(instance,data) {
if (!!opts.behavior && instance['_callback_'+opts.behavior] !== undefined) {
instance['_callback_'+opts.behavior].call($(opts.contentSelector)[0], data);
}
if (callback) {
callback.call($(opts.contentSelector)[0], data, opts);
}
};
this._setup();
// Return true to indicate successful creation
return true;
},
// Console log wrapper
_debug: function infscr_debug() {
if (this.options && this.options.debug) {
return window.console && console.log.call(console, arguments);
}
},
// find the number to increment in the path.
_determinepath: function infscr_determinepath(path) {
var opts = this.options;
// if behavior is defined and this function is extended, call that instead of default
if (!!opts.behavior && this['_determinepath_'+opts.behavior] !== undefined) {
this['_determinepath_'+opts.behavior].call(this,path);
return;
}
if (!!opts.pathParse) {
this._debug('pathParse manual');
return opts.pathParse(path, this.options.state.currPage+1);
} else if (path.match(/^(.*?)\b2\b(.*?$)/)) {
path = path.match(/^(.*?)\b2\b(.*?$)/).slice(1);
// if there is any 2 in the url at all.
} else if (path.match(/^(.*?)2(.*?$)/)) {
// 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);
return path;
}
path = path.match(/^(.*?)2(.*?$)/).slice(1);
} else {
// page= is used in drupal too but second page is page=1 not page=2:
// thx Jerod Fritz, vladikoff
if (path.match(/^(.*?page=)1(\/.*|$)/)) {
path = path.match(/^(.*?page=)1(\/.*|$)/).slice(1);
return path;
} else {
this._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.');
// Get rid of isInvalidPage to allow permalink to state
opts.state.isInvalidPage = true; //prevent it from running on this page.
}
}
this._debug('determinePath', path);
return path;
},
// Custom error
_error: function infscr_error(xhr) {
var opts = this.options;
// if behavior is defined and this function is extended, call that instead of default
if (!!opts.behavior && this['_error_'+opts.behavior] !== undefined) {
this['_error_'+opts.behavior].call(this,xhr);
return;
}
if (xhr !== 'destroy' && xhr !== 'end') {
xhr = 'unknown';
}
this._debug('Error', xhr);
if (xhr == 'end') {
this._showdonemsg();
}
opts.state.isDone = true;
opts.state.currPage = 1; // if you need to go back to this instance
opts.state.isPaused = false;
this._binding('unbind');
},
// Load Callback
_loadcallback: function infscr_loadcallback(box, data) {
var opts = this.options,
callback = this.options.callback, // GLOBAL OBJECT FOR CALLBACK
result = (opts.state.isDone) ? 'done' : (!opts.appendCallback) ? 'no-append' : 'append',
frag;
// if behavior is defined and this function is extended, call that instead of default
if (!!opts.behavior && this['_loadcallback_'+opts.behavior] !== undefined) {
this['_loadcallback_'+opts.behavior].call(this,box,data);
return;
}
switch (result) {
case 'done':
this._showdonemsg();
return false;
break;
case 'no-append':
if (opts.dataType == 'html') {
data = '' + data + '
';
data = $(data).find(opts.itemSelector);
}
break;
case 'append':
var children = box.children();
// if it didn't return anything
if (children.length == 0) {
return this._error('end');
}
// use a documentFragment because it works when content is going into a table or UL
frag = document.createDocumentFragment();
while (box[0].firstChild) {
frag.appendChild(box[0].firstChild);
}
this._debug('contentSelector', $(opts.contentSelector)[0])
$(opts.contentSelector)[0].appendChild(frag);
// 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.
data = children.get();
break;
}
// loadingEnd function
opts.loading.finished.call($(opts.contentSelector)[0],opts)
// 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.state.isDuringAjax = false; });
}
if (!opts.animate) opts.state.isDuringAjax = false; // once the call is done, we can allow it again.
callback(this,data);
},
_nearbottom: function infscr_nearbottom() {
var opts = this.options,
pixelsFromWindowBottomToBottom = 0 + $(document).height() - (opts.binder.scrollTop()) - $(window).height();
// if behavior is defined and this function is extended, call that instead of default
if (!!opts.behavior && this['_nearbottom_'+opts.behavior] !== undefined) {
return this['_nearbottom_'+opts.behavior].call(this);
}
this._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);
},
// Pause / temporarily disable plugin from firing
_pausing: function infscr_pausing(pause) {
var opts = this.options;
// if behavior is defined and this function is extended, call that instead of default
if (!!opts.behavior && this['_pausing_'+opts.behavior] !== undefined) {
this['_pausing_'+opts.behavior].call(this,pause);
return;
}
// If pause is not 'pause' or 'resume', toggle it's value
if (pause !== 'pause' && pause !== 'resume' && pause !== null) {
this._debug('Invalid argument. Toggling pause value instead');
};
pause = (pause && (pause == 'pause' || pause == 'resume')) ? pause : 'toggle';
switch (pause) {
case 'pause':
opts.state.isPaused = true;
break;
case 'resume':
opts.state.isPaused = false;
break;
case 'toggle':
opts.state.isPaused = !opts.state.isPaused;
break;
}
this._debug('Paused', opts.state.isPaused);
return false;
},
// Behavior is determined
// If the behavior option is undefined, it will set to default and bind to scroll
_setup: function infscr_setup() {
var opts = this.options;
// if behavior is defined and this function is extended, call that instead of default
if (!!opts.behavior && this['_setup_'+opts.behavior] !== undefined) {
this['_setup_'+opts.behavior].call(this);
return;
}
this._binding('bind');
return false;
},
// Show done message
_showdonemsg: function infscr_showdonemsg() {
var opts = this.options;
// if behavior is defined and this function is extended, call that instead of default
if (!!opts.behavior && this['_showdonemsg_'+opts.behavior] !== undefined) {
this['_showdonemsg_'+opts.behavior].call(this);
return;
}
opts.loading.msg
.find('img')
.hide()
.parent()
.find('div').html(opts.loading.finishedMsg).animate({ opacity: 1 }, 2000, function () {
$(this).parent().fadeOut('normal');
});
// user provided callback when done
opts.errorCallback.call($(opts.contentSelector)[0],'done');
},
// grab each selector option and see if any fail
_validate: function infscr_validate(opts) {
for (var key in opts) {
if (key.indexOf && key.indexOf('Selector') > -1 && $(opts[key]).length === 0) {
this._debug('Your ' + key + ' found no elements.');
return false;
}
}
return true;
},
/*
----------------------------
Public methods
----------------------------
*/
// Bind to scroll
bind: function infscr_bind() {
this._binding('bind');
},
// Destroy current instance of plugin
destroy: function infscr_destroy() {
this.options.state.isDestroyed = true;
return this._error('destroy');
},
// Set pause value to false
pause: function infscr_pause() {
this._pausing('pause');
},
// Set pause value to false
resume: function infscr_resume() {
this._pausing('resume');
},
// Retrieve next set of content items
retrieve: function infscr_retrieve(pageNum) {
var instance = this,
opts = instance.options,
path = opts.path,
box, frag, desturl, method, condition,
pageNum = pageNum || null,
getPage = (!!pageNum) ? pageNum : opts.state.currPage;
beginAjax = function infscr_ajax(opts) {
// increment the URL bit. e.g. /page/3/
opts.state.currPage++;
instance._debug('heading into ajax', path);
// if we're dealing with a table we can't use DIVs
box = $(opts.contentSelector).is('table') ? $('') : $('');
desturl = path.join(opts.state.currPage);
method = (opts.dataType == 'html' || opts.dataType == 'json' ) ? opts.dataType : 'html+callback';
if (opts.appendCallback && opts.dataType == 'html') method += '+callback'
switch (method) {
case 'html+callback':
instance._debug('Using HTML via .load() method');
box.load(desturl + ' ' + opts.itemSelector, null, function infscr_ajax_callback(responseText) {
instance._loadcallback(box, responseText);
});
break;
case 'html':
instance._debug('Using ' + (method.toUpperCase()) + ' via $.ajax() method');
$.ajax({
// params
url: desturl,
dataType: opts.dataType,
complete: function infscr_ajax_callback(jqXHR, textStatus) {
condition = (typeof (jqXHR.isResolved) !== 'undefined') ? (jqXHR.isResolved()) : (textStatus === "success" || textStatus === "notmodified");
(condition) ? instance._loadcallback(box, jqXHR.responseText) : instance._error('end');
}
});
break;
case 'json':
instance._debug('Using ' + (method.toUpperCase()) + ' via $.ajax() method');
$.ajax({
dataType: 'json',
type: 'GET',
url: desturl,
success: function(data, textStatus, jqXHR) {
condition = (typeof (jqXHR.isResolved) !== 'undefined') ? (jqXHR.isResolved()) : (textStatus === "success" || textStatus === "notmodified");
if(opts.appendCallback) {
// if appendCallback is true, you must defined template in options.
// note that data passed into _loadcallback is already an html (after processed in opts.template(data)).
if(opts.template != undefined) {
var theData = opts.template(data);
box.append(theData);
(condition) ? instance._loadcallback(box, theData) : instance._error('end');
} else {
instance._debug("template must be defined.");
instance._error('end');
}
} else {
// if appendCallback is false, we will pass in the JSON object. you should handle it yourself in your callback.
(condition) ? instance._loadcallback(box, data) : instance._error('end');
}
},
error: function(jqXHR, textStatus, errorThrown) {
instance._debug("JSON ajax request failed.");
instance._error('end');
}
});
break;
}
};
// if behavior is defined and this function is extended, call that instead of default
if (!!opts.behavior && this['retrieve_'+opts.behavior] !== undefined) {
this['retrieve_'+opts.behavior].call(this,pageNum);
return;
}
// for manual triggers, if destroyed, get out of here
if (opts.state.isDestroyed) {
this._debug('Instance is destroyed');
return false;
};
// we dont want to fire the ajax multiple times
opts.state.isDuringAjax = true;
opts.loading.start.call($(opts.contentSelector)[0],opts);
},
// Check to see next page is needed
scroll: function infscr_scroll() {
var opts = this.options,
state = opts.state;
// if behavior is defined and this function is extended, call that instead of default
if (!!opts.behavior && this['scroll_'+opts.behavior] !== undefined) {
this['scroll_'+opts.behavior].call(this);
return;
}
if (state.isDuringAjax || state.isInvalidPage || state.isDone || state.isDestroyed || state.isPaused) return;
if (!this._nearbottom()) return;
this.retrieve();
},
// Toggle pause value
toggle: function infscr_toggle() {
this._pausing();
},
// Unbind from scroll
unbind: function infscr_unbind() {
this._binding('unbind');
},
// update options
update: function infscr_options(key) {
if ($.isPlainObject(key)) {
this.options = $.extend(true,this.options,key);
}
}
}
/*
----------------------------
Infinite Scroll function
----------------------------
Borrowed logic from the following...
jQuery UI
- https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js
jCarousel
- https://github.com/jsor/jcarousel/blob/master/lib/jquery.jcarousel.js
Masonry
- https://github.com/desandro/masonry/blob/master/jquery.masonry.js
*/
$.fn.infinitescroll = function infscr_init(options, callback) {
var thisCall = typeof options;
switch (thisCall) {
// method
case 'string':
var args = Array.prototype.slice.call(arguments, 1);
this.each(function () {
var instance = $.data(this, 'infinitescroll');
if (!instance) {
// not setup yet
// return $.error('Method ' + options + ' cannot be called until Infinite Scroll is setup');
return false;
}
if (!$.isFunction(instance[options]) || options.charAt(0) === "_") {
// return $.error('No such method ' + options + ' for Infinite Scroll');
return false;
}
// no errors!
instance[options].apply(instance, args);
});
break;
// creation
case 'object':
this.each(function () {
var instance = $.data(this, 'infinitescroll');
if (instance) {
// update options of current instance
instance.update(options);
} else {
// initialize new instance
instance = new $.infinitescroll(options, callback, this);
// don't attach if instantiation failed
if (!instance.failed) {
$.data(this, 'infinitescroll', instance);
}
}
});
break;
}
return this;
};
/*
* smartscroll: debounced scroll event for jQuery *
* https://github.com/lukeshumard/smartscroll
* Based on smartresize by @louis_remi: https://github.com/lrbabe/jquery.smartresize.js *
* Copyright 2011 Louis-Remi & Luke Shumard * Licensed under the MIT license. *
*/
var event = $.event,
scrollTimeout;
event.special.smartscroll = {
setup: function () {
$(this).bind("scroll", event.special.smartscroll.handler);
},
teardown: function () {
$(this).unbind("scroll", event.special.smartscroll.handler);
},
handler: function (event, execAsap) {
// Save the context
var context = this,
args = arguments;
// set correct event type
event.type = "smartscroll";
if (scrollTimeout) { clearTimeout(scrollTimeout); }
scrollTimeout = setTimeout(function () {
$.event.handle.apply(context, args);
}, execAsap === "execAsap" ? 0 : 100);
}
};
$.fn.smartscroll = function (fn) {
return fn ? this.bind("smartscroll", fn) : this.trigger("smartscroll", ["execAsap"]);
};
})(window, jQuery);