UNPKG

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
/** * 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);