flickity
Version:
Touch, responsive, flickable carousels
1,917 lines (1,638 loc) • 120 kB
JavaScript
/*!
* Flickity PACKAGED v2.3.0
* Touch, responsive, flickable carousels
*
* Licensed GPLv3 for open source use
* or Flickity Commercial License for commercial use
*
* https://flickity.metafizzy.co
* Copyright 2015-2021 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;
}
// copy over to avoid interference if .off() in listener
listeners = listeners.slice(0);
args = args || [];
// once stuff
var onceListeners = this._onceEvents && this._onceEvents[ eventName ];
for ( var i=0; i < listeners.length; i++ ) {
var listener = listeners[i]
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 );
}
return this;
};
proto.allOff = function() {
delete this._events;
delete this._onceEvents;
};
return EvEmitter;
}));
/*!
* getSize v2.0.3
* measure size of elements
* MIT license
*/
/* jshint browser: true, strict: true, undef: true, unused: true */
/* globals console: false */
( function( window, factory ) {
/* jshint strict: false */ /* globals define, module */
if ( typeof define == 'function' && define.amd ) {
// AMD
define( 'get-size/get-size',factory );
} else if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory();
} else {
// browser global
window.getSize = factory();
}
})( window, function factory() {
'use strict';
// -------------------------- helpers -------------------------- //
// get a number from a string, not a percentage
function getStyleSize( value ) {
var num = parseFloat( value );
// not a percent like '100%', and a number
var isValid = value.indexOf('%') == -1 && !isNaN( num );
return isValid && num;
}
function noop() {}
var logError = typeof console == 'undefined' ? noop :
function( message ) {
console.error( message );
};
// -------------------------- measurements -------------------------- //
var measurements = [
'paddingLeft',
'paddingRight',
'paddingTop',
'paddingBottom',
'marginLeft',
'marginRight',
'marginTop',
'marginBottom',
'borderLeftWidth',
'borderRightWidth',
'borderTopWidth',
'borderBottomWidth'
];
var measurementsLength = measurements.length;
function getZeroSize() {
var size = {
width: 0,
height: 0,
innerWidth: 0,
innerHeight: 0,
outerWidth: 0,
outerHeight: 0
};
for ( var i=0; i < measurementsLength; i++ ) {
var measurement = measurements[i];
size[ measurement ] = 0;
}
return size;
}
// -------------------------- getStyle -------------------------- //
/**
* getStyle, get style of element, check for Firefox bug
* https://bugzilla.mozilla.org/show_bug.cgi?id=548397
*/
function getStyle( elem ) {
var style = getComputedStyle( elem );
if ( !style ) {
logError( 'Style returned ' + style +
'. Are you running this code in a hidden iframe on Firefox? ' +
'See https://bit.ly/getsizebug1' );
}
return style;
}
// -------------------------- setup -------------------------- //
var isSetup = false;
var isBoxSizeOuter;
/**
* setup
* check isBoxSizerOuter
* do on first getSize() rather than on page load for Firefox bug
*/
function setup() {
// setup once
if ( isSetup ) {
return;
}
isSetup = true;
// -------------------------- box sizing -------------------------- //
/**
* Chrome & Safari measure the outer-width on style.width on border-box elems
* IE11 & Firefox<29 measures the inner-width
*/
var div = document.createElement('div');
div.style.width = '200px';
div.style.padding = '1px 2px 3px 4px';
div.style.borderStyle = 'solid';
div.style.borderWidth = '1px 2px 3px 4px';
div.style.boxSizing = 'border-box';
var body = document.body || document.documentElement;
body.appendChild( div );
var style = getStyle( div );
// round value for browser zoom. desandro/masonry#928
isBoxSizeOuter = Math.round( getStyleSize( style.width ) ) == 200;
getSize.isBoxSizeOuter = isBoxSizeOuter;
body.removeChild( div );
}
// -------------------------- getSize -------------------------- //
function getSize( elem ) {
setup();
// use querySeletor if elem is string
if ( typeof elem == 'string' ) {
elem = document.querySelector( elem );
}
// do not proceed on non-objects
if ( !elem || typeof elem != 'object' || !elem.nodeType ) {
return;
}
var style = getStyle( elem );
// if hidden, everything is 0
if ( style.display == 'none' ) {
return getZeroSize();
}
var size = {};
size.width = elem.offsetWidth;
size.height = elem.offsetHeight;
var isBorderBox = size.isBorderBox = style.boxSizing == 'border-box';
// get all measurements
for ( var i=0; i < measurementsLength; i++ ) {
var measurement = measurements[i];
var value = style[ measurement ];
var num = parseFloat( value );
// any 'auto', 'medium' value will be 0
size[ measurement ] = !isNaN( num ) ? num : 0;
}
var paddingWidth = size.paddingLeft + size.paddingRight;
var paddingHeight = size.paddingTop + size.paddingBottom;
var marginWidth = size.marginLeft + size.marginRight;
var marginHeight = size.marginTop + size.marginBottom;
var borderWidth = size.borderLeftWidth + size.borderRightWidth;
var borderHeight = size.borderTopWidth + size.borderBottomWidth;
var isBorderBoxSizeOuter = isBorderBox && isBoxSizeOuter;
// overwrite width and height if we can get it from style
var styleWidth = getStyleSize( style.width );
if ( styleWidth !== false ) {
size.width = styleWidth +
// add padding and border unless it's already including it
( isBorderBoxSizeOuter ? 0 : paddingWidth + borderWidth );
}
var styleHeight = getStyleSize( style.height );
if ( styleHeight !== false ) {
size.height = styleHeight +
// add padding and border unless it's already including it
( isBorderBoxSizeOuter ? 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;
});
/**
* 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.7
* 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 ----- //
var arraySlice = Array.prototype.slice;
// turn element or nodeList into an array
utils.makeArray = function( obj ) {
if ( Array.isArray( obj ) ) {
// use object if already an array
return obj;
}
// return empty array if undefined or null. #6
if ( obj === null || obj === undefined ) {
return [];
}
var isArrayLike = typeof obj == 'object' && typeof obj.length == 'number';
if ( isArrayLike ) {
// convert nodeList to array
return arraySlice.call( obj );
}
// array of single index
return [ obj ];
};
// ----- 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 ) {
threshold = threshold || 100;
// original method
var method = _class.prototype[ methodName ];
var timeoutName = methodName + 'Timeout';
_class.prototype[ methodName ] = function() {
var timeout = this[ timeoutName ];
clearTimeout( timeout );
var args = arguments;
var _this = this;
this[ timeoutName ] = setTimeout( function() {
method.apply( _this, args );
delete _this[ timeoutName ];
}, threshold );
};
};
// ----- 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;
}));
// Flickity.Cell
( function( window, factory ) {
// universal module definition
if ( typeof define == 'function' && define.amd ) {
// AMD
define( 'flickity/js/cell',[
'get-size/get-size',
], function( getSize ) {
return factory( window, getSize );
} );
} else if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory(
window,
require('get-size')
);
} else {
// browser global
window.Flickity = window.Flickity || {};
window.Flickity.Cell = factory(
window,
window.getSize
);
}
}( window, function factory( window, getSize ) {
function Cell( elem, parent ) {
this.element = elem;
this.parent = parent;
this.create();
}
var proto = Cell.prototype;
proto.create = function() {
this.element.style.position = 'absolute';
this.element.setAttribute( 'aria-hidden', 'true' );
this.x = 0;
this.shift = 0;
this.element.style[ this.parent.originSide ] = 0;
};
proto.destroy = function() {
// reset style
this.unselect();
this.element.style.position = '';
var side = this.parent.originSide;
this.element.style[ side ] = '';
this.element.style.transform = '';
this.element.removeAttribute('aria-hidden');
};
proto.getSize = function() {
this.size = getSize( this.element );
};
proto.setPosition = function( x ) {
this.x = x;
this.updateTarget();
this.renderPosition( x );
};
// setDefaultTarget v1 method, backwards compatibility, remove in v3
proto.updateTarget = proto.setDefaultTarget = function() {
var marginProperty = this.parent.originSide == 'left' ? 'marginLeft' : 'marginRight';
this.target = this.x + this.size[ marginProperty ] +
this.size.width * this.parent.cellAlign;
};
proto.renderPosition = function( x ) {
// render position of cell with in slider
var sideOffset = this.parent.originSide === 'left' ? 1 : -1;
var adjustedX = this.parent.options.percentPosition ?
x * sideOffset * ( this.parent.size.innerWidth / this.size.width ) :
x * sideOffset;
this.element.style.transform = 'translateX(' +
this.parent.getPositionValue( adjustedX ) + ')';
};
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' );
};
/**
* @param {Integer} shift - 0, 1, or -1
*/
proto.wrapShift = function( shift ) {
this.shift = shift;
this.renderPosition( this.x + this.parent.slideableWidth * shift );
};
proto.remove = function() {
this.element.parentNode.removeChild( this.element );
};
return Cell;
} ) );
// slide
( function( window, factory ) {
// universal module definition
if ( typeof define == 'function' && define.amd ) {
// AMD
define( 'flickity/js/slide',factory );
} else if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory();
} else {
// browser global
window.Flickity = window.Flickity || {};
window.Flickity.Slide = factory();
}
}( window, function factory() {
'use strict';
function Slide( parent ) {
this.parent = parent;
this.isOriginLeft = parent.originSide == 'left';
this.cells = [];
this.outerWidth = 0;
this.height = 0;
}
var 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
var beginMargin = this.isOriginLeft ? 'marginLeft' : 'marginRight';
this.firstMargin = cell.size[ beginMargin ];
}
};
proto.updateTarget = function() {
var endMargin = this.isOriginLeft ? 'marginRight' : 'marginLeft';
var lastCell = this.getLastCell();
var lastMargin = lastCell ? lastCell.size[ endMargin ] : 0;
var slideWidth = this.outerWidth - ( this.firstMargin + lastMargin );
this.target = this.x + this.firstMargin + slideWidth * this.parent.cellAlign;
};
proto.getLastCell = function() {
return this.cells[ this.cells.length - 1 ];
};
proto.select = function() {
this.cells.forEach( function( cell ) {
cell.select();
} );
};
proto.unselect = function() {
this.cells.forEach( function( cell ) {
cell.unselect();
} );
};
proto.getCellElements = function() {
return this.cells.map( function( cell ) {
return cell.element;
} );
};
return Slide;
} ) );
// animate
( function( window, factory ) {
// universal module definition
if ( typeof define == 'function' && define.amd ) {
// AMD
define( 'flickity/js/animate',[
'fizzy-ui-utils/utils',
], function( utils ) {
return factory( window, utils );
} );
} else if ( typeof module == 'object' && module.exports ) {
// CommonJS
module.exports = factory(
window,
require('fizzy-ui-utils')
);
} else {
// browser global
window.Flickity = window.Flickity || {};
window.Flickity.animatePrototype = factory(
window,
window.fizzyUIUtils
);
}
}( window, function factory( window, utils ) {
// -------------------------- animate -------------------------- //
var proto = {};
proto.startAnimation = function() {
if ( this.isAnimating ) {
return;
}
this.isAnimating = true;
this.restingFrames = 0;
this.animate();
};
proto.animate = function() {
this.applyDragForce();
this.applySelectedAttraction();
var previousX = this.x;
this.integratePhysics();
this.positionSlider();
this.settle( previousX );
// animate next frame
if ( this.isAnimating ) {
var _this = this;
requestAnimationFrame( function animateFrame() {
_this.animate();
} );
}
};
proto.positionSlider = function() {
var x = this.x;
// wrap position around
if ( this.options.wrapAround && this.cells.length > 1 ) {
x = utils.modulo( x, this.slideableWidth );
x -= 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
x = this.options.rightToLeft ? -x : x;
var 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() {
var firstSlide = this.slides[0];
if ( !firstSlide ) {
return;
}
var positionX = -this.x - firstSlide.target;
var 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
var 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
var beforeGap = this.cursorPosition + x;
this._shiftCells( this.beforeShiftCells, beforeGap, -1 );
// shift after cells
var afterGap = this.size.innerWidth - ( x + this.slideableWidth + this.cursorPosition );
this._shiftCells( this.afterShiftCells, afterGap, 1 );
};
proto._shiftCells = function( cells, gap, shift ) {
for ( var i = 0; i < cells.length; i++ ) {
var cell = cells[i];
var cellShift = gap > 0 ? shift : 0;
cell.wrapShift( cellShift );
gap -= cell.size.outerWidth;
}
};
proto._unshiftCells = function( cells ) {
if ( !cells || !cells.length ) {
return;
}
for ( var i = 0; i < cells.length; i++ ) {
cells[i].wrapShift( 0 );
}
};
// -------------------------- 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
var dragVelocity = this.dragX - this.x;
var dragForce = dragVelocity - this.velocity;
this.applyForce( dragForce );
};
proto.applySelectedAttraction = function() {
// do not attract if pointer down or no slides
var dragDown = this.isDraggable && this.isPointerDown;
if ( dragDown || this.isFreeScrolling || !this.slides.length ) {
return;
}
var distance = this.selectedSlide.target * -1 - this.x;
var force = distance * this.options.selectedAttraction;
this.applyForce( force );
};
return proto;
} ) );
// Flickity main
/* eslint-disable max-params */
( function( window, factory ) {
// universal module definition
if ( typeof define == 'function' && define.amd ) {
// AMD
define( 'flickity/js/flickity',[
'ev-emitter/ev-emitter',
'get-size/get-size',
'fizzy-ui-utils/utils',
'./cell',
'./slide',
'./animate',
], function( EvEmitter, getSize, utils, Cell, Slide, animatePrototype ) {
return factory( window, EvEmitter, getSize, utils, Cell, Slide, animatePrototype );
} );
} else 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
var _Flickity = window.Flickity;
window.Flickity = factory(
window,
window.EvEmitter,
window.getSize,
window.fizzyUIUtils,
_Flickity.Cell,
_Flickity.Slide,
_Flickity.animatePrototype
);
}
}( window, function factory( window, EvEmitter, getSize,
utils, Cell, Slide, animatePrototype ) {
/* eslint-enable max-params */
// vars
var jQuery = window.jQuery;
var getComputedStyle = window.getComputedStyle;
var console = window.console;
function moveElements( elems, toElem ) {
elems = utils.makeArray( elems );
while ( elems.length ) {
toElem.appendChild( elems.shift() );
}
}
// -------------------------- Flickity -------------------------- //
// globally unique identifiers
var GUID = 0;
// internal store of all Flickity intances
var instances = {};
function Flickity( element, options ) {
var 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 ) {
var instance = instances[ this.element.flickityGUID ];
if ( instance ) instance.option( options );
return instance;
}
// add jQuery
if ( jQuery ) {
this.$element = jQuery( this.element );
}
// options
this.options = utils.extend( {}, 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.createMethods = [];
var proto = Flickity.prototype;
// inherit EventEmitter
utils.extend( proto, EvEmitter.prototype );
proto._create = function() {
// add id for Flickity.data
var 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.originSide = this.options.rightToLeft ? 'right' : 'left';
// create viewport & slider
this.viewport = document.createElement('div');
this.viewport.className = 'flickity-viewport';
this._createSlider();
if ( this.options.resize || this.options.watchCSS ) {
window.addEventListener( 'resize', this );
}
// add listeners from on option
for ( var eventName in this.options.on ) {
var listener = this.options.on[ eventName ];
this.on( eventName, listener );
}
Flickity.createMethods.forEach( function( method ) {
this[ method ]();
}, this );
if ( this.options.watchCSS ) {
this.watchCSS();
} else {
this.activate();
}
};
/**
* set options
* @param {Object} opts - options to extend
*/
proto.option = function( opts ) {
utils.extend( 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
var cellElems = this._filterFindCellElements( this.element.children );
moveElements( cellElems, this.slider );
this.viewport.appendChild( this.slider );
this.element.appendChild( 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
var slider = document.createElement('div');
slider.className = 'flickity-slider';
slider.style[ this.originSide ] = 0;
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._getWrapShiftCells();
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 ) {
var cellElems = this._filterFindCellElements( elems );
// create new Flickity for collection
var cells = cellElems.map( function( cellElem ) {
return new Cell( cellElem, this );
}, this );
return cells;
};
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;
var cellX = 0;
// get cellX
if ( index > 0 ) {
var startCell = this.cells[ index - 1 ];
cellX = startCell.x + startCell.size.outerWidth;
}
var len = this.cells.length;
for ( var i = index; i < len; i++ ) {
var cell = this.cells[i];
cell.setPosition( 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 = len ? this.getLastSlide().target - this.slides[0].target : 0;
};
/**
* cell.getSize() on multiple cells
* @param {Array} cells - cells to size
*/
proto._sizeCells = function( cells ) {
cells.forEach( function( cell ) {
cell.getSize();
} );
};
// -------------------------- -------------------------- //
proto.updateSlides = function() {
this.slides = [];
if ( !this.cells.length ) {
return;
}
var slide = new Slide( this );
this.slides.push( slide );
var isOriginLeft = this.originSide == 'left';
var nextMargin = isOriginLeft ? 'marginRight' : 'marginLeft';
var canCellFit = this._getCanCellFit();
this.cells.forEach( function( cell, i ) {
// just add cell if first cell in slide
if ( !slide.cells.length ) {
slide.addCell( cell );
return;
}
var slideWidth = ( slide.outerWidth - slide.firstMargin ) +
( cell.size.outerWidth - cell.size[ nextMargin ] );
if ( canCellFit.call( this, i, slideWidth ) ) {
slide.addCell( cell );
} else {
// doesn't fit, new slide
slide.updateTarget();
slide = new Slide( this );
this.slides.push( slide );
slide.addCell( cell );
}
}, this );
// last slide
slide.updateTarget();
// update .selectedSlide
this.updateSelectedSlide();
};
proto._getCanCellFit = function() {
var groupCells = this.options.groupCells;
if ( !groupCells ) {
return function() {
return false;
};
} else if ( typeof groupCells == 'number' ) {
// group by number. 3 -> [0,1,2], [3,4,5], ...
var number = parseInt( groupCells, 10 );
return function( i ) {
return ( i % number ) !== 0;
};
}
// default, group by width of slide
// parse '75%
var percentMatch = typeof groupCells == 'string' &&
groupCells.match( /^(\d+)%$/ );
var percent = percentMatch ? parseInt( percentMatch[1], 10 ) / 100 : 1;
return function( i, slideWidth ) {
/* eslint-disable-next-line no-invalid-this */
return slideWidth <= ( this.size.innerWidth + 1 ) * percent;
};
};
// 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;
};
var cellAlignShorthands = {
// cell align, then based on origin side
center: {
left: 0.5,
right: 0.5,
},
left: {
left: 0,
right: 1,
},
right: {
right: 0,
left: 1,
},
};
proto.setCellAlign = function() {
var shorthand = cellAlignShorthands[ this.options.cellAlign ];
this.cellAlign = shorthand ? shorthand[ this.originSide ] : this.options.cellAlign;
};
proto.setGallerySize = function() {
if ( this.options.setGallerySize ) {
var height = this.options.adaptiveHeight && this.selectedSlide ?
this.selectedSlide.height : this.maxCellHeight;
this.viewport.style.height = height + 'px';
}
};
proto._getWrapShiftCells = function() {
// only for wrap-around
if ( !this.options.wrapAround ) {
return;
}
// unshift previous cells
this._unshiftCells( this.beforeShiftCells );
this._unshiftCells( this.afterShiftCells );
// get before cells
// initial gap
var gapX = this.cursorPosition;
var cellIndex = this.cells.length - 1;
this.beforeShiftCells = this._getGapCells( gapX, cellIndex, -1 );
// get after cells
// ending gap between last cell and end of gallery viewport
gapX = this.size.innerWidth - this.cursorPosition;
// start cloning at first cell, working forwards
this.afterShiftCells = this._getGapCells( gapX, 0, 1 );
};
proto._getGapCells = function( gapX, cellIndex, increment ) {
// keep adding cells until the cover the initial gap
var cells = [];
while ( gapX > 0 ) {
var cell = this.cells[ cellIndex ];
if ( !cell ) {
break;
}
cells.push( cell );
cellIndex += increment;
gapX -= cell.size.outerWidth;
}
return cells;
};
// ----- contain ----- //
// contain cell targets so no excess sliding
proto._containSlides = function() {
if ( !this.options.contain || this.options.wrapAround || !this.cells.length ) {
return;
}
var isRightToLeft = this.options.rightToLeft;
var beginMargin = isRightToLeft ? 'marginRight' : 'marginLeft';
var endMargin = isRightToLeft ? 'marginLeft' : 'marginRight';
var contentWidth = this.slideableWidth - this.getLastCell().size[ endMargin ];
// content is less than gallery size
var isContentSmaller = contentWidth < this.size.innerWidth;
// bounds
var beginBound = this.cursorPosition + this.cells[0].size[ beginMargin ];
var endBound = contentWidth - this.size.innerWidth * ( 1 - this.cellAlign );
// contain each cell target
this.slides.forEach( function( slide ) {
if ( isContentSmaller ) {
// all cells fit inside gallery
slide.target = contentWidth * this.cellAlign;
} else {
// contain to bounds
slide.target = Math.max( slide.target, beginBound );
slide.target = Math.min( slide.target, endBound );
}
}, this );
};
// ----- ----- //
/**
* emits events via eventEmitter and jQuery events
* @param {String} type - name of event
* @param {Event} event - original event
* @param {Array} args - extra arguments
*/
proto.dispatchEvent = function( type, event, args ) {
var emitArgs = event ? [ event ].concat( args ) : args;
this.emitEvent( type, emitArgs );
if ( jQuery && this.$element ) {
// default trigger with type if no event
type += this.options.namespaceJQueryEvents ? '.flickity' : '';
var $event = type;
if ( event ) {
// create jQuery event
var jQEvent = new jQuery.Event( event );
jQEvent.type = type;
$event = jQEvent;
}
this.$element.trigger( $event, args );
}
};
// -------------------------- select -------------------------- //
/**
* @param {Integer} index - index of the slide
* @param {Boolean} isWrap - will wrap-around to last/first if at the end
* @param {Boolean} isInstant - will immediately set position at selected cell
*/
proto.select = function( index, isWrap, isInstant ) {
if ( !this.isActive ) {
return;
}
index = parseInt( index, 10 );
this._wrapSelect( index );
if ( this.options.wrapAround || isWrap ) {
index = utils.modulo( index, this.slides.length );
}
// bail if invalid index
if ( !this.slides[ index ] ) {
return;
}
var prevIndex = this.selectedIndex;
this.selectedIndex = index;
this.updateSelectedSlide();
if ( isInstant ) {
this.positionSliderAtSelected();
} else {
this.startAnimation();
}
if ( this.options.adaptiveHeight ) {
this.setGallerySize();
}
// events
this.dispatchEvent( 'select', null, [ index ] );
// change event if new index
if ( index != prevIndex ) {
this.dispatchEvent( 'change', null, [ index ] );
}
// old v1 event name, remove in v3
this.dispatchEvent('cellSelect');
};
// wraps position for wrapAround, to move to closest slide. #113
proto._wrapSelect = function( index ) {
var len = this.slides.length;
var isWrapping = this.options.wrapAround && len > 1;
if ( !isWrapping ) {
return index;
}
var wrapIndex = utils.modulo( index, len );
// go to shortest
var delta = Math.abs( wrapIndex - this.selectedIndex );
var backWrapDelta = Math.abs( ( wrapIndex + len ) - this.selectedIndex );
var forewardWrapDelta = Math.abs( ( wrapIndex - len ) - this.selectedIndex );
if ( !this.isDragSelect && backWrapDelta < delta ) {
index += len;
} else if ( !this.isDragSelect && forewardWrapDelta < delta ) {
index -= len;
}
// wrap position so slider is within normal area
if ( index < 0 ) {
this.x -= this.slideableWidth;
} else if ( index >= len ) {
this.x += this.slideableWidth;
}
};
proto.previous = function( isWrap, isInstant ) {
this.select( this.selectedIndex - 1, isWrap, isInstant );
};
proto.next = function( isWrap, isInstant ) {
this.select( this.selectedIndex + 1, isWrap, isInstant );
};
proto.updateSelectedSlide = function() {
var slide = this.slides[ this.selectedIndex ];
// selectedIndex could be outside of slides, if triggered before resize()
if ( !slide ) {
return;
}
// unselect previous selected slide
this.unselectSelectedSlide();
// update new selected slide
this.selectedSlide = slide;
slide.select();
this.selectedCells = slide.cells;
this.selectedElements = slide.getCellElements();
// HACK: selectedCell & selectedElement is first cell in slide, backwards compatibility
// Remove in v3?
this.selectedCell = slide.cells[0];
this.selectedElement = this.selectedElements[0];
};
proto.unselectSelectedSlide = function() {
if ( this.selectedSlide ) {
this.selectedSlide.unselect();
}
};
proto.selectInitialIndex = function() {
var initialIndex = this.options.initialIndex;
// already activated, select previous selectedIndex
if ( this.isInitActivated ) {
this.select( this.selectedIndex, false, true );
return;
}
// select with selector string
if ( initialIndex && typeof initialIndex == 'string' ) {
var cell = this.queryCell( initialIndex );
if ( cell ) {
this.selectCell( initialIndex, false, true );
return;
}
}
var index = 0;
// select with number
if ( initialIndex && this.slides[ initialIndex ] ) {
index = initialIndex;
}
// select instantly
this.select( index, false, true );
};
/**
* select slide from number or cell element
* @param {[Element, Number]} value - zero-based index or element to select
* @param {Boolean} isWrap - enables wrapping around for extra index
* @param {Boolean} isInstant - disables slide animation
*/
proto.selectCell = function( value, isWrap, isInstant ) {
// get cell
var cell = this.queryCell( value );
if ( !cell ) {
return;
}
var index = this.getCellSlideIndex( cell );
this.select( index, isWrap, isInstant );
};
proto.getCellSlideIndex = function( cell ) {
// get index of slides that has cell
for ( var i = 0; i < this.slides.length; i++ ) {
var slide = this.slides[i];
var index = slide.cells.indexOf( cell );
if ( index != -1 ) {
return i;
}
}
};
// -------------------------- get cells -------------------------- //
/**
* get Flickity.Cell, given an Element
* @param {Element} elem - matching cell element
* @returns {Flickity.Cell} cell - matching cell
*/
proto.getCell = function( elem ) {
// loop through cells to get the one that matches
for ( var i = 0; i < this.cells.length; i++ ) {
var cell = this.cells[i];
if ( cell.element == elem ) {
return cell;
}
}
};
/**
* get collection of Flickity.Cells, given Elements
* @param {[Element, Array, NodeList]} elems - multiple elements
* @returns {Array} cells - Flickity.Cells
*/
proto.getCells = function( elems ) {
elems = utils.makeArray( elems );
var cells = [];
elems.forEach( function( elem ) {
var cell = this.getCell( elem );
if ( cell ) {
cells.push( cell );
}
}, this );
return cells;
};
/**
* get cell elements
* @returns {Array} cellElems
*/
proto.getCellElements = function() {
return this.cells.map( function( cell ) {
return cell.element;
} );
};
/**
* get parent cell from an element
* @param {Element} elem - child element
* @returns {Flickit.Cell} cell - parent cell
*/
proto.getParentCell = function( elem ) {
// first check if elem is cell
var cell = this.getCell( elem );
if ( cell ) {
return cell;
}
// try to get parent cell elem
elem = utils.getParent( elem, '.flickity-slider > *' );
return this.getCell( elem );
};
/**
* get cells adjacent to a slide
* @param {Integer} adjCount - number of adjacent slides
* @param {Integer} index - index of slide to start
* @returns {Array} cells - array of Flickity.Cells
*/
proto.getAdjacentCellElements = function( adjCount, index ) {
if ( !adjCount ) {
return this.selectedSlide.getCellElements();
}
index = index === undefined ? this.selectedIndex : index;
var len = this.slides.length;
if ( 1 + ( adjCount * 2 ) >= len ) {
return this.getCellElements();
}
var cellElems = [];
for ( var i = index - adjCount; i <= index + adjCount; i++ ) {
var slideIndex = this.options.wrapAround ? utils.modulo( i, len ) : i;
var slide = this.slides[ slideIndex ];
if ( slide ) {
cellElems = cellElems.concat( slide.getCellElements() );
}
}
return cellElems;
};
/**
* select slide from number or cell element
* @param {[Element, String, Number]} selector - element, selector string, or index
* @returns {Flickity.Cell} - matching cell
*/
proto.queryCell = function( selector ) {
if ( typeof selector == 'number' ) {
// use number as index
return this.cells[ selector ];
}
if ( typeof selector == 'string' ) {
// do not select invalid selectors from hash: #123, #/. #791
if ( selector.match( /^[#.]?[\d/]/ ) ) {
return;
}
// use string as selector, get element
selector = this.element.querySelector( selector );
}
// get cell from element
return this