flickity
Version:
Touch, responsive, flickable carousels
1,849 lines (1,529 loc) • 95.1 kB
JavaScript
/*!
* Flickity PACKAGED v3.0.0
* Touch, responsive, flickable carousels
*
* Licensed GPLv3 for open source use
* or Flickity Commercial License for commercial use
*
* https://flickity.metafizzy.co
* Copyright 2015-2022 Metafizzy
*/
/**
* Bridget makes jQuery widgets
* v3.0.1
* 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.1.1
* 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;
} ) );
/*!
* Infinite Scroll v2.0.4
* measure size of elements
* MIT license
*/
( function( window, factory ) {
if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory();
} else {
// browser global
window.getSize = factory();
}
} )( window, function factory() {
// -------------------------- helpers -------------------------- //
// get a number from a string, not a percentage
function getStyleSize( value ) {
let num = parseFloat( value );
// not a percent like '100%', and a number
let isValid = value.indexOf('%') == -1 && !isNaN( num );
return isValid && num;
}
// -------------------------- measurements -------------------------- //
let measurements = [
'paddingLeft',
'paddingRight',
'paddingTop',
'paddingBottom',
'marginLeft',
'marginRight',
'marginTop',
'marginBottom',
'borderLeftWidth',
'borderRightWidth',
'borderTopWidth',
'borderBottomWidth',
];
let measurementsLength = measurements.length;
function getZeroSize() {
let size = {
width: 0,
height: 0,
innerWidth: 0,
innerHeight: 0,
outerWidth: 0,
outerHeight: 0,
};
measurements.forEach( ( measurement ) => {
size[ measurement ] = 0;
} );
return size;
}
// -------------------------- getSize -------------------------- //
function getSize( elem ) {
// use querySeletor if elem is string
if ( typeof elem == 'string' ) elem = document.querySelector( elem );
// do not proceed on non-objects
let isElement = elem && typeof elem == 'object' && elem.nodeType;
if ( !isElement ) return;
let style = getComputedStyle( elem );
// if hidden, everything is 0
if ( style.display == 'none' ) return getZeroSize();
let size = {};
size.width = elem.offsetWidth;
size.height = elem.offsetHeight;
let isBorderBox = size.isBorderBox = style.boxSizing == 'border-box';
// get all measurements
measurements.forEach( ( measurement ) => {
let value = style[ measurement ];
let num = parseFloat( value );
// any 'auto', 'medium' value will be 0
size[ measurement ] = !isNaN( num ) ? num : 0;
} );
let paddingWidth = size.paddingLeft + size.paddingRight;
let paddingHeight = size.paddingTop + size.paddingBottom;
let marginWidth = size.marginLeft + size.marginRight;
let marginHeight = size.marginTop + size.marginBottom;
let borderWidth = size.borderLeftWidth + size.borderRightWidth;
let borderHeight = size.borderTopWidth + size.borderBottomWidth;
// overwrite width and height if we can get it from style
let styleWidth = getStyleSize( style.width );
if ( styleWidth !== false ) {
size.width = styleWidth +
// add padding and border unless it's already including it
( isBorderBox ? 0 : paddingWidth + borderWidth );
}
let styleHeight = getStyleSize( style.height );
if ( styleHeight !== false ) {
size.height = styleHeight +
// add padding and border unless it's already including it
( isBorderBox ? 0 : paddingHeight + borderHeight );
}
size.innerWidth = size.width - ( paddingWidth + borderWidth );
size.innerHeight = size.height - ( paddingHeight + borderHeight );
size.outerWidth = size.width + marginWidth;
size.outerHeight = size.height + marginHeight;
return size;
}
return getSize;
} );
/**
* 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;
} ) );
/*!
* Unidragger v3.0.0
* Draggable base class
* MIT license
*/
( function( window, factory ) {
// universal module definition
if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory(
window,
require('ev-emitter'),
);
} else {
// browser global
window.Unidragger = factory(
window,
window.EvEmitter,
);
}
}( typeof window != 'undefined' ? window : this, function factory( window, EvEmitter ) {
function Unidragger() {}
// inherit EvEmitter
let proto = Unidragger.prototype = Object.create( EvEmitter.prototype );
// ----- bind start ----- //
// trigger handler methods for events
proto.handleEvent = function( event ) {
let method = 'on' + event.type;
if ( this[ method ] ) {
this[ method ]( event );
}
};
let startEvent, activeEvents;
if ( 'ontouchstart' in window ) {
// HACK prefer Touch Events as you can preventDefault on touchstart to
// disable scroll in iOS & mobile Chrome metafizzy/flickity#1177
startEvent = 'touchstart';
activeEvents = [ 'touchmove', 'touchend', 'touchcancel' ];
} else if ( window.PointerEvent ) {
// Pointer Events
startEvent = 'pointerdown';
activeEvents = [ 'pointermove', 'pointerup', 'pointercancel' ];
} else {
// mouse events
startEvent = 'mousedown';
activeEvents = [ 'mousemove', 'mouseup' ];
}
// prototype so it can be overwriteable by Flickity
proto.touchActionValue = 'none';
proto.bindHandles = function() {
this._bindHandles( 'addEventListener', this.touchActionValue );
};
proto.unbindHandles = function() {
this._bindHandles( 'removeEventListener', '' );
};
/**
* Add or remove start event
* @param {String} bindMethod - addEventListener or removeEventListener
* @param {String} touchAction - value for touch-action CSS property
*/
proto._bindHandles = function( bindMethod, touchAction ) {
this.handles.forEach( ( handle ) => {
handle[ bindMethod ]( startEvent, this );
handle[ bindMethod ]( 'click', this );
// touch-action: none to override browser touch gestures. metafizzy/flickity#540
if ( window.PointerEvent ) handle.style.touchAction = touchAction;
} );
};
proto.bindActivePointerEvents = function() {
activeEvents.forEach( ( eventName ) => {
window.addEventListener( eventName, this );
} );
};
proto.unbindActivePointerEvents = function() {
activeEvents.forEach( ( eventName ) => {
window.removeEventListener( eventName, this );
} );
};
// ----- event handler helpers ----- //
// trigger method with matching pointer
proto.withPointer = function( methodName, event ) {
if ( event.pointerId == this.pointerIdentifier ) {
this[ methodName ]( event, event );
}
};
// trigger method with matching touch
proto.withTouch = function( methodName, event ) {
let touch;
for ( let changedTouch of event.changedTouches ) {
if ( changedTouch.identifier == this.pointerIdentifier ) {
touch = changedTouch;
}
}
if ( touch ) this[ methodName ]( event, touch );
};
// ----- start event ----- //
proto.onmousedown = function( event ) {
this.pointerDown( event, event );
};
proto.ontouchstart = function( event ) {
this.pointerDown( event, event.changedTouches[0] );
};
proto.onpointerdown = function( event ) {
this.pointerDown( event, event );
};
// nodes that have text fields
const cursorNodes = [ 'TEXTAREA', 'INPUT', 'SELECT', 'OPTION' ];
// input types that do not have text fields
const clickTypes = [ 'radio', 'checkbox', 'button', 'submit', 'image', 'file' ];
/**
* any time you set `event, pointer` it refers to:
* @param {Event} event
* @param {Event | Touch} pointer
*/
proto.pointerDown = function( event, pointer ) {
// dismiss multi-touch taps, right clicks, and clicks on text fields
let isCursorNode = cursorNodes.includes( event.target.nodeName );
let isClickType = clickTypes.includes( event.target.type );
let isOkayElement = !isCursorNode || isClickType;
let isOkay = !this.isPointerDown && !event.button && isOkayElement;
if ( !isOkay ) return;
this.isPointerDown = true;
// save pointer identifier to match up touch events
this.pointerIdentifier = pointer.pointerId !== undefined ?
// pointerId for pointer events, touch.indentifier for touch events
pointer.pointerId : pointer.identifier;
// track position for move
this.pointerDownPointer = {
pageX: pointer.pageX,
pageY: pointer.pageY,
};
this.bindActivePointerEvents();
this.emitEvent( 'pointerDown', [ event, pointer ] );
};
// ----- move ----- //
proto.onmousemove = function( event ) {
this.pointerMove( event, event );
};
proto.onpointermove = function( event ) {
this.withPointer( 'pointerMove', event );
};
proto.ontouchmove = function( event ) {
this.withTouch( 'pointerMove', event );
};
proto.pointerMove = function( event, pointer ) {
let moveVector = {
x: pointer.pageX - this.pointerDownPointer.pageX,
y: pointer.pageY - this.pointerDownPointer.pageY,
};
this.emitEvent( 'pointerMove', [ event, pointer, moveVector ] );
// start drag if pointer has moved far enough to start drag
let isDragStarting = !this.isDragging && this.hasDragStarted( moveVector );
if ( isDragStarting ) this.dragStart( event, pointer );
if ( this.isDragging ) this.dragMove( event, pointer, moveVector );
};
// condition if pointer has moved far enough to start drag
proto.hasDragStarted = function( moveVector ) {
return Math.abs( moveVector.x ) > 3 || Math.abs( moveVector.y ) > 3;
};
// ----- drag ----- //
proto.dragStart = function( event, pointer ) {
this.isDragging = true;
this.isPreventingClicks = true; // set flag to prevent clicks
this.emitEvent( 'dragStart', [ event, pointer ] );
};
proto.dragMove = function( event, pointer, moveVector ) {
this.emitEvent( 'dragMove', [ event, pointer, moveVector ] );
};
// ----- end ----- //
proto.onmouseup = function( event ) {
this.pointerUp( event, event );
};
proto.onpointerup = function( event ) {
this.withPointer( 'pointerUp', event );
};
proto.ontouchend = function( event ) {
this.withTouch( 'pointerUp', event );
};
proto.pointerUp = function( event, pointer ) {
this.pointerDone();
this.emitEvent( 'pointerUp', [ event, pointer ] );
if ( this.isDragging ) {
this.dragEnd( event, pointer );
} else {
// pointer didn't move enough for drag to start
this.staticClick( event, pointer );
}
};
proto.dragEnd = function( event, pointer ) {
this.isDragging = false; // reset flag
// re-enable clicking async
setTimeout( () => delete this.isPreventingClicks );
this.emitEvent( 'dragEnd', [ event, pointer ] );
};
// triggered on pointer up & pointer cancel
proto.pointerDone = function() {
this.isPointerDown = false;
delete this.pointerIdentifier;
this.unbindActivePointerEvents();
this.emitEvent('pointerDone');
};
// ----- cancel ----- //
proto.onpointercancel = function( event ) {
this.withPointer( 'pointerCancel', event );
};
proto.ontouchcancel = function( event ) {
this.withTouch( 'pointerCancel', event );
};
proto.pointerCancel = function( event, pointer ) {
this.pointerDone();
this.emitEvent( 'pointerCancel', [ event, pointer ] );
};
// ----- click ----- //
// handle all clicks and prevent clicks when dragging
proto.onclick = function( event ) {
if ( this.isPreventingClicks ) event.preventDefault();
};
// triggered after pointer down & up with no/tiny movement
proto.staticClick = function( event, pointer ) {
// ignore emulated mouse up clicks
let isMouseup = event.type == 'mouseup';
if ( isMouseup && this.isIgnoringMouseUp ) return;
this.emitEvent( 'staticClick', [ event, pointer ] );
// set flag for emulated clicks 300ms after touchend
if ( isMouseup ) {
this.isIgnoringMouseUp = true;
// reset flag after 400ms
setTimeout( () => {
delete this.isIgnoringMouseUp;
}, 400 );
}
};
// ----- ----- //
return Unidragger;
} ) );
/*!
* imagesLoaded v5.0.0
* JavaScript is all like "You images are done yet or what?"
* MIT License
*/
( function( window, factory ) {
// universal module definition
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,
function factory( window, EvEmitter ) {
let $ = window.jQuery;
let console = window.console;
// -------------------------- helpers -------------------------- //
// turn element or nodeList into an array
function makeArray( obj ) {
// use object if already an array
if ( Array.isArray( obj ) ) return obj;
let isArrayLike = typeof obj == 'object' && typeof obj.length == 'number';
// convert nodeList to array
if ( isArrayLike ) return [ ...obj ];
// array of single index
return [ obj ];
}
// -------------------------- imagesLoaded -------------------------- //
/**
* @param {[Array, Element, NodeList, String]} elem
* @param {[Object, Function]} options - if function, use as callback
* @param {Function} onAlways - callback function
* @returns {ImagesLoaded}
*/
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
let queryElem = elem;
if ( typeof elem == 'string' ) {
queryElem = document.querySelectorAll( elem );
}
// bail if bad element
if ( !queryElem ) {
console.error(`Bad element for imagesLoaded ${queryElem || elem}`);
return;
}
this.elements = makeArray( queryElem );
this.options = {};
// shift arguments if no options set
if ( typeof options == 'function' ) {
onAlways = options;
} else {
Object.assign( this.options, options );
}
if ( onAlways ) this.on( 'always', onAlways );
this.getImages();
// add jQuery Deferred object
if ( $ ) this.jqDeferred = new $.Deferred();
// HACK check async to allow time to bind listeners
setTimeout( this.check.bind( this ) );
}
ImagesLoaded.prototype = Object.create( EvEmitter.prototype );
ImagesLoaded.prototype.getImages = function() {
this.images = [];
// filter & find items if we have an item selector
this.elements.forEach( this.addElementImages, this );
};
const elementNodeTypes = [ 1, 9, 11 ];
/**
* @param {Node} elem
*/
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
let { nodeType } = elem;
if ( !nodeType || !elementNodeTypes.includes( nodeType ) ) return;
let childImgs = elem.querySelectorAll('img');
// concat childElems to filterFound array
for ( let img of childImgs ) {
this.addImage( img );
}
// get child background images
if ( typeof this.options.background == 'string' ) {
let children = elem.querySelectorAll( this.options.background );
for ( let child of children ) {
this.addElementBackgroundImages( child );
}
}
};
const reURL = /url\((['"])?(.*?)\1\)/gi;
ImagesLoaded.prototype.addElementBackgroundImages = function( elem ) {
let style = getComputedStyle( elem );
// Firefox returns null if in a hidden iframe https://bugzil.la/548397
if ( !style ) return;
// get url inside url("...")
let matches = reURL.exec( style.backgroundImage );
while ( matches !== null ) {
let url = matches && matches[2];
if ( url ) {
this.addBackground( url, elem );
}
matches = reURL.exec( style.backgroundImage );
}
};
/**
* @param {Image} img
*/
ImagesLoaded.prototype.addImage = function( img ) {
let loadingImage = new LoadingImage( img );
this.images.push( loadingImage );
};
ImagesLoaded.prototype.addBackground = function( url, elem ) {
let background = new Background( url, elem );
this.images.push( background );
};
ImagesLoaded.prototype.check = function() {
this.progressedCount = 0;
this.hasAnyBroken = false;
// complete if no images
if ( !this.images.length ) {
this.complete();
return;
}
/* eslint-disable-next-line func-style */
let onProgress = ( image, elem, message ) => {
// HACK - Chrome triggers event before object properties have changed. #83
setTimeout( () => {
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() {
let eventName = this.hasAnyBroken ? 'fail' : 'done';
this.isComplete = true;
this.emitEvent( eventName, [ this ] );
this.emitEvent( 'always', [ this ] );
if ( this.jqDeferred ) {
let 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.
let 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();
// add crossOrigin attribute. #204
if ( this.img.crossOrigin ) {
this.proxyImage.crossOrigin = this.img.crossOrigin;
}
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.currentSrc || this.img.src;
};
LoadingImage.prototype.getIsImageComplete = function() {
// check for non-zero, non-undefined naturalWidth
// fixes Safari+InfiniteScroll+Masonry bug infinite-scroll#671
return this.img.complete && this.img.naturalWidth;
};
LoadingImage.prototype.confirm = function( isLoaded, message ) {
this.isLoaded = isLoaded;
let { parentNode } = this.img;
// emit progress with parent <picture> or self <img>
let elem = parentNode.nodeName === 'PICTURE' ? parentNode : this.img;
this.emitEvent( 'progress', [ this, elem, message ] );
};
// ----- events ----- //
// trigger specified handler for event type
LoadingImage.prototype.handleEvent = function( event ) {
let 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
let 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, onAlways ) {
let instance = new ImagesLoaded( this, options, onAlways );
return instance.jqDeferred.promise( $( this ) );
};
};
// try making plugin
ImagesLoaded.makeJQueryPlugin();
// -------------------------- -------------------------- //
return ImagesLoaded;
} );
// Flickity.Cell
( function( window, factory ) {
// universal module definition
if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory( require('get-size') );
} else {
// browser global
window.Flickity = window.Flickity || {};
window.Flickity.Cell = factory( window.getSize );
}
}( typeof window != 'undefined' ? window : this, function factory( getSize ) {
const cellClassName = 'flickity-cell';
function Cell( elem ) {
this.element = elem;
this.element.classList.add( cellClassName );
this.x = 0;
this.unselect();
}
let proto = Cell.prototype;
proto.destroy = function() {
// reset style
this.unselect();
this.element.classList.remove( cellClassName );
this.element.style.transform = '';
this.element.removeAttribute('aria-hidden');
};
proto.getSize = function() {
this.size = getSize( this.element );
};
proto.select = function() {
this.element.classList.add('is-selected');
this.element.removeAttribute('aria-hidden');
};
proto.unselect = function() {
this.element.classList.remove('is-selected');
this.element.setAttribute( 'aria-hidden', 'true' );
};
proto.remove = function() {
this.element.remove();
};
return Cell;
} ) );
// slide
( function( window, factory ) {
// universal module definition
if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory();
} else {
// browser global
window.Flickity = window.Flickity || {};
window.Flickity.Slide = factory();
}
}( typeof window != 'undefined' ? window : this, function factory() {
function Slide( beginMargin, endMargin, cellAlign ) {
this.beginMargin = beginMargin;
this.endMargin = endMargin;
this.cellAlign = cellAlign;
this.cells = [];
this.outerWidth = 0;
this.height = 0;
}
let proto = Slide.prototype;
proto.addCell = function( cell ) {
this.cells.push( cell );
this.outerWidth += cell.size.outerWidth;
this.height = Math.max( cell.size.outerHeight, this.height );
// first cell stuff
if ( this.cells.length === 1 ) {
this.x = cell.x; // x comes from first cell
this.firstMargin = cell.size[ this.beginMargin ];
}
};
proto.updateTarget = function() {
let lastCell = this.getLastCell();
let lastMargin = lastCell ? lastCell.size[ this.endMargin ] : 0;
let slideWidth = this.outerWidth - ( this.firstMargin + lastMargin );
this.target = this.x + this.firstMargin + slideWidth * this.cellAlign;
};
proto.getLastCell = function() {
return this.cells[ this.cells.length - 1 ];
};
proto.select = function() {
this.cells.forEach( ( cell ) => cell.select() );
};
proto.unselect = function() {
this.cells.forEach( ( cell ) => cell.unselect() );
};
proto.getCellElements = function() {
return this.cells.map( ( cell ) => cell.element );
};
return Slide;
} ) );
// animate
( function( window, factory ) {
// universal module definition
if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory( require('fizzy-ui-utils') );
} else {
// browser global
window.Flickity = window.Flickity || {};
window.Flickity.animatePrototype = factory( window.fizzyUIUtils );
}
}( typeof window != 'undefined' ? window : this, function factory( utils ) {
// -------------------------- animate -------------------------- //
let proto = {};
proto.startAnimation = function() {
if ( this.isAnimating ) return;
this.isAnimating = true;
this.restingFrames = 0;
this.animate();
};
proto.animate = function() {
this.applyDragForce();
this.applySelectedAttraction();
let previousX = this.x;
this.integratePhysics();
this.positionSlider();
this.settle( previousX );
// animate next frame
if ( this.isAnimating ) requestAnimationFrame( () => this.animate() );
};
proto.positionSlider = function() {
let x = this.x;
// wrap position around
if ( this.isWrapping ) {
x = utils.modulo( x, this.slideableWidth ) - this.slideableWidth;
this.shiftWrapCells( x );
}
this.setTranslateX( x, this.isAnimating );
this.dispatchScrollEvent();
};
proto.setTranslateX = function( x, is3d ) {
x += this.cursorPosition;
// reverse if right-to-left and using transform
if ( this.options.rightToLeft ) x = -x;
let translateX = this.getPositionValue( x );
// use 3D transforms for hardware acceleration on iOS
// but use 2D when settled, for better font-rendering
this.slider.style.transform = is3d ?
`translate3d(${translateX},0,0)` : `translateX(${translateX})`;
};
proto.dispatchScrollEvent = function() {
let firstSlide = this.slides[0];
if ( !firstSlide ) return;
let positionX = -this.x - firstSlide.target;
let progress = positionX / this.slidesWidth;
this.dispatchEvent( 'scroll', null, [ progress, positionX ] );
};
proto.positionSliderAtSelected = function() {
if ( !this.cells.length ) return;
this.x = -this.selectedSlide.target;
this.velocity = 0; // stop wobble
this.positionSlider();
};
proto.getPositionValue = function( position ) {
if ( this.options.percentPosition ) {
// percent position, round to 2 digits, like 12.34%
return ( Math.round( ( position / this.size.innerWidth ) * 10000 ) * 0.01 ) + '%';
} else {
// pixel positioning
return Math.round( position ) + 'px';
}
};
proto.settle = function( previousX ) {
// keep track of frames where x hasn't moved
let isResting = !this.isPointerDown &&
Math.round( this.x * 100 ) === Math.round( previousX * 100 );
if ( isResting ) this.restingFrames++;
// stop animating if resting for 3 or more frames
if ( this.restingFrames > 2 ) {
this.isAnimating = false;
delete this.isFreeScrolling;
// render position with translateX when settled
this.positionSlider();
this.dispatchEvent( 'settle', null, [ this.selectedIndex ] );
}
};
proto.shiftWrapCells = function( x ) {
// shift before cells
let beforeGap = this.cursorPosition + x;
this._shiftCells( this.beforeShiftCells, beforeGap, -1 );
// shift after cells
let afterGap = this.size.innerWidth - ( x + this.slideableWidth + this.cursorPosition );
this._shiftCells( this.afterShiftCells, afterGap, 1 );
};
proto._shiftCells = function( cells, gap, shift ) {
cells.forEach( ( cell ) => {
let cellShift = gap > 0 ? shift : 0;
this._wrapShiftCell( cell, cellShift );
gap -= cell.size.outerWidth;
} );
};
proto._unshiftCells = function( cells ) {
if ( !cells || !cells.length ) return;
cells.forEach( ( cell ) => this._wrapShiftCell( cell, 0 ) );
};
// @param {Integer} shift - 0, 1, or -1
proto._wrapShiftCell = function( cell, shift ) {
this._renderCellPosition( cell, cell.x + this.slideableWidth * shift );
};
// -------------------------- physics -------------------------- //
proto.integratePhysics = function() {
this.x += this.velocity;
this.velocity *= this.getFrictionFactor();
};
proto.applyForce = function( force ) {
this.velocity += force;
};
proto.getFrictionFactor = function() {
return 1 - this.options[ this.isFreeScrolling ? 'freeScrollFriction' : 'friction' ];
};
proto.getRestingPosition = function() {
// my thanks to Steven Wittens, who simplified this math greatly
return this.x + this.velocity / ( 1 - this.getFrictionFactor() );
};
proto.applyDragForce = function() {
if ( !this.isDraggable || !this.isPointerDown ) return;
// change the position to drag position by applying force
let dragVelocity = this.dragX - this.x;
let dragForce = dragVelocity - this.velocity;
this.applyForce( dragForce );
};
proto.applySelectedAttraction = function() {
// do not attract if pointer down or no slides
let dragDown = this.isDraggable && this.isPointerDown;
if ( dragDown || this.isFreeScrolling || !this.slides.length ) return;
let distance = this.selectedSlide.target * -1 - this.x;
let force = distance * this.options.selectedAttraction;
this.applyForce( force );
};
return proto;
} ) );
// Flickity main
/* eslint-disable max-params */
( function( window, factory ) {
// universal module definition
if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory(
window,
require('ev-emitter'),
require('get-size'),
require('fizzy-ui-utils'),
require('./cell'),
require('./slide'),
require('./animate'),
);
} else {
// browser global
let _Flickity = window.Flickity;
window.Flickity = factory(
window,
window.EvEmitter,
window.getSize,
window.fizzyUIUtils,
_Flickity.Cell,
_Flickity.Slide,
_Flickity.animatePrototype,
);
}
}( typeof window != 'undefined' ? window : this,
function factory( window, EvEmitter, getSize, utils, Cell, Slide, animatePrototype ) {
/* eslint-enable max-params */
// vars
const { getComputedStyle, console } = window;
let { jQuery } = window;
// -------------------------- Flickity -------------------------- //
// globally unique identifiers
let GUID = 0;
// internal store of all Flickity intances
let instances = {};
function Flickity( element, options ) {
let queryElement = utils.getQueryElement( element );
if ( !queryElement ) {
if ( console ) console.error(`Bad element for Flickity: ${queryElement || element}`);
return;
}
this.element = queryElement;
// do not initialize twice on same element
if ( this.element.flickityGUID ) {
let instance = instances[ this.element.flickityGUID ];
if ( instance ) instance.option( options );
return instance;
}
// add jQuery
if ( jQuery ) {
this.$element = jQuery( this.element );
}
// options
this.options = { ...this.constructor.defaults };
this.option( options );
// kick things off
this._create();
}
Flickity.defaults = {
accessibility: true,
// adaptiveHeight: false,
cellAlign: 'center',
// cellSelector: undefined,
// contain: false,
freeScrollFriction: 0.075, // friction when free-scrolling
friction: 0.28, // friction when selecting
namespaceJQueryEvents: true,
// initialIndex: 0,
percentPosition: true,
resize: true,
selectedAttraction: 0.025,
setGallerySize: true,
// watchCSS: false,
// wrapAround: false
};
// hash of methods triggered on _create()
Flickity.create = {};
let proto = Flickity.prototype;
// inherit EventEmitter
Object.assign( proto, EvEmitter.prototype );
proto._create = function() {
let { resize, watchCSS, rightToLeft } = this.options;
// add id for Flickity.data
let id = this.guid = ++GUID;
this.element.flickityGUID = id; // expando
instances[ id ] = this; // associate via id
// initial properties
this.selectedIndex = 0;
// how many frames slider has been in same position
this.restingFrames = 0;
// initial physics properties
this.x = 0;
this.velocity = 0;
this.beginMargin = rightToLeft ? 'marginRight' : 'marginLeft';
this.endMargin = rightToLeft ? 'marginLeft' : 'marginRight';
// create viewport & slider
this.viewport = document.createElement('div');
this.viewport.className = 'flickity-viewport';
this._createSlider();
// used for keyboard navigation
this.focusableElems = [ this.element ];
if ( resize || watchCSS ) {
window.addEventListener( 'resize', this );
}
// add listeners from on option
for ( let eventName in this.options.on ) {
let listener = this.options.on[ eventName ];
this.on( eventName, listener );
}
for ( let method in Flickity.create ) {
Flickity.create[ method ].call( this );
}
if ( watchCSS ) {
this.watchCSS();
} else {
this.activate();
}
};
/**
* set options
* @param {Object} opts - options to extend
*/
proto.option = function( opts ) {
Object.assign( this.options, opts );
};
proto.activate = function() {
if ( this.isActive ) return;
this.isActive = true;
this.element.classList.add('flickity-enabled');
if ( this.options.rightToLeft ) {
this.element.classList.add('flickity-rtl');
}
this.getSize();
// move initial cell elements so they can be loaded as cells
let cellElems = this._filterFindCellElements( this.element.children );
this.slider.append( ...cellElems );
this.viewport.append( this.slider );
this.element.append( this.viewport );
// get cells from children
this.reloadCells();
if ( this.options.accessibility ) {
// allow element to focusable
this.element.tabIndex = 0;
// listen for key presses
this.element.addEventListener( 'keydown', this );
}
this.emitEvent('activate');
this.selectInitialIndex();
// flag for initial activation, for using initialIndex
this.isInitActivated = true;
// ready event. #493
this.dispatchEvent('ready');
};
// slider positions the cells
proto._createSlider = function() {
// slider element does all the positioning
let slider = document.createElement('div');
slider.className = 'flickity-slider';
this.slider = slider;
};
proto._filterFindCellElements = function( elems ) {
return utils.filterFindElements( elems, this.options.cellSelector );
};
// goes through all children
proto.reloadCells = function() {
// collection of item elements
this.cells = this._makeCells( this.slider.children );
this.positionCells();
this._updateWrapShiftCells();
this.setGallerySize();
};
/**
* turn elements into Flickity.Cells
* @param {[Array, NodeList, HTMLElement]} elems - elements to make into cells
* @returns {Array} items - collection of new Flickity Cells
*/
proto._makeCells = function( elems ) {
let cellElems = this._filterFindCellElements( elems );
// create new Cells for collection
return cellElems.map( ( cellElem ) => new Cell( cellElem ) );
};
proto.getLastCell = function() {
return this.cells[ this.cells.length - 1 ];
};
proto.getLastSlide = function() {
return this.slides[ this.slides.length - 1 ];
};
// positions all cells
proto.positionCells = function() {
// size all cells
this._sizeCells( this.cells );
// position all cells
this._positionCells( 0 );
};
/**
* position certain cells
* @param {Integer} index - which cell to start with
*/
proto._positionCells = function( index ) {
index = index || 0;
// also measure maxCellHeight
// start 0 if positioning all cells
this.maxCellHeight = index ? this.maxCellHeight || 0 : 0;
let cellX = 0;
// get cellX
if ( index > 0 ) {
let startCell = this.cells[ index - 1 ];
cellX = startCell.x + startCell.size.outerWidth;
}
this.cells.slice( index ).forEach( ( cell ) => {
cell.x = cellX;
this._renderCellPosition( cell, cellX );
cellX += cell.size.outerWidth;
this.maxCellHeight = Math.max( cell.size.outerHeight, this.maxCellHeight );
} );
// keep track of cellX for wrap-around
this.slideableWidth = cellX;
// slides
this.updateSlides();
// contain slides target
this._containSlides();
// update slidesWidth
this.slidesWidth = this.cells.length ?
this.getLastSlide().target - this.slides[0].target : 0;
};
proto._renderCellPosition = function( cell, x ) {
// render position of cell with in slider
let sideOffset = this.options.rightToLeft ? -1 : 1;
let renderX = x * sideOffset;
if ( this.options.percentPosition ) renderX *= this.size.innerWidth / cell.size.width;
let positionValue = this.getPositionValue( renderX );
cell.element.style.transform = `translateX( ${positionValue} )`;
};
/**
* cell.getSize() on multiple cells
* @param {Array} cells - cells to size
*/
proto._sizeCells = function( cells ) {
cells.forEach( ( cell ) => cell.getSize() );
};
// -------------------------- -------------------------- //
proto.updateSlides = function() {
this.slides = [];
if ( !this.cells.length ) return;
let { beginMargin, endMargin } = this;
let slide = new Slide( beginMargin, endMargin, this.cellAlign );
this.slides.push( slide );
let canCellFit = this._getCanCellFit();
this.cells.forEach( ( cell, i ) => {
// just add cell if first cell in slide
if ( !slide.cells.length ) {
slide.addCell( cell );
return;
}
let slideWidth = ( slide.outerWidth - slide.firstMargin ) +
( cell.size.outerWidth - cell.size[ endMargin ] );
if ( canCellFit( i, slideWidth ) ) {
slide.addCell( cell );
} else {
// doesn't fit, new slide
slide.updateTarget();
slide = new Slide( beginMargin, endMargin, this.cellAlign );
this.slides.push( slide );
slide.addCell( cell );
}
} );
// last slide
slide.updateTarget();
// update .selectedSlide
this.updateSelectedSlide();
};
proto._getCanCellFit = function() {
let { groupCells } = this.options;
if ( !groupCells ) return () => false;
if ( typeof groupCells == 'number' ) {
// group by number. 3 -> [0,1,2], [3,4,5], ...
let number = parseInt( groupCells, 10 );
return ( i ) => ( i % number ) !== 0;
}
// default, group by width of slide
let percent = 1;
// parse '75%
let percentMatch = typeof groupCells == 'string' && groupCells.match( /^(\d+)%$/ );
if ( percentMatch ) percent = parseInt( percentMatch[1], 10 ) / 100;
let groupWidth = ( this.size.innerWidth + 1 ) * percent;
return ( i, slideWidth ) => slideWidth <= groupWidth;
};
// alias _init for jQuery plugin .flickity()
proto._init =
proto.reposition = function() {
this.positionCells();
this.positionSliderAtSelected();
};
proto.getSize = function() {
this.size = getSize( this.element );
this.setCellAlign();
this.cursorPosition = this.size.innerWidth * this.cellAlign;
};
let cellAlignShorthands = {
left: 0,
center: 0.5,
right: 1,
};
proto.setCellAlign = function() {
let { cellAlign, rightToLeft } = this.options;
let shorthand = cellAlignShorthands[ cellAlign ];
this.cellAlign = shorthand !== undefined ? shorthand : cellAlign;
if ( rightToLeft ) this.cellAlign = 1 - this.cellAlign;
};
proto.setGallerySize = function() {
if ( !this.options.setGallerySize ) return;
let height = this.options.adaptiveHeight && this.selectedSlide ?
this.selectedSlide.height : this.maxCellHeight;
this.viewport.style.height = `${height}px`;
};
proto._updateWrapShiftCells = function() {
// update isWrapping
this.isWrapping = this.getIsWrapping();
// only for wrap-around
if ( !this.isWrapping ) return;
// unshift previous cells
this._unshiftCells( this.beforeShiftCells );
this._unshiftCells( this.afterShiftCells );
// get before cells
// initial gap
let beforeGapX = this.cursorPosition;
let lastIndex = this.cells.length - 1;
this.beforeShiftCells = this._getGapCells( beforeGapX, lastIndex, -1 );
// get after cells
// ending gap between last cell and end of gallery viewport
let afterGapX = this.size.innerWidth - this.cursorPosition;
// start cloning at first cell, working forwards
this.afterShiftCells = this._getGapCells( afterGapX,