jquery-ias-es6
Version:
A jQuery plugin that turns your server-side pagination into an infinite scrolling one using AJAX
689 lines (573 loc) • 16.4 kB
JavaScript
/**
* Infinite Ajax Scroll v2.3.1
* A jQuery plugin for infinite scrolling
* https://infiniteajaxscroll.com
*
* Commercial use requires one-time purchase of a commercial license
* https://infiniteajaxscroll.com/docs/license.html
*
* Non-commercial use is licensed under the MIT License
*
* Copyright 2014-2018 Webcreate (Jeroen Fiege)
*/
import IASCallbacks from './callbacks';
export default (($) => {
'use strict';
var UNDETERMINED_SCROLLOFFSET = -1;
var IAS = function($element, options) {
this.itemsContainerSelector = options.container;
this.itemSelector = options.item;
this.nextSelector = options.next;
this.paginationSelector = options.pagination;
this.$scrollContainer = $element;
this.$container = (window === $element.get(0) ? $(document) : $element);
this.defaultDelay = options.delay;
this.negativeMargin = options.negativeMargin;
this.nextUrl = null;
this.isBound = false;
this.isPaused = false;
this.isInitialized = false;
this.jsXhr = false;
this.listeners = {
next: new IASCallbacks($),
load: new IASCallbacks($),
loaded: new IASCallbacks($),
render: new IASCallbacks($),
rendered: new IASCallbacks($),
scroll: new IASCallbacks($),
noneLeft: new IASCallbacks($),
ready: new IASCallbacks($)
};
this.extensions = [];
/**
* Scroll event handler
*
* Note: calls to this functions should be throttled
*
* @private
*/
this.scrollHandler = function() {
// the throttle method can call the scrollHandler even thought we have called unbind()
if (!this.isBound || this.isPaused) {
return;
}
var currentScrollOffset = this.getCurrentScrollOffset(this.$scrollContainer),
scrollThreshold = this.getScrollThreshold()
;
// invalid scrollThreshold. The DOM might not have loaded yet...
if (UNDETERMINED_SCROLLOFFSET == scrollThreshold) {
return;
}
this.fire('scroll', [currentScrollOffset, scrollThreshold]);
if (currentScrollOffset >= scrollThreshold) {
this.next();
}
};
/**
* Returns the items container currently in the DOM
*
* @private
* @returns {object}
*/
this.getItemsContainer = function() {
return $(this.itemsContainerSelector, this.$container);
};
/**
* Returns the last item currently in the DOM
*
* @private
* @returns {object}
*/
this.getLastItem = function() {
return $(this.itemSelector, this.getItemsContainer().get(0)).last();
};
/**
* Returns the first item currently in the DOM
*
* @private
* @returns {object}
*/
this.getFirstItem = function() {
return $(this.itemSelector, this.getItemsContainer().get(0)).first();
};
/**
* Returns scroll threshold. This threshold marks the line from where
* IAS should start loading the next page.
*
* @private
* @param negativeMargin defaults to {this.negativeMargin}
* @return {number}
*/
this.getScrollThreshold = function(negativeMargin) {
var $lastElement;
negativeMargin = negativeMargin || this.negativeMargin;
negativeMargin = (negativeMargin >= 0 ? negativeMargin * -1 : negativeMargin);
$lastElement = this.getLastItem();
// if the don't have a last element, the DOM might not have been loaded,
// or the selector is invalid
if (0 === $lastElement.length) {
return UNDETERMINED_SCROLLOFFSET;
}
return ($lastElement.offset().top + $lastElement.height() + negativeMargin);
};
/**
* Returns current scroll offset for the given scroll container
*
* @private
* @param $container
* @returns {number}
*/
this.getCurrentScrollOffset = function($container) {
var scrollTop = 0,
containerHeight = $container.height();
if (window === $container.get(0)) {
scrollTop = $container.scrollTop();
} else {
scrollTop = $container.offset().top;
}
// compensate for iPhone
if (navigator.platform.indexOf("iPhone") != -1 || navigator.platform.indexOf("iPod") != -1) {
containerHeight += 80;
}
return (scrollTop + containerHeight);
};
/**
* Returns the url for the next page
*
* @private
*/
this.getNextUrl = function(container) {
container = container || this.$container;
// always take the last matching item
return $(this.nextSelector, container).last().attr('href');
};
/**
* Loads a page url
*
* @param url
* @param callback
* @param delay
* @returns {object} jsXhr object
*/
this.load = function(url, callback, delay) {
var self = this,
$itemContainer,
items = [],
timeStart = +new Date(),
timeDiff;
delay = delay || this.defaultDelay;
var loadEvent = {
url: url,
ajaxOptions: {
dataType: 'html'
}
};
self.fire('load', [loadEvent]);
function xhrDoneCallback(data) {
$itemContainer = $(this.itemsContainerSelector, data).eq(0);
if (0 === $itemContainer.length) {
$itemContainer = $(data).filter(this.itemsContainerSelector).eq(0);
}
if ($itemContainer) {
$itemContainer.find(this.itemSelector).each(function() {
items.push(this);
});
}
self.fire('loaded', [data, items]);
if (callback) {
timeDiff = +new Date() - timeStart;
if (timeDiff < delay) {
setTimeout(function() {
callback.call(self, data, items);
}, delay - timeDiff);
} else {
callback.call(self, data, items);
}
}
}
this.jsXhr = $.ajax(loadEvent.url, loadEvent.ajaxOptions)
.done($.proxy(xhrDoneCallback, self));
return this.jsXhr;
};
/**
* Renders items
*
* @param callback
* @param items
*/
this.render = function(items, callback) {
var self = this,
$lastItem = this.getLastItem(),
count = 0;
var promise = this.fire('render', [items]);
promise.done(function() {
$(items).hide(); // at first, hide it so we can fade it in later
$lastItem.after(items);
$(items).fadeIn(400, function() {
// complete callback get fired for each item,
// only act on the last item
if (++count < items.length) {
return;
}
self.fire('rendered', [items]);
if (callback) {
callback();
}
});
});
promise.fail(function() {
if (callback) {
callback();
}
});
};
/**
* Hides the pagination
*/
this.hidePagination = function() {
if (this.paginationSelector) {
$(this.paginationSelector, this.$container).hide();
}
};
/**
* Restores the pagination
*/
this.restorePagination = function() {
if (this.paginationSelector) {
$(this.paginationSelector, this.$container).show();
}
};
/**
* Throttles a method
*
* Adopted from Ben Alman's jQuery throttle / debounce plugin
*
* @param callback
* @param delay
* @return {object}
*/
this.throttle = function(callback, delay) {
var lastExecutionTime = 0,
wrapper,
timerId
;
wrapper = function() {
var that = this,
args = arguments,
diff = +new Date() - lastExecutionTime;
function execute() {
lastExecutionTime = +new Date();
callback.apply(that, args);
}
if (!timerId) {
execute();
} else {
clearTimeout(timerId);
}
if (diff > delay) {
execute();
} else {
timerId = setTimeout(execute, delay);
}
};
if ($.guid) {
wrapper.guid = callback.guid = callback.guid || $.guid++;
}
return wrapper;
};
/**
* Fires an event with the ability to cancel further processing. This
* can be achieved by returning false in a listener.
*
* @param event
* @param args
* @returns {*}
*/
this.fire = function(event, args) {
return this.listeners[event].fireWith(this, args);
};
/**
* Pauses the scroll handler
*
* Note: internal use only, if you need to pause IAS use `unbind` method.
*
* @private
*/
this.pause = function() {
this.isPaused = true;
};
/**
* Resumes the scroll handler
*
* Note: internal use only, if you need to resume IAS use `bind` method.
*
* @private
*/
this.resume = function() {
this.isPaused = false;
};
return this;
};
/**
* Initialize IAS
*
* Note: Should be called when the document is ready
*
* @public
*/
IAS.prototype.initialize = function() {
if (this.isInitialized) {
return false;
}
var supportsOnScroll = (!!('onscroll' in this.$scrollContainer.get(0))),
currentScrollOffset = this.getCurrentScrollOffset(this.$scrollContainer),
scrollThreshold = this.getScrollThreshold();
// bail out when the browser doesn't support the scroll event
if (!supportsOnScroll) {
return false;
}
this.hidePagination();
this.bind();
this.nextUrl = this.getNextUrl();
if (!this.nextUrl) {
this.fire('noneLeft', [this.getLastItem()]);
}
// start loading next page if content is shorter than page fold
if (this.nextUrl && currentScrollOffset >= scrollThreshold) {
this.next();
// flag as initialized when rendering is completed
this.one('rendered', function() {
this.isInitialized = true;
this.fire('ready');
});
} else {
this.isInitialized = true;
this.fire('ready');
}
return this;
};
/**
* Reinitializes IAS, for example after an ajax page update
*
* @public
*/
IAS.prototype.reinitialize = function () {
this.isInitialized = false;
this.unbind();
this.initialize();
};
/**
* Binds IAS to DOM events
*
* @public
*/
IAS.prototype.bind = function() {
if (this.isBound) {
return;
}
this.$scrollContainer.on('scroll', $.proxy(this.throttle(this.scrollHandler, 150), this));
for (var i = 0, l = this.extensions.length; i < l; i++) {
this.extensions[i].bind(this);
}
this.isBound = true;
this.resume();
};
/**
* Unbinds IAS to events
*
* @public
*/
IAS.prototype.unbind = function() {
if (!this.isBound) {
return;
}
this.$scrollContainer.off('scroll', this.scrollHandler);
// notify extensions about unbinding
for (var i = 0, l = this.extensions.length; i < l; i++) {
if (typeof this.extensions[i]['unbind'] != 'undefined') {
this.extensions[i].unbind(this);
}
}
this.isBound = false;
};
/**
* Destroys IAS instance
*
* @public
*/
IAS.prototype.destroy = function() {
try {
this.jsXhr.abort();
} catch (e) {}
this.unbind();
this.$scrollContainer.data('ias', null);
};
/**
* Registers an eventListener
*
* Note: chainable
*
* @public
* @returns IAS
*/
IAS.prototype.on = function(event, callback, priority) {
if (typeof this.listeners[event] == 'undefined') {
throw new Error('There is no event called "' + event + '"');
}
priority = priority || 0;
this.listeners[event].add($.proxy(callback, this), priority);
// ready is already fired, before on() could even be called, so
// let's call the callback right away
if (this.isInitialized) {
if (event === 'ready') {
$.proxy(callback, this)();
}
// same applies to noneLeft
else if (event === 'noneLeft' && !this.nextUrl) {
$.proxy(callback, this)();
}
}
return this;
};
/**
* Registers an eventListener which only gets
* fired once.
*
* Note: chainable
*
* @public
* @returns IAS
*/
IAS.prototype.one = function(event, callback) {
var self = this;
var remover = function() {
self.off(event, callback);
self.off(event, remover);
};
this.on(event, callback);
this.on(event, remover);
return this;
};
/**
* Removes an eventListener
*
* Note: chainable
*
* @public
* @returns IAS
*/
IAS.prototype.off = function(event, callback) {
if (typeof this.listeners[event] == 'undefined') {
throw new Error('There is no event called "' + event + '"');
}
this.listeners[event].remove(callback);
return this;
};
/**
* Load the next page
*
* @public
*/
IAS.prototype.next = function() {
var url = this.nextUrl,
self = this;
if (!url) {
return false;
}
this.pause();
var promise = this.fire('next', [url]);
promise.done(function() {
self.load(url, function(data, items) {
self.render(items, function() {
self.nextUrl = self.getNextUrl(data);
if (!self.nextUrl) {
self.fire('noneLeft', [self.getLastItem()]);
}
self.resume();
});
});
});
promise.fail(function() {
self.resume();
});
return true;
};
/**
* Adds an extension
*
* @public
*/
IAS.prototype.extension = function(extension) {
if (typeof extension['bind'] == 'undefined') {
throw new Error('Extension doesn\'t have required method "bind"');
}
if (typeof extension['initialize'] != 'undefined') {
extension.initialize(this);
}
this.extensions.push(extension);
if (this.isBound) {
this.reinitialize();
}
return this;
};
/**
* Shortcut. Sets the window as scroll container.
*
* @public
* @param option
* @returns {*}
*/
$.ias = function(option) {
var $window = $(window);
return $window.ias.apply($window, arguments);
};
/**
* jQuery plugin initialization
*
* @public
* @param option
* @returns {*} the last IAS instance will be returned
*/
$.fn.ias = function(option) {
var args = Array.prototype.slice.call(arguments);
var retval = this;
this.each(function() {
var $this = $(this),
instance = $this.data('ias'),
options = $.extend({}, $.fn.ias.defaults, $this.data(), typeof option == 'object' && option)
;
// set a new instance as data
if (!instance) {
$this.data('ias', (instance = new IAS($this, options)));
if (options.initialize) {
$(document).ready($.proxy(instance.initialize, instance));
}
}
// when the plugin is called with a method
if (typeof option === 'string') {
if (typeof instance[option] !== 'function') {
throw new Error('There is no method called "' + option + '"');
}
args.shift(); // remove first argument ('option')
instance[option].apply(instance, args);
}
retval = instance;
});
return retval;
};
/**
* Plugin defaults
*
* @public
* @type {object}
*/
$.fn.ias.defaults = {
item: '.item',
container: '.listing',
next: '.next',
pagination: false,
delay: 600,
negativeMargin: 10,
initialize: true
};
})(jQuery);