UNPKG

@webcreate/infinite-ajax-scroll

Version:

Turn your existing pagination into infinite scrolling pages with ease

1,365 lines (1,063 loc) 33.4 kB
/** * Infinite Ajax Scroll v3.1.0 * Turn your existing pagination into infinite scrolling pages with ease * * Commercial use requires one-time purchase of a commercial license * https://infiniteajaxscroll.com/docs/license.html * * Copyright 2014-2023 Webcreate (Jeroen Fiege) * https://infiniteajaxscroll.com */ import $ from 'tealight'; import extend from 'extend'; import throttle from 'lodash.throttle'; import Emitter from 'tiny-emitter'; var defaults$3 = { item: undefined, next: undefined, prev: undefined, pagination: undefined, responseType: 'document', bind: true, scrollContainer: window, spinner: false, logger: true, loadOnScroll: true, negativeMargin: 0, trigger: false, prefill: true, }; /* eslint no-console: "off" */ var Assert = { singleElement: function singleElement(elementOrSelector, property) { var $element = $(elementOrSelector); if ($element.length > 1) { throw new Error(("Expected single element for \"" + property + "\"")); } if ($element.length === 0) { throw new Error(("Element \"" + elementOrSelector + "\" not found for \"" + property + "\"")); } }, anyElement: function anyElement(elementOrSelector, property) { var $element = $(elementOrSelector); if ($element.length === 0) { throw new Error(("Element \"" + elementOrSelector + "\" not found for \"" + property + "\"")); } }, warn: function warn(fn) { var args = [], len = arguments.length - 1; while ( len-- > 0 ) args[ len ] = arguments[ len + 1 ]; try { fn.apply(void 0, args); } catch (e) { if (console && console.warn) { console.warn(e.message); } } } }; function getScrollPosition(el) { if (el !== window) { return { x: el.scrollLeft, y: el.scrollTop, }; } var supportPageOffset = window.pageXOffset !== undefined; var isCSS1Compat = ((document.compatMode || "") === "CSS1Compat"); return { x: supportPageOffset ? window.pageXOffset : isCSS1Compat ? document.documentElement.scrollLeft : document.body.scrollLeft, y: supportPageOffset ? window.pageYOffset : isCSS1Compat ? document.documentElement.scrollTop : document.body.scrollTop }; } function getRootRect(el) { var rootRect; if (el !== window) { rootRect = el.getBoundingClientRect(); } else { // Use <html>/<body> instead of window since scroll bars affect size. var html = document.documentElement; var body = document.body; rootRect = { top: 0, left: 0, right: html.clientWidth || body.clientWidth, width: html.clientWidth || body.clientWidth, bottom: html.clientHeight || body.clientHeight, height: html.clientHeight || body.clientHeight }; } return rootRect; } function getDistanceToFold(sentinel, scrollContainerScrollPosition, scrollContainerRootRect) { var rootRect = scrollContainerRootRect; // this means the container the doesn't have any items yet - it's empty if (!sentinel) { return rootRect.height * -1; } var scrollYTop = scrollContainerScrollPosition.y; var boundingRect = sentinel.getBoundingClientRect(); var scrollYBottom = scrollYTop + rootRect.height; var bottom = scrollYTop + boundingRect.bottom - rootRect.top; return Math.trunc(bottom - scrollYBottom); } var APPEND = 'append'; var APPENDED = 'appended'; var PREPEND = 'prepend'; var PREPENDED = 'prepended'; var BINDED = 'binded'; var UNBINDED = 'unbinded'; var HIT = 'hit'; var TOP = 'top'; var LOAD = 'load'; var LOADED = 'loaded'; var ERROR = 'error'; var FIRST = 'first'; var LAST = 'last'; var NEXT = 'next'; var NEXTED = 'nexted'; var PREV = 'prev'; var PREVED = 'preved'; var READY = 'ready'; var SCROLLED = 'scrolled'; var RESIZED = 'resized'; var PAGE = 'page'; var PREFILL = 'prefill'; var PREFILLED = 'prefilled'; var events = { APPEND: APPEND, APPENDED: APPENDED, PREPEND: PREPEND, PREPENDED: PREPENDED, BINDED: BINDED, UNBINDED: UNBINDED, HIT: HIT, TOP: TOP, LOAD: LOAD, LOADED: LOADED, ERROR: ERROR, FIRST: FIRST, LAST: LAST, NEXT: NEXT, NEXTED: NEXTED, PREV: PREV, PREVED: PREVED, READY: READY, SCROLLED: SCROLLED, RESIZED: RESIZED, PAGE: PAGE, PREFILL: PREFILL, PREFILLED: PREFILLED, }; var defaultLastScroll = { y: 0, x: 0, deltaY: 0, deltaX: 0 }; function calculateScroll(scrollContainer, lastScroll) { var scroll = getScrollPosition(scrollContainer); scroll.deltaY = scroll.y - (lastScroll ? lastScroll.y : scroll.y); scroll.deltaX = scroll.x - (lastScroll ? lastScroll.x : scroll.x); return scroll; } function scrollHandler() { var ias = this; var lastScroll = ias._lastScroll || defaultLastScroll; var scroll = ias._lastScroll = calculateScroll(ias.scrollContainer, lastScroll); this.emitter.emit(SCROLLED, {scroll: scroll}); } function resizeHandler() { var ias = this; var lastScroll = ias._lastScroll || defaultLastScroll; var scroll = ias._lastScroll = calculateScroll(ias.scrollContainer, lastScroll); this.emitter.emit(RESIZED, {scroll: scroll}); } function nextHandler(pageIndex) { var ias = this; var lastResponse = ias._lastResponse || document.body; var nextEl = $(ias.options.next, lastResponse)[0]; if (!nextEl) { Assert.warn(Assert.singleElement, ias.options.next, 'options.next'); return; } var nextUrl = nextEl.href; return ias.load(nextUrl) .then(function (data) { lastResponse = ias._lastResponse = data.xhr.response; var nextEl = $(ias.options.next, lastResponse)[0]; return ias.append(data.items) .then(function () { return !!nextEl; }) .then(function (hasNextEl) { // only warn for first page, because at some point it's expected that there is no next element if (!hasNextEl && pageIndex <= 1 && console && console.warn) { console.warn(("Element \"" + (ias.options.next) + "\" not found for \"options.next\" on \"" + (data.url) + "\"")); } return hasNextEl; }); }); } function prevHandler(pageIndex) { var ias = this; var prevEl = ias._prevEl || $(ias.options.prev, document.body)[0]; if (ias.options.prev === undefined) { return; } if (!prevEl) { Assert.warn(Assert.singleElement, ias.options.prev, 'options.prev'); return; } var prevUrl = prevEl.href; return ias.load(prevUrl) .then(function (data) { var prevEl = ias._prevEl = $(ias.options.prev, data.xhr.response)[0]; return ias.prepend(data.items) .then(function () { return !!prevEl; }) // TODO: evaluate if this makes sense }); } var defaults$2 = { element: undefined, hide: false }; function expand$3(options) { if (typeof options === 'string' || (typeof options === 'object' && options.nodeType === Node.ELEMENT_NODE)) { options = { element: options, hide: true, }; } else if (typeof options === 'boolean') { options = { element: undefined, hide: options, }; } return options; } var Pagination = function Pagination(ias, options) { this.options = extend({}, defaults$2, expand$3(options)); this.originalDisplayStyles = new WeakMap(); if (!this.options.hide) { return; } Assert.warn(Assert.anyElement, this.options.element, 'pagination.element'); ias.on(BINDED, this.hide.bind(this)); ias.on(UNBINDED, this.restore.bind(this)); }; Pagination.prototype.hide = function hide () { var this$1$1 = this; var els = $(this.options.element); els.forEach(function (el) { this$1$1.originalDisplayStyles.set(el, window.getComputedStyle(el).display); el.style.display = 'none'; }); }; Pagination.prototype.restore = function restore () { var this$1$1 = this; var els = $(this.options.element); els.forEach(function (el) { el.style.display = this$1$1.originalDisplayStyles.get(el) || 'block'; }); }; var defaults$1 = { element: undefined, delay: 600, show: function (element) { element.style.opacity = '1'; }, hide: function (element) { element.style.opacity = '0'; } }; function expand$2(options) { if (typeof options === 'string' || (typeof options === 'object' && options.nodeType === Node.ELEMENT_NODE)) { options = { element: options, }; } return options; } var Spinner = function Spinner(ias, options) { // no spinner wanted if (options === false) { return; } this.ias = ias; this.options = extend({}, defaults$1, expand$2(options)); if (this.options.element !== undefined) { Assert.singleElement(this.options.element, 'spinner.element'); } this.element = $(this.options.element)[0]; // @todo should we really cache this? this.hideFn = this.options.hide; this.showFn = this.options.show; ias.on(BINDED, this.bind.bind(this)); ias.on(BINDED, this.hide.bind(this)); }; Spinner.prototype.bind = function bind () { var startTime, endTime, diff, delay, self = this, ias = this.ias; ias.on(NEXT, function () { startTime = +new Date(); self.show(); }); ias.on(LAST, function () { self.hide(); }); // setup delay ias.on(APPEND, function (event) { endTime = +new Date(); diff = endTime - startTime; delay = Math.max(0, self.options.delay - diff); var _appendFn = event.appendFn.bind({}); event.appendFn = function(items, parent, last) { return new Promise(function (resolve) { setTimeout(function() { self.hide().then(function() { _appendFn(items, parent, last); resolve(); }); }, delay); }); }; }); }; Spinner.prototype.show = function show () { return Promise.resolve(this.showFn(this.element)); }; Spinner.prototype.hide = function hide () { return Promise.resolve(this.hideFn(this.element)); }; /* eslint no-console: "off" */ var defaultLogger = { hit: function () { console.log("Hit scroll threshold"); }, top: function () { console.log("Hit top scroll threshold"); }, binded: function () { console.log("Binded event handlers"); }, unbinded: function () { console.log("Unbinded event handlers"); }, // scrolled: (event) => { // console.log('Scrolled'); // }, // resized: (event) => { // console.log('Resized'); // }, next: function (event) { console.log(("Next page triggered [pageIndex=" + (event.pageIndex) + "]")); }, nexted: function (event) { console.log(("Next page completed [pageIndex=" + (event.pageIndex) + "]")); }, prev: function (event) { console.log(("Previous page triggered [pageIndex=" + (event.pageIndex) + "]")); }, preved: function (event) { console.log(("Previous page completed [pageIndex=" + (event.pageIndex) + "]")); }, load: function (event) { console.log(("Start loading " + (event.url))); }, loaded: function () { console.log("Finished loading"); }, append: function () { console.log("Start appending items"); }, appended: function (event) { console.log(("Finished appending " + (event.items.length) + " item(s)")); }, prepend: function () { console.log("Start prepending items"); }, prepended: function (event) { console.log(("Finished prepending " + (event.items.length) + " item(s)")); }, last: function () { console.log("No more pages left to load"); }, first: function () { console.log("Reached first page"); }, page: function (event) { console.log(("Page changed [pageIndex=" + (event.pageIndex) + "]")); }, prefill: function (event) { console.log("Start prefilling"); }, prefilled: function (event) { console.log("Finished prefilling"); }, }; function expand$1(options) { if (options === true) { options = defaultLogger; } return options; } var Logger = function Logger(ias, options) { // no logger wanted if (options === false) { return; } var logger = expand$1(options); Object.keys(logger).forEach(function (key) { ias.on(key, logger[key]); }); }; function getPageBreak(pageBreaks, scrollTop, scrollContainer) { var rootRect = getRootRect(scrollContainer); var scrollBottom = scrollTop + rootRect.height; for (var b = pageBreaks.length - 1; b >= 0; b--) { var bottom = pageBreaks[b].sentinel.getBoundingClientRect().bottom + scrollTop; if (scrollBottom > bottom) { var x = Math.min(b + 1, pageBreaks.length - 1); return pageBreaks[x]; } } return pageBreaks[0]; } var Paging = function Paging(ias) { this.ias = ias; this.pageBreaks = []; this.currentPageIndex = ias.pageIndex; this.currentScrollTop = 0; ias.on(BINDED, this.binded.bind(this)); ias.on(NEXT, this.next.bind(this)); ias.on(PREV, this.prev.bind(this)); ias.on(SCROLLED, this.scrolled.bind(this)); ias.on(RESIZED, this.scrolled.bind(this)); }; Paging.prototype.binded = function binded () { var sentinel = this.ias.sentinel(); if (!sentinel) { return; } this.pageBreaks.push({ pageIndex: this.currentPageIndex, url: document.location.toString(), title: document.title, sentinel: this.ias.sentinel() }); }; Paging.prototype.next = function next () { var this$1$1 = this; var url = document.location.toString(); var title = document.title; var loaded = function (event) { url = event.url; if (event.xhr.response) { title = event.xhr.response.title; } }; this.ias.once(LOADED, loaded); this.ias.once(NEXTED, function (event) { this$1$1.pageBreaks.push({ pageIndex: event.pageIndex, url: url, title: title, sentinel: this$1$1.ias.sentinel() }); this$1$1.update(); this$1$1.ias.off(LOADED, loaded); }); }; Paging.prototype.prev = function prev () { var this$1$1 = this; var url = document.location.toString(); var title = document.title; var loaded = function (event) { url = event.url; if (event.xhr.response) { title = event.xhr.response.title; } }; this.ias.once(LOADED, loaded); this.ias.once(PREVED, function (event) { this$1$1.pageBreaks.unshift({ pageIndex: event.pageIndex, url: url, title: title, sentinel: this$1$1.ias.first() }); this$1$1.update(); this$1$1.ias.off(LOADED, loaded); }); }; Paging.prototype.scrolled = function scrolled (event) { this.update(event.scroll.y); }; Paging.prototype.update = function update (scrollTop) { this.currentScrollTop = scrollTop || this.currentScrollTop; var pageBreak = getPageBreak(this.pageBreaks, this.currentScrollTop, this.ias.scrollContainer); if (pageBreak && pageBreak.pageIndex !== this.currentPageIndex) { this.ias.emitter.emit(PAGE, pageBreak); this.currentPageIndex = pageBreak.pageIndex; } }; var defaults = { element: undefined, when: function (pageIndex) { return true; }, show: function (element) { element.style.opacity = '1'; }, hide: function (element) { element.style.opacity = '0'; } }; function expand(options) { if (typeof options === 'string' || typeof options === 'function' || (typeof options === 'object' && options.nodeType === Node.ELEMENT_NODE)) { options = { element: options, }; } if (typeof options.element === 'function') { options.element = options.element(); } // expand array to a function, e.g.: // [0, 1, 2] -> function(pageIndex) { /* return true when pageIndex in [0, 1, 2] */ } if (options.when && Array.isArray(options.when)) { var when = options.when; options.when = function(pageIndex) { return when.indexOf(pageIndex) !== -1; }; } return options; } var Trigger = function Trigger(ias, options) { var this$1$1 = this; // no trigger wanted if (options === false) { return; } this.ias = ias; this.options = extend({}, defaults, expand(options)); if (this.options.element !== undefined) { Assert.singleElement(this.options.element, 'trigger.element'); } this.element = $(this.options.element)[0]; // @todo should we really cache this? this.hideFn = this.options.hide; this.showFn = this.options.show; this.voter = this.options.when; this.showing = undefined; this.enabled = undefined; ias.on(BINDED, this.bind.bind(this)); ias.on(UNBINDED, this.unbind.bind(this)); ias.on(HIT, this.hit.bind(this)); ias.on(NEXT, function (e) { return this$1$1.ias.once(APPENDED, function () { return this$1$1.update(e.pageIndex); }); }); }; Trigger.prototype.bind = function bind () { this.hide(); this.update(this.ias.pageIndex); this.element.addEventListener('click', this.clickHandler.bind(this)); }; Trigger.prototype.unbind = function unbind () { this.element.removeEventListener('click', this.clickHandler.bind(this)); }; Trigger.prototype.clickHandler = function clickHandler () { this.hide().then(this.ias.next.bind(this.ias)); }; Trigger.prototype.update = function update (pageIndex) { this.enabled = this.voter(pageIndex); if (this.enabled) { this.ias.disableLoadOnScroll(); } else { this.ias.enableLoadOnScroll(); } }; Trigger.prototype.hit = function hit () { if (!this.enabled) { return; } this.show(); }; Trigger.prototype.show = function show () { if (this.showing) { return; } this.showing = true; return Promise.resolve(this.showFn(this.element)); }; Trigger.prototype.hide = function hide () { if (!this.showing && this.showing !== undefined) { return; } this.showing = false; return Promise.resolve(this.hideFn(this.element)); }; function appendFn(items, parent, last) { var sibling = last ? last.nextSibling : null; var insert = document.createDocumentFragment(); items.forEach(function (item) { insert.appendChild(item); }); parent.insertBefore(insert, sibling); } function prependFn(items, parent, first) { var insert = document.createDocumentFragment(); items.forEach(function (item) { insert.appendChild(item); }); parent.insertBefore(insert, first); } /* eslint no-console: "off" */ var NativeResizeObserver = window.ResizeObserver; var EventListenerResizeObserver = function EventListenerResizeObserver(el, listener) { this.el = el; this.listener = listener; }; EventListenerResizeObserver.prototype.observe = function observe () { this.el.addEventListener('resize', this.listener); }; EventListenerResizeObserver.prototype.unobserve = function unobserve () { this.el.removeEventListener('resize', this.listener); }; var NativeWrapperResizeObserver = function NativeWrapperResizeObserver(el, listener) { this.el = el; this.listener = listener; this.ro = new NativeResizeObserver(this.listener); }; NativeWrapperResizeObserver.prototype.observe = function observe () { this.ro.observe(this.el); }; NativeWrapperResizeObserver.prototype.unobserve = function unobserve () { this.ro.unobserve(); }; var PollingResizeObserver = function PollingResizeObserver(el, listener) { this.el = el; this.listener = listener; this.interval = null; this.lastHeight = null; }; PollingResizeObserver.prototype.pollHeight = function pollHeight () { var height = Math.trunc(getRootRect(this.el).height); if (this.lastHeight !== null && this.lastHeight !== height) { this.listener(); } this.lastHeight = height; }; PollingResizeObserver.prototype.observe = function observe () { this.interval = setInterval(this.pollHeight.bind(this), 200); }; PollingResizeObserver.prototype.unobserve = function unobserve () { clearInterval(this.interval); }; function ResizeObserverFactory(ias, el) { var listener = throttle(resizeHandler, 200).bind(ias); if (el === window) { return new EventListenerResizeObserver(el, listener); } if (NativeResizeObserver) { return new NativeWrapperResizeObserver(el, listener); } if (console && console.warn) { console.warn('ResizeObserver not supported. Falling back on polling.'); } return new PollingResizeObserver(el, listener); } var Prefill = function Prefill(ias, options) { this.ias = ias; this.enabled = options; }; Prefill.prototype.prefill = function prefill () { var this$1$1 = this; if (!this.enabled) { return; } this.ias.emitter.emit(events.PREFILL); return Promise.all([this._prefillNext(), this._prefillPrev()]).then(function () { this$1$1.ias.emitter.emit(events.PREFILLED); // @todo reevaluate if we should actually call `measure` here. this$1$1.ias.measure(); }); }; Prefill.prototype._prefillNext = function _prefillNext () { var this$1$1 = this; var distance = this.ias.distance(); if (distance > 0) { return; } return this.ias.next() .then(function (hasNextUrl) { if (!hasNextUrl) { return; } var distance = this$1$1.ias.distance(); if (distance < 0) { return this$1$1._prefillNext(); } }) ; }; Prefill.prototype._prefillPrev = function _prefillPrev () { if (!this.ias.options.prev) { return; } return this.ias.prev(); }; var InfiniteAjaxScroll = function InfiniteAjaxScroll(container, options) { var this$1$1 = this; if ( options === void 0 ) options = {}; Assert.singleElement(container, 'container'); this.container = $(container)[0]; this.options = extend({}, defaults$3, options); this.emitter = new Emitter(); this.options.loadOnScroll ? this.enableLoadOnScroll() : this.disableLoadOnScroll(); this.negativeMargin = Math.abs(this.options.negativeMargin); this.scrollContainer = this.options.scrollContainer; if (this.options.scrollContainer !== window) { Assert.singleElement(this.options.scrollContainer, 'options.scrollContainer'); this.scrollContainer = $(this.options.scrollContainer)[0]; } this.nextHandler = nextHandler; this.prevHandler = prevHandler; if (this.options.next === false) { this.nextHandler = function() {}; } else if (typeof this.options.next === 'function') { this.nextHandler = this.options.next; } if (this.options.prev === false) { this.prevHandler = function() {}; } else if (typeof this.options.prev === 'function') { this.prevHandler = this.options.prev; } this.resizeObserver = ResizeObserverFactory(this, this.scrollContainer); this._scrollListener = throttle(scrollHandler, 200).bind(this); this.ready = false; this.bindOnReady = true; this.binded = false; this.paused = false; this.pageIndexPrev = 0; this.pageIndex = this.pageIndexNext = this.sentinel() ? 0 : -1; this.on(HIT, function () { if (!this$1$1.loadOnScroll) { return; } this$1$1.next(); }); this.on(TOP, function () { if (!this$1$1.loadOnScroll) { return; } this$1$1.prev(); }); this.on(SCROLLED, this.measure); this.on(RESIZED, this.measure); // initialize extensions this.pagination = new Pagination(this, this.options.pagination); this.spinner = new Spinner(this, this.options.spinner); this.logger = new Logger(this, this.options.logger); this.paging = new Paging(this); this.trigger = new Trigger(this, this.options.trigger); this.prefill = new Prefill(this, this.options.prefill); // prefill/measure after all plugins are done binding this.on(BINDED, this.prefill.prefill.bind(this.prefill)); this.hitFirst = this.hitLast = false; this.on(LAST, function () { return this$1$1.hitLast = true; }); this.on(FIRST, function () { return this$1$1.hitFirst = true; }); var ready = function () { if (this$1$1.ready) { return; } this$1$1.ready = true; this$1$1.emitter.emit(READY); if (this$1$1.bindOnReady && this$1$1.options.bind) { this$1$1.bind(); } }; if (document.readyState === "complete" || document.readyState === "interactive") { setTimeout(ready, 1); } else { window.addEventListener('DOMContentLoaded', ready); } }; InfiniteAjaxScroll.prototype.bind = function bind () { if (this.binded) { return; } // If we manually call bind before the dom is ready, we assume that we want // to take control over the bind flow. if (!this.ready) { this.bindOnReady = false; } this.scrollContainer.addEventListener('scroll', this._scrollListener); this.resizeObserver.observe(); this.binded = true; this.emitter.emit(BINDED); }; InfiniteAjaxScroll.prototype.unbind = function unbind () { if (!this.binded) { if (!this.ready) { this.once(BINDED, this.unbind); } return; } this.resizeObserver.unobserve(); this.scrollContainer.removeEventListener('scroll', this._scrollListener); this.binded = false; this.emitter.emit(UNBINDED); }; InfiniteAjaxScroll.prototype.next = function next () { var this$1$1 = this; if (this.hitLast) { return; } if (!this.binded) { if (!this.ready) { return this.once(BINDED, this.next); } return; } this.pause(); var pageIndex = this.pageIndexNext + 1; this.emitter.emit(NEXT, {pageIndex: this.pageIndexNext + 1}); return Promise.resolve(this.nextHandler(pageIndex)) .then(function (hasNextUrl) { this$1$1.pageIndexNext = pageIndex; if (!hasNextUrl) { this$1$1.emitter.emit(LAST); } this$1$1.resume(); return hasNextUrl; }).then(function (hasNextUrl) { this$1$1.emitter.emit(NEXTED, {pageIndex: this$1$1.pageIndexNext}); return hasNextUrl; }); }; InfiniteAjaxScroll.prototype.prev = function prev () { var this$1$1 = this; if (!this.binded || this.hitFirst) { return; } this.pause(); var pageIndex = this.pageIndexPrev - 1; this.emitter.emit(PREV, {pageIndex: this.pageIndexPrev - 1}); return Promise.resolve(this.prevHandler(pageIndex)) .then(function (hasPrevUrl) { this$1$1.pageIndexPrev = pageIndex; this$1$1.resume(); if (!hasPrevUrl) { this$1$1.emitter.emit(FIRST); } return hasPrevUrl; }).then(function (hasPrevUrl) { this$1$1.emitter.emit(PREVED, {pageIndex: this$1$1.pageIndexPrev}); return hasPrevUrl; }); }; /** * @param {string} url * @returns {Promise} returns LOADED event on success */ InfiniteAjaxScroll.prototype.load = function load (url) { var ias = this; return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); var loadEvent = { url: url, xhr: xhr, method: 'GET', body: null, nocache: false, responseType: ias.options.responseType, headers: { 'X-Requested-With': 'XMLHttpRequest', }, }; // event properties are mutable ias.emitter.emit(LOAD, loadEvent); var finalUrl = loadEvent.url; var method = loadEvent.method; var responseType = loadEvent.responseType; var headers = loadEvent.headers; var body = loadEvent.body; if (!loadEvent.nocache) { // @see https://developer.mozilla.org/nl/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache finalUrl = finalUrl + ((/\?/).test(finalUrl) ? "&" : "?") + (new Date()).getTime(); } xhr.onreadystatechange = function() { if (xhr.readyState !== XMLHttpRequest.DONE) { return; } if (xhr.status === 0) ; else if (xhr.status === 200) { var items = xhr.response; if (responseType === 'document') { items = $(ias.options.item, xhr.response); // @todo assert there actually are items in the response } // we don't use a shared loadedEvent variable here, because these values should be immutable ias.emitter.emit(LOADED, {items: items, url: finalUrl, xhr: xhr}); resolve({items: items, url: finalUrl, xhr: xhr}); } else { ias.emitter.emit(ERROR, {url: finalUrl, method: method, xhr: xhr}); reject(xhr); } }; xhr.onerror = function() { ias.emitter.emit(ERROR, {url: finalUrl, method: method, xhr: xhr}); reject(xhr); }; xhr.open(method, finalUrl, true); xhr.responseType = responseType; for (var header in headers) { xhr.setRequestHeader(header, headers[header]); } xhr.send(body); }); }; /** * @param {array<Element>} items * @param {Element|null} parent */ InfiniteAjaxScroll.prototype.append = function append (items, parent) { var ias = this; parent = parent || ias.container; var event = { items: items, parent: parent, appendFn: appendFn }; ias.emitter.emit(APPEND, event); var executor = function (resolve) { window.requestAnimationFrame(function () { Promise.resolve(event.appendFn(event.items, event.parent, ias.sentinel())).then(function () { resolve({items: items, parent: parent}); }); }); }; return (new Promise(executor)).then(function (event) { ias.emitter.emit(APPENDED, event); }); }; /** * @param {array<Element>} items * @param {Element|null} parent */ InfiniteAjaxScroll.prototype.prepend = function prepend (items, parent) { var this$1$1 = this; var ias = this; parent = parent || ias.container; var event = { items: items, parent: parent, prependFn: prependFn }; ias.emitter.emit(PREPEND, event); var executor = function (resolve) { window.requestAnimationFrame(function () { var first = ias.first(); var scrollPositionStart = getScrollPosition(this$1$1.scrollContainer); var topStart = first.getBoundingClientRect().top + scrollPositionStart.y; Promise.resolve(event.prependFn(event.items, event.parent, ias.first())) .then(function () { var scrollPositionEnd = getScrollPosition(this$1$1.scrollContainer); var topEnd = first.getBoundingClientRect().top + scrollPositionEnd.y; var deltaY = topEnd - topStart; this$1$1.scrollContainer.scrollTo(scrollPositionEnd.x, deltaY); }) .then(function () { resolve({items: items, parent: parent}); }); }); }; return (new Promise(executor)).then(function (event) { ias.emitter.emit(PREPENDED, event); }); }; InfiniteAjaxScroll.prototype.sentinel = function sentinel () { var items = $(this.options.item, this.container); if (!items.length) { return null; } return items[items.length-1]; }; InfiniteAjaxScroll.prototype.first = function first () { var items = $(this.options.item, this.container); if (!items.length) { return null; } return items[0]; }; InfiniteAjaxScroll.prototype.pause = function pause () { this.paused = true; }; InfiniteAjaxScroll.prototype.resume = function resume () { this.paused = false; }; InfiniteAjaxScroll.prototype.enableLoadOnScroll = function enableLoadOnScroll () { this.loadOnScroll = true; }; InfiniteAjaxScroll.prototype.disableLoadOnScroll = function disableLoadOnScroll () { this.loadOnScroll = false; }; /** * @deprecated replaced by distanceBottom */ InfiniteAjaxScroll.prototype.distance = function distance (rootRect, sentinel) { return this.distanceBottom(rootRect, sentinel); }; InfiniteAjaxScroll.prototype.distanceBottom = function distanceBottom (rootRect, sentinel) { var _rootRect = rootRect || getRootRect(this.scrollContainer); var _sentinel = sentinel || this.sentinel(); var scrollPosition = getScrollPosition(this.scrollContainer); var distance = getDistanceToFold(_sentinel, scrollPosition, _rootRect); // apply negative margin distance -= this.negativeMargin; return distance; }; InfiniteAjaxScroll.prototype.distanceTop = function distanceTop () { var scrollPosition = getScrollPosition(this.scrollContainer); return scrollPosition.y - this.negativeMargin; }; InfiniteAjaxScroll.prototype.measure = function measure () { if (this.paused || (this.hitFirst && this.hitLast)) { return; } var rootRect = getRootRect(this.scrollContainer); // When the scroll container has no height, this could indicate that // the element is not visible (display = none). Without a height // we cannot calculate the distance to fold. On the other hand we don't // have to, because it's not visible anyway. Our resize observer will // monitor the height, once it's greater than 0 everything will resume as normal. if (rootRect.height === 0) { // @todo DX: show warning in console that this is happening return; } if (!this.hitFirst) { var distanceTop = this.distanceTop(); if (distanceTop <= 0) { this.emitter.emit(TOP, {distance: distanceTop}); } } if (!this.hitLast) { var distanceBottom = this.distanceBottom(rootRect, this.sentinel()); if (distanceBottom <= 0) { this.emitter.emit(HIT, {distance: distanceBottom}); } } }; InfiniteAjaxScroll.prototype.on = function on (event, callback) { this.emitter.on(event, callback, this); if (event === BINDED && this.binded) { callback.bind(this)(); } }; InfiniteAjaxScroll.prototype.off = function off (event, callback) { this.emitter.off(event, callback, this); }; InfiniteAjaxScroll.prototype.once = function once (event, callback) { var this$1$1 = this; return new Promise(function (resolve) { this$1$1.emitter.once(event, function() { Promise.resolve(callback.apply(this, arguments)).then(resolve); }, this$1$1); if (event === BINDED && this$1$1.binded) { callback.bind(this$1$1)(); resolve(); } }) }; export { InfiniteAjaxScroll as default };