Repository: metafizzy/infinite-scroll
Branch: master
Commit: 38a464fbbbe4
Files: 85
Total size: 228.8 KB
Directory structure:
gitextract_pwag9y0f/
├── .eslintrc.js
├── .github/
│ ├── contributing.md
│ ├── issue_template.md
│ └── workflows/
│ └── nodejs.yml
├── .gitignore
├── .nvmrc
├── LICENSE.txt
├── README.md
├── bin/
│ ├── build-dist.js
│ └── version.js
├── bower.json
├── dist/
│ └── infinite-scroll.pkgd.js
├── js/
│ ├── button.js
│ ├── core.js
│ ├── history.js
│ ├── index.js
│ ├── page-load.js
│ ├── scroll-watch.js
│ └── status.js
├── package.json
├── sandbox/
│ ├── button-class.html
│ ├── button-first.html
│ ├── button-load.html
│ ├── container-scroll.html
│ ├── css/
│ │ ├── blog.css
│ │ ├── loader-ellips.css
│ │ └── masonry-images.css
│ ├── element-scroll.html
│ ├── html-init.html
│ ├── jquery-plugin.html
│ ├── js/
│ │ ├── masonry-images.js
│ │ ├── scroll-loader.js
│ │ ├── unsplash-masonry.js
│ │ └── unsplash.js
│ ├── masonry-images/
│ │ ├── index.html
│ │ ├── page2.html
│ │ ├── page3.html
│ │ ├── page4.html
│ │ └── page5.html
│ ├── page/
│ │ ├── 2.html
│ │ ├── 3.html
│ │ ├── 4.html
│ │ ├── 5.html
│ │ └── 6.html
│ ├── prefill.html
│ ├── scroll-3.html
│ ├── scroll-loader.html
│ ├── unsplash-masonry.html
│ └── unsplash.html
└── test/
├── _get-server.js
├── _with-page.js
├── check-last-page.js
├── dist-jquery.js
├── dist.js
├── history.js
├── html/
│ ├── _serial-t.js
│ ├── dist-jquery.html
│ ├── dist.html
│ ├── history.html
│ ├── outlayer.html
│ ├── page/
│ │ ├── 2.html
│ │ ├── 2.json
│ │ ├── 3.html
│ │ ├── 3.json
│ │ ├── empty.html
│ │ ├── fill.html
│ │ ├── no-access.html
│ │ ├── outlayer2.html
│ │ └── outlayer3.html
│ ├── page-index.html
│ ├── page-load.html
│ ├── path.html
│ ├── prefill.html
│ ├── scroll-watch-element.html
│ ├── scroll-watch-window.html
│ └── test.css
├── load-next-page-promise.js
├── outlayer.js
├── page-index.js
├── page-load-error.js
├── page-load-json.js
├── page-load.js
├── path.js
├── prefill.js
└── scroll-watch.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.js
================================================
/* eslint-env node */
module.exports = {
plugins: [ 'metafizzy' ],
extends: 'plugin:metafizzy/browser',
env: {
browser: true,
commonjs: true,
},
parserOptions: {
ecmaVersion: 2018,
},
globals: {
InfiniteScroll: 'readonly',
Promise: 'readonly',
QUnit: 'readonly',
serialT: 'readonly',
},
rules: {
},
};
================================================
FILE: .github/contributing.md
================================================
## Submitting issues
### Reduced test case required
All bug reports and problem issues require a [**reduced test case**](https://css-tricks.com/reduced-test-cases/) or **live URL**.
+ A reduced test case clearly demonstrates the bug or issue.
+ It contains the bare minimum HTML, CSS, and JavaScript required to demonstrate the bug.
+ A link to your production site is **not** a reduced test case.
Create one by forking any one of the [CodePen demos](https://codepen.io/collection/DZejqa?grid_type=list&sort_by=item_created_at) from [the docs](https://infinite-scroll.com).
+ [Scroll & append](https://codepen.io/desandro/pen/yLaKLop)
+ [Scroll & append, vanilla JS](https://codepen.io/desandro/pen/vYXRYeY)
Providing a reduced test case is the best way to get your issue addressed. They help you point out the problem. They help me verify and debug the problem. They help others understand the problem. Without a reduced test case or live URL, your issue may be closed.
## Pull requests
Contributions are welcome!
+ **For typos and one-line fixes,** send those right in.
+ **For larger features,** open an issue before starting any significant work. Let's discuss to see how your feature fits within Infinite Scroll's vision.
+ **Check code style.** Run `npm run lint`. Spaces in brackets, semicolons, trailing commas.
+ **Do not edit `infinite-scroll.pkgd.js`.** Make your edits to source files in `js/`.
+ **Do not run the `dist` task to update `dist/` files.** I'll take care of this when I create a new release.
Your code will be used as part of a commercial product if merged. By submitting a Pull Request, you are giving your consent for your code to be integrated into Infinite Scroll as part of a commercial product.
================================================
FILE: .github/issue_template.md
================================================
**Test case:** https://codepen.io/desandro/pen/yLaKLop
================================================
FILE: .github/workflows/nodejs.yml
================================================
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
# - run: npm run dist
- run: npm test
env:
CI: true
================================================
FILE: .gitignore
================================================
bower_components/
node_modules/
================================================
FILE: .nvmrc
================================================
14
================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)
Copyright (c) Metafizzy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Infinite Scroll
_Automatically add next page_
See [infinite-scroll.com](https://infinite-scroll.com) for complete docs and demos.
## Install
### Download
- [infinite-scroll.pkgd.min.js](https://unpkg.com/infinite-scroll@5/dist/infinite-scroll.pkgd.min.js) minified, or
- [infinite-scroll.pkgd.js](https://unpkg.com/infinite-scroll@5/dist/infinite-scroll.pkgd.js) un-minified
### CDN
Link directly to Infinite Scroll files on [unpkg](https://unpkg.com).
``` html
```
### Package managers
npm: `npm install infinite-scroll`
Yarn: `yarn add infinite-scroll`
## License
Infinite Scroll v5 is licensed under the MIT License.
Whereas earlier versions of Infinite Scroll were previously dual licensed for commercial and closed-source usage, Infinite Scroll v5 licensing no longer has this distinction. You are free to use Infinite Scroll v5 in commercial and closed-source applications.
## Usage
Infinite Scroll works on a container element with its child item elements
``` html
```
### Options
``` js
let infScroll = new InfiniteScroll( '.container', {
// defaults listed
path: undefined,
// REQUIRED. Determines the URL for the next page
// Set to selector string to use the href of the next page's link
// path: '.pagination__next'
// Or set with {{#}} in place of the page number in the url
// path: '/blog/page/{{#}}'
// or set with function
// path: function() {
// return return '/articles/P' + ( ( this.loadCount + 1 ) * 10 );
// }
append: undefined,
// REQUIRED for appending content
// Appends selected elements from loaded page to the container
checkLastPage: true,
// Checks if page has path selector element
// Set to string if path is not set as selector string:
// checkLastPage: '.pagination__next'
prefill: false,
// Loads and appends pages on intialization until scroll requirement is met.
responseBody: 'text',
// Sets the method used on the response.
// Set to 'json' to load JSON.
domParseResponse: true,
// enables parsing response body into a DOM
// disable to load flat text
fetchOptions: undefined,
// sets custom settings for the fetch() request
// for setting headers, cors, or POST method
// can be set to an object, or a function that returns an object
outlayer: false,
// Integrates Masonry, Isotope or Packery
// Appended items will be added to the layout
scrollThreshold: 400,
// Sets the distance between the viewport to scroll area
// for scrollThreshold event to be triggered.
elementScroll: false,
// Sets scroller to an element for overflow element scrolling
loadOnScroll: true,
// Loads next page when scroll crosses over scrollThreshold
history: 'replace',
// Changes the browser history and URL.
// Set to 'push' to use history.pushState()
// to create new history entries for each page change.
historyTitle: true,
// Updates the window title. Requires history enabled.
hideNav: undefined,
// Hides navigation element
status: undefined,
// Displays status elements indicating state of page loading:
// .infinite-scroll-request, .infinite-scroll-load, .infinite-scroll-error
// status: '.page-load-status'
button: undefined,
// Enables a button to load pages on click
// button: '.load-next-button'
onInit: undefined,
// called on initialization
// useful for binding events on init
// onInit: function() {
// this.on( 'append', function() {...})
// }
debug: false,
// Logs events and state changes to the console.
})
```
## Browser support
Infinite Scroll v4 supports Chrome 60+, Edge 79+, Firefox 55+, Safari 11+.
For IE10 and Android 4 support, try [Infinite Scroll v3](https://v3.infinite-scroll.com/).
## Development
This package is developed with Node.js v14 and npm v6. Manage Node and npm version with [nvm](https://github.com/nvm-sh/nvm).
``` sh
nvm use
```
Install dependencies
``` sh
npm install
```
Lint
``` sh
npm run lint
```
Run tests
``` sh
npm test
```
---
By [Metafizzy 🌈🐻](https://metafizzy.co)
================================================
FILE: bin/build-dist.js
================================================
const fs = require('fs');
const { execSync } = require('child_process');
const { minify } = require('terser');
const indexPath = 'js/index.js';
const distPath = 'dist/infinite-scroll.pkgd.js';
const distMinPath = 'dist/infinite-scroll.pkgd.min.js';
let indexContent = fs.readFileSync( `./${indexPath}`, 'utf8' );
// get file paths from index.js
let cjsBlockRegex = /module\.exports = factory\([\w ,'.\-()/\n]+;/i;
let cjsBlockMatch = indexContent.match( cjsBlockRegex );
let jsPaths = cjsBlockMatch[0].match( /require\('([.\-/\w]+)'\)/gi );
jsPaths = jsPaths.map( function( path ) {
return path.replace( "require('.", 'js' ).replace( "')", '.js' );
} );
let paths = [
'node_modules/jquery-bridget/jquery-bridget.js',
'node_modules/ev-emitter/ev-emitter.js',
'node_modules/fizzy-ui-utils/utils.js',
...jsPaths,
'node_modules/imagesloaded/imagesloaded.js',
];
// concatenate files
execSync(`cat ${paths.join(' ')} > ${distPath}`);
// add banner
let banner = indexContent.split(' */')[0] + ' */\n\n';
banner = banner.replace( 'Infinite Scroll', 'Infinite Scroll PACKAGED' );
let distJsContent = fs.readFileSync( distPath, 'utf8' );
distJsContent = banner + distJsContent;
fs.writeFileSync( distPath, distJsContent );
// minify
( async function() {
let { code } = await minify( distJsContent, { mangle: true } );
fs.writeFileSync( distMinPath, code );
} )();
================================================
FILE: bin/version.js
================================================
/* eslint-env node */
const fs = require('fs');
const path = require('path');
const { version } = require('../package.json');
function dir( file ) {
return path.resolve( __dirname, file );
}
let content = fs.readFileSync( dir('../js/index.js'), 'utf8' );
content = content.replace( /Infinite Scroll v[\w.-]+/,
`Infinite Scroll v${version}` );
fs.writeFileSync( dir('../js/index.js'), content, 'utf8' );
================================================
FILE: bower.json
================================================
{
"name": "infinite-scroll",
"authors": [
"David DeSandro "
],
"description": "infinite scroll",
"main": "js/index.js",
"dependencies": {
"ev-emitter": "^2.1.0",
"fizzy-ui-utils": "^3.0.0"
},
"devDependencies": {},
"keywords": [
"infinite scroll",
"infinite",
"scroll",
"plugin"
],
"license": "MIT",
"homepage": "https://infinite-scroll.com",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"sandbox",
"package.json",
"gulpfile.js"
]
}
================================================
FILE: dist/infinite-scroll.pkgd.js
================================================
/*!
* Infinite Scroll PACKAGED v5.0.0
* Automatically add next page
* MIT License
* https://infinite-scroll.com
* Copyright 2018-2025 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