/*! * Infinite Scroll PACKAGED v4.0.1 * Automatically add next page * * Licensed GPLv3 for open source use * or Infinite Scroll Commercial License for commercial use * * https://infinite-scroll.com * Copyright 2018-2020 Metafizzy */ /** * Bridget makes jQuery widgets * v3.0.0 * MIT license */ (function (window, factory) { // module definition if (typeof module == 'object' && module.exports) { // CommonJS module.exports = factory( window, require('jquery'), ); } else { // browser global window.jQueryBridget = factory( window, window.jQuery, ); } }(window, function factory(window, jQuery) { // ----- utils ----- // // helper function for logging errors // $.error breaks jQuery chaining let console = window.console; let logError = typeof console == 'undefined' ? function () { } : function (message) { console.error(message); }; // ----- jQueryBridget ----- // function jQueryBridget(namespace, PluginClass, $) { $ = $ || jQuery || window.jQuery; if (!$) { return; } // add option method -> $().plugin('option', {...}) if (!PluginClass.prototype.option) { // option setter PluginClass.prototype.option = function (opts) { if (!opts) return; this.options = Object.assign(this.options || {}, opts); }; } // make jQuery plugin $.fn[namespace] = function (arg0, ...args) { if (typeof arg0 == 'string') { // method call $().plugin( 'methodName', { options } ) return methodCall(this, arg0, args); } // just $().plugin({ options }) plainCall(this, arg0); return this; }; // $().plugin('methodName') function methodCall($elems, methodName, args) { let returnValue; let pluginMethodStr = `$().${namespace}("${methodName}")`; $elems.each(function (i, elem) { // get instance let instance = $.data(elem, namespace); if (!instance) { logError(`${namespace} not initialized.` + ` Cannot call method ${pluginMethodStr}`); return; } let method = instance[methodName]; if (!method || methodName.charAt(0) == '_') { logError(`${pluginMethodStr} is not a valid method`); return; } // apply method, get return value let value = method.apply(instance, args); // set return value if value is returned, use only first value returnValue = returnValue === undefined ? value : returnValue; }); return returnValue !== undefined ? returnValue : $elems; } function plainCall($elems, options) { $elems.each(function (i, elem) { let instance = $.data(elem, namespace); if (instance) { // set options & init instance.option(options); instance._init(); } else { // initialize new instance instance = new PluginClass(elem, options); $.data(elem, namespace, instance); } }); } } // ----- ----- // return jQueryBridget; })); /** * EvEmitter v2.0.0 * Lil' event emitter * MIT License */ (function (global, factory) { // universal module definition if (typeof module == 'object' && module.exports) { // CommonJS - Browserify, Webpack module.exports = factory(); } else { // Browser globals global.EvEmitter = factory(); } }(typeof window != 'undefined' ? window : this, function () { function EvEmitter() { } let proto = EvEmitter.prototype; proto.on = function (eventName, listener) { if (!eventName || !listener) return this; // set events hash let events = this._events = this._events || {}; // set listeners array let listeners = events[eventName] = events[eventName] || []; // only add once if (!listeners.includes(listener)) { listeners.push(listener); } return this; }; proto.once = function (eventName, listener) { if (!eventName || !listener) return this; // add event this.on(eventName, listener); // set once flag // set onceEvents hash let onceEvents = this._onceEvents = this._onceEvents || {}; // set onceListeners object let onceListeners = onceEvents[eventName] = onceEvents[eventName] || {}; // set flag onceListeners[listener] = true; return this; }; proto.off = function (eventName, listener) { let listeners = this._events && this._events[eventName]; if (!listeners || !listeners.length) return this; let index = listeners.indexOf(listener); if (index != -1) { listeners.splice(index, 1); } return this; }; proto.emitEvent = function (eventName, args) { let listeners = this._events && this._events[eventName]; if (!listeners || !listeners.length) return this; // copy over to avoid interference if .off() in listener listeners = listeners.slice(0); args = args || []; // once stuff let onceListeners = this._onceEvents && this._onceEvents[eventName]; for (let listener of listeners) { let isOnce = onceListeners && onceListeners[listener]; if (isOnce) { // remove listener // remove before trigger to prevent recursion this.off(eventName, listener); // unset once flag delete onceListeners[listener]; } // trigger listener listener.apply(this, args); } return this; }; proto.allOff = function () { delete this._events; delete this._onceEvents; return this; }; return EvEmitter; })); /** * Fizzy UI utils v3.0.0 * MIT license */ (function (global, factory) { // universal module definition if (typeof module == 'object' && module.exports) { // CommonJS module.exports = factory(global); } else { // browser global global.fizzyUIUtils = factory(global); } }(this, function factory(global) { let utils = {}; // ----- extend ----- // // extends objects utils.extend = function (a, b) { return Object.assign(a, b); }; // ----- modulo ----- // utils.modulo = function (num, div) { return ((num % div) + div) % div; }; // ----- makeArray ----- // // turn element or nodeList into an array utils.makeArray = function (obj) { // use object if already an array if (Array.isArray(obj)) return obj; // return empty array if undefined or null. #6 if (obj === null || obj === undefined) return []; let isArrayLike = typeof obj == 'object' && typeof obj.length == 'number'; // convert nodeList to array if (isArrayLike) return [...obj]; // array of single index return [obj]; }; // ----- removeFrom ----- // utils.removeFrom = function (ary, obj) { let index = ary.indexOf(obj); if (index != -1) { ary.splice(index, 1); } }; // ----- getParent ----- // utils.getParent = function (elem, selector) { while (elem.parentNode && elem != document.body) { elem = elem.parentNode; if (elem.matches(selector)) return elem; } }; // ----- getQueryElement ----- // // use element as selector string utils.getQueryElement = function (elem) { if (typeof elem == 'string') { return document.querySelector(elem); } return elem; }; // ----- handleEvent ----- // // enable .ontype to trigger from .addEventListener( elem, 'type' ) utils.handleEvent = function (event) { let method = 'on' + event.type; if (this[method]) { this[method](event); } }; // ----- filterFindElements ----- // utils.filterFindElements = function (elems, selector) { // make array of elems elems = utils.makeArray(elems); return elems // check that elem is an actual element .filter((elem) => elem instanceof HTMLElement) .reduce((ffElems, elem) => { // add elem if no selector if (!selector) { ffElems.push(elem); return ffElems; } // filter & find items if we have a selector // filter if (elem.matches(selector)) { ffElems.push(elem); } // find children let childElems = elem.querySelectorAll(selector); // concat childElems to filterFound array ffElems = ffElems.concat(...childElems); return ffElems; }, []); }; // ----- debounceMethod ----- // utils.debounceMethod = function (_class, methodName, threshold) { threshold = threshold || 100; // original method let method = _class.prototype[methodName]; let timeoutName = methodName + 'Timeout'; _class.prototype[methodName] = function () { clearTimeout(this[timeoutName]); let args = arguments; this[timeoutName] = setTimeout(() => { method.apply(this, args); delete this[timeoutName]; }, threshold); }; }; // ----- docReady ----- // utils.docReady = function (onDocReady) { let readyState = document.readyState; if (readyState == 'complete' || readyState == 'interactive') { // do async to allow for other scripts to run. metafizzy/flickity#441 setTimeout(onDocReady); } else { document.addEventListener('DOMContentLoaded', onDocReady); } }; // ----- htmlInit ----- // // http://bit.ly/3oYLusc utils.toDashed = function (str) { return str.replace(/(.)([A-Z])/g, function (match, $1, $2) { return $1 + '-' + $2; }).toLowerCase(); }; let console = global.console; // allow user to initialize classes via [data-namespace] or .js-namespace class // htmlInit( Widget, 'widgetName' ) // options are parsed from data-namespace-options utils.htmlInit = function (WidgetClass, namespace) { utils.docReady(function () { let dashedNamespace = utils.toDashed(namespace); let dataAttr = 'data-' + dashedNamespace; let dataAttrElems = document.querySelectorAll(`[${dataAttr}]`); let jQuery = global.jQuery; [...dataAttrElems].forEach((elem) => { let attr = elem.getAttribute(dataAttr); let options; try { options = attr && JSON.parse(attr); } catch (error) { // log error, do not initialize if (console) { console.error(`Error parsing ${dataAttr} on ${elem.className}: ${error}`); } return; } // initialize let instance = new WidgetClass(elem, options); // make available via $().data('namespace') if (jQuery) { jQuery.data(elem, namespace, instance); } }); }); }; // ----- ----- // return utils; })); // core (function (window, factory) { // universal module definition if (typeof module == 'object' && module.exports) { // CommonJS module.exports = factory( window, require('ev-emitter'), require('fizzy-ui-utils'), ); } else { // browser global window.InfiniteScroll = factory( window, window.EvEmitter, window.fizzyUIUtils, ); } }(window, function factory(window, EvEmitter, utils) { let jQuery = window.jQuery; // internal store of all InfiniteScroll intances let instances = {}; function InfiniteScroll(element, options) { let queryElem = utils.getQueryElement(element); if (!queryElem) { console.error('Bad element for InfiniteScroll: ' + (queryElem || element)); return; } element = queryElem; // do not initialize twice on same element if (element.infiniteScrollGUID) { let instance = instances[element.infiniteScrollGUID]; instance.option(options); return instance; } this.element = element; // options this.options = {...InfiniteScroll.defaults}; this.option(options); // add jQuery if (jQuery) { this.$element = jQuery(this.element); } this.create(); } // defaults InfiniteScroll.defaults = { // path: null, // hideNav: null, // debug: false, }; // create & destroy methods InfiniteScroll.create = {}; InfiniteScroll.destroy = {}; let proto = InfiniteScroll.prototype; // inherit EvEmitter Object.assign(proto, EvEmitter.prototype); // -------------------------- -------------------------- // // globally unique identifiers let GUID = 0; proto.create = function () { // create core // add id for InfiniteScroll.data let id = this.guid = ++GUID; this.element.infiniteScrollGUID = id; // expando instances[id] = this; // associate via id // properties this.pageIndex = 1; // default to first page this.loadCount = 0; this.updateGetPath(); // bail if getPath not set, or returns falsey #776 let hasPath = this.getPath && this.getPath(); if (!hasPath) { console.error('Disabling InfiniteScroll'); return; } this.updateGetAbsolutePath(); this.log('initialized', [this.element.className]); this.callOnInit(); // create features for (let method in InfiniteScroll.create) { InfiniteScroll.create[method].call(this); } }; proto.option = function (opts) { Object.assign(this.options, opts); }; // call onInit option, used for binding events on init proto.callOnInit = function () { let onInit = this.options.onInit; if (onInit) { onInit.call(this, this); } }; // ----- events ----- // proto.dispatchEvent = function (type, event, args) { this.log(type, args); let emitArgs = event ? [event].concat(args) : args; this.emitEvent(type, emitArgs); // trigger jQuery event if (!jQuery || !this.$element) { return; } // namespace jQuery event type += '.infiniteScroll'; let $event = type; if (event) { // create jQuery event /* eslint-disable-next-line new-cap */ let jQEvent = jQuery.Event(event); jQEvent.type = type; $event = jQEvent; } this.$element.trigger($event, args); }; let loggers = { initialized: (className) => `on ${className}`, request: (path) => `URL: ${path}`, load: (response, path) => `${response.title || ''}. URL: ${path}`, error: (error, path) => `${error}. URL: ${path}`, append: (response, path, items) => `${items.length} items. URL: ${path}`, last: (response, path) => `URL: ${path}`, history: (title, path) => `URL: ${path}`, pageIndex: function (index, origin) { return `current page determined to be: ${index} from ${origin}`; }, }; // log events proto.log = function (type, args) { if (!this.options.debug) return; let message = `[InfiniteScroll] ${type}`; let logger = loggers[type]; if (logger) message += '. ' + logger.apply(this, args); console.log(message); }; // -------------------------- methods used amoung features -------------------------- // proto.updateMeasurements = function () { this.windowHeight = window.innerHeight; let rect = this.element.getBoundingClientRect(); this.top = rect.top + window.scrollY; }; proto.updateScroller = function () { let elementScroll = this.options.elementScroll; if (!elementScroll) { // default, use window this.scroller = window; return; } // if true, set to element, otherwise use option this.scroller = elementScroll === true ? this.element : utils.getQueryElement(elementScroll); if (!this.scroller) { throw new Error(`Unable to find elementScroll: ${elementScroll}`); } }; // -------------------------- page path -------------------------- // proto.updateGetPath = function () { let optPath = this.options.path; if (!optPath) { console.error(`InfiniteScroll path option required. Set as: ${optPath}`); return; } // function let type = typeof optPath; if (type == 'function') { this.getPath = optPath; return; } // template string: '/pages/{{#}}.html' let templateMatch = type == 'string' && optPath.match('{{#}}'); if (templateMatch) { this.updateGetPathTemplate(optPath); return; } // selector: '.next-page-selector' this.updateGetPathSelector(optPath); }; proto.updateGetPathTemplate = function (optPath) { // set getPath with template string this.getPath = () => { let nextIndex = this.pageIndex + 1; return optPath.replace('{{#}}', nextIndex); }; // get pageIndex from location // convert path option into regex to look for pattern in location // escape query (?) in url, allows for parsing GET parameters let regexString = optPath .replace(/(\\\?|\?)/, '\\?') .replace('{{#}}', '(\\d\\d?\\d?)'); let templateRe = new RegExp(regexString); let match = location.href.match(templateRe); if (match) { this.pageIndex = parseInt(match[1], 10); this.log('pageIndex', [this.pageIndex, 'template string']); } }; let pathRegexes = [ // WordPress & Tumblr - example.com/page/2 // Jekyll - example.com/page2 /^(.*?\/?page\/?)(\d\d?\d?)(.*?$)/, // Drupal - example.com/?page=1 /^(.*?\/?\?page=)(\d\d?\d?)(.*?$)/, // catch all, last occurence of a number /(.*?)(\d\d?\d?)(?!.*\d)(.*?$)/, ]; // try matching href to pathRegexes patterns let getPathParts = InfiniteScroll.getPathParts = function (href) { if (!href) return; for (let regex of pathRegexes) { let match = href.match(regex); if (match) { let [, begin, index, end] = match; return {begin, index, end}; } } }; proto.updateGetPathSelector = function (optPath) { // parse href of link: '.next-page-link' let hrefElem = document.querySelector(optPath); if (!hrefElem) { console.error(`Bad InfiniteScroll path option. Next link not found: ${optPath}`); return; } let href = hrefElem.getAttribute('href'); let pathParts = getPathParts(href); if (!pathParts) { console.error(`InfiniteScroll unable to parse next link href: ${href}`); return; } let {begin, index, end} = pathParts; this.isPathSelector = true; // flag for checkLastPage() this.getPath = () => begin + (this.pageIndex + 1) + end; // get pageIndex from href this.pageIndex = parseInt(index, 10) - 1; this.log('pageIndex', [this.pageIndex, 'next link']); }; proto.updateGetAbsolutePath = function () { let path = this.getPath(); // path doesn't start with http or / let isAbsolute = path.match(/^http/) || path.match(/^\//); if (isAbsolute) { this.getAbsolutePath = this.getPath; return; } let {pathname} = location; // query parameter #829. example.com/?pg=2 let isQuery = path.match(/^\?/); // /foo/bar/index.html => /foo/bar let directory = pathname.substring(0, pathname.lastIndexOf('/')); let pathStart = isQuery ? pathname : directory + '/'; this.getAbsolutePath = () => pathStart + this.getPath(); }; // -------------------------- nav -------------------------- // // hide navigation InfiniteScroll.create.hideNav = function () { let nav = utils.getQueryElement(this.options.hideNav); if (!nav) return; nav.style.display = 'none'; this.nav = nav; }; InfiniteScroll.destroy.hideNav = function () { if (this.nav) this.nav.style.display = ''; }; // -------------------------- destroy -------------------------- // proto.destroy = function () { this.allOff(); // remove all event listeners // call destroy methods for (let method in InfiniteScroll.destroy) { InfiniteScroll.destroy[method].call(this); } delete this.element.infiniteScrollGUID; delete instances[this.guid]; // remove jQuery data. #807 if (jQuery && this.$element) { jQuery.removeData(this.element, 'infiniteScroll'); } }; // -------------------------- utilities -------------------------- // // https://remysharp.com/2010/07/21/throttling-function-calls InfiniteScroll.throttle = function (fn, threshold) { threshold = threshold || 200; let last, timeout; return function () { let now = +new Date(); let args = arguments; let trigger = () => { last = now; fn.apply(this, args); }; if (last && now < last + threshold) { // hold on to it clearTimeout(timeout); timeout = setTimeout(trigger, threshold); } else { trigger(); } }; }; InfiniteScroll.data = function (elem) { elem = utils.getQueryElement(elem); let id = elem && elem.infiniteScrollGUID; return id && instances[id]; }; // set internal jQuery, for Webpack + jQuery v3 InfiniteScroll.setJQuery = function (jqry) { jQuery = jqry; }; // -------------------------- setup -------------------------- // utils.htmlInit(InfiniteScroll, 'infinite-scroll'); // add noop _init method for jQuery Bridget. #768 proto._init = function () { }; let {jQueryBridget} = window; if (jQuery && jQueryBridget) { jQueryBridget('infiniteScroll', InfiniteScroll, jQuery); } // -------------------------- -------------------------- // return InfiniteScroll; })); // page-load (function (window, factory) { // universal module definition if (typeof module == 'object' && module.exports) { // CommonJS module.exports = factory( window, require('./core'), ); } else { // browser global factory( window, window.InfiniteScroll, ); } }(window, function factory(window, InfiniteScroll) { let proto = InfiniteScroll.prototype; Object.assign(InfiniteScroll.defaults, { // append: false, loadOnScroll: true, checkLastPage: true, responseBody: 'text', domParseResponse: true, // prefill: false, // outlayer: null, }); InfiniteScroll.create.pageLoad = function () { this.canLoad = true; this.on('scrollThreshold', this.onScrollThresholdLoad); this.on('load', this.checkLastPage); if (this.options.outlayer) { this.on('append', this.onAppendOutlayer); } }; proto.onScrollThresholdLoad = function () { if (this.options.loadOnScroll) this.loadNextPage(); }; let domParser = new DOMParser(); proto.loadNextPage = function () { if (this.isLoading || !this.canLoad) return; let {responseBody, domParseResponse, fetchOptions} = this.options; let path = this.getAbsolutePath(); this.isLoading = true; if (typeof fetchOptions == 'function') fetchOptions = fetchOptions(); let fetchPromise = fetch(path, fetchOptions) .then((response) => { if (!response.ok) { let error = new Error(response.statusText); this.onPageError(error, path, response); return {response}; } return response[responseBody]().then((body) => { let canDomParse = responseBody == 'text' && domParseResponse; if (canDomParse) { body = domParser.parseFromString(body, 'text/html'); } if (response.status == 204) { this.lastPageReached(body, path); return {body, response}; } else { return this.onPageLoad(body, path, response); } }); }) .catch((error) => { this.onPageError(error, path); }); this.dispatchEvent('request', null, [path, fetchPromise]); return fetchPromise; }; proto.onPageLoad = function (body, path, response) { // done loading if not appending if (!this.options.append) { this.isLoading = false; } this.pageIndex++; this.loadCount++; this.dispatchEvent('load', null, [body, path, response]); return this.appendNextPage(body, path, response); }; proto.appendNextPage = function (body, path, response) { let {append, responseBody, domParseResponse} = this.options; // do not append json let isDocument = responseBody == 'text' && domParseResponse; if (!isDocument || !append) return {body, response}; let items = body.querySelectorAll(append); let promiseValue = {body, response, items}; // last page hit if no items. #840 if (!items || !items.length) { this.lastPageReached(body, path); return promiseValue; } let fragment = getItemsFragment(items); let appendReady = () => { this.appendItems(items, fragment); this.isLoading = false; this.dispatchEvent('append', null, [body, path, items, response]); return promiseValue; }; // TODO add hook for option to trigger appendReady if (this.options.outlayer) { return this.appendOutlayerItems(fragment, appendReady); } else { return appendReady(); } }; proto.appendItems = function (items, fragment) { if (!items || !items.length) return; // get fragment if not provided fragment = fragment || getItemsFragment(items); refreshScripts(fragment); this.element.appendChild(fragment); }; function getItemsFragment(items) { // add items to fragment let fragment = document.createDocumentFragment(); if (items) fragment.append(...items); return fragment; } // replace