'use strict'; /** * Infinite Scroll * @param {Element} element Target DOM element * @param {Object} config Configuration object */ function InfiniteScroller(element, config) { // Private variables var currentPage; var state; var busy = false; var scrollTimeout = null; // Default options var options = { placeholderClass: 'placeholder', /** * AJAX request handler. * When instantiating this module this function must be specified as an option. * This function is where you should do your AJAX call to return HTML to this plugin. * * @param {Number} page Page number to fetch. 1 based. * @return {Promise} Promise which should resolve with the HTML to be appended to the main element */ request: function(page) { return new Promise(function(resolve, reject) { resolve('

Please override options.request when instantiating this plugin

'); }); }, /** * Callback which can be used to process the returned results before they are put in the DOM * @param {DocumentFragment} results Collection of DOM items */ processResults: function(results) { } }; // Lightweight extend to avoid dependency on a deep extend function for (var property in options) { if (options.hasOwnProperty(property)) { options[property] = config[property]; } } /** * Init method */ function init() { reset(); // Bind a scroll listener which we'll use to trigger new pages to load window.addEventListener('scroll', scrollListener); } /** * Public method to reset the product list * Called on init() and also exposed publicy so the list can be reset from * the calling script. */ function reset() { currentPage = 0; state = { pages: {} }; element.innerHTML = ''; loadInitialBatch(); } /** * Private helper method to load an initial batch of results */ function loadInitialBatch() { // For as long as the bottom of the element is visible, let's request pages to fill up the viewport requestNextPage() .then(function() { if(pageAtBottom()) { loadInitialBatch(); } }); } /** * Private method to request the next page of results */ function requestNextPage() { var page = ++currentPage; busy = true; // Pop a placeholder in the DOM straight away which we will replace with // the results later // Otherwise, if multiple pages got requested in quick succession, there // would be a race condition which could result in the pages being // appended to the DOM out of order. var placeholder = document.createElement('div'); placeholder.classList.add(options.placeholderClass); element.appendChild(placeholder); // Get the next page and append the results to the DOM var promise = getPage(page) .then(function(results) { // Fire off our callback to process the results // Used by the product listing to add adverts options.processResults(results); element.replaceChild(results, placeholder); busy = false; }); // Pre-fetch the page after that, but don't do anything with the results getPage(++page); return promise; } /** * Private method to get a page of results * @param {Number} requestedPage Page number to fetch * @return {Promise} Promise that resolves when the request is complete */ function getPage(requestedPage) { return new Promise(function(resolve, reject) { // If we have already fetched this page, send it right back if(state.pages[requestedPage]) { resolve(state.pages[requestedPage]); } else { // Fire off a request using the passed in handler options.request(requestedPage) .then(function(html) { // And then append the resulting items to the DOM // We will do this via a document fragment in order to keep DOM manipulation to a minimum var fragment = document.createDocumentFragment(); var temp = document.createElement('div'); // Pop the html into a temporary element before slurping it out again as you cannot use innerHTML on a document fragment temp.innerHTML = html; while (temp.hasChildNodes()) { fragment.appendChild(temp.removeChild(temp.firstChild)); } // Store the fragment on our state object state.pages[requestedPage] = fragment; // And then return the fragment resolve(fragment); }); } }); } /** * Scroll event listener to trigger new page loads * @param {Event} e The scroll event */ function scrollListener(e) { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(function() { if(!busy && pageAtBottom()) { requestNextPage(); } }, 100); } /** * Helper method to determine if the page is at the bottom * @return {Boolean} Boolean representing whether the page is at the bottom */ function pageAtBottom() { return (element.getBoundingClientRect().top + element.clientHeight + window.pageYOffset - 50) < (window.pageYOffset + window.innerHeight); } /** * Tear everything down again */ function destroy() { window.removeEventListener('scroll', scrollListener); } /** * Public methods */ var self = { init: init, reset: reset, destroy: destroy }; return self; } // Expose as a CommonJS module for browserify if (typeof module !== 'undefined' && module.exports) { module.exports = InfiniteScroller; } else { // Otherwise expose as a global for non browserify users window.InfiniteScroller = InfiniteScroller; }