mirror of
https://github.com/DanielnetoDotCom/YouPHPTube
synced 2025-10-03 01:39:24 +02:00
1909 lines
49 KiB
JavaScript
1909 lines
49 KiB
JavaScript
/*!
|
|
* 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 <script>s with copies so they load
|
|
// <script>s added by InfiniteScroll will not load
|
|
// similar to https://stackoverflow.com/questions/610995
|
|
function refreshScripts( fragment ) {
|
|
let scripts = fragment.querySelectorAll('script');
|
|
for ( let script of scripts ) {
|
|
let freshScript = document.createElement('script');
|
|
// copy attributes
|
|
let attrs = script.attributes;
|
|
for ( let attr of attrs ) {
|
|
freshScript.setAttribute( attr.name, attr.value );
|
|
}
|
|
// copy inner script code. #718, #782
|
|
freshScript.innerHTML = script.innerHTML;
|
|
script.parentNode.replaceChild( freshScript, script );
|
|
}
|
|
}
|
|
|
|
// ----- outlayer ----- //
|
|
|
|
proto.appendOutlayerItems = function( fragment, appendReady ) {
|
|
let imagesLoaded = InfiniteScroll.imagesLoaded || window.imagesLoaded;
|
|
if ( !imagesLoaded ) {
|
|
console.error('[InfiniteScroll] imagesLoaded required for outlayer option');
|
|
this.isLoading = false;
|
|
return;
|
|
}
|
|
// append once images loaded
|
|
return new Promise( function( resolve ) {
|
|
imagesLoaded( fragment, function() {
|
|
let bodyResponse = appendReady();
|
|
resolve( bodyResponse );
|
|
} );
|
|
} );
|
|
};
|
|
|
|
proto.onAppendOutlayer = function( response, path, items ) {
|
|
this.options.outlayer.appended( items );
|
|
};
|
|
|
|
// ----- checkLastPage ----- //
|
|
|
|
// check response for next element
|
|
proto.checkLastPage = function( body, path ) {
|
|
let { checkLastPage, path: pathOpt } = this.options;
|
|
if ( !checkLastPage ) return;
|
|
|
|
// if path is function, check if next path is truthy
|
|
if ( typeof pathOpt == 'function' ) {
|
|
let nextPath = this.getPath();
|
|
if ( !nextPath ) {
|
|
this.lastPageReached( body, path );
|
|
return;
|
|
}
|
|
}
|
|
// get selector from checkLastPage or path option
|
|
let selector;
|
|
if ( typeof checkLastPage == 'string' ) {
|
|
selector = checkLastPage;
|
|
} else if ( this.isPathSelector ) {
|
|
// path option is selector string
|
|
selector = pathOpt;
|
|
}
|
|
// check last page for selector
|
|
// bail if no selector or not document response
|
|
if ( !selector || !body.querySelector ) return;
|
|
|
|
// check if response has selector
|
|
let nextElem = body.querySelector( selector );
|
|
if ( !nextElem ) this.lastPageReached( body, path );
|
|
};
|
|
|
|
proto.lastPageReached = function( body, path ) {
|
|
this.canLoad = false;
|
|
this.dispatchEvent( 'last', null, [ body, path ] );
|
|
};
|
|
|
|
// ----- error ----- //
|
|
|
|
proto.onPageError = function( error, path, response ) {
|
|
this.isLoading = false;
|
|
this.canLoad = false;
|
|
this.dispatchEvent( 'error', null, [ error, path, response ] );
|
|
return error;
|
|
};
|
|
|
|
// -------------------------- prefill -------------------------- //
|
|
|
|
InfiniteScroll.create.prefill = function() {
|
|
if ( !this.options.prefill ) return;
|
|
|
|
let append = this.options.append;
|
|
if ( !append ) {
|
|
console.error(`append option required for prefill. Set as :${append}`);
|
|
return;
|
|
}
|
|
this.updateMeasurements();
|
|
this.updateScroller();
|
|
this.isPrefilling = true;
|
|
this.on( 'append', this.prefill );
|
|
this.once( 'error', this.stopPrefill );
|
|
this.once( 'last', this.stopPrefill );
|
|
this.prefill();
|
|
};
|
|
|
|
proto.prefill = function() {
|
|
let distance = this.getPrefillDistance();
|
|
this.isPrefilling = distance >= 0;
|
|
if ( this.isPrefilling ) {
|
|
this.log('prefill');
|
|
this.loadNextPage();
|
|
} else {
|
|
this.stopPrefill();
|
|
}
|
|
};
|
|
|
|
proto.getPrefillDistance = function() {
|
|
// element scroll
|
|
if ( this.options.elementScroll ) {
|
|
return this.scroller.clientHeight - this.scroller.scrollHeight;
|
|
}
|
|
// window
|
|
return this.windowHeight - this.element.clientHeight;
|
|
};
|
|
|
|
proto.stopPrefill = function() {
|
|
this.log('stopPrefill');
|
|
this.off( 'append', this.prefill );
|
|
};
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
return InfiniteScroll;
|
|
|
|
} ) );
|
|
// scroll-watch
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
if ( typeof module == 'object' && module.exports ) {
|
|
// CommonJS
|
|
module.exports = factory(
|
|
window,
|
|
require('./core'),
|
|
require('fizzy-ui-utils'),
|
|
);
|
|
} else {
|
|
// browser global
|
|
factory(
|
|
window,
|
|
window.InfiniteScroll,
|
|
window.fizzyUIUtils,
|
|
);
|
|
}
|
|
|
|
}( window, function factory( window, InfiniteScroll, utils ) {
|
|
|
|
let proto = InfiniteScroll.prototype;
|
|
|
|
// default options
|
|
Object.assign( InfiniteScroll.defaults, {
|
|
scrollThreshold: 400,
|
|
// elementScroll: null,
|
|
} );
|
|
|
|
InfiniteScroll.create.scrollWatch = function() {
|
|
// events
|
|
this.pageScrollHandler = this.onPageScroll.bind( this );
|
|
this.resizeHandler = this.onResize.bind( this );
|
|
|
|
let scrollThreshold = this.options.scrollThreshold;
|
|
let isEnable = scrollThreshold || scrollThreshold === 0;
|
|
if ( isEnable ) this.enableScrollWatch();
|
|
};
|
|
|
|
InfiniteScroll.destroy.scrollWatch = function() {
|
|
this.disableScrollWatch();
|
|
};
|
|
|
|
proto.enableScrollWatch = function() {
|
|
if ( this.isScrollWatching ) return;
|
|
|
|
this.isScrollWatching = true;
|
|
this.updateMeasurements();
|
|
this.updateScroller();
|
|
// TODO disable after error?
|
|
this.on( 'last', this.disableScrollWatch );
|
|
this.bindScrollWatchEvents( true );
|
|
};
|
|
|
|
proto.disableScrollWatch = function() {
|
|
if ( !this.isScrollWatching ) return;
|
|
|
|
this.bindScrollWatchEvents( false );
|
|
delete this.isScrollWatching;
|
|
};
|
|
|
|
proto.bindScrollWatchEvents = function( isBind ) {
|
|
let addRemove = isBind ? 'addEventListener' : 'removeEventListener';
|
|
this.scroller[ addRemove ]( 'scroll', this.pageScrollHandler );
|
|
window[ addRemove ]( 'resize', this.resizeHandler );
|
|
};
|
|
|
|
proto.onPageScroll = InfiniteScroll.throttle( function() {
|
|
let distance = this.getBottomDistance();
|
|
if ( distance <= this.options.scrollThreshold ) {
|
|
this.dispatchEvent('scrollThreshold');
|
|
}
|
|
} );
|
|
|
|
proto.getBottomDistance = function() {
|
|
let bottom, scrollY;
|
|
if ( this.options.elementScroll ) {
|
|
bottom = this.scroller.scrollHeight;
|
|
scrollY = this.scroller.scrollTop + this.scroller.clientHeight;
|
|
} else {
|
|
bottom = this.top + this.element.clientHeight;
|
|
scrollY = window.scrollY + this.windowHeight;
|
|
}
|
|
return bottom - scrollY;
|
|
};
|
|
|
|
proto.onResize = function() {
|
|
this.updateMeasurements();
|
|
};
|
|
|
|
utils.debounceMethod( InfiniteScroll, 'onResize', 150 );
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
return InfiniteScroll;
|
|
|
|
} ) );
|
|
// history
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
if ( typeof module == 'object' && module.exports ) {
|
|
// CommonJS
|
|
module.exports = factory(
|
|
window,
|
|
require('./core'),
|
|
require('fizzy-ui-utils'),
|
|
);
|
|
} else {
|
|
// browser global
|
|
factory(
|
|
window,
|
|
window.InfiniteScroll,
|
|
window.fizzyUIUtils,
|
|
);
|
|
}
|
|
|
|
}( window, function factory( window, InfiniteScroll, utils ) {
|
|
|
|
let proto = InfiniteScroll.prototype;
|
|
|
|
Object.assign( InfiniteScroll.defaults, {
|
|
history: 'replace',
|
|
// historyTitle: false,
|
|
} );
|
|
|
|
let link = document.createElement('a');
|
|
|
|
// ----- create/destroy ----- //
|
|
|
|
InfiniteScroll.create.history = function() {
|
|
if ( !this.options.history ) return;
|
|
|
|
// check for same origin
|
|
link.href = this.getAbsolutePath();
|
|
// MS Edge does not have origin on link
|
|
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12236493/
|
|
let linkOrigin = link.origin || link.protocol + '//' + link.host;
|
|
let isSameOrigin = linkOrigin == location.origin;
|
|
if ( !isSameOrigin ) {
|
|
console.error( '[InfiniteScroll] cannot set history with different origin: ' +
|
|
`${link.origin} on ${location.origin} . History behavior disabled.` );
|
|
return;
|
|
}
|
|
|
|
// two ways to handle changing history
|
|
if ( this.options.append ) {
|
|
this.createHistoryAppend();
|
|
} else {
|
|
this.createHistoryPageLoad();
|
|
}
|
|
};
|
|
|
|
proto.createHistoryAppend = function() {
|
|
this.updateMeasurements();
|
|
this.updateScroller();
|
|
// array of scroll positions of appended pages
|
|
this.scrollPages = [
|
|
// first page
|
|
{
|
|
top: 0,
|
|
path: location.href,
|
|
title: document.title,
|
|
},
|
|
];
|
|
this.scrollPage = this.scrollPages[0];
|
|
// events
|
|
this.scrollHistoryHandler = this.onScrollHistory.bind( this );
|
|
this.unloadHandler = this.onUnload.bind( this );
|
|
this.scroller.addEventListener( 'scroll', this.scrollHistoryHandler );
|
|
this.on( 'append', this.onAppendHistory );
|
|
this.bindHistoryAppendEvents( true );
|
|
};
|
|
|
|
proto.bindHistoryAppendEvents = function( isBind ) {
|
|
let addRemove = isBind ? 'addEventListener' : 'removeEventListener';
|
|
this.scroller[ addRemove ]( 'scroll', this.scrollHistoryHandler );
|
|
window[ addRemove ]( 'unload', this.unloadHandler );
|
|
};
|
|
|
|
proto.createHistoryPageLoad = function() {
|
|
this.on( 'load', this.onPageLoadHistory );
|
|
};
|
|
|
|
InfiniteScroll.destroy.history =
|
|
proto.destroyHistory = function() {
|
|
let isHistoryAppend = this.options.history && this.options.append;
|
|
if ( isHistoryAppend ) {
|
|
this.bindHistoryAppendEvents( false );
|
|
}
|
|
};
|
|
|
|
// ----- append history ----- //
|
|
|
|
proto.onAppendHistory = function( response, path, items ) {
|
|
// do not proceed if no items. #779
|
|
if ( !items || !items.length ) return;
|
|
|
|
let firstItem = items[0];
|
|
let elemScrollY = this.getElementScrollY( firstItem );
|
|
// resolve path
|
|
link.href = path;
|
|
// add page data to hash
|
|
this.scrollPages.push({
|
|
top: elemScrollY,
|
|
path: link.href,
|
|
title: response.title,
|
|
});
|
|
};
|
|
|
|
proto.getElementScrollY = function( elem ) {
|
|
if ( this.options.elementScroll ) {
|
|
return elem.offsetTop - this.top;
|
|
} else {
|
|
let rect = elem.getBoundingClientRect();
|
|
return rect.top + window.scrollY;
|
|
}
|
|
};
|
|
|
|
proto.onScrollHistory = function() {
|
|
// cycle through positions, find biggest without going over
|
|
let scrollPage = this.getClosestScrollPage();
|
|
// set history if changed
|
|
if ( scrollPage != this.scrollPage ) {
|
|
this.scrollPage = scrollPage;
|
|
this.setHistory( scrollPage.title, scrollPage.path );
|
|
}
|
|
};
|
|
|
|
utils.debounceMethod( InfiniteScroll, 'onScrollHistory', 150 );
|
|
|
|
proto.getClosestScrollPage = function() {
|
|
let scrollViewY;
|
|
if ( this.options.elementScroll ) {
|
|
scrollViewY = this.scroller.scrollTop + this.scroller.clientHeight / 2;
|
|
} else {
|
|
scrollViewY = window.scrollY + this.windowHeight / 2;
|
|
}
|
|
|
|
let scrollPage;
|
|
for ( let page of this.scrollPages ) {
|
|
if ( page.top >= scrollViewY ) break;
|
|
|
|
scrollPage = page;
|
|
}
|
|
return scrollPage;
|
|
};
|
|
|
|
proto.setHistory = function( title, path ) {
|
|
let optHistory = this.options.history;
|
|
let historyMethod = optHistory && history[ optHistory + 'State' ];
|
|
if ( !historyMethod ) return;
|
|
|
|
history[ optHistory + 'State' ]( null, title, path );
|
|
if ( this.options.historyTitle ) document.title = title;
|
|
this.dispatchEvent( 'history', null, [ title, path ] );
|
|
};
|
|
|
|
// scroll to top to prevent initial scroll-reset after page refresh
|
|
// https://stackoverflow.com/a/18633915/182183
|
|
proto.onUnload = function() {
|
|
if ( this.scrollPage.top === 0 ) return;
|
|
|
|
// calculate where scroll position would be on refresh
|
|
let scrollY = window.scrollY - this.scrollPage.top + this.top;
|
|
// disable scroll event before setting scroll #679
|
|
this.destroyHistory();
|
|
scrollTo( 0, scrollY );
|
|
};
|
|
|
|
// ----- load history ----- //
|
|
|
|
// update URL
|
|
proto.onPageLoadHistory = function( response, path ) {
|
|
this.setHistory( response.title, path );
|
|
};
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
return InfiniteScroll;
|
|
|
|
} ) );
|
|
// button
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
if ( typeof module == 'object' && module.exports ) {
|
|
// CommonJS
|
|
module.exports = factory(
|
|
window,
|
|
require('./core'),
|
|
require('fizzy-ui-utils'),
|
|
);
|
|
} else {
|
|
// browser global
|
|
factory(
|
|
window,
|
|
window.InfiniteScroll,
|
|
window.fizzyUIUtils,
|
|
);
|
|
}
|
|
|
|
}( window, function factory( window, InfiniteScroll, utils ) {
|
|
|
|
// -------------------------- InfiniteScrollButton -------------------------- //
|
|
|
|
class InfiniteScrollButton {
|
|
constructor( element, infScroll ) {
|
|
this.element = element;
|
|
this.infScroll = infScroll;
|
|
// events
|
|
this.clickHandler = this.onClick.bind( this );
|
|
this.element.addEventListener( 'click', this.clickHandler );
|
|
infScroll.on( 'request', this.disable.bind( this ) );
|
|
infScroll.on( 'load', this.enable.bind( this ) );
|
|
infScroll.on( 'error', this.hide.bind( this ) );
|
|
infScroll.on( 'last', this.hide.bind( this ) );
|
|
}
|
|
|
|
onClick( event ) {
|
|
event.preventDefault();
|
|
this.infScroll.loadNextPage();
|
|
}
|
|
|
|
enable() {
|
|
this.element.removeAttribute('disabled');
|
|
}
|
|
|
|
disable() {
|
|
this.element.disabled = 'disabled';
|
|
}
|
|
|
|
hide() {
|
|
this.element.style.display = 'none';
|
|
}
|
|
|
|
destroy() {
|
|
this.element.removeEventListener( 'click', this.clickHandler );
|
|
}
|
|
|
|
}
|
|
|
|
// -------------------------- InfiniteScroll methods -------------------------- //
|
|
|
|
// InfiniteScroll.defaults.button = null;
|
|
|
|
InfiniteScroll.create.button = function() {
|
|
let buttonElem = utils.getQueryElement( this.options.button );
|
|
if ( buttonElem ) {
|
|
this.button = new InfiniteScrollButton( buttonElem, this );
|
|
}
|
|
};
|
|
|
|
InfiniteScroll.destroy.button = function() {
|
|
if ( this.button ) this.button.destroy();
|
|
};
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
InfiniteScroll.Button = InfiniteScrollButton;
|
|
|
|
return InfiniteScroll;
|
|
|
|
} ) );
|
|
// status
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
if ( typeof module == 'object' && module.exports ) {
|
|
// CommonJS
|
|
module.exports = factory(
|
|
window,
|
|
require('./core'),
|
|
require('fizzy-ui-utils'),
|
|
);
|
|
} else {
|
|
// browser global
|
|
factory(
|
|
window,
|
|
window.InfiniteScroll,
|
|
window.fizzyUIUtils,
|
|
);
|
|
}
|
|
|
|
}( window, function factory( window, InfiniteScroll, utils ) {
|
|
|
|
let proto = InfiniteScroll.prototype;
|
|
|
|
// InfiniteScroll.defaults.status = null;
|
|
|
|
InfiniteScroll.create.status = function() {
|
|
let statusElem = utils.getQueryElement( this.options.status );
|
|
if ( !statusElem ) return;
|
|
|
|
// elements
|
|
this.statusElement = statusElem;
|
|
this.statusEventElements = {
|
|
request: statusElem.querySelector('.infinite-scroll-request'),
|
|
error: statusElem.querySelector('.infinite-scroll-error'),
|
|
last: statusElem.querySelector('.infinite-scroll-last'),
|
|
};
|
|
// events
|
|
this.on( 'request', this.showRequestStatus );
|
|
this.on( 'error', this.showErrorStatus );
|
|
this.on( 'last', this.showLastStatus );
|
|
this.bindHideStatus('on');
|
|
};
|
|
|
|
proto.bindHideStatus = function( bindMethod ) {
|
|
let hideEvent = this.options.append ? 'append' : 'load';
|
|
this[ bindMethod ]( hideEvent, this.hideAllStatus );
|
|
};
|
|
|
|
proto.showRequestStatus = function() {
|
|
this.showStatus('request');
|
|
};
|
|
|
|
proto.showErrorStatus = function() {
|
|
this.showStatus('error');
|
|
};
|
|
|
|
proto.showLastStatus = function() {
|
|
this.showStatus('last');
|
|
// prevent last then append event race condition from showing last status #706
|
|
this.bindHideStatus('off');
|
|
};
|
|
|
|
proto.showStatus = function( eventName ) {
|
|
show( this.statusElement );
|
|
this.hideStatusEventElements();
|
|
let eventElem = this.statusEventElements[ eventName ];
|
|
show( eventElem );
|
|
};
|
|
|
|
proto.hideAllStatus = function() {
|
|
hide( this.statusElement );
|
|
this.hideStatusEventElements();
|
|
};
|
|
|
|
proto.hideStatusEventElements = function() {
|
|
for ( let type in this.statusEventElements ) {
|
|
let eventElem = this.statusEventElements[ type ];
|
|
hide( eventElem );
|
|
}
|
|
};
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
function hide( elem ) {
|
|
setDisplay( elem, 'none' );
|
|
}
|
|
|
|
function show( elem ) {
|
|
setDisplay( elem, 'block' );
|
|
}
|
|
|
|
function setDisplay( elem, value ) {
|
|
if ( elem ) {
|
|
elem.style.display = value;
|
|
}
|
|
}
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
return InfiniteScroll;
|
|
|
|
} ) );
|
|
/*!
|
|
* imagesLoaded v4.1.4
|
|
* JavaScript is all like "You images are done yet or what?"
|
|
* MIT License
|
|
*/
|
|
|
|
( function( window, factory ) { 'use strict';
|
|
// universal module definition
|
|
|
|
/*global define: false, module: false, require: false */
|
|
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD
|
|
define( [
|
|
'ev-emitter/ev-emitter'
|
|
], function( EvEmitter ) {
|
|
return factory( window, EvEmitter );
|
|
});
|
|
} else if ( typeof module == 'object' && module.exports ) {
|
|
// CommonJS
|
|
module.exports = factory(
|
|
window,
|
|
require('ev-emitter')
|
|
);
|
|
} else {
|
|
// browser global
|
|
window.imagesLoaded = factory(
|
|
window,
|
|
window.EvEmitter
|
|
);
|
|
}
|
|
|
|
})( typeof window !== 'undefined' ? window : this,
|
|
|
|
// -------------------------- factory -------------------------- //
|
|
|
|
function factory( window, EvEmitter ) {
|
|
|
|
'use strict';
|
|
|
|
var $ = window.jQuery;
|
|
var console = window.console;
|
|
|
|
// -------------------------- helpers -------------------------- //
|
|
|
|
// extend objects
|
|
function extend( a, b ) {
|
|
for ( var prop in b ) {
|
|
a[ prop ] = b[ prop ];
|
|
}
|
|
return a;
|
|
}
|
|
|
|
var arraySlice = Array.prototype.slice;
|
|
|
|
// turn element or nodeList into an array
|
|
function makeArray( obj ) {
|
|
if ( Array.isArray( obj ) ) {
|
|
// use object if already an array
|
|
return obj;
|
|
}
|
|
|
|
var isArrayLike = typeof obj == 'object' && typeof obj.length == 'number';
|
|
if ( isArrayLike ) {
|
|
// convert nodeList to array
|
|
return arraySlice.call( obj );
|
|
}
|
|
|
|
// array of single index
|
|
return [ obj ];
|
|
}
|
|
|
|
// -------------------------- imagesLoaded -------------------------- //
|
|
|
|
/**
|
|
* @param {Array, Element, NodeList, String} elem
|
|
* @param {Object or Function} options - if function, use as callback
|
|
* @param {Function} onAlways - callback function
|
|
*/
|
|
function ImagesLoaded( elem, options, onAlways ) {
|
|
// coerce ImagesLoaded() without new, to be new ImagesLoaded()
|
|
if ( !( this instanceof ImagesLoaded ) ) {
|
|
return new ImagesLoaded( elem, options, onAlways );
|
|
}
|
|
// use elem as selector string
|
|
var queryElem = elem;
|
|
if ( typeof elem == 'string' ) {
|
|
queryElem = document.querySelectorAll( elem );
|
|
}
|
|
// bail if bad element
|
|
if ( !queryElem ) {
|
|
console.error( 'Bad element for imagesLoaded ' + ( queryElem || elem ) );
|
|
return;
|
|
}
|
|
|
|
this.elements = makeArray( queryElem );
|
|
this.options = extend( {}, this.options );
|
|
// shift arguments if no options set
|
|
if ( typeof options == 'function' ) {
|
|
onAlways = options;
|
|
} else {
|
|
extend( this.options, options );
|
|
}
|
|
|
|
if ( onAlways ) {
|
|
this.on( 'always', onAlways );
|
|
}
|
|
|
|
this.getImages();
|
|
|
|
if ( $ ) {
|
|
// add jQuery Deferred object
|
|
this.jqDeferred = new $.Deferred();
|
|
}
|
|
|
|
// HACK check async to allow time to bind listeners
|
|
setTimeout( this.check.bind( this ) );
|
|
}
|
|
|
|
ImagesLoaded.prototype = Object.create( EvEmitter.prototype );
|
|
|
|
ImagesLoaded.prototype.options = {};
|
|
|
|
ImagesLoaded.prototype.getImages = function() {
|
|
this.images = [];
|
|
|
|
// filter & find items if we have an item selector
|
|
this.elements.forEach( this.addElementImages, this );
|
|
};
|
|
|
|
/**
|
|
* @param {Node} element
|
|
*/
|
|
ImagesLoaded.prototype.addElementImages = function( elem ) {
|
|
// filter siblings
|
|
if ( elem.nodeName == 'IMG' ) {
|
|
this.addImage( elem );
|
|
}
|
|
// get background image on element
|
|
if ( this.options.background === true ) {
|
|
this.addElementBackgroundImages( elem );
|
|
}
|
|
|
|
// find children
|
|
// no non-element nodes, #143
|
|
var nodeType = elem.nodeType;
|
|
if ( !nodeType || !elementNodeTypes[ nodeType ] ) {
|
|
return;
|
|
}
|
|
var childImgs = elem.querySelectorAll('img');
|
|
// concat childElems to filterFound array
|
|
for ( var i=0; i < childImgs.length; i++ ) {
|
|
var img = childImgs[i];
|
|
this.addImage( img );
|
|
}
|
|
|
|
// get child background images
|
|
if ( typeof this.options.background == 'string' ) {
|
|
var children = elem.querySelectorAll( this.options.background );
|
|
for ( i=0; i < children.length; i++ ) {
|
|
var child = children[i];
|
|
this.addElementBackgroundImages( child );
|
|
}
|
|
}
|
|
};
|
|
|
|
var elementNodeTypes = {
|
|
1: true,
|
|
9: true,
|
|
11: true
|
|
};
|
|
|
|
ImagesLoaded.prototype.addElementBackgroundImages = function( elem ) {
|
|
var style = getComputedStyle( elem );
|
|
if ( !style ) {
|
|
// Firefox returns null if in a hidden iframe https://bugzil.la/548397
|
|
return;
|
|
}
|
|
// get url inside url("...")
|
|
var reURL = /url\((['"])?(.*?)\1\)/gi;
|
|
var matches = reURL.exec( style.backgroundImage );
|
|
while ( matches !== null ) {
|
|
var url = matches && matches[2];
|
|
if ( url ) {
|
|
this.addBackground( url, elem );
|
|
}
|
|
matches = reURL.exec( style.backgroundImage );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {Image} img
|
|
*/
|
|
ImagesLoaded.prototype.addImage = function( img ) {
|
|
var loadingImage = new LoadingImage( img );
|
|
this.images.push( loadingImage );
|
|
};
|
|
|
|
ImagesLoaded.prototype.addBackground = function( url, elem ) {
|
|
var background = new Background( url, elem );
|
|
this.images.push( background );
|
|
};
|
|
|
|
ImagesLoaded.prototype.check = function() {
|
|
var _this = this;
|
|
this.progressedCount = 0;
|
|
this.hasAnyBroken = false;
|
|
// complete if no images
|
|
if ( !this.images.length ) {
|
|
this.complete();
|
|
return;
|
|
}
|
|
|
|
function onProgress( image, elem, message ) {
|
|
// HACK - Chrome triggers event before object properties have changed. #83
|
|
setTimeout( function() {
|
|
_this.progress( image, elem, message );
|
|
});
|
|
}
|
|
|
|
this.images.forEach( function( loadingImage ) {
|
|
loadingImage.once( 'progress', onProgress );
|
|
loadingImage.check();
|
|
});
|
|
};
|
|
|
|
ImagesLoaded.prototype.progress = function( image, elem, message ) {
|
|
this.progressedCount++;
|
|
this.hasAnyBroken = this.hasAnyBroken || !image.isLoaded;
|
|
// progress event
|
|
this.emitEvent( 'progress', [ this, image, elem ] );
|
|
if ( this.jqDeferred && this.jqDeferred.notify ) {
|
|
this.jqDeferred.notify( this, image );
|
|
}
|
|
// check if completed
|
|
if ( this.progressedCount == this.images.length ) {
|
|
this.complete();
|
|
}
|
|
|
|
if ( this.options.debug && console ) {
|
|
console.log( 'progress: ' + message, image, elem );
|
|
}
|
|
};
|
|
|
|
ImagesLoaded.prototype.complete = function() {
|
|
var eventName = this.hasAnyBroken ? 'fail' : 'done';
|
|
this.isComplete = true;
|
|
this.emitEvent( eventName, [ this ] );
|
|
this.emitEvent( 'always', [ this ] );
|
|
if ( this.jqDeferred ) {
|
|
var jqMethod = this.hasAnyBroken ? 'reject' : 'resolve';
|
|
this.jqDeferred[ jqMethod ]( this );
|
|
}
|
|
};
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
function LoadingImage( img ) {
|
|
this.img = img;
|
|
}
|
|
|
|
LoadingImage.prototype = Object.create( EvEmitter.prototype );
|
|
|
|
LoadingImage.prototype.check = function() {
|
|
// If complete is true and browser supports natural sizes,
|
|
// try to check for image status manually.
|
|
var isComplete = this.getIsImageComplete();
|
|
if ( isComplete ) {
|
|
// report based on naturalWidth
|
|
this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
|
|
return;
|
|
}
|
|
|
|
// If none of the checks above matched, simulate loading on detached element.
|
|
this.proxyImage = new Image();
|
|
this.proxyImage.addEventListener( 'load', this );
|
|
this.proxyImage.addEventListener( 'error', this );
|
|
// bind to image as well for Firefox. #191
|
|
this.img.addEventListener( 'load', this );
|
|
this.img.addEventListener( 'error', this );
|
|
this.proxyImage.src = this.img.src;
|
|
};
|
|
|
|
LoadingImage.prototype.getIsImageComplete = function() {
|
|
// check for non-zero, non-undefined naturalWidth
|
|
// fixes Safari+InfiniteScroll+Masonry bug infinite-scroll#671
|
|
return this.img.complete && this.img.naturalWidth;
|
|
};
|
|
|
|
LoadingImage.prototype.confirm = function( isLoaded, message ) {
|
|
this.isLoaded = isLoaded;
|
|
this.emitEvent( 'progress', [ this, this.img, message ] );
|
|
};
|
|
|
|
// ----- events ----- //
|
|
|
|
// trigger specified handler for event type
|
|
LoadingImage.prototype.handleEvent = function( event ) {
|
|
var method = 'on' + event.type;
|
|
if ( this[ method ] ) {
|
|
this[ method ]( event );
|
|
}
|
|
};
|
|
|
|
LoadingImage.prototype.onload = function() {
|
|
this.confirm( true, 'onload' );
|
|
this.unbindEvents();
|
|
};
|
|
|
|
LoadingImage.prototype.onerror = function() {
|
|
this.confirm( false, 'onerror' );
|
|
this.unbindEvents();
|
|
};
|
|
|
|
LoadingImage.prototype.unbindEvents = function() {
|
|
this.proxyImage.removeEventListener( 'load', this );
|
|
this.proxyImage.removeEventListener( 'error', this );
|
|
this.img.removeEventListener( 'load', this );
|
|
this.img.removeEventListener( 'error', this );
|
|
};
|
|
|
|
// -------------------------- Background -------------------------- //
|
|
|
|
function Background( url, element ) {
|
|
this.url = url;
|
|
this.element = element;
|
|
this.img = new Image();
|
|
}
|
|
|
|
// inherit LoadingImage prototype
|
|
Background.prototype = Object.create( LoadingImage.prototype );
|
|
|
|
Background.prototype.check = function() {
|
|
this.img.addEventListener( 'load', this );
|
|
this.img.addEventListener( 'error', this );
|
|
this.img.src = this.url;
|
|
// check if image is already complete
|
|
var isComplete = this.getIsImageComplete();
|
|
if ( isComplete ) {
|
|
this.confirm( this.img.naturalWidth !== 0, 'naturalWidth' );
|
|
this.unbindEvents();
|
|
}
|
|
};
|
|
|
|
Background.prototype.unbindEvents = function() {
|
|
this.img.removeEventListener( 'load', this );
|
|
this.img.removeEventListener( 'error', this );
|
|
};
|
|
|
|
Background.prototype.confirm = function( isLoaded, message ) {
|
|
this.isLoaded = isLoaded;
|
|
this.emitEvent( 'progress', [ this, this.element, message ] );
|
|
};
|
|
|
|
// -------------------------- jQuery -------------------------- //
|
|
|
|
ImagesLoaded.makeJQueryPlugin = function( jQuery ) {
|
|
jQuery = jQuery || window.jQuery;
|
|
if ( !jQuery ) {
|
|
return;
|
|
}
|
|
// set local variable
|
|
$ = jQuery;
|
|
// $().imagesLoaded()
|
|
$.fn.imagesLoaded = function( options, callback ) {
|
|
var instance = new ImagesLoaded( this, options, callback );
|
|
return instance.jqDeferred.promise( $(this) );
|
|
};
|
|
};
|
|
// try making plugin
|
|
ImagesLoaded.makeJQueryPlugin();
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
return ImagesLoaded;
|
|
|
|
});
|