2184 lines
55 KiB
JavaScript
2184 lines
55 KiB
JavaScript
/*!
|
|
* Infinite Scroll PACKAGED v3.0.2
|
|
* Automatically add next page
|
|
*
|
|
* Licensed GPLv3 for open source use
|
|
* or Infinite Scroll Commercial License for commercial use
|
|
*
|
|
* https://infinite-scroll.com
|
|
* Copyright 2017 Metafizzy
|
|
*/
|
|
|
|
/**
|
|
* Bridget makes jQuery widgets
|
|
* v2.0.1
|
|
* MIT license
|
|
*/
|
|
|
|
/* jshint browser: true, strict: true, undef: true, unused: true */
|
|
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
/*jshint strict: false */ /* globals define, module, require */
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD
|
|
define( 'jquery-bridget/jquery-bridget',[ 'jquery' ], function( jQuery ) {
|
|
return factory( window, jQuery );
|
|
});
|
|
} else 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 ) {
|
|
'use strict';
|
|
|
|
// ----- utils ----- //
|
|
|
|
var arraySlice = Array.prototype.slice;
|
|
|
|
// helper function for logging errors
|
|
// $.error breaks jQuery chaining
|
|
var console = window.console;
|
|
var 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 ) {
|
|
// bail out if not an object
|
|
if ( !$.isPlainObject( opts ) ){
|
|
return;
|
|
}
|
|
this.options = $.extend( true, this.options, opts );
|
|
};
|
|
}
|
|
|
|
// make jQuery plugin
|
|
$.fn[ namespace ] = function( arg0 /*, arg1 */ ) {
|
|
if ( typeof arg0 == 'string' ) {
|
|
// method call $().plugin( 'methodName', { options } )
|
|
// shift arguments by 1
|
|
var args = arraySlice.call( arguments, 1 );
|
|
return methodCall( this, arg0, args );
|
|
}
|
|
// just $().plugin({ options })
|
|
plainCall( this, arg0 );
|
|
return this;
|
|
};
|
|
|
|
// $().plugin('methodName')
|
|
function methodCall( $elems, methodName, args ) {
|
|
var returnValue;
|
|
var pluginMethodStr = '$().' + namespace + '("' + methodName + '")';
|
|
|
|
$elems.each( function( i, elem ) {
|
|
// get instance
|
|
var instance = $.data( elem, namespace );
|
|
if ( !instance ) {
|
|
logError( namespace + ' not initialized. Cannot call methods, i.e. ' +
|
|
pluginMethodStr );
|
|
return;
|
|
}
|
|
|
|
var method = instance[ methodName ];
|
|
if ( !method || methodName.charAt(0) == '_' ) {
|
|
logError( pluginMethodStr + ' is not a valid method' );
|
|
return;
|
|
}
|
|
|
|
// apply method, get return value
|
|
var 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 ) {
|
|
var 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 );
|
|
}
|
|
});
|
|
}
|
|
|
|
updateJQuery( $ );
|
|
|
|
}
|
|
|
|
// ----- updateJQuery ----- //
|
|
|
|
// set $.bridget for v1 backwards compatibility
|
|
function updateJQuery( $ ) {
|
|
if ( !$ || ( $ && $.bridget ) ) {
|
|
return;
|
|
}
|
|
$.bridget = jQueryBridget;
|
|
}
|
|
|
|
updateJQuery( jQuery || window.jQuery );
|
|
|
|
// ----- ----- //
|
|
|
|
return jQueryBridget;
|
|
|
|
}));
|
|
|
|
/**
|
|
* EvEmitter v1.1.0
|
|
* Lil' event emitter
|
|
* MIT License
|
|
*/
|
|
|
|
/* jshint unused: true, undef: true, strict: true */
|
|
|
|
( function( global, factory ) {
|
|
// universal module definition
|
|
/* jshint strict: false */ /* globals define, module, window */
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD - RequireJS
|
|
define( 'ev-emitter/ev-emitter',factory );
|
|
} else 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() {}
|
|
|
|
var proto = EvEmitter.prototype;
|
|
|
|
proto.on = function( eventName, listener ) {
|
|
if ( !eventName || !listener ) {
|
|
return;
|
|
}
|
|
// set events hash
|
|
var events = this._events = this._events || {};
|
|
// set listeners array
|
|
var listeners = events[ eventName ] = events[ eventName ] || [];
|
|
// only add once
|
|
if ( listeners.indexOf( listener ) == -1 ) {
|
|
listeners.push( listener );
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
proto.once = function( eventName, listener ) {
|
|
if ( !eventName || !listener ) {
|
|
return;
|
|
}
|
|
// add event
|
|
this.on( eventName, listener );
|
|
// set once flag
|
|
// set onceEvents hash
|
|
var onceEvents = this._onceEvents = this._onceEvents || {};
|
|
// set onceListeners object
|
|
var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {};
|
|
// set flag
|
|
onceListeners[ listener ] = true;
|
|
|
|
return this;
|
|
};
|
|
|
|
proto.off = function( eventName, listener ) {
|
|
var listeners = this._events && this._events[ eventName ];
|
|
if ( !listeners || !listeners.length ) {
|
|
return;
|
|
}
|
|
var index = listeners.indexOf( listener );
|
|
if ( index != -1 ) {
|
|
listeners.splice( index, 1 );
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
proto.emitEvent = function( eventName, args ) {
|
|
var listeners = this._events && this._events[ eventName ];
|
|
if ( !listeners || !listeners.length ) {
|
|
return;
|
|
}
|
|
var i = 0;
|
|
var listener = listeners[i];
|
|
args = args || [];
|
|
// once stuff
|
|
var onceListeners = this._onceEvents && this._onceEvents[ eventName ];
|
|
|
|
while ( listener ) {
|
|
var 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 );
|
|
// get next listener
|
|
i += isOnce ? 0 : 1;
|
|
listener = listeners[i];
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
proto.allOff =
|
|
proto.removeAllListeners = function() {
|
|
delete this._events;
|
|
delete this._onceEvents;
|
|
};
|
|
|
|
return EvEmitter;
|
|
|
|
}));
|
|
|
|
/**
|
|
* matchesSelector v2.0.2
|
|
* matchesSelector( element, '.selector' )
|
|
* MIT license
|
|
*/
|
|
|
|
/*jshint browser: true, strict: true, undef: true, unused: true */
|
|
|
|
( function( window, factory ) {
|
|
/*global define: false, module: false */
|
|
'use strict';
|
|
// universal module definition
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD
|
|
define( 'desandro-matches-selector/matches-selector',factory );
|
|
} else if ( typeof module == 'object' && module.exports ) {
|
|
// CommonJS
|
|
module.exports = factory();
|
|
} else {
|
|
// browser global
|
|
window.matchesSelector = factory();
|
|
}
|
|
|
|
}( window, function factory() {
|
|
'use strict';
|
|
|
|
var matchesMethod = ( function() {
|
|
var ElemProto = window.Element.prototype;
|
|
// check for the standard method name first
|
|
if ( ElemProto.matches ) {
|
|
return 'matches';
|
|
}
|
|
// check un-prefixed
|
|
if ( ElemProto.matchesSelector ) {
|
|
return 'matchesSelector';
|
|
}
|
|
// check vendor prefixes
|
|
var prefixes = [ 'webkit', 'moz', 'ms', 'o' ];
|
|
|
|
for ( var i=0; i < prefixes.length; i++ ) {
|
|
var prefix = prefixes[i];
|
|
var method = prefix + 'MatchesSelector';
|
|
if ( ElemProto[ method ] ) {
|
|
return method;
|
|
}
|
|
}
|
|
})();
|
|
|
|
return function matchesSelector( elem, selector ) {
|
|
return elem[ matchesMethod ]( selector );
|
|
};
|
|
|
|
}));
|
|
|
|
/**
|
|
* Fizzy UI utils v2.0.5
|
|
* MIT license
|
|
*/
|
|
|
|
/*jshint browser: true, undef: true, unused: true, strict: true */
|
|
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
/*jshint strict: false */ /*globals define, module, require */
|
|
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD
|
|
define( 'fizzy-ui-utils/utils',[
|
|
'desandro-matches-selector/matches-selector'
|
|
], function( matchesSelector ) {
|
|
return factory( window, matchesSelector );
|
|
});
|
|
} else if ( typeof module == 'object' && module.exports ) {
|
|
// CommonJS
|
|
module.exports = factory(
|
|
window,
|
|
require('desandro-matches-selector')
|
|
);
|
|
} else {
|
|
// browser global
|
|
window.fizzyUIUtils = factory(
|
|
window,
|
|
window.matchesSelector
|
|
);
|
|
}
|
|
|
|
}( window, function factory( window, matchesSelector ) {
|
|
|
|
|
|
|
|
var utils = {};
|
|
|
|
// ----- extend ----- //
|
|
|
|
// extends objects
|
|
utils.extend = function( a, b ) {
|
|
for ( var prop in b ) {
|
|
a[ prop ] = b[ prop ];
|
|
}
|
|
return a;
|
|
};
|
|
|
|
// ----- modulo ----- //
|
|
|
|
utils.modulo = function( num, div ) {
|
|
return ( ( num % div ) + div ) % div;
|
|
};
|
|
|
|
// ----- makeArray ----- //
|
|
|
|
// turn element or nodeList into an array
|
|
utils.makeArray = function( obj ) {
|
|
var ary = [];
|
|
if ( Array.isArray( obj ) ) {
|
|
// use object if already an array
|
|
ary = obj;
|
|
} else if ( obj && typeof obj == 'object' &&
|
|
typeof obj.length == 'number' ) {
|
|
// convert nodeList to array
|
|
for ( var i=0; i < obj.length; i++ ) {
|
|
ary.push( obj[i] );
|
|
}
|
|
} else {
|
|
// array of single index
|
|
ary.push( obj );
|
|
}
|
|
return ary;
|
|
};
|
|
|
|
// ----- removeFrom ----- //
|
|
|
|
utils.removeFrom = function( ary, obj ) {
|
|
var 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 ( matchesSelector( elem, 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 ) {
|
|
var method = 'on' + event.type;
|
|
if ( this[ method ] ) {
|
|
this[ method ]( event );
|
|
}
|
|
};
|
|
|
|
// ----- filterFindElements ----- //
|
|
|
|
utils.filterFindElements = function( elems, selector ) {
|
|
// make array of elems
|
|
elems = utils.makeArray( elems );
|
|
var ffElems = [];
|
|
|
|
elems.forEach( function( elem ) {
|
|
// check that elem is an actual element
|
|
if ( !( elem instanceof HTMLElement ) ) {
|
|
return;
|
|
}
|
|
// add elem if no selector
|
|
if ( !selector ) {
|
|
ffElems.push( elem );
|
|
return;
|
|
}
|
|
// filter & find items if we have a selector
|
|
// filter
|
|
if ( matchesSelector( elem, selector ) ) {
|
|
ffElems.push( elem );
|
|
}
|
|
// find children
|
|
var childElems = elem.querySelectorAll( selector );
|
|
// concat childElems to filterFound array
|
|
for ( var i=0; i < childElems.length; i++ ) {
|
|
ffElems.push( childElems[i] );
|
|
}
|
|
});
|
|
|
|
return ffElems;
|
|
};
|
|
|
|
// ----- debounceMethod ----- //
|
|
|
|
utils.debounceMethod = function( _class, methodName, threshold ) {
|
|
// original method
|
|
var method = _class.prototype[ methodName ];
|
|
var timeoutName = methodName + 'Timeout';
|
|
|
|
_class.prototype[ methodName ] = function() {
|
|
var timeout = this[ timeoutName ];
|
|
if ( timeout ) {
|
|
clearTimeout( timeout );
|
|
}
|
|
var args = arguments;
|
|
|
|
var _this = this;
|
|
this[ timeoutName ] = setTimeout( function() {
|
|
method.apply( _this, args );
|
|
delete _this[ timeoutName ];
|
|
}, threshold || 100 );
|
|
};
|
|
};
|
|
|
|
// ----- docReady ----- //
|
|
|
|
utils.docReady = function( callback ) {
|
|
var readyState = document.readyState;
|
|
if ( readyState == 'complete' || readyState == 'interactive' ) {
|
|
// do async to allow for other scripts to run. metafizzy/flickity#441
|
|
setTimeout( callback );
|
|
} else {
|
|
document.addEventListener( 'DOMContentLoaded', callback );
|
|
}
|
|
};
|
|
|
|
// ----- htmlInit ----- //
|
|
|
|
// http://jamesroberts.name/blog/2010/02/22/string-functions-for-javascript-trim-to-camel-case-to-dashed-and-to-underscore/
|
|
utils.toDashed = function( str ) {
|
|
return str.replace( /(.)([A-Z])/g, function( match, $1, $2 ) {
|
|
return $1 + '-' + $2;
|
|
}).toLowerCase();
|
|
};
|
|
|
|
var console = window.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() {
|
|
var dashedNamespace = utils.toDashed( namespace );
|
|
var dataAttr = 'data-' + dashedNamespace;
|
|
var dataAttrElems = document.querySelectorAll( '[' + dataAttr + ']' );
|
|
var jsDashElems = document.querySelectorAll( '.js-' + dashedNamespace );
|
|
var elems = utils.makeArray( dataAttrElems )
|
|
.concat( utils.makeArray( jsDashElems ) );
|
|
var dataOptionsAttr = dataAttr + '-options';
|
|
var jQuery = window.jQuery;
|
|
|
|
elems.forEach( function( elem ) {
|
|
var attr = elem.getAttribute( dataAttr ) ||
|
|
elem.getAttribute( dataOptionsAttr );
|
|
var 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
|
|
var 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
|
|
/* globals define, module, require */
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD
|
|
define( 'infinite-scroll/js/core',[
|
|
'ev-emitter/ev-emitter',
|
|
'fizzy-ui-utils/utils',
|
|
], function( EvEmitter, utils) {
|
|
return factory( window, EvEmitter, utils );
|
|
});
|
|
} else 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 ) {
|
|
|
|
var jQuery = window.jQuery;
|
|
// internal store of all InfiniteScroll intances
|
|
var instances = {};
|
|
|
|
function InfiniteScroll( element, options ) {
|
|
var 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 ) {
|
|
var instance = instances[ element.infiniteScrollGUID ];
|
|
instance.option( options );
|
|
return instance;
|
|
}
|
|
|
|
this.element = element;
|
|
// options
|
|
this.options = utils.extend( {}, 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 = {};
|
|
|
|
var proto = InfiniteScroll.prototype;
|
|
// inherit EvEmitter
|
|
utils.extend( proto, EvEmitter.prototype );
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
// globally unique identifiers
|
|
var GUID = 0;
|
|
|
|
proto.create = function() {
|
|
// create core
|
|
// add id for InfiniteScroll.data
|
|
var 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
|
|
if ( !this.getPath ) {
|
|
console.error('Disabling InfiniteScroll');
|
|
return;
|
|
}
|
|
this.updateGetAbsolutePath();
|
|
this.log( 'initialized', [ this.element.className ] );
|
|
this.callOnInit();
|
|
// create features
|
|
for ( var method in InfiniteScroll.create ) {
|
|
InfiniteScroll.create[ method ].call( this );
|
|
}
|
|
};
|
|
|
|
proto.option = function( opts ) {
|
|
utils.extend( this.options, opts );
|
|
};
|
|
|
|
// call onInit option, used for binding events on init
|
|
proto.callOnInit = function() {
|
|
var onInit = this.options.onInit;
|
|
if ( onInit ) {
|
|
onInit.call( this, this );
|
|
}
|
|
};
|
|
|
|
// ----- events ----- //
|
|
|
|
proto.dispatchEvent = function( type, event, args ) {
|
|
this.log( type, args );
|
|
var emitArgs = event ? [ event ].concat( args ) : args;
|
|
this.emitEvent( type, emitArgs );
|
|
// trigger jQuery event
|
|
if ( !jQuery || !this.$element ) {
|
|
return;
|
|
}
|
|
// namespace jQuery event
|
|
type += '.infiniteScroll';
|
|
var $event = type;
|
|
if ( event ) {
|
|
// create jQuery event
|
|
var jQEvent = jQuery.Event( event );
|
|
jQEvent.type = type;
|
|
$event = jQEvent;
|
|
}
|
|
this.$element.trigger( $event, args );
|
|
};
|
|
|
|
var loggers = {
|
|
initialized: function( className ) {
|
|
return 'on ' + className;
|
|
},
|
|
request: function( path ) {
|
|
return 'URL: ' + path;
|
|
},
|
|
load: function( response, path ) {
|
|
return ( response.title || '' ) + '. URL: ' + path;
|
|
},
|
|
error: function( error, path ) {
|
|
return error + '. URL: ' + path;
|
|
},
|
|
append: function( response, path, items ) {
|
|
return items.length + ' items. URL: ' + path;
|
|
},
|
|
last: function( response, path ) {
|
|
return 'URL: ' + path;
|
|
},
|
|
history: function( title, path ) {
|
|
return '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;
|
|
}
|
|
var message = '[InfiniteScroll] ' + type;
|
|
var logger = loggers[ type ];
|
|
if ( logger ) {
|
|
message += '. ' + logger.apply( this, args );
|
|
}
|
|
console.log( message );
|
|
};
|
|
|
|
// -------------------------- methods used amoung features -------------------------- //
|
|
|
|
proto.updateMeasurements = function() {
|
|
this.windowHeight = window.innerHeight;
|
|
var rect = this.element.getBoundingClientRect();
|
|
this.top = rect.top + window.pageYOffset;
|
|
};
|
|
|
|
proto.updateScroller = function() {
|
|
var 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 'Unable to find elementScroll: ' + elementScroll;
|
|
}
|
|
};
|
|
|
|
// -------------------------- page path -------------------------- //
|
|
|
|
proto.updateGetPath = function() {
|
|
var optPath = this.options.path;
|
|
if ( !optPath ) {
|
|
console.error( 'InfiniteScroll path option required. Set as: ' + optPath );
|
|
return;
|
|
}
|
|
// function
|
|
var type = typeof optPath;
|
|
if ( type == 'function' ) {
|
|
this.getPath = optPath;
|
|
return;
|
|
}
|
|
// template string: '/pages/{{#}}.html'
|
|
var 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 = function() {
|
|
var nextIndex = this.pageIndex + 1;
|
|
return optPath.replace( '{{#}}', nextIndex );
|
|
}.bind( this );
|
|
// get pageIndex from location
|
|
// convert path option into regex to look for pattern in location
|
|
var regexString = optPath.replace( '{{#}}', '(\\d\\d?\\d?)' );
|
|
var templateRe = new RegExp( regexString );
|
|
var match = location.href.match( templateRe );
|
|
if ( match ) {
|
|
this.pageIndex = parseInt( match[1], 10 );
|
|
this.log( 'pageIndex', this.pageIndex, 'template string' );
|
|
}
|
|
};
|
|
|
|
var 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)(.*?$)/,
|
|
];
|
|
|
|
proto.updateGetPathSelector = function( optPath ) {
|
|
// parse href of link: '.next-page-link'
|
|
var hrefElem = document.querySelector( optPath );
|
|
if ( !hrefElem ) {
|
|
console.error( 'Bad InfiniteScroll path option. Next link not found: ' +
|
|
optPath );
|
|
return;
|
|
}
|
|
var href = hrefElem.getAttribute('href');
|
|
// try matching href to pathRegexes patterns
|
|
var pathParts, regex;
|
|
for ( var i=0; href && i < pathRegexes.length; i++ ) {
|
|
regex = pathRegexes[i];
|
|
var match = href.match( regex );
|
|
if ( match ) {
|
|
pathParts = match.slice(1); // remove first part
|
|
break;
|
|
}
|
|
}
|
|
if ( !pathParts ) {
|
|
console.error( 'InfiniteScroll unable to parse next link href: ' + href );
|
|
return;
|
|
}
|
|
this.isPathSelector = true; // flag for checkLastPage()
|
|
this.getPath = function() {
|
|
var nextIndex = this.pageIndex + 1;
|
|
return pathParts[0] + nextIndex + pathParts[2];
|
|
}.bind( this );
|
|
// get pageIndex from href
|
|
this.pageIndex = parseInt( pathParts[1], 10 ) - 1;
|
|
this.log( 'pageIndex', [ this.pageIndex, 'next link' ] );
|
|
};
|
|
|
|
proto.updateGetAbsolutePath = function() {
|
|
var path = this.getPath();
|
|
// path doesn't start with http or /
|
|
var isAbsolute = path.match( /^http/ ) || path.match( /^\// );
|
|
if ( isAbsolute ) {
|
|
this.getAbsolutePath = this.getPath;
|
|
return;
|
|
}
|
|
|
|
var pathname = location.pathname;
|
|
// /foo/bar/index.html => /foo/bar
|
|
var directory = pathname.substring( 0, pathname.lastIndexOf('/') );
|
|
|
|
this.getAbsolutePath = function() {
|
|
return directory + '/' + this.getPath();
|
|
};
|
|
};
|
|
|
|
// -------------------------- nav -------------------------- //
|
|
|
|
// hide navigation
|
|
InfiniteScroll.create.hideNav = function() {
|
|
var 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 ( var method in InfiniteScroll.destroy ) {
|
|
InfiniteScroll.destroy[ method ].call( this );
|
|
}
|
|
|
|
delete this.element.infiniteScrollGUID;
|
|
delete instances[ this.guid ];
|
|
};
|
|
|
|
// -------------------------- utilities -------------------------- //
|
|
|
|
// https://remysharp.com/2010/07/21/throttling-function-calls
|
|
InfiniteScroll.throttle = function( fn, threshold ) {
|
|
threshold = threshold || 200;
|
|
var last, timeout;
|
|
|
|
return function() {
|
|
var now = +new Date();
|
|
var args = arguments;
|
|
var trigger = function() {
|
|
last = now;
|
|
fn.apply( this, args );
|
|
}.bind( this );
|
|
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 );
|
|
var id = elem && elem.infiniteScrollGUID;
|
|
return id && instances[ id ];
|
|
};
|
|
|
|
// set internal jQuery, for Webpack + jQuery v3
|
|
InfiniteScroll.setJQuery = function( $ ) {
|
|
jQuery = $;
|
|
};
|
|
|
|
// -------------------------- setup -------------------------- //
|
|
|
|
utils.htmlInit( InfiniteScroll, 'infinite-scroll' );
|
|
|
|
if ( jQuery && jQuery.bridget ) {
|
|
jQuery.bridget( 'infiniteScroll', InfiniteScroll );
|
|
}
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
return InfiniteScroll;
|
|
|
|
}));
|
|
|
|
// page-load
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
/* globals define, module, require */
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD
|
|
define( 'infinite-scroll/js/page-load',[
|
|
'./core',
|
|
], function( InfiniteScroll ) {
|
|
return factory( window, InfiniteScroll );
|
|
});
|
|
} else 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 ) {
|
|
|
|
var proto = InfiniteScroll.prototype;
|
|
|
|
// InfiniteScroll.defaults.append = false;
|
|
InfiniteScroll.defaults.loadOnScroll = true;
|
|
InfiniteScroll.defaults.checkLastPage = true;
|
|
InfiniteScroll.defaults.responseType = 'document';
|
|
// InfiniteScroll.defaults.prefill = false;
|
|
// InfiniteScroll.defaults.outlayer = null;
|
|
|
|
InfiniteScroll.create.pageLoad = function() {
|
|
this.canLoad = true;
|
|
this.on( 'scrollThreshold', this.onScrollThresholdLoad );
|
|
this.on( 'append', this.checkLastPage );
|
|
if ( this.options.outlayer ) {
|
|
this.on( 'append', this.onAppendOutlayer );
|
|
}
|
|
};
|
|
|
|
proto.onScrollThresholdLoad = function() {
|
|
if ( this.options.loadOnScroll ) {
|
|
this.loadNextPage();
|
|
}
|
|
};
|
|
|
|
proto.loadNextPage = function() {
|
|
if ( this.isLoading || !this.canLoad ) {
|
|
return;
|
|
}
|
|
|
|
var path = this.getAbsolutePath();
|
|
this.isLoading = true;
|
|
|
|
var onLoad = function( response ) {
|
|
this.onPageLoad( response, path );
|
|
}.bind( this );
|
|
|
|
var onError = function( error ) {
|
|
this.onPageError( error, path );
|
|
}.bind( this );
|
|
|
|
request( path, this.options.responseType, onLoad, onError );
|
|
this.dispatchEvent( 'request', null, [ path ] );
|
|
};
|
|
|
|
proto.onPageLoad = function( response, path ) {
|
|
// done loading if not appending
|
|
if ( !this.options.append ) {
|
|
this.isLoading = false;
|
|
}
|
|
this.pageIndex++;
|
|
this.loadCount++;
|
|
this.dispatchEvent( 'load', null, [ response, path ] );
|
|
this.appendNextPage( response, path );
|
|
return response;
|
|
};
|
|
|
|
proto.appendNextPage = function( response, path ) {
|
|
var optAppend = this.options.append;
|
|
// do not append json
|
|
var isDocument = this.options.responseType == 'document';
|
|
if ( !isDocument || !optAppend ) {
|
|
return;
|
|
}
|
|
|
|
var items = response.querySelectorAll( optAppend );
|
|
var fragment = getItemsFragment( items );
|
|
var appendReady = function () {
|
|
this.appendItems( items, fragment );
|
|
this.isLoading = false;
|
|
this.dispatchEvent( 'append', null, [ response, path, items ] );
|
|
}.bind( this );
|
|
|
|
// TODO add hook for option to trigger appendReady
|
|
if ( this.options.outlayer ) {
|
|
this.appendOutlayerItems( fragment, appendReady );
|
|
} else {
|
|
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
|
|
var fragment = document.createDocumentFragment();
|
|
for ( var i=0; items && i < items.length; i++ ) {
|
|
fragment.appendChild( items[i] );
|
|
}
|
|
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 ) {
|
|
var scripts = fragment.querySelectorAll('script');
|
|
for ( var i=0; i < scripts.length; i++ ) {
|
|
var script = scripts[i];
|
|
var freshScript = document.createElement('script');
|
|
copyAttributes( script, freshScript );
|
|
script.parentNode.replaceChild( freshScript, script );
|
|
}
|
|
}
|
|
|
|
function copyAttributes( fromNode, toNode ) {
|
|
var attrs = fromNode.attributes;
|
|
for ( var i=0; i < attrs.length; i++ ) {
|
|
var attr = attrs[i];
|
|
toNode.setAttribute( attr.name, attr.value );
|
|
}
|
|
}
|
|
|
|
// ----- outlayer ----- //
|
|
|
|
proto.appendOutlayerItems = function( fragment, appendReady ) {
|
|
var imagesLoaded = InfiniteScroll.imagesLoaded || window.imagesLoaded;
|
|
if ( !imagesLoaded ) {
|
|
console.error('[InfiniteScroll] imagesLoaded required for outlayer option');
|
|
this.isLoading = false;
|
|
return;
|
|
}
|
|
// append once images loaded
|
|
imagesLoaded( fragment, appendReady );
|
|
};
|
|
|
|
proto.onAppendOutlayer = function( response, path, items ) {
|
|
this.options.outlayer.appended( items );
|
|
};
|
|
|
|
// ----- checkLastPage ----- //
|
|
|
|
// check response for next element
|
|
proto.checkLastPage = function( response, path ) {
|
|
var checkLastPage = this.options.checkLastPage;
|
|
if ( !checkLastPage ) {
|
|
return;
|
|
}
|
|
|
|
var pathOpt = this.options.path;
|
|
// if path is function, check if next path is truthy
|
|
if ( typeof pathOpt == 'function' ) {
|
|
var nextPath = this.getPath();
|
|
if ( !nextPath ) {
|
|
this.lastPageReached( response, path );
|
|
return;
|
|
}
|
|
}
|
|
// get selector from checkLastPage or path option
|
|
var 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 || !response.querySelector ) {
|
|
return;
|
|
}
|
|
// check if response has selector
|
|
var nextElem = response.querySelector( selector );
|
|
if ( !nextElem ) {
|
|
this.lastPageReached( response, path );
|
|
}
|
|
};
|
|
|
|
proto.lastPageReached = function( response, path ) {
|
|
this.canLoad = false;
|
|
this.dispatchEvent( 'last', null, [ response, path ] );
|
|
};
|
|
|
|
// ----- error ----- //
|
|
|
|
proto.onPageError = function( error, path ) {
|
|
this.isLoading = false;
|
|
this.canLoad = false;
|
|
this.dispatchEvent( 'error', null, [ error, path ] );
|
|
return error;
|
|
};
|
|
|
|
// -------------------------- prefill -------------------------- //
|
|
|
|
InfiniteScroll.create.prefill = function() {
|
|
if ( !this.options.prefill ) {
|
|
return;
|
|
}
|
|
var 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() {
|
|
var 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() {
|
|
console.log('stopping prefill');
|
|
this.off( 'append', this.prefill );
|
|
};
|
|
|
|
// -------------------------- request -------------------------- //
|
|
|
|
function request( url, responseType, onLoad, onError ) {
|
|
var req = new XMLHttpRequest();
|
|
req.open( 'GET', url, true );
|
|
// set responseType document to return DOM
|
|
req.responseType = responseType || '';
|
|
|
|
// set X-Requested-With header to check that is ajax request
|
|
req.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
|
|
|
req.onload = function() {
|
|
if ( req.status == 200 ) {
|
|
onLoad( req.response );
|
|
} else {
|
|
// not 200 OK, error
|
|
var error = new Error( req.statusText );
|
|
onError( error );
|
|
}
|
|
};
|
|
|
|
// Handle network errors
|
|
req.onerror = function() {
|
|
var error = new Error( 'Network error requesting ' + url );
|
|
onError( error );
|
|
};
|
|
|
|
req.send();
|
|
}
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
return InfiniteScroll;
|
|
|
|
}));
|
|
|
|
// scroll-watch
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
/* globals define, module, require */
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD
|
|
define( 'infinite-scroll/js/scroll-watch',[
|
|
'./core',
|
|
'fizzy-ui-utils/utils',
|
|
], function( InfiniteScroll, utils ) {
|
|
return factory( window, InfiniteScroll, utils );
|
|
});
|
|
} else 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 ) {
|
|
|
|
var proto = InfiniteScroll.prototype;
|
|
|
|
// default options
|
|
InfiniteScroll.defaults.scrollThreshold = 400;
|
|
// InfiniteScroll.defaults.elementScroll = null;
|
|
|
|
InfiniteScroll.create.scrollWatch = function() {
|
|
// events
|
|
this.pageScrollHandler = this.onPageScroll.bind( this );
|
|
this.resizeHandler = this.onResize.bind( this );
|
|
|
|
var scrollThreshold = this.options.scrollThreshold;
|
|
var 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 ) {
|
|
var addRemove = isBind ? 'addEventListener' : 'removeEventListener';
|
|
this.scroller[ addRemove ]( 'scroll', this.pageScrollHandler );
|
|
window[ addRemove ]( 'resize', this.resizeHandler );
|
|
};
|
|
|
|
proto.onPageScroll = InfiniteScroll.throttle( function() {
|
|
var distance = this.getBottomDistance();
|
|
if ( distance <= this.options.scrollThreshold ) {
|
|
this.dispatchEvent('scrollThreshold');
|
|
}
|
|
});
|
|
|
|
proto.getBottomDistance = function() {
|
|
if ( this.options.elementScroll ) {
|
|
return this.getElementBottomDistance();
|
|
} else {
|
|
return this.getWindowBottomDistance();
|
|
}
|
|
};
|
|
|
|
proto.getWindowBottomDistance = function() {
|
|
var bottom = this.top + this.element.clientHeight;
|
|
var scrollY = window.pageYOffset + this.windowHeight;
|
|
return bottom - scrollY;
|
|
};
|
|
|
|
proto.getElementBottomDistance = function() {
|
|
var bottom = this.scroller.scrollHeight;
|
|
var scrollY = this.scroller.scrollTop + this.scroller.clientHeight;
|
|
return bottom - scrollY;
|
|
};
|
|
|
|
proto.onResize = function() {
|
|
this.updateMeasurements();
|
|
};
|
|
|
|
utils.debounceMethod( InfiniteScroll, 'onResize', 150 );
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
return InfiniteScroll;
|
|
|
|
}));
|
|
|
|
// history
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
/* globals define, module, require */
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD
|
|
define( 'infinite-scroll/js/history',[
|
|
'./core',
|
|
'fizzy-ui-utils/utils',
|
|
], function( InfiniteScroll, utils ) {
|
|
return factory( window, InfiniteScroll, utils );
|
|
});
|
|
} else 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 ) {
|
|
|
|
var proto = InfiniteScroll.prototype;
|
|
|
|
InfiniteScroll.defaults.history = 'replace';
|
|
// InfiniteScroll.defaults.historyTitle = false;
|
|
|
|
var 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/
|
|
var linkOrigin = link.origin || link.protocol + '//' + link.host;
|
|
var 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.scrollPageIndex = 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 ) {
|
|
var 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() {
|
|
var isHistoryAppend = this.options.history && this.options.append;
|
|
if ( isHistoryAppend ) {
|
|
this.bindHistoryAppendEvents( false );
|
|
}
|
|
};
|
|
|
|
// ----- append history ----- //
|
|
|
|
proto.onAppendHistory = function( response, path, items ) {
|
|
var firstItem = items[0];
|
|
var 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 this.getElementElementScrollY( elem );
|
|
} else {
|
|
return this.getElementWindowScrollY( elem );
|
|
}
|
|
};
|
|
|
|
proto.getElementWindowScrollY = function( elem ) {
|
|
var rect = elem.getBoundingClientRect();
|
|
return rect.top + window.pageYOffset;
|
|
};
|
|
|
|
// wow, stupid name
|
|
proto.getElementElementScrollY = function( elem ) {
|
|
return elem.offsetTop - this.top;
|
|
};
|
|
|
|
proto.onScrollHistory = function() {
|
|
// cycle through positions, find biggest without going over
|
|
var scrollViewY = this.getScrollViewY();
|
|
var pageIndex, page;
|
|
for ( var i=0; i < this.scrollPages.length; i++ ) {
|
|
var scrollPage = this.scrollPages[i];
|
|
if ( scrollPage.top >= scrollViewY ) {
|
|
break;
|
|
}
|
|
pageIndex = i;
|
|
page = scrollPage;
|
|
}
|
|
// set history if changed
|
|
if ( pageIndex != this.scrollPageIndex ) {
|
|
this.scrollPageIndex = pageIndex;
|
|
this.setHistory( page.title, page.path );
|
|
}
|
|
};
|
|
|
|
utils.debounceMethod( InfiniteScroll, 'onScrollHistory', 150 );
|
|
|
|
proto.getScrollViewY = function() {
|
|
if ( this.options.elementScroll ) {
|
|
return this.scroller.scrollTop + this.scroller.clientHeight/2;
|
|
} else {
|
|
return window.pageYOffset + this.windowHeight/2;
|
|
}
|
|
};
|
|
|
|
proto.setHistory = function( title, path ) {
|
|
var optHistory = this.options.history;
|
|
var 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
|
|
// http://stackoverflow.com/a/18633915/182183
|
|
proto.onUnload = function() {
|
|
var pageIndex = this.scrollPageIndex;
|
|
if ( pageIndex === 0 ) {
|
|
return;
|
|
}
|
|
// calculate where scroll position would be on refresh
|
|
var scrollPage = this.scrollPages[ pageIndex ];
|
|
var scrollY = window.pageYOffset - 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
|
|
/* globals define, module, require */
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD
|
|
define( 'infinite-scroll/js/button',[
|
|
'./core',
|
|
'fizzy-ui-utils/utils',
|
|
], function( InfiniteScroll, utils ) {
|
|
return factory( window, InfiniteScroll, utils );
|
|
});
|
|
} else 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 ) {
|
|
|
|
// InfiniteScroll.defaults.button = null;
|
|
|
|
InfiniteScroll.create.button = function() {
|
|
var buttonElem = utils.getQueryElement( this.options.button );
|
|
if ( buttonElem ) {
|
|
this.button = new InfiniteScrollButton( buttonElem, this );
|
|
return;
|
|
}
|
|
};
|
|
|
|
InfiniteScroll.destroy.button = function() {
|
|
if ( this.button ) {
|
|
this.button.destroy();
|
|
}
|
|
};
|
|
|
|
// -------------------------- InfiniteScrollButton -------------------------- //
|
|
|
|
function InfiniteScrollButton( 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 ) );
|
|
}
|
|
|
|
InfiniteScrollButton.prototype.onClick = function( event ) {
|
|
event.preventDefault();
|
|
this.infScroll.loadNextPage();
|
|
};
|
|
|
|
InfiniteScrollButton.prototype.enable = function() {
|
|
this.element.removeAttribute('disabled');
|
|
};
|
|
|
|
InfiniteScrollButton.prototype.disable = function() {
|
|
this.element.disabled = 'disabled';
|
|
};
|
|
|
|
InfiniteScrollButton.prototype.hide = function() {
|
|
this.element.style.display = 'none';
|
|
};
|
|
|
|
InfiniteScrollButton.prototype.destroy = function() {
|
|
this.element.removeEventListener( 'click', this.clickHandler );
|
|
};
|
|
|
|
// -------------------------- -------------------------- //
|
|
|
|
InfiniteScroll.Button = InfiniteScrollButton;
|
|
|
|
return InfiniteScroll;
|
|
|
|
}));
|
|
|
|
// status
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
/* globals define, module, require */
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD
|
|
define( 'infinite-scroll/js/status',[
|
|
'./core',
|
|
'fizzy-ui-utils/utils',
|
|
], function( InfiniteScroll, utils ) {
|
|
return factory( window, InfiniteScroll, utils );
|
|
});
|
|
} else 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 ) {
|
|
|
|
var proto = InfiniteScroll.prototype;
|
|
|
|
// InfiniteScroll.defaults.status = null;
|
|
|
|
InfiniteScroll.create.status = function() {
|
|
var 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 ) {
|
|
var 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();
|
|
var eventElem = this.statusEventElements[ eventName ];
|
|
show( eventElem );
|
|
};
|
|
|
|
proto.hideAllStatus = function() {
|
|
hide( this.statusElement );
|
|
this.hideStatusEventElements();
|
|
};
|
|
|
|
proto.hideStatusEventElements = function() {
|
|
for ( var type in this.statusEventElements ) {
|
|
var 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;
|
|
|
|
}));
|
|
|
|
/*!
|
|
* Infinite Scroll v3.0.2
|
|
* Automatically add next page
|
|
*
|
|
* Licensed GPLv3 for open source use
|
|
* or Infinite Scroll Commercial License for commercial use
|
|
*
|
|
* https://infinite-scroll.com
|
|
* Copyright 2017 Metafizzy
|
|
*/
|
|
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
/* globals define, module, require */
|
|
if ( typeof define == 'function' && define.amd ) {
|
|
// AMD
|
|
define( [
|
|
'infinite-scroll/js/core',
|
|
'infinite-scroll/js/page-load',
|
|
'infinite-scroll/js/scroll-watch',
|
|
'infinite-scroll/js/history',
|
|
'infinite-scroll/js/button',
|
|
'infinite-scroll/js/status',
|
|
], factory );
|
|
} else if ( typeof module == 'object' && module.exports ) {
|
|
// CommonJS
|
|
module.exports = factory(
|
|
require('./core'),
|
|
require('./page-load'),
|
|
require('./scroll-watch'),
|
|
require('./history'),
|
|
require('./button'),
|
|
require('./status')
|
|
);
|
|
}
|
|
|
|
})( window, function factory( InfiniteScroll ) {
|
|
return InfiniteScroll;
|
|
});
|
|
|
|
/*!
|
|
* imagesLoaded v4.1.3
|
|
* 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( 'imagesloaded/imagesloaded',[
|
|
'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 ) {
|
|
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
// turn element or nodeList into an array
|
|
function makeArray( obj ) {
|
|
var ary = [];
|
|
if ( Array.isArray( obj ) ) {
|
|
// use object if already an array
|
|
ary = obj;
|
|
} else if ( typeof obj.length == 'number' ) {
|
|
// convert nodeList to array
|
|
for ( var i=0; i < obj.length; i++ ) {
|
|
ary.push( obj[i] );
|
|
}
|
|
} else {
|
|
// array of single index
|
|
ary.push( obj );
|
|
}
|
|
return ary;
|
|
}
|
|
|
|
// -------------------------- 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
|
|
if ( typeof elem == 'string' ) {
|
|
elem = document.querySelectorAll( elem );
|
|
}
|
|
|
|
this.elements = makeArray( elem );
|
|
this.options = extend( {}, this.options );
|
|
|
|
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( function() {
|
|
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() {
|
|
return this.img.complete && this.img.naturalWidth !== undefined;
|
|
};
|
|
|
|
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;
|
|
|
|
});
|
|
|