jquery-infinite-scroll-helper
Version:
A lightweight implementation of the infinite scroll mechanic. By providing two essential callbacks, loadMore and doneLoading, the jQuery Infinite Scroll Helper plugin makes it a breeze to add infinite scrolling functionality to your page.
356 lines (306 loc) • 10.3 kB
JavaScript
/**
* @author Ryan Ogden
*/
;(function($, window) {
'use strict';
var pluginName = 'infiniteScrollHelper',
namespace = 'plugin_' + pluginName;
/*-------------------------------------------- */
/** Plugin Defaults */
/*-------------------------------------------- */
var defaults = {
/**
* The amount of pixels from the bottom of the scrolling element in which the loadMore callback will be invoked
* @type {number}
*/
bottomBuffer: 0,
/**
* The interval, in milliseconds that the scroll event handler will be debounced
* @type {number}
*/
debounceInt: 100,
/**
* A callback that must return `true` or `false`, signaling whether loading has completed. This callback is passed a `pageCount` argument.
* @type {function}
*/
doneLoading: null,
/**
* The interval, in milliseconds, that the doneLoading callback will be called
* @type {number}
*/
interval: 300,
/**
* The class that will be added to the target element once loadMore has been invoked
* @type {string}
*/
loadingClass: 'loading',
/**
* A selector targeting the element that will receive the class specified by the `loadingClass` option
* @type {string}
*/
loadingClassTarget: null,
/**
* The amount of time, in milliseconds, before the loadMore callback is invoked once the bottom of the scroll container has been reached
* @type {number}
*/
loadMoreDelay: 0,
/**
* A callback function that will be invoked when the scrollbar eclipses the bottom threshold of the scrolling element,
* @type {function}
*/
loadMore: $.noop,
/**
* If provided, the element that the scroll listener will be attached to. This can either be a selector or a DOM
* element reference. If not specified, the plugin will try to find the first scrollable parent if the element itself
* is not scrollable.
*/
scrollContainer: null,
/**
* The starting page count that the plugin increment each time loadMore is invoked
* @type {number}
*/
startingPageCount: 1,
/**
* Whether or not the plugin should make an initial call to loadMore. This can be set to true if, for instance, you need to load
* the initial content asynchronously on page load
* @type {boolean}
*/
triggerInitialLoad: false
};
/*-------------------------------------------- */
/** Plugin Constructor */
/*-------------------------------------------- */
/**
* The Plugin constructor
* @constructor
* @param {HTMLElement} element The element that will be monitored
* @param {object} options The plugin options
*/
function Plugin(element, options) {
this.options = $.extend({}, defaults, options);
this.$element = $(element);
this.$win = $(window);
this.$loadingClassTarget = this._getLoadingClassTarget();
this.$scrollContainer = this._getScrollContainer();
this.loading = false;
this.doneLoadingInt = null;
this.pageCount = this.options.triggerInitialLoad ? this.options.startingPageCount - 1 : this.options.startingPageCount;
this.destroyed = false;
this._init();
}
/*-------------------------------------------- */
/** Private Methods */
/*-------------------------------------------- */
/**
* Initializes the plugin
* @private
*/
Plugin.prototype._init = function() {
this._addListeners();
/* Call initial begin load if option is true. If not, simulate a scroll incase
the scroll element container height is greater than the contents */
if (this.options.triggerInitialLoad) {
this._beginLoadMore(this.options.loadMoreDelay);
} else {
this._handleScroll();
}
};
/**
* Returns the element that should have the loading class applied to it when
* the plugin is in the loading state
* @return {jQuery} The jQuery wrapped element
* @private
*/
Plugin.prototype._getLoadingClassTarget = function() {
return this.options.loadingClassTarget ? $(this.options.loadingClassTarget) : this.$element;
};
/**
* Finds the element that acts as the scroll container for the infinite
* scroll content
* @return {jQuery} The jQuery object that wraps the scroll container
*/
Plugin.prototype._getScrollContainer = function() {
// Use the options scrollContainer if provided
if (this.options.scrollContainer) return $(this.options.scrollContainer);
var self = this,
$scrollContainer = null;
// see if the target element is scrollable. If so, it is the scroll container
if (this._isScrollableElement(this.$element)) {
$scrollContainer = this.$element;
}
// Find first parent that is scrollable and use it as the scroll container
if (!$scrollContainer) {
$scrollContainer = this.$element.parents().filter(function() {
return self._isScrollableElement($(this));
});
}
// if the target element or any parent aren't scrollable,
// assume the window as the scroll container
$scrollContainer = $scrollContainer.length > 0 ? $scrollContainer : this.$win;
return $scrollContainer;
};
/**
* Determines if the provided $el is scrollable or not
* @param {jQuery} $el The jQuery instance to check
* @returns {boolean} Returns true if scrollable, false if not
* @private
*/
Plugin.prototype._isScrollableElement = function($el) {
return (/(auto|scroll)/).test($el.css('overflow') + $el.css('overflow-y'));
};
/**
* Adds listeners required for plugin to function
* @private
*/
Plugin.prototype._addListeners = function() {
var self = this;
this.$scrollContainer.on('scroll.' + pluginName, debounce(function() {
self._handleScroll();
}, this.options.debounceInt));
};
/**
* Removes all listeners required by the plugin
* @private
*/
Plugin.prototype._removeListeners = function() {
this.$scrollContainer.off('scroll.' + pluginName);
};
/**
* Handles the scroll logic and determines when to trigger the load more callback
* @private
*/
Plugin.prototype._handleScroll = function(e) {
var self = this;
if (this._shouldTriggerLoad()) {
this._beginLoadMore(this.options.loadMoreDelay);
// if a the doneLoading callback was provided, set an interval to check when to call it
if (this.options.doneLoading) {
this.doneLoadingInt = setInterval(
function() {
if (self.options.doneLoading(self.pageCount)) {
self._endLoadMore();
}
},
this.options.interval
);
}
}
};
/**
* Determines if the user scrolled far enough to trigger the load more callback
* @return {boolean} true if the load more callback should be triggered, false otherwise
* @private
*/
Plugin.prototype._shouldTriggerLoad = function() {
var elementBottom = this._getElementBottom(),
scrollBottom = this.$scrollContainer.scrollTop() + this.$scrollContainer.height() + this.options.bottomBuffer;
return (!this.loading && scrollBottom >= elementBottom && this.$element.is(':visible'));
};
/**
* Retrieves the height of the element being scrolled
* @return {number} The height of the element being scrolled
* @private
*/
Plugin.prototype._getElementHeight = function() {
if (this.$element == this.$scrollContainer) {
return this.$element[0].scrollHeight;
} else {
return this.$element.height();
}
};
/**
* Calculate the pixel height to the bottom of the scrolling element
* @returns {number} The pixel height to the bottom of the scrolling element.
* @private
*/
Plugin.prototype._getElementBottom = function() {
if (this.$element == this.$scrollContainer) {
return this._getElementHeight();
}
return this._getElementHeight() + this.$element.offset().top;
};
/**
* Initialize a call to the loadMore callback and set to loading state
* @param {number} delay The amount of time, in milliseconds, to wait before calling the load more callback
* @private
*/
Plugin.prototype._beginLoadMore = function(delay) {
delay = delay || 0;
setTimeout($.proxy(function() {
this.loading = true;
this._removeListeners();
this.pageCount++;
this.$loadingClassTarget.addClass(this.options.loadingClass);
this.options.loadMore(this.pageCount, $.proxy(this._endLoadMore, this));
}, this), delay);
};
/**
* Return the plugin to the not loading state
* @private
*/
Plugin.prototype._endLoadMore = function() {
clearInterval(this.doneLoadingInt);
this.loading = false;
this.$loadingClassTarget.removeClass(this.options.loadingClass);
!this.destroyed && this._addListeners();
};
/*-------------------------------------------- */
/** Public Methods */
/*-------------------------------------------- */
/**
* Destroys the plugin instance
* @public
*/
Plugin.prototype.destroy = function() {
this._removeListeners();
this.options.loadMore = null;
this.options.doneLoading = null;
$.data(this.$element[0], namespace, null);
clearInterval(this.doneLoadingInt);
this.destroyed = true;
};
/*-------------------------------------------- */
/** Helpers */
/*-------------------------------------------- */
// A utility method for calling methods on the plugin instance
function callMethod(instance, method, args) {
if ( instance && $.isFunction(instance[method]) ) {
instance[method].apply(instance, args);
}
}
// Borrowed from Underscore.js (http://underscorejs.org/)
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
/*-------------------------------------------- */
/** Plugin Definition */
/*-------------------------------------------- */
$.fn[pluginName] = function(options) {
var method = false,
methodArgs = arguments;
if (typeof options == 'string') {
method = options;
}
return this.each(function() {
var plugin = $.data(this, namespace);
if (!plugin && !method) {
$.data(this, namespace, new Plugin(this, options));
} else if (method) {
callMethod(plugin, method, Array.prototype.slice.call(methodArgs, 1));
}
});
};
// expose plugin constructor on window
window.InfiniteScrollHelper = window.InfiniteScrollHelper || Plugin;
})(jQuery, window);