UNPKG

flickity

Version:

Touch, responsive, flickable carousels

1,849 lines (1,529 loc) 95.1 kB
/*! * 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,