/*! * 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