Repository: metafizzy/flickity Branch: master Commit: a64cc3305215 Files: 81 Total size: 284.3 KB Directory structure: gitextract_uopm_3sr/ ├── .eslintrc.js ├── .github/ │ ├── contributing.md │ ├── issue_template.md │ └── workflows/ │ └── nodejs.yml ├── .gitignore ├── .nvmrc ├── README.md ├── bin/ │ ├── .eslintrc.js │ ├── bundle-css.js │ ├── bundle-js.js │ ├── lint-json.js │ └── version.js ├── bower.json ├── css/ │ └── flickity.css ├── dist/ │ ├── flickity.css │ └── flickity.pkgd.js ├── js/ │ ├── add-remove-cell.js │ ├── animate.js │ ├── cell.js │ ├── core.js │ ├── drag.js │ ├── imagesloaded.js │ ├── index.js │ ├── lazyload.js │ ├── page-dots.js │ ├── player.js │ ├── prev-next-button.js │ └── slide.js ├── package.json ├── sandbox/ │ ├── adaptive-height.html │ ├── add-remove.html │ ├── ajax.html │ ├── basic.html │ ├── freescroll.html │ ├── group-cells.html │ ├── jquery.html │ ├── js/ │ │ ├── add-remove.js │ │ ├── basic.js │ │ ├── jquery.js │ │ ├── scroll-event.js │ │ ├── tricky-drag.js │ │ ├── v2-sizzle.js │ │ └── wrap-around.js │ ├── lazyload.html │ ├── media.html │ ├── right-to-left.html │ ├── sandbox.css │ ├── scroll-event.html │ ├── single.html │ ├── styles.html │ ├── tricky-drag.html │ ├── v2-sizzle.html │ └── wrap-around.html ├── stylelint.config.js └── test/ ├── drag.html ├── index.html ├── test.css └── unit/ ├── adaptive-height.js ├── add-remove-cells.js ├── auto-play.js ├── cell-selector.js ├── change.js ├── contain.js ├── destroy.js ├── drag.js ├── empty.js ├── get-parent-cell.js ├── get-wrap-cells.js ├── group-cells.js ├── imagesloaded.js ├── init.js ├── initial-index.js ├── lazyload-srcset.js ├── lazyload.js ├── page-dots.js ├── position-cells.js ├── prev-next-buttons.js ├── resize.js ├── select-cell.js ├── watch.js └── wrap-around-fill.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ /* eslint-env node */ module.exports = { plugins: [ 'metafizzy' ], extends: 'plugin:metafizzy/browser', env: { browser: true, commonjs: true, }, parserOptions: { ecmaVersion: 2018, }, globals: { Flickity: 'readonly', QUnit: 'readonly', }, rules: { 'prefer-object-spread': 'error', }, ignorePatterns: [ 'bower_components' ], }; ================================================ FILE: .github/contributing.md ================================================ ## Submitting issues ### Reduced test case required All bug reports and problem issues require a [**reduced test case**](https://css-tricks.com/reduced-test-cases/). Create one by forking any one of the [CodePen demos](https://codepen.io/desandro/pens/tags/?grid_type=list&selected_tag=flickity-docs&sort_order=asc) from [the docs](https://flickity.metafizzy.co). **CodePens** + [Basic](https://codepen.io/desandro/pen/azqbop) + [imagesLoaded](https://codepen.io/desandro/pen/MYQWEe) + [lazyLoad](https://codepen.io/desandro/pen/vOeGzL) + [autoPlay](https://codepen.io/desandro/pen/RNQwaB) **Test cases** + A reduced test case clearly demonstrates the bug or issue. + It contains the bare minimum HTML, CSS, and JavaScript required to demonstrate the bug. + A link to your production site is **not** a reduced test case. Providing a reduced test case is the best way to get your issue addressed. They help you point out the problem. They help me verify and debug the problem. They help others understand the problem. Without a reduced test case, your issue may be closed. ## Pull requests Contributions are welcome! + **For typos and one-line fixes,** send those right in. + **For larger features,** open an issue before starting any significant work. Let's discuss to see how your feature fits within Flickity's vision. + **Follow the code style.** Spaces in brackets, semicolons, trailing commas. + **Do not edit `dist/` files.** Make your edits to source files in `js/` and `css/`. + **Do not run `gulp` to update `dist/` files.** I'll take care of this when I create a new release. Your code will be used as part of a commercial product if merged. By submitting a Pull Request, you are giving your consent for your code to be integrated into Flickity as part of a commercial product. ================================================ FILE: .github/issue_template.md ================================================ **Test case:** https://codepen.io/desandro/pen/azqbop ================================================ FILE: .github/workflows/nodejs.yml ================================================ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [16.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run lint env: CI: true ================================================ FILE: .gitignore ================================================ bower_components/ node_modules/ /**/browserify/bundle.js notes.md sandbox/yoshi-parallax ================================================ FILE: .nvmrc ================================================ 16 ================================================ FILE: README.md ================================================ # Flickity _Touch, responsive, flickable carousels_ See [flickity.metafizzy.co](https://flickity.metafizzy.co) for complete docs and demos. ## Install ### Download + CSS: - [flickity.min.css](https://unpkg.com/flickity@2/dist/flickity.min.css) minified, or - [flickity.css](https://unpkg.com/flickity@2/dist/flickity.css) un-minified + JavaScript: - [flickity.pkgd.min.js](https://unpkg.com/flickity@2/dist/flickity.pkgd.min.js) minified, or - [flickity.pkgd.js](https://unpkg.com/flickity@2/dist/flickity.pkgd.js) un-minified ### CDN Link directly to Flickity files on [unpkg](https://unpkg.com). ``` html ``` ``` html ``` ### Package managers Bower: `bower install flickity --save` npm: `npm install flickity --save` ## License ### Commercial license If you want to use Flickity to develop commercial sites, themes, projects, and applications, the Commercial license is the appropriate license. With this option, your source code is kept proprietary. Purchase a Flickity Commercial License at [flickity.metafizzy.co](https://flickity.metafizzy.co/#commercial-license) ### Open source license If you are creating an open source application under a license compatible with the [GNU GPL license v3](https://www.gnu.org/licenses/gpl-3.0.html), you may use Flickity under the terms of the GPLv3. [Read more about Flickity's license](https://flickity.metafizzy.co/license.html). ## Usage Flickity works with a container element and a set of child cell elements ``` html ``` ### Options ``` js var flky = new Flickity( '.gallery', { // options, defaults listed accessibility: true, // enable keyboard navigation, pressing left & right keys adaptiveHeight: false, // set carousel height to the selected slide autoPlay: false, // advances to the next cell // if true, default is 3 seconds // or set time between advances in milliseconds // i.e. `autoPlay: 1000` will advance every 1 second cellAlign: 'center', // alignment of cells, 'center', 'left', or 'right' // or a decimal 0-1, 0 is beginning (left) of container, 1 is end (right) cellSelector: undefined, // specify selector for cell elements contain: false, // will contain cells to container // so no excess scroll at beginning or end // has no effect if wrapAround is enabled draggable: '>1', // enables dragging & flicking // if at least 2 cells dragThreshold: 3, // number of pixels a user must scroll horizontally to start dragging // increase to allow more room for vertical scroll for touch devices freeScroll: false, // enables content to be freely scrolled and flicked // without aligning cells friction: 0.2, // smaller number = easier to flick farther groupCells: false, // group cells together in slides initialIndex: 0, // zero-based index of the initial selected cell lazyLoad: true, // enable lazy-loading images // set img data-flickity-lazyload="src.jpg" // set to number to load images adjacent cells percentPosition: true, // sets positioning in percent values, rather than pixels // Enable if items have percent widths // Disable if items have pixel widths, like images prevNextButtons: true, // creates and enables buttons to click to previous & next cells pageDots: true, // create and enable page dots resize: true, // listens to window resize events to adjust size & positions rightToLeft: false, // enables right-to-left layout setGallerySize: true, // sets the height of gallery // disable if gallery already has height set with CSS watchCSS: false, // watches the content of :after of the element // activates if #element:after { content: 'flickity' } wrapAround: false // at end of cells, wraps-around to first for infinite scrolling }); ``` --- By [Metafizzy 🌈🐻](https://metafizzy.co) ================================================ FILE: bin/.eslintrc.js ================================================ module.exports = { plugins: [ 'metafizzy' ], extends: 'plugin:metafizzy/node', }; ================================================ FILE: bin/bundle-css.js ================================================ const CleanCss = require('clean-css'); const fs = require('fs'); let srcCss = fs.readFileSync( 'css/flickity.css', 'utf8' ); let minifiedCss = new CleanCss().minify( srcCss ).styles.replace( '*/', '*/\n' ); fs.writeFileSync( 'dist/flickity.min.css', minifiedCss ); ================================================ FILE: bin/bundle-js.js ================================================ const fs = require('fs'); const { execSync } = require('child_process'); const { minify } = require('terser'); const indexPath = 'js/index.js'; const distPath = 'dist/flickity.pkgd.js'; const distMinPath = 'dist/flickity.pkgd.min.js'; let indexContent = fs.readFileSync( `./${indexPath}`, 'utf8' ); // get file paths from index.js let jsPaths = indexContent.match( /require\('([.\-/\w]+)'\)/gi ) .map( ( path ) => path.replace( "require('.", 'js' ).replace( "')", '.js' ) ); let paths = [ 'node_modules/jquery-bridget/jquery-bridget.js', 'node_modules/ev-emitter/ev-emitter.js', 'node_modules/get-size/get-size.js', 'node_modules/fizzy-ui-utils/utils.js', 'node_modules/unidragger/unidragger.js', 'node_modules/imagesloaded/imagesloaded.js', 'js/cell.js', 'js/slide.js', 'js/animate.js', ...jsPaths, ]; // concatenate files execSync(`cat ${paths.join(' ')} > ${distPath}`); // add banner let banner = indexContent.split(' */')[0] + ' */\n\n'; banner = banner.replace( 'Flickity', 'Flickity PACKAGED' ); let distJsContent = fs.readFileSync( distPath, 'utf8' ); distJsContent = banner + distJsContent; fs.writeFileSync( distPath, distJsContent ); // minify ( async function() { let { code } = await minify( distJsContent, { mangle: true } ); fs.writeFileSync( distMinPath, code ); } )(); ================================================ FILE: bin/lint-json.js ================================================ require('../package.json'); require('../bower.json'); ================================================ FILE: bin/version.js ================================================ const fs = require('fs'); const version = require('../package.json').version; [ 'css/flickity.css', 'js/index.js' ].forEach( ( file ) => { let src = fs.readFileSync( file, 'utf8' ); src = src.replace( /Flickity v\d+\.\d+\.\d+/, `Flickity v${version}` ); fs.writeFileSync( file, src, 'utf8' ); } ); ================================================ FILE: bower.json ================================================ { "name": "flickity", "description": "Touch, responsive, flickable carousels", "main": [ "js/index.js", "css/flickity.css" ], "dependencies": { "ev-emitter": "^2.1.2", "fizzy-ui-utils": "^3.0.0", "get-size": "^3.0.0", "unidragger": "^3.0.1" }, "devDependencies": { }, "keywords": [ "gallery", "carousel", "touch" ], "homepage": "https://flickity.metafizzy.co", "authors": [ "Metafizzy" ], "license": "GPL-3.0", "ignore": [ "**/.*", "node_modules", "bower_components", "test", "tests", "sandbox", "package.json", "gulpfile.js", "notes.md" ] } ================================================ FILE: css/flickity.css ================================================ /*! Flickity v3.0.0 https://flickity.metafizzy.co ---------------------------------------------- */ .flickity-enabled { position: relative; } .flickity-enabled:focus { outline: none; } .flickity-viewport { overflow: hidden; position: relative; height: 100%; touch-action: pan-y; } .flickity-slider { position: absolute; width: 100%; height: 100%; left: 0; } .flickity-rtl .flickity-slider { left: unset; right: 0; } /* draggable */ .flickity-enabled.is-draggable { -webkit-tap-highlight-color: transparent; user-select: none; } .flickity-enabled.is-draggable .flickity-viewport { cursor: move; cursor: grab; } .flickity-enabled.is-draggable .flickity-viewport.is-pointer-down { cursor: grabbing; } /* ---- flickity-cell ---- */ .flickity-cell { position: absolute; left: 0; } .flickity-rtl .flickity-cell { left: unset; right: 0; } /* ---- flickity-button ---- */ .flickity-button { position: absolute; background: hsla(0, 0%, 100%, 75%); border: none; color: hsl(0, 0%, 20%); } .flickity-button:hover { background: white; cursor: pointer; } .flickity-button:focus { outline: none; box-shadow: 0 0 0 5px #19F; } .flickity-button:active { color: #19F; } .flickity-button:disabled { opacity: 0.3; cursor: auto; /* prevent disabled button from capturing pointer up event. #716 */ pointer-events: none; } .flickity-button-icon { fill: currentcolor; } /* ---- previous/next buttons ---- */ .flickity-prev-next-button { top: 50%; width: 44px; height: 44px; z-index: 1; /* above viewport */ border-radius: 50%; /* vertically center */ transform: translateY(-50%); } .flickity-prev-next-button.previous { left: 10px; } .flickity-prev-next-button.next { right: 10px; } /* right to left */ .flickity-rtl .flickity-prev-next-button.previous { left: auto; right: 10px; } .flickity-rtl .flickity-prev-next-button.next { right: auto; left: 10px; } .flickity-prev-next-button .flickity-button-icon { position: absolute; left: 20%; top: 20%; width: 60%; height: 60%; } /* ---- page dots ---- */ .flickity-page-dots { position: absolute; width: 100%; bottom: -25px; z-index: 1; /* above viewport */ text-align: center; display: flex; justify-content: center; flex-wrap: wrap; } .flickity-rtl .flickity-page-dots { direction: rtl; } .flickity-page-dot { position: relative; display: block; width: 10px; height: 10px; padding: 0; margin: 0 8px; background: hsl(0, 0%, 20%, 25%); border-radius: 50%; cursor: pointer; appearance: none; border: none; text-indent: -9999px; overflow: hidden; } .flickity-rtl .flickity-page-dot { text-indent: 9999px; } .flickity-page-dot:hover { background: hsla(0, 0%, 20%, 75%); } .flickity-page-dot:active { background: #19F; } .flickity-page-dot.is-selected { background: hsl(0, 0%, 20%); } ================================================ FILE: dist/flickity.css ================================================ /*! Flickity v3.0.0 https://flickity.metafizzy.co ---------------------------------------------- */ .flickity-enabled { position: relative; } .flickity-enabled:focus { outline: none; } .flickity-viewport { overflow: hidden; position: relative; height: 100%; touch-action: pan-y; } .flickity-slider { position: absolute; width: 100%; height: 100%; left: 0; } .flickity-rtl .flickity-slider { left: unset; right: 0; } /* draggable */ .flickity-enabled.is-draggable { -webkit-tap-highlight-color: transparent; user-select: none; } .flickity-enabled.is-draggable .flickity-viewport { cursor: move; cursor: grab; } .flickity-enabled.is-draggable .flickity-viewport.is-pointer-down { cursor: grabbing; } /* ---- flickity-cell ---- */ .flickity-cell { position: absolute; left: 0; } .flickity-rtl .flickity-cell { left: unset; right: 0; } /* ---- flickity-button ---- */ .flickity-button { position: absolute; background: hsl(0 0% 100% / 75%); border: none; color: #333; } .flickity-button:hover { background: white; cursor: pointer; } .flickity-button:focus { outline: none; box-shadow: 0 0 0 5px #19F; } .flickity-button:active { opacity: 0.6; } .flickity-button:disabled { opacity: 0.3; cursor: auto; /* prevent disabled button from capturing pointer up event. #716 */ pointer-events: none; } .flickity-button-icon { fill: currentColor; } /* ---- previous/next buttons ---- */ .flickity-prev-next-button { top: 50%; width: 44px; height: 44px; border-radius: 50%; /* vertically center */ transform: translateY(-50%); } .flickity-prev-next-button.previous { left: 10px; } .flickity-prev-next-button.next { right: 10px; } /* right to left */ .flickity-rtl .flickity-prev-next-button.previous { left: auto; right: 10px; } .flickity-rtl .flickity-prev-next-button.next { right: auto; left: 10px; } .flickity-prev-next-button .flickity-button-icon { position: absolute; left: 20%; top: 20%; width: 60%; height: 60%; } /* ---- page dots ---- */ .flickity-page-dots { position: absolute; width: 100%; bottom: -25px; text-align: center; display: flex; justify-content: center; flex-wrap: wrap; } .flickity-rtl .flickity-page-dots { direction: rtl; } .flickity-page-dot { display: block; width: 10px; height: 10px; padding: 0; margin: 0 8px; background: hsl(0 0% 20% / 25%); border-radius: 50%; cursor: pointer; appearance: none; border: none; text-indent: -9999px; overflow: hidden; } .flickity-rtl .flickity-page-dot { text-indent: 9999px; } .flickity-page-dot:focus { outline: none; box-shadow: 0 0 0 5px #19F; } .flickity-page-dot.is-selected { background: hsl(0 0% 20% / 100%); } ================================================ FILE: dist/flickity.pkgd.js ================================================ /*! * 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 or self 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, 0, 1 ); }; proto.getIsWrapping = function() { let { wrapAround } = this.options; if ( !wrapAround || this.slides.length < 2 ) return false; if ( wrapAround !== 'fill' ) return true; // check that slides can fit let gapWidth = this.slideableWidth - this.size.innerWidth; if ( gapWidth > this.size.innerWidth ) return true; // gap * 2x big, all good // check that content width - shifting cell is bigger than viewport width for ( let cell of this.cells ) { if ( cell.size.outerWidth > gapWidth ) return false; } return true; }; proto._getGapCells = function( gapX, cellIndex, increment ) { // keep adding cells until the cover the initial gap let cells = []; while ( gapX > 0 ) { let cell = this.cells[ cellIndex ]; if ( !cell ) break; cells.push( cell ); cellIndex += increment; gapX -= cell.size.outerWidth; } return cells; }; // ----- contain & wrap ----- // // contain cell targets so no excess sliding proto._containSlides = function() { let isContaining = this.options.contain && !this.isWrapping && this.cells.length; if ( !isContaining ) return; let contentWidth = this.slideableWidth - this.getLastCell().size[ this.endMargin ]; // content is less than gallery size let isContentSmaller = contentWidth < this.size.innerWidth; if ( isContentSmaller ) { // all cells fit inside gallery this.slides.forEach( ( slide ) => { slide.target = contentWidth * this.cellAlign; } ); } else { // contain to bounds let beginBound = this.cursorPosition + this.cells[0].size[ this.beginMargin ]; let endBound = contentWidth - this.size.innerWidth * ( 1 - this.cellAlign ); this.slides.forEach( ( slide ) => { slide.target = Math.max( slide.target, beginBound ); slide.target = Math.min( slide.target, endBound ); } ); } }; // ----- events ----- // /** * 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 ) { let 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' : ''; let $event = type; if ( event ) { // create jQuery event let jQEvent = new jQuery.Event( event ); jQEvent.type = type; $event = jQEvent; } this.$element.trigger( $event, args ); } }; const unidraggerEvents = [ 'dragStart', 'dragMove', 'dragEnd', 'pointerDown', 'pointerMove', 'pointerEnd', 'staticClick', ]; let _emitEvent = proto.emitEvent; proto.emitEvent = function( eventName, args ) { if ( eventName === 'staticClick' ) { // add cellElem and cellIndex args to staticClick let clickedCell = this.getParentCell( args[0].target ); let cellElem = clickedCell && clickedCell.element; let cellIndex = clickedCell && this.cells.indexOf( clickedCell ); args = args.concat( cellElem, cellIndex ); } // do regular thing _emitEvent.call( this, eventName, args ); // duck-punch in jQuery events for Unidragger events let isUnidraggerEvent = unidraggerEvents.includes( eventName ); if ( !isUnidraggerEvent || !jQuery || !this.$element ) return; eventName += this.options.namespaceJQueryEvents ? '.flickity' : ''; let event = args.shift( 0 ); let jQEvent = new jQuery.Event( event ); jQEvent.type = eventName; this.$element.trigger( jQEvent, 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.isWrapping || isWrap ) { index = utils.modulo( index, this.slides.length ); } // bail if invalid index if ( !this.slides[ index ] ) return; let 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 ] ); } }; // wraps position for wrapAround, to move to closest slide. #113 proto._wrapSelect = function( index ) { if ( !this.isWrapping ) return; const { selectedIndex, slideableWidth, slides: { length } } = this; // shift index for wrap, do not wrap dragSelect if ( !this.isDragSelect ) { let wrapIndex = utils.modulo( index, length ); // go to shortest let delta = Math.abs( wrapIndex - selectedIndex ); let backWrapDelta = Math.abs( ( wrapIndex + length ) - selectedIndex ); let forewardWrapDelta = Math.abs( ( wrapIndex - length ) - selectedIndex ); if ( backWrapDelta < delta ) { index += length; } else if ( forewardWrapDelta < delta ) { index -= length; } } // wrap position so slider is within normal area if ( index < 0 ) { this.x -= slideableWidth; } else if ( index >= length ) { this.x += 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() { let 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 this.selectedCell = slide.cells[0]; this.selectedElement = this.selectedElements[0]; }; proto.unselectSelectedSlide = function() { if ( this.selectedSlide ) this.selectedSlide.unselect(); }; proto.selectInitialIndex = function() { let 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' ) { let cell = this.queryCell( initialIndex ); if ( cell ) { this.selectCell( initialIndex, false, true ); return; } } let 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 let cell = this.queryCell( value ); if ( !cell ) return; let index = this.getCellSlideIndex( cell ); this.select( index, isWrap, isInstant ); }; proto.getCellSlideIndex = function( cell ) { // get index of slide that has cell let cellSlide = this.slides.find( ( slide ) => slide.cells.includes( cell ) ); return this.slides.indexOf( cellSlide ); }; // -------------------------- 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 ( let cell of this.cells ) { 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 ); return elems.map( ( elem ) => this.getCell( elem ) ).filter( Boolean ); }; /** * get cell elements * @returns {Array} cellElems */ proto.getCellElements = function() { return this.cells.map( ( cell ) => 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 let cell = this.getCell( elem ); if ( cell ) return cell; // try to get parent cell elem let closest = elem.closest('.flickity-slider > *'); return this.getCell( closest ); }; /** * 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; let len = this.slides.length; if ( 1 + ( adjCount * 2 ) >= len ) { return this.getCellElements(); // get all } let cellElems = []; for ( let i = index - adjCount; i <= index + adjCount; i++ ) { let slideIndex = this.isWrapping ? utils.modulo( i, len ) : i; let 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 ]; } // do not select invalid selectors from hash: #123, #/. #791 let isSelectorString = typeof selector == 'string' && !selector.match( /^[#.]?[\d/]/ ); if ( isSelectorString ) { // use string as selector, get element selector = this.element.querySelector( selector ); } // get cell from element return this.getCell( selector ); }; // -------------------------- events -------------------------- // proto.uiChange = function() { this.emitEvent('uiChange'); }; // ----- resize ----- // proto.onresize = function() { this.watchCSS(); this.resize(); }; utils.debounceMethod( Flickity, 'onresize', 150 ); proto.resize = function() { // #1177 disable resize behavior when animating or dragging for iOS 15 if ( !this.isActive || this.isAnimating || this.isDragging ) return; this.getSize(); // wrap values if ( this.isWrapping ) { this.x = utils.modulo( this.x, this.slideableWidth ); } this.positionCells(); this._updateWrapShiftCells(); this.setGallerySize(); this.emitEvent('resize'); // update selected index for group slides, instant // TODO: position can be lost between groups of various numbers let selectedElement = this.selectedElements && this.selectedElements[0]; this.selectCell( selectedElement, false, true ); }; // watches the :after property, activates/deactivates proto.watchCSS = function() { if ( !this.options.watchCSS ) return; let afterContent = getComputedStyle( this.element, ':after' ).content; // activate if :after { content: 'flickity' } if ( afterContent.includes('flickity') ) { this.activate(); } else { this.deactivate(); } }; // ----- keydown ----- // // go previous/next if left/right keys pressed proto.onkeydown = function( event ) { let { activeElement } = document; let handler = Flickity.keyboardHandlers[ event.key ]; // only work if element is in focus if ( !this.options.accessibility || !activeElement || !handler ) return; let isFocused = this.focusableElems.some( ( elem ) => activeElement === elem ); if ( isFocused ) handler.call( this ); }; Flickity.keyboardHandlers = { ArrowLeft: function() { this.uiChange(); let leftMethod = this.options.rightToLeft ? 'next' : 'previous'; this[ leftMethod ](); }, ArrowRight: function() { this.uiChange(); let rightMethod = this.options.rightToLeft ? 'previous' : 'next'; this[ rightMethod ](); }, }; // ----- focus ----- // proto.focus = function() { this.element.focus({ preventScroll: true }); }; // -------------------------- destroy -------------------------- // // deactivate all Flickity functionality, but keep stuff available proto.deactivate = function() { if ( !this.isActive ) return; this.element.classList.remove('flickity-enabled'); this.element.classList.remove('flickity-rtl'); this.unselectSelectedSlide(); // destroy cells this.cells.forEach( ( cell ) => cell.destroy() ); this.viewport.remove(); // move child elements back into element this.element.append( ...this.slider.children ); if ( this.options.accessibility ) { this.element.removeAttribute('tabIndex'); this.element.removeEventListener( 'keydown', this ); } // set flags this.isActive = false; this.emitEvent('deactivate'); }; proto.destroy = function() { this.deactivate(); window.removeEventListener( 'resize', this ); this.allOff(); this.emitEvent('destroy'); if ( jQuery && this.$element ) { jQuery.removeData( this.element, 'flickity' ); } delete this.element.flickityGUID; delete instances[ this.guid ]; }; // -------------------------- prototype -------------------------- // Object.assign( proto, animatePrototype ); // -------------------------- extras -------------------------- // /** * get Flickity instance from element * @param {[Element, String]} elem - element or selector string * @returns {Flickity} - Flickity instance */ Flickity.data = function( elem ) { elem = utils.getQueryElement( elem ); if ( elem ) return instances[ elem.flickityGUID ]; }; utils.htmlInit( Flickity, 'flickity' ); let { jQueryBridget } = window; if ( jQuery && jQueryBridget ) { jQueryBridget( 'flickity', Flickity, jQuery ); } // set internal jQuery, for Webpack + jQuery v3, #478 Flickity.setJQuery = function( jq ) { jQuery = jq; }; Flickity.Cell = Cell; Flickity.Slide = Slide; return Flickity; } ) ); // drag ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( window, require('./core'), require('unidragger'), require('fizzy-ui-utils'), ); } else { // browser global window.Flickity = factory( window, window.Flickity, window.Unidragger, window.fizzyUIUtils, ); } }( typeof window != 'undefined' ? window : this, function factory( window, Flickity, Unidragger, utils ) { // ----- defaults ----- // Object.assign( Flickity.defaults, { draggable: '>1', dragThreshold: 3, } ); // -------------------------- drag prototype -------------------------- // let proto = Flickity.prototype; Object.assign( proto, Unidragger.prototype ); // inherit Unidragger proto.touchActionValue = ''; // -------------------------- -------------------------- // Flickity.create.drag = function() { this.on( 'activate', this.onActivateDrag ); this.on( 'uiChange', this._uiChangeDrag ); this.on( 'deactivate', this.onDeactivateDrag ); this.on( 'cellChange', this.updateDraggable ); this.on( 'pointerDown', this.handlePointerDown ); this.on( 'pointerUp', this.handlePointerUp ); this.on( 'pointerDown', this.handlePointerDone ); this.on( 'dragStart', this.handleDragStart ); this.on( 'dragMove', this.handleDragMove ); this.on( 'dragEnd', this.handleDragEnd ); this.on( 'staticClick', this.handleStaticClick ); // TODO updateDraggable on resize? if groupCells & slides change }; proto.onActivateDrag = function() { this.handles = [ this.viewport ]; this.bindHandles(); this.updateDraggable(); }; proto.onDeactivateDrag = function() { this.unbindHandles(); this.element.classList.remove('is-draggable'); }; proto.updateDraggable = function() { // disable dragging if less than 2 slides. #278 if ( this.options.draggable === '>1' ) { this.isDraggable = this.slides.length > 1; } else { this.isDraggable = this.options.draggable; } this.element.classList.toggle( 'is-draggable', this.isDraggable ); }; proto._uiChangeDrag = function() { delete this.isFreeScrolling; }; // -------------------------- pointer events -------------------------- // proto.handlePointerDown = function( event ) { if ( !this.isDraggable ) { // proceed for staticClick this.bindActivePointerEvents( event ); return; } let isTouchStart = event.type === 'touchstart'; let isTouchPointer = event.pointerType === 'touch'; let isFocusNode = event.target.matches('input, textarea, select'); if ( !isTouchStart && !isTouchPointer && !isFocusNode ) event.preventDefault(); if ( !isFocusNode ) this.focus(); // blur if ( document.activeElement !== this.element ) document.activeElement.blur(); // stop if it was moving this.dragX = this.x; this.viewport.classList.add('is-pointer-down'); // track scrolling this.pointerDownScroll = getScrollPosition(); window.addEventListener( 'scroll', this ); this.bindActivePointerEvents( event ); }; // ----- move ----- // proto.hasDragStarted = function( moveVector ) { return Math.abs( moveVector.x ) > this.options.dragThreshold; }; // ----- up ----- // proto.handlePointerUp = function() { delete this.isTouchScrolling; this.viewport.classList.remove('is-pointer-down'); }; proto.handlePointerDone = function() { window.removeEventListener( 'scroll', this ); delete this.pointerDownScroll; }; // -------------------------- dragging -------------------------- // proto.handleDragStart = function() { if ( !this.isDraggable ) return; this.dragStartPosition = this.x; this.startAnimation(); window.removeEventListener( 'scroll', this ); }; proto.handleDragMove = function( event, pointer, moveVector ) { if ( !this.isDraggable ) return; event.preventDefault(); this.previousDragX = this.dragX; // reverse if right-to-left let direction = this.options.rightToLeft ? -1 : 1; // wrap around move. #589 if ( this.isWrapping ) moveVector.x %= this.slideableWidth; let dragX = this.dragStartPosition + moveVector.x * direction; if ( !this.isWrapping ) { // slow drag let originBound = Math.max( -this.slides[0].target, this.dragStartPosition ); dragX = dragX > originBound ? ( dragX + originBound ) * 0.5 : dragX; let endBound = Math.min( -this.getLastSlide().target, this.dragStartPosition ); dragX = dragX < endBound ? ( dragX + endBound ) * 0.5 : dragX; } this.dragX = dragX; this.dragMoveTime = new Date(); }; proto.handleDragEnd = function() { if ( !this.isDraggable ) return; let { freeScroll } = this.options; if ( freeScroll ) this.isFreeScrolling = true; // set selectedIndex based on where flick will end up let index = this.dragEndRestingSelect(); if ( freeScroll && !this.isWrapping ) { // if free-scroll & not wrap around // do not free-scroll if going outside of bounding slides // so bounding slides can attract slider, and keep it in bounds let restingX = this.getRestingPosition(); this.isFreeScrolling = -restingX > this.slides[0].target && -restingX < this.getLastSlide().target; } else if ( !freeScroll && index === this.selectedIndex ) { // boost selection if selected index has not changed index += this.dragEndBoostSelect(); } delete this.previousDragX; // apply selection // HACK, set flag so dragging stays in correct direction this.isDragSelect = this.isWrapping; this.select( index ); delete this.isDragSelect; }; proto.dragEndRestingSelect = function() { let restingX = this.getRestingPosition(); // how far away from selected slide let distance = Math.abs( this.getSlideDistance( -restingX, this.selectedIndex ) ); // get closet resting going up and going down let positiveResting = this._getClosestResting( restingX, distance, 1 ); let negativeResting = this._getClosestResting( restingX, distance, -1 ); // use closer resting for wrap-around return positiveResting.distance < negativeResting.distance ? positiveResting.index : negativeResting.index; }; /** * given resting X and distance to selected cell * get the distance and index of the closest cell * @param {Number} restingX - estimated post-flick resting position * @param {Number} distance - distance to selected cell * @param {Integer} increment - +1 or -1, going up or down * @returns {Object} - { distance: {Number}, index: {Integer} } */ proto._getClosestResting = function( restingX, distance, increment ) { let index = this.selectedIndex; let minDistance = Infinity; let condition = this.options.contain && !this.isWrapping ? // if containing, keep going if distance is equal to minDistance ( dist, minDist ) => dist <= minDist : ( dist, minDist ) => dist < minDist; while ( condition( distance, minDistance ) ) { // measure distance to next cell index += increment; minDistance = distance; distance = this.getSlideDistance( -restingX, index ); if ( distance === null ) break; distance = Math.abs( distance ); } return { distance: minDistance, // selected was previous index index: index - increment, }; }; /** * measure distance between x and a slide target * @param {Number} x - horizontal position * @param {Integer} index - slide index * @returns {Number} - slide distance */ proto.getSlideDistance = function( x, index ) { let len = this.slides.length; // wrap around if at least 2 slides let isWrapAround = this.options.wrapAround && len > 1; let slideIndex = isWrapAround ? utils.modulo( index, len ) : index; let slide = this.slides[ slideIndex ]; if ( !slide ) return null; // add distance for wrap-around slides let wrap = isWrapAround ? this.slideableWidth * Math.floor( index/len ) : 0; return x - ( slide.target + wrap ); }; proto.dragEndBoostSelect = function() { // do not boost if no previousDragX or dragMoveTime if ( this.previousDragX === undefined || !this.dragMoveTime || // or if drag was held for 100 ms new Date() - this.dragMoveTime > 100 ) { return 0; } let distance = this.getSlideDistance( -this.dragX, this.selectedIndex ); let delta = this.previousDragX - this.dragX; if ( distance > 0 && delta > 0 ) { // boost to next if moving towards the right, and positive velocity return 1; } else if ( distance < 0 && delta < 0 ) { // boost to previous if moving towards the left, and negative velocity return -1; } return 0; }; // ----- scroll ----- // proto.onscroll = function() { let scroll = getScrollPosition(); let scrollMoveX = this.pointerDownScroll.x - scroll.x; let scrollMoveY = this.pointerDownScroll.y - scroll.y; // cancel click/tap if scroll is too much if ( Math.abs( scrollMoveX ) > 3 || Math.abs( scrollMoveY ) > 3 ) { this.pointerDone(); } }; // ----- utils ----- // function getScrollPosition() { return { x: window.pageXOffset, y: window.pageYOffset, }; } // ----- ----- // return Flickity; } ) ); // prev/next buttons ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core') ); } else { // browser global factory( window.Flickity ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity ) { const svgURI = 'http://www.w3.org/2000/svg'; // -------------------------- PrevNextButton -------------------------- // function PrevNextButton( increment, direction, arrowShape ) { this.increment = increment; this.direction = direction; this.isPrevious = increment === 'previous'; this.isLeft = direction === 'left'; this._create( arrowShape ); } PrevNextButton.prototype._create = function( arrowShape ) { // properties let element = this.element = document.createElement('button'); element.className = `flickity-button flickity-prev-next-button ${this.increment}`; let label = this.isPrevious ? 'Previous' : 'Next'; // prevent button from submitting form https://stackoverflow.com/a/10836076/182183 element.setAttribute( 'type', 'button' ); element.setAttribute( 'aria-label', label ); // init as disabled this.disable(); // create arrow let svg = this.createSVG( label, arrowShape ); element.append( svg ); }; PrevNextButton.prototype.createSVG = function( label, arrowShape ) { let svg = document.createElementNS( svgURI, 'svg' ); svg.setAttribute( 'class', 'flickity-button-icon' ); svg.setAttribute( 'viewBox', '0 0 100 100' ); // add title #1189 let title = document.createElementNS( svgURI, 'title' ); title.append( label ); // add path let path = document.createElementNS( svgURI, 'path' ); let pathMovements = getArrowMovements( arrowShape ); path.setAttribute( 'd', pathMovements ); path.setAttribute( 'class', 'arrow' ); // rotate arrow if ( !this.isLeft ) { path.setAttribute( 'transform', 'translate(100, 100) rotate(180)' ); } svg.append( title, path ); return svg; }; // get SVG path movmement function getArrowMovements( shape ) { // use shape as movement if string if ( typeof shape == 'string' ) return shape; let { x0, x1, x2, x3, y1, y2 } = shape; // create movement string return `M ${x0}, 50 L ${x1}, ${y1 + 50} L ${x2}, ${y2 + 50} L ${x3}, 50 L ${x2}, ${50 - y2} L ${x1}, ${50 - y1} Z`; } // ----- ----- // PrevNextButton.prototype.enable = function() { this.element.removeAttribute('disabled'); }; PrevNextButton.prototype.disable = function() { this.element.setAttribute( 'disabled', true ); }; // -------------------------- Flickity prototype -------------------------- // Object.assign( Flickity.defaults, { prevNextButtons: true, arrowShape: { x0: 10, x1: 60, y1: 50, x2: 70, y2: 40, x3: 30, }, } ); Flickity.create.prevNextButtons = function() { if ( !this.options.prevNextButtons ) return; let { rightToLeft, arrowShape } = this.options; let prevDirection = rightToLeft ? 'right' : 'left'; let nextDirection = rightToLeft ? 'left' : 'right'; this.prevButton = new PrevNextButton( 'previous', prevDirection, arrowShape ); this.nextButton = new PrevNextButton( 'next', nextDirection, arrowShape ); this.focusableElems.push( this.prevButton.element ); this.focusableElems.push( this.nextButton.element ); this.handlePrevButtonClick = () => { this.uiChange(); this.previous(); }; this.handleNextButtonClick = () => { this.uiChange(); this.next(); }; this.on( 'activate', this.activatePrevNextButtons ); this.on( 'select', this.updatePrevNextButtons ); }; let proto = Flickity.prototype; proto.updatePrevNextButtons = function() { let lastIndex = this.slides.length ? this.slides.length - 1 : 0; this.updatePrevNextButton( this.prevButton, 0 ); this.updatePrevNextButton( this.nextButton, lastIndex ); }; proto.updatePrevNextButton = function( button, disabledIndex ) { // enable is wrapAround and at least 2 slides if ( this.isWrapping && this.slides.length > 1 ) { button.enable(); return; } let isEnabled = this.selectedIndex !== disabledIndex; button[ isEnabled ? 'enable' : 'disable' ](); // if disabling button that is focused, // shift focus to element to maintain keyboard accessibility let isDisabledFocused = !isEnabled && document.activeElement === button.element; if ( isDisabledFocused ) this.focus(); }; proto.activatePrevNextButtons = function() { this.prevButton.element.addEventListener( 'click', this.handlePrevButtonClick ); this.nextButton.element.addEventListener( 'click', this.handleNextButtonClick ); this.element.append( this.prevButton.element, this.nextButton.element ); this.on( 'deactivate', this.deactivatePrevNextButtons ); }; proto.deactivatePrevNextButtons = function() { this.prevButton.element.remove(); this.nextButton.element.remove(); this.prevButton.element.removeEventListener( 'click', this.handlePrevButtonClick ); this.nextButton.element.removeEventListener( 'click', this.handleNextButtonClick ); this.off( 'deactivate', this.deactivatePrevNextButtons ); }; // -------------------------- -------------------------- // Flickity.PrevNextButton = PrevNextButton; return Flickity; } ) ); // page dots ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core'), require('fizzy-ui-utils'), ); } else { // browser global factory( window.Flickity, window.fizzyUIUtils, ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity, utils ) { // -------------------------- PageDots -------------------------- // function PageDots() { // create holder element this.holder = document.createElement('div'); this.holder.className = 'flickity-page-dots'; // create dots, array of elements this.dots = []; } PageDots.prototype.setDots = function( slidesLength ) { // get difference between number of slides and number of dots let delta = slidesLength - this.dots.length; if ( delta > 0 ) { this.addDots( delta ); } else if ( delta < 0 ) { this.removeDots( -delta ); } }; PageDots.prototype.addDots = function( count ) { let newDots = new Array( count ).fill() .map( ( item, i ) => { let dot = document.createElement('button'); dot.setAttribute( 'type', 'button' ); let num = i + 1 + this.dots.length; dot.className = 'flickity-page-dot'; dot.textContent = `View slide ${num}`; return dot; } ); this.holder.append( ...newDots ); this.dots = this.dots.concat( newDots ); }; PageDots.prototype.removeDots = function( count ) { // remove from this.dots collection let removeDots = this.dots.splice( this.dots.length - count, count ); // remove from DOM removeDots.forEach( ( dot ) => dot.remove() ); }; PageDots.prototype.updateSelected = function( index ) { // remove selected class on previous if ( this.selectedDot ) { this.selectedDot.classList.remove('is-selected'); this.selectedDot.removeAttribute('aria-current'); } // don't proceed if no dots if ( !this.dots.length ) return; this.selectedDot = this.dots[ index ]; this.selectedDot.classList.add('is-selected'); this.selectedDot.setAttribute( 'aria-current', 'step' ); }; Flickity.PageDots = PageDots; // -------------------------- Flickity -------------------------- // Object.assign( Flickity.defaults, { pageDots: true, } ); Flickity.create.pageDots = function() { if ( !this.options.pageDots ) return; this.pageDots = new PageDots(); this.handlePageDotsClick = this.onPageDotsClick.bind( this ); // events this.on( 'activate', this.activatePageDots ); this.on( 'select', this.updateSelectedPageDots ); this.on( 'cellChange', this.updatePageDots ); this.on( 'resize', this.updatePageDots ); this.on( 'deactivate', this.deactivatePageDots ); }; let proto = Flickity.prototype; proto.activatePageDots = function() { this.pageDots.setDots( this.slides.length ); this.focusableElems.push( ...this.pageDots.dots ); this.pageDots.holder.addEventListener( 'click', this.handlePageDotsClick ); this.element.append( this.pageDots.holder ); }; proto.onPageDotsClick = function( event ) { let index = this.pageDots.dots.indexOf( event.target ); if ( index === -1 ) return; // only dot clicks this.uiChange(); this.select( index ); }; proto.updateSelectedPageDots = function() { this.pageDots.updateSelected( this.selectedIndex ); }; proto.updatePageDots = function() { this.pageDots.dots.forEach( ( dot ) => { utils.removeFrom( this.focusableElems, dot ); } ); this.pageDots.setDots( this.slides.length ); this.focusableElems.push( ...this.pageDots.dots ); }; proto.deactivatePageDots = function() { this.pageDots.holder.remove(); this.pageDots.holder.removeEventListener( 'click', this.handlePageDotsClick ); }; // ----- ----- // Flickity.PageDots = PageDots; return Flickity; } ) ); // player & autoPlay ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core') ); } else { // browser global factory( window.Flickity ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity ) { // -------------------------- Player -------------------------- // function Player( autoPlay, onTick ) { this.autoPlay = autoPlay; this.onTick = onTick; this.state = 'stopped'; // visibility change event handler this.onVisibilityChange = this.visibilityChange.bind( this ); this.onVisibilityPlay = this.visibilityPlay.bind( this ); } // start play Player.prototype.play = function() { if ( this.state === 'playing' ) return; // do not play if page is hidden, start playing when page is visible let isPageHidden = document.hidden; if ( isPageHidden ) { document.addEventListener( 'visibilitychange', this.onVisibilityPlay ); return; } this.state = 'playing'; // listen to visibility change document.addEventListener( 'visibilitychange', this.onVisibilityChange ); // start ticking this.tick(); }; Player.prototype.tick = function() { // do not tick if not playing if ( this.state !== 'playing' ) return; // default to 3 seconds let time = typeof this.autoPlay == 'number' ? this.autoPlay : 3000; // HACK: reset ticks if stopped and started within interval this.clear(); this.timeout = setTimeout( () => { this.onTick(); this.tick(); }, time ); }; Player.prototype.stop = function() { this.state = 'stopped'; this.clear(); // remove visibility change event document.removeEventListener( 'visibilitychange', this.onVisibilityChange ); }; Player.prototype.clear = function() { clearTimeout( this.timeout ); }; Player.prototype.pause = function() { if ( this.state === 'playing' ) { this.state = 'paused'; this.clear(); } }; Player.prototype.unpause = function() { // re-start play if paused if ( this.state === 'paused' ) this.play(); }; // pause if page visibility is hidden, unpause if visible Player.prototype.visibilityChange = function() { let isPageHidden = document.hidden; this[ isPageHidden ? 'pause' : 'unpause' ](); }; Player.prototype.visibilityPlay = function() { this.play(); document.removeEventListener( 'visibilitychange', this.onVisibilityPlay ); }; // -------------------------- Flickity -------------------------- // Object.assign( Flickity.defaults, { pauseAutoPlayOnHover: true, } ); Flickity.create.player = function() { this.player = new Player( this.options.autoPlay, () => { this.next( true ); } ); this.on( 'activate', this.activatePlayer ); this.on( 'uiChange', this.stopPlayer ); this.on( 'pointerDown', this.stopPlayer ); this.on( 'deactivate', this.deactivatePlayer ); }; let proto = Flickity.prototype; proto.activatePlayer = function() { if ( !this.options.autoPlay ) return; this.player.play(); this.element.addEventListener( 'mouseenter', this ); }; // Player API, don't hate the ... thanks I know where the door is proto.playPlayer = function() { this.player.play(); }; proto.stopPlayer = function() { this.player.stop(); }; proto.pausePlayer = function() { this.player.pause(); }; proto.unpausePlayer = function() { this.player.unpause(); }; proto.deactivatePlayer = function() { this.player.stop(); this.element.removeEventListener( 'mouseenter', this ); }; // ----- mouseenter/leave ----- // // pause auto-play on hover proto.onmouseenter = function() { if ( !this.options.pauseAutoPlayOnHover ) return; this.player.pause(); this.element.addEventListener( 'mouseleave', this ); }; // resume auto-play on hover off proto.onmouseleave = function() { this.player.unpause(); this.element.removeEventListener( 'mouseleave', this ); }; // ----- ----- // Flickity.Player = Player; return Flickity; } ) ); // add, remove cell ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core'), require('fizzy-ui-utils'), ); } else { // browser global factory( window.Flickity, window.fizzyUIUtils, ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity, utils ) { // append cells to a document fragment function getCellsFragment( cells ) { let fragment = document.createDocumentFragment(); cells.forEach( ( cell ) => fragment.appendChild( cell.element ) ); return fragment; } // -------------------------- add/remove cell prototype -------------------------- // let proto = Flickity.prototype; /** * Insert, prepend, or append cells * @param {[Element, Array, NodeList]} elems - Elements to insert * @param {Integer} index - Zero-based number to insert */ proto.insert = function( elems, index ) { let cells = this._makeCells( elems ); if ( !cells || !cells.length ) return; let len = this.cells.length; // default to append index = index === undefined ? len : index; // add cells with document fragment let fragment = getCellsFragment( cells ); // append to slider let isAppend = index === len; if ( isAppend ) { this.slider.appendChild( fragment ); } else { let insertCellElement = this.cells[ index ].element; this.slider.insertBefore( fragment, insertCellElement ); } // add to this.cells if ( index === 0 ) { // prepend, add to start this.cells = cells.concat( this.cells ); } else if ( isAppend ) { // append, add to end this.cells = this.cells.concat( cells ); } else { // insert in this.cells let endCells = this.cells.splice( index, len - index ); this.cells = this.cells.concat( cells ).concat( endCells ); } this._sizeCells( cells ); this.cellChange( index ); this.positionSliderAtSelected(); }; proto.append = function( elems ) { this.insert( elems, this.cells.length ); }; proto.prepend = function( elems ) { this.insert( elems, 0 ); }; /** * Remove cells * @param {[Element, Array, NodeList]} elems - ELements to remove */ proto.remove = function( elems ) { let cells = this.getCells( elems ); if ( !cells || !cells.length ) return; let minCellIndex = this.cells.length - 1; // remove cells from collection & DOM cells.forEach( ( cell ) => { cell.remove(); let index = this.cells.indexOf( cell ); minCellIndex = Math.min( index, minCellIndex ); utils.removeFrom( this.cells, cell ); } ); this.cellChange( minCellIndex ); this.positionSliderAtSelected(); }; /** * logic to be run after a cell's size changes * @param {Element} elem - cell's element */ proto.cellSizeChange = function( elem ) { let cell = this.getCell( elem ); if ( !cell ) return; cell.getSize(); let index = this.cells.indexOf( cell ); this.cellChange( index ); // do not position slider after lazy load }; /** * logic any time a cell is changed: added, removed, or size changed * @param {Integer} changedCellIndex - index of the changed cell, optional */ proto.cellChange = function( changedCellIndex ) { let prevSelectedElem = this.selectedElement; this._positionCells( changedCellIndex ); this._updateWrapShiftCells(); this.setGallerySize(); // update selectedIndex, try to maintain position & select previous selected element let cell = this.getCell( prevSelectedElem ); if ( cell ) this.selectedIndex = this.getCellSlideIndex( cell ); this.selectedIndex = Math.min( this.slides.length - 1, this.selectedIndex ); this.emitEvent( 'cellChange', [ changedCellIndex ] ); // position slider this.select( this.selectedIndex ); }; // ----- ----- // return Flickity; } ) ); // lazyload ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core'), require('fizzy-ui-utils'), ); } else { // browser global factory( window.Flickity, window.fizzyUIUtils, ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity, utils ) { const lazyAttr = 'data-flickity-lazyload'; const lazySrcAttr = `${lazyAttr}-src`; const lazySrcsetAttr = `${lazyAttr}-srcset`; const imgSelector = `img[${lazyAttr}], img[${lazySrcAttr}], ` + `img[${lazySrcsetAttr}], source[${lazySrcsetAttr}]`; Flickity.create.lazyLoad = function() { this.on( 'select', this.lazyLoad ); this.handleLazyLoadComplete = this.onLazyLoadComplete.bind( this ); }; let proto = Flickity.prototype; proto.lazyLoad = function() { let { lazyLoad } = this.options; if ( !lazyLoad ) return; // get adjacent cells, use lazyLoad option for adjacent count let adjCount = typeof lazyLoad == 'number' ? lazyLoad : 0; // lazy load images this.getAdjacentCellElements( adjCount ) .map( getCellLazyImages ) .flat() .forEach( ( img ) => new LazyLoader( img, this.handleLazyLoadComplete ) ); }; function getCellLazyImages( cellElem ) { // check if cell element is lazy image if ( cellElem.matches('img') ) { let cellAttr = cellElem.getAttribute( lazyAttr ); let cellSrcAttr = cellElem.getAttribute( lazySrcAttr ); let cellSrcsetAttr = cellElem.getAttribute( lazySrcsetAttr ); if ( cellAttr || cellSrcAttr || cellSrcsetAttr ) { return cellElem; } } // select lazy images in cell return [ ...cellElem.querySelectorAll( imgSelector ) ]; } proto.onLazyLoadComplete = function( img, event ) { let cell = this.getParentCell( img ); let cellElem = cell && cell.element; this.cellSizeChange( cellElem ); this.dispatchEvent( 'lazyLoad', event, cellElem ); }; // -------------------------- LazyLoader -------------------------- // /** * class to handle loading images * @param {Image} img - Image element * @param {Function} onComplete - callback function */ function LazyLoader( img, onComplete ) { this.img = img; this.onComplete = onComplete; this.load(); } LazyLoader.prototype.handleEvent = utils.handleEvent; LazyLoader.prototype.load = function() { this.img.addEventListener( 'load', this ); this.img.addEventListener( 'error', this ); // get src & srcset let src = this.img.getAttribute( lazyAttr ) || this.img.getAttribute( lazySrcAttr ); let srcset = this.img.getAttribute( lazySrcsetAttr ); // set src & serset this.img.src = src; if ( srcset ) this.img.setAttribute( 'srcset', srcset ); // remove attr this.img.removeAttribute( lazyAttr ); this.img.removeAttribute( lazySrcAttr ); this.img.removeAttribute( lazySrcsetAttr ); }; LazyLoader.prototype.onload = function( event ) { this.complete( event, 'flickity-lazyloaded' ); }; LazyLoader.prototype.onerror = function( event ) { this.complete( event, 'flickity-lazyerror' ); }; LazyLoader.prototype.complete = function( event, className ) { // unbind events this.img.removeEventListener( 'load', this ); this.img.removeEventListener( 'error', this ); let mediaElem = this.img.parentNode.matches('picture') ? this.img.parentNode : this.img; mediaElem.classList.add( className ); this.onComplete( this.img, event ); }; // ----- ----- // Flickity.LazyLoader = LazyLoader; return Flickity; } ) ); // imagesloaded ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core'), require('imagesloaded'), ); } else { // browser global factory( window.Flickity, window.imagesLoaded, ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity, imagesLoaded ) { Flickity.create.imagesLoaded = function() { this.on( 'activate', this.imagesLoaded ); }; Flickity.prototype.imagesLoaded = function() { if ( !this.options.imagesLoaded ) return; let onImagesLoadedProgress = ( instance, image ) => { let cell = this.getParentCell( image.img ); this.cellSizeChange( cell && cell.element ); if ( !this.options.freeScroll ) this.positionSliderAtSelected(); }; imagesLoaded( this.slider ).on( 'progress', onImagesLoadedProgress ); }; return Flickity; } ) ); ================================================ FILE: js/add-remove-cell.js ================================================ // add, remove cell ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core'), require('fizzy-ui-utils'), ); } else { // browser global factory( window.Flickity, window.fizzyUIUtils, ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity, utils ) { // append cells to a document fragment function getCellsFragment( cells ) { let fragment = document.createDocumentFragment(); cells.forEach( ( cell ) => fragment.appendChild( cell.element ) ); return fragment; } // -------------------------- add/remove cell prototype -------------------------- // let proto = Flickity.prototype; /** * Insert, prepend, or append cells * @param {[Element, Array, NodeList]} elems - Elements to insert * @param {Integer} index - Zero-based number to insert */ proto.insert = function( elems, index ) { let cells = this._makeCells( elems ); if ( !cells || !cells.length ) return; let len = this.cells.length; // default to append index = index === undefined ? len : index; // add cells with document fragment let fragment = getCellsFragment( cells ); // append to slider let isAppend = index === len; if ( isAppend ) { this.slider.appendChild( fragment ); } else { let insertCellElement = this.cells[ index ].element; this.slider.insertBefore( fragment, insertCellElement ); } // add to this.cells if ( index === 0 ) { // prepend, add to start this.cells = cells.concat( this.cells ); } else if ( isAppend ) { // append, add to end this.cells = this.cells.concat( cells ); } else { // insert in this.cells let endCells = this.cells.splice( index, len - index ); this.cells = this.cells.concat( cells ).concat( endCells ); } this._sizeCells( cells ); this.cellChange( index ); this.positionSliderAtSelected(); }; proto.append = function( elems ) { this.insert( elems, this.cells.length ); }; proto.prepend = function( elems ) { this.insert( elems, 0 ); }; /** * Remove cells * @param {[Element, Array, NodeList]} elems - ELements to remove */ proto.remove = function( elems ) { let cells = this.getCells( elems ); if ( !cells || !cells.length ) return; let minCellIndex = this.cells.length - 1; // remove cells from collection & DOM cells.forEach( ( cell ) => { cell.remove(); let index = this.cells.indexOf( cell ); minCellIndex = Math.min( index, minCellIndex ); utils.removeFrom( this.cells, cell ); } ); this.cellChange( minCellIndex ); this.positionSliderAtSelected(); }; /** * logic to be run after a cell's size changes * @param {Element} elem - cell's element */ proto.cellSizeChange = function( elem ) { let cell = this.getCell( elem ); if ( !cell ) return; cell.getSize(); let index = this.cells.indexOf( cell ); this.cellChange( index ); // do not position slider after lazy load }; /** * logic any time a cell is changed: added, removed, or size changed * @param {Integer} changedCellIndex - index of the changed cell, optional */ proto.cellChange = function( changedCellIndex ) { let prevSelectedElem = this.selectedElement; this._positionCells( changedCellIndex ); this._updateWrapShiftCells(); this.setGallerySize(); // update selectedIndex, try to maintain position & select previous selected element let cell = this.getCell( prevSelectedElem ); if ( cell ) this.selectedIndex = this.getCellSlideIndex( cell ); this.selectedIndex = Math.min( this.slides.length - 1, this.selectedIndex ); this.emitEvent( 'cellChange', [ changedCellIndex ] ); // position slider this.select( this.selectedIndex ); }; // ----- ----- // return Flickity; } ) ); ================================================ FILE: js/animate.js ================================================ // 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; } ) ); ================================================ FILE: js/cell.js ================================================ // 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; } ) ); ================================================ FILE: js/core.js ================================================ // 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, 0, 1 ); }; proto.getIsWrapping = function() { let { wrapAround } = this.options; if ( !wrapAround || this.slides.length < 2 ) return false; if ( wrapAround !== 'fill' ) return true; // check that slides can fit let gapWidth = this.slideableWidth - this.size.innerWidth; if ( gapWidth > this.size.innerWidth ) return true; // gap * 2x big, all good // check that content width - shifting cell is bigger than viewport width for ( let cell of this.cells ) { if ( cell.size.outerWidth > gapWidth ) return false; } return true; }; proto._getGapCells = function( gapX, cellIndex, increment ) { // keep adding cells until the cover the initial gap let cells = []; while ( gapX > 0 ) { let cell = this.cells[ cellIndex ]; if ( !cell ) break; cells.push( cell ); cellIndex += increment; gapX -= cell.size.outerWidth; } return cells; }; // ----- contain & wrap ----- // // contain cell targets so no excess sliding proto._containSlides = function() { let isContaining = this.options.contain && !this.isWrapping && this.cells.length; if ( !isContaining ) return; let contentWidth = this.slideableWidth - this.getLastCell().size[ this.endMargin ]; // content is less than gallery size let isContentSmaller = contentWidth < this.size.innerWidth; if ( isContentSmaller ) { // all cells fit inside gallery this.slides.forEach( ( slide ) => { slide.target = contentWidth * this.cellAlign; } ); } else { // contain to bounds let beginBound = this.cursorPosition + this.cells[0].size[ this.beginMargin ]; let endBound = contentWidth - this.size.innerWidth * ( 1 - this.cellAlign ); this.slides.forEach( ( slide ) => { slide.target = Math.max( slide.target, beginBound ); slide.target = Math.min( slide.target, endBound ); } ); } }; // ----- events ----- // /** * 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 ) { let 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' : ''; let $event = type; if ( event ) { // create jQuery event let jQEvent = new jQuery.Event( event ); jQEvent.type = type; $event = jQEvent; } this.$element.trigger( $event, args ); } }; const unidraggerEvents = [ 'dragStart', 'dragMove', 'dragEnd', 'pointerDown', 'pointerMove', 'pointerEnd', 'staticClick', ]; let _emitEvent = proto.emitEvent; proto.emitEvent = function( eventName, args ) { if ( eventName === 'staticClick' ) { // add cellElem and cellIndex args to staticClick let clickedCell = this.getParentCell( args[0].target ); let cellElem = clickedCell && clickedCell.element; let cellIndex = clickedCell && this.cells.indexOf( clickedCell ); args = args.concat( cellElem, cellIndex ); } // do regular thing _emitEvent.call( this, eventName, args ); // duck-punch in jQuery events for Unidragger events let isUnidraggerEvent = unidraggerEvents.includes( eventName ); if ( !isUnidraggerEvent || !jQuery || !this.$element ) return; eventName += this.options.namespaceJQueryEvents ? '.flickity' : ''; let event = args.shift( 0 ); let jQEvent = new jQuery.Event( event ); jQEvent.type = eventName; this.$element.trigger( jQEvent, 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.isWrapping || isWrap ) { index = utils.modulo( index, this.slides.length ); } // bail if invalid index if ( !this.slides[ index ] ) return; let 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 ] ); } }; // wraps position for wrapAround, to move to closest slide. #113 proto._wrapSelect = function( index ) { if ( !this.isWrapping ) return; const { selectedIndex, slideableWidth, slides: { length } } = this; // shift index for wrap, do not wrap dragSelect if ( !this.isDragSelect ) { let wrapIndex = utils.modulo( index, length ); // go to shortest let delta = Math.abs( wrapIndex - selectedIndex ); let backWrapDelta = Math.abs( ( wrapIndex + length ) - selectedIndex ); let forewardWrapDelta = Math.abs( ( wrapIndex - length ) - selectedIndex ); if ( backWrapDelta < delta ) { index += length; } else if ( forewardWrapDelta < delta ) { index -= length; } } // wrap position so slider is within normal area if ( index < 0 ) { this.x -= slideableWidth; } else if ( index >= length ) { this.x += 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() { let 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 this.selectedCell = slide.cells[0]; this.selectedElement = this.selectedElements[0]; }; proto.unselectSelectedSlide = function() { if ( this.selectedSlide ) this.selectedSlide.unselect(); }; proto.selectInitialIndex = function() { let 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' ) { let cell = this.queryCell( initialIndex ); if ( cell ) { this.selectCell( initialIndex, false, true ); return; } } let 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 let cell = this.queryCell( value ); if ( !cell ) return; let index = this.getCellSlideIndex( cell ); this.select( index, isWrap, isInstant ); }; proto.getCellSlideIndex = function( cell ) { // get index of slide that has cell let cellSlide = this.slides.find( ( slide ) => slide.cells.includes( cell ) ); return this.slides.indexOf( cellSlide ); }; // -------------------------- 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 ( let cell of this.cells ) { 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 ); return elems.map( ( elem ) => this.getCell( elem ) ).filter( Boolean ); }; /** * get cell elements * @returns {Array} cellElems */ proto.getCellElements = function() { return this.cells.map( ( cell ) => 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 let cell = this.getCell( elem ); if ( cell ) return cell; // try to get parent cell elem let closest = elem.closest('.flickity-slider > *'); return this.getCell( closest ); }; /** * 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; let len = this.slides.length; let cellElems = []; for ( let i = index - adjCount; i <= index + adjCount; i++ ) { let slideIndex = this.isWrapping ? utils.modulo( i, len ) : i; let 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 ]; } // do not select invalid selectors from hash: #123, #/. #791 let isSelectorString = typeof selector == 'string' && !selector.match( /^[#.]?[\d/]/ ); if ( isSelectorString ) { // use string as selector, get element selector = this.element.querySelector( selector ); } // get cell from element return this.getCell( selector ); }; // -------------------------- events -------------------------- // proto.uiChange = function() { this.emitEvent('uiChange'); }; // ----- resize ----- // proto.onresize = function() { this.watchCSS(); this.resize(); }; utils.debounceMethod( Flickity, 'onresize', 150 ); proto.resize = function() { // #1177 disable resize behavior when animating or dragging for iOS 15 if ( !this.isActive || this.isAnimating || this.isDragging ) return; this.getSize(); // wrap values if ( this.isWrapping ) { this.x = utils.modulo( this.x, this.slideableWidth ); } this.positionCells(); this._updateWrapShiftCells(); this.setGallerySize(); this.emitEvent('resize'); // update selected index for group slides, instant // TODO: position can be lost between groups of various numbers let selectedElement = this.selectedElements && this.selectedElements[0]; this.selectCell( selectedElement, false, true ); }; // watches the :after property, activates/deactivates proto.watchCSS = function() { if ( !this.options.watchCSS ) return; let afterContent = getComputedStyle( this.element, ':after' ).content; // activate if :after { content: 'flickity' } if ( afterContent.includes('flickity') ) { this.activate(); } else { this.deactivate(); } }; // ----- keydown ----- // // go previous/next if left/right keys pressed proto.onkeydown = function( event ) { let { activeElement } = document; let handler = Flickity.keyboardHandlers[ event.key ]; // only work if element is in focus if ( !this.options.accessibility || !activeElement || !handler ) return; let isFocused = this.focusableElems.some( ( elem ) => activeElement === elem ); if ( isFocused ) handler.call( this ); }; Flickity.keyboardHandlers = { ArrowLeft: function() { this.uiChange(); let leftMethod = this.options.rightToLeft ? 'next' : 'previous'; this[ leftMethod ](); }, ArrowRight: function() { this.uiChange(); let rightMethod = this.options.rightToLeft ? 'previous' : 'next'; this[ rightMethod ](); }, }; // ----- focus ----- // proto.focus = function() { this.element.focus({ preventScroll: true }); }; // -------------------------- destroy -------------------------- // // deactivate all Flickity functionality, but keep stuff available proto.deactivate = function() { if ( !this.isActive ) return; this.element.classList.remove('flickity-enabled'); this.element.classList.remove('flickity-rtl'); this.unselectSelectedSlide(); // destroy cells this.cells.forEach( ( cell ) => cell.destroy() ); this.viewport.remove(); // move child elements back into element this.element.append( ...this.slider.children ); if ( this.options.accessibility ) { this.element.removeAttribute('tabIndex'); this.element.removeEventListener( 'keydown', this ); } // set flags this.isActive = false; this.emitEvent('deactivate'); }; proto.destroy = function() { this.deactivate(); window.removeEventListener( 'resize', this ); this.allOff(); this.emitEvent('destroy'); if ( jQuery && this.$element ) { jQuery.removeData( this.element, 'flickity' ); } delete this.element.flickityGUID; delete instances[ this.guid ]; }; // -------------------------- prototype -------------------------- // Object.assign( proto, animatePrototype ); // -------------------------- extras -------------------------- // /** * get Flickity instance from element * @param {[Element, String]} elem - element or selector string * @returns {Flickity} - Flickity instance */ Flickity.data = function( elem ) { elem = utils.getQueryElement( elem ); if ( elem ) return instances[ elem.flickityGUID ]; }; utils.htmlInit( Flickity, 'flickity' ); let { jQueryBridget } = window; if ( jQuery && jQueryBridget ) { jQueryBridget( 'flickity', Flickity, jQuery ); } // set internal jQuery, for Webpack + jQuery v3, #478 Flickity.setJQuery = function( jq ) { jQuery = jq; }; Flickity.Cell = Cell; Flickity.Slide = Slide; return Flickity; } ) ); ================================================ FILE: js/drag.js ================================================ // drag ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( window, require('./core'), require('unidragger'), require('fizzy-ui-utils'), ); } else { // browser global window.Flickity = factory( window, window.Flickity, window.Unidragger, window.fizzyUIUtils, ); } }( typeof window != 'undefined' ? window : this, function factory( window, Flickity, Unidragger, utils ) { // ----- defaults ----- // Object.assign( Flickity.defaults, { draggable: '>1', dragThreshold: 3, } ); // -------------------------- drag prototype -------------------------- // let proto = Flickity.prototype; Object.assign( proto, Unidragger.prototype ); // inherit Unidragger proto.touchActionValue = ''; // -------------------------- -------------------------- // Flickity.create.drag = function() { this.on( 'activate', this.onActivateDrag ); this.on( 'uiChange', this._uiChangeDrag ); this.on( 'deactivate', this.onDeactivateDrag ); this.on( 'cellChange', this.updateDraggable ); this.on( 'pointerDown', this.handlePointerDown ); this.on( 'pointerUp', this.handlePointerUp ); this.on( 'pointerDown', this.handlePointerDone ); this.on( 'dragStart', this.handleDragStart ); this.on( 'dragMove', this.handleDragMove ); this.on( 'dragEnd', this.handleDragEnd ); this.on( 'staticClick', this.handleStaticClick ); // TODO updateDraggable on resize? if groupCells & slides change }; proto.onActivateDrag = function() { this.handles = [ this.viewport ]; this.bindHandles(); this.updateDraggable(); }; proto.onDeactivateDrag = function() { this.unbindHandles(); this.element.classList.remove('is-draggable'); }; proto.updateDraggable = function() { // disable dragging if less than 2 slides. #278 if ( this.options.draggable === '>1' ) { this.isDraggable = this.slides.length > 1; } else { this.isDraggable = this.options.draggable; } this.element.classList.toggle( 'is-draggable', this.isDraggable ); }; proto._uiChangeDrag = function() { delete this.isFreeScrolling; }; // -------------------------- pointer events -------------------------- // proto.handlePointerDown = function( event ) { if ( !this.isDraggable ) { // proceed for staticClick this.bindActivePointerEvents( event ); return; } let isTouchStart = event.type === 'touchstart'; let isTouchPointer = event.pointerType === 'touch'; let isFocusNode = event.target.matches('input, textarea, select'); if ( !isTouchStart && !isTouchPointer && !isFocusNode ) event.preventDefault(); if ( !isFocusNode ) this.focus(); // blur if ( document.activeElement !== this.element ) document.activeElement.blur(); // stop if it was moving this.dragX = this.x; this.viewport.classList.add('is-pointer-down'); // track scrolling this.pointerDownScroll = getScrollPosition(); window.addEventListener( 'scroll', this ); this.bindActivePointerEvents( event ); }; // ----- move ----- // proto.hasDragStarted = function( moveVector ) { return Math.abs( moveVector.x ) > this.options.dragThreshold; }; // ----- up ----- // proto.handlePointerUp = function() { delete this.isTouchScrolling; this.viewport.classList.remove('is-pointer-down'); }; proto.handlePointerDone = function() { window.removeEventListener( 'scroll', this ); delete this.pointerDownScroll; }; // -------------------------- dragging -------------------------- // proto.handleDragStart = function() { if ( !this.isDraggable ) return; this.dragStartPosition = this.x; this.startAnimation(); window.removeEventListener( 'scroll', this ); }; proto.handleDragMove = function( event, pointer, moveVector ) { if ( !this.isDraggable ) return; event.preventDefault(); this.previousDragX = this.dragX; // reverse if right-to-left let direction = this.options.rightToLeft ? -1 : 1; // wrap around move. #589 if ( this.isWrapping ) moveVector.x %= this.slideableWidth; let dragX = this.dragStartPosition + moveVector.x * direction; if ( !this.isWrapping ) { // slow drag let originBound = Math.max( -this.slides[0].target, this.dragStartPosition ); dragX = dragX > originBound ? ( dragX + originBound ) * 0.5 : dragX; let endBound = Math.min( -this.getLastSlide().target, this.dragStartPosition ); dragX = dragX < endBound ? ( dragX + endBound ) * 0.5 : dragX; } this.dragX = dragX; this.dragMoveTime = new Date(); }; proto.handleDragEnd = function() { if ( !this.isDraggable ) return; let { freeScroll } = this.options; if ( freeScroll ) this.isFreeScrolling = true; // set selectedIndex based on where flick will end up let index = this.dragEndRestingSelect(); if ( freeScroll && !this.isWrapping ) { // if free-scroll & not wrap around // do not free-scroll if going outside of bounding slides // so bounding slides can attract slider, and keep it in bounds let restingX = this.getRestingPosition(); this.isFreeScrolling = -restingX > this.slides[0].target && -restingX < this.getLastSlide().target; } else if ( !freeScroll && index === this.selectedIndex ) { // boost selection if selected index has not changed index += this.dragEndBoostSelect(); } delete this.previousDragX; // apply selection // HACK, set flag so dragging stays in correct direction this.isDragSelect = this.isWrapping; this.select( index ); delete this.isDragSelect; }; proto.dragEndRestingSelect = function() { let restingX = this.getRestingPosition(); // how far away from selected slide let distance = Math.abs( this.getSlideDistance( -restingX, this.selectedIndex ) ); // get closet resting going up and going down let positiveResting = this._getClosestResting( restingX, distance, 1 ); let negativeResting = this._getClosestResting( restingX, distance, -1 ); // use closer resting for wrap-around return positiveResting.distance < negativeResting.distance ? positiveResting.index : negativeResting.index; }; /** * given resting X and distance to selected cell * get the distance and index of the closest cell * @param {Number} restingX - estimated post-flick resting position * @param {Number} distance - distance to selected cell * @param {Integer} increment - +1 or -1, going up or down * @returns {Object} - { distance: {Number}, index: {Integer} } */ proto._getClosestResting = function( restingX, distance, increment ) { let index = this.selectedIndex; let minDistance = Infinity; let condition = this.options.contain && !this.isWrapping ? // if containing, keep going if distance is equal to minDistance ( dist, minDist ) => dist <= minDist : ( dist, minDist ) => dist < minDist; while ( condition( distance, minDistance ) ) { // measure distance to next cell index += increment; minDistance = distance; distance = this.getSlideDistance( -restingX, index ); if ( distance === null ) break; distance = Math.abs( distance ); } return { distance: minDistance, // selected was previous index index: index - increment, }; }; /** * measure distance between x and a slide target * @param {Number} x - horizontal position * @param {Integer} index - slide index * @returns {Number} - slide distance */ proto.getSlideDistance = function( x, index ) { let len = this.slides.length; // wrap around if at least 2 slides let isWrapAround = this.options.wrapAround && len > 1; let slideIndex = isWrapAround ? utils.modulo( index, len ) : index; let slide = this.slides[ slideIndex ]; if ( !slide ) return null; // add distance for wrap-around slides let wrap = isWrapAround ? this.slideableWidth * Math.floor( index/len ) : 0; return x - ( slide.target + wrap ); }; proto.dragEndBoostSelect = function() { // do not boost if no previousDragX or dragMoveTime if ( this.previousDragX === undefined || !this.dragMoveTime || // or if drag was held for 100 ms new Date() - this.dragMoveTime > 100 ) { return 0; } let distance = this.getSlideDistance( -this.dragX, this.selectedIndex ); let delta = this.previousDragX - this.dragX; if ( distance > 0 && delta > 0 ) { // boost to next if moving towards the right, and positive velocity return 1; } else if ( distance < 0 && delta < 0 ) { // boost to previous if moving towards the left, and negative velocity return -1; } return 0; }; // ----- scroll ----- // proto.onscroll = function() { let scroll = getScrollPosition(); let scrollMoveX = this.pointerDownScroll.x - scroll.x; let scrollMoveY = this.pointerDownScroll.y - scroll.y; // cancel click/tap if scroll is too much if ( Math.abs( scrollMoveX ) > 3 || Math.abs( scrollMoveY ) > 3 ) { this.pointerDone(); } }; // ----- utils ----- // function getScrollPosition() { return { x: window.pageXOffset, y: window.pageYOffset, }; } // ----- ----- // return Flickity; } ) ); ================================================ FILE: js/imagesloaded.js ================================================ // imagesloaded ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core'), require('imagesloaded'), ); } else { // browser global factory( window.Flickity, window.imagesLoaded, ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity, imagesLoaded ) { Flickity.create.imagesLoaded = function() { this.on( 'activate', this.imagesLoaded ); }; Flickity.prototype.imagesLoaded = function() { if ( !this.options.imagesLoaded ) return; let onImagesLoadedProgress = ( instance, image ) => { let cell = this.getParentCell( image.img ); this.cellSizeChange( cell && cell.element ); if ( !this.options.freeScroll ) this.positionSliderAtSelected(); }; imagesLoaded( this.slider ).on( 'progress', onImagesLoadedProgress ); }; return Flickity; } ) ); ================================================ FILE: js/index.js ================================================ /*! * Flickity 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 */ if ( typeof module == 'object' && module.exports ) { const Flickity = require('./core'); require('./drag'); require('./prev-next-button'); require('./page-dots'); require('./player'); require('./add-remove-cell'); require('./lazyload'); require('./imagesloaded'); module.exports = Flickity; } ================================================ FILE: js/lazyload.js ================================================ // lazyload ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core'), require('fizzy-ui-utils'), ); } else { // browser global factory( window.Flickity, window.fizzyUIUtils, ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity, utils ) { const lazyAttr = 'data-flickity-lazyload'; const lazySrcAttr = `${lazyAttr}-src`; const lazySrcsetAttr = `${lazyAttr}-srcset`; const imgSelector = `img[${lazyAttr}], img[${lazySrcAttr}], ` + `img[${lazySrcsetAttr}], source[${lazySrcsetAttr}]`; Flickity.create.lazyLoad = function() { this.on( 'select', this.lazyLoad ); this.handleLazyLoadComplete = this.onLazyLoadComplete.bind( this ); }; let proto = Flickity.prototype; proto.lazyLoad = function() { let { lazyLoad } = this.options; if ( !lazyLoad ) return; // get adjacent cells, use lazyLoad option for adjacent count let adjCount = typeof lazyLoad == 'number' ? lazyLoad : 0; // lazy load images this.getAdjacentCellElements( adjCount ) .map( getCellLazyImages ) .flat() .forEach( ( img ) => new LazyLoader( img, this.handleLazyLoadComplete ) ); }; function getCellLazyImages( cellElem ) { // check if cell element is lazy image if ( cellElem.matches('img') ) { let cellAttr = cellElem.getAttribute( lazyAttr ); let cellSrcAttr = cellElem.getAttribute( lazySrcAttr ); let cellSrcsetAttr = cellElem.getAttribute( lazySrcsetAttr ); if ( cellAttr || cellSrcAttr || cellSrcsetAttr ) { return cellElem; } } // select lazy images in cell return [ ...cellElem.querySelectorAll( imgSelector ) ]; } proto.onLazyLoadComplete = function( img, event ) { let cell = this.getParentCell( img ); let cellElem = cell && cell.element; this.cellSizeChange( cellElem ); this.dispatchEvent( 'lazyLoad', event, cellElem ); }; // -------------------------- LazyLoader -------------------------- // /** * class to handle loading images * @param {Image} img - Image element * @param {Function} onComplete - callback function */ function LazyLoader( img, onComplete ) { this.img = img; this.onComplete = onComplete; this.load(); } LazyLoader.prototype.handleEvent = utils.handleEvent; LazyLoader.prototype.load = function() { this.img.addEventListener( 'load', this ); this.img.addEventListener( 'error', this ); // get src & srcset let src = this.img.getAttribute( lazyAttr ) || this.img.getAttribute( lazySrcAttr ); let srcset = this.img.getAttribute( lazySrcsetAttr ); // set src & serset this.img.src = src; if ( srcset ) this.img.setAttribute( 'srcset', srcset ); // remove attr this.img.removeAttribute( lazyAttr ); this.img.removeAttribute( lazySrcAttr ); this.img.removeAttribute( lazySrcsetAttr ); }; LazyLoader.prototype.onload = function( event ) { this.complete( event, 'flickity-lazyloaded' ); }; LazyLoader.prototype.onerror = function( event ) { this.complete( event, 'flickity-lazyerror' ); }; LazyLoader.prototype.complete = function( event, className ) { // unbind events this.img.removeEventListener( 'load', this ); this.img.removeEventListener( 'error', this ); let mediaElem = this.img.parentNode.matches('picture') ? this.img.parentNode : this.img; mediaElem.classList.add( className ); this.onComplete( this.img, event ); }; // ----- ----- // Flickity.LazyLoader = LazyLoader; return Flickity; } ) ); ================================================ FILE: js/page-dots.js ================================================ // page dots ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core'), require('fizzy-ui-utils'), ); } else { // browser global factory( window.Flickity, window.fizzyUIUtils, ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity, utils ) { // -------------------------- PageDots -------------------------- // function PageDots() { // create holder element this.holder = document.createElement('div'); this.holder.className = 'flickity-page-dots'; // create dots, array of elements this.dots = []; } PageDots.prototype.setDots = function( slidesLength ) { // get difference between number of slides and number of dots let delta = slidesLength - this.dots.length; if ( delta > 0 ) { this.addDots( delta ); } else if ( delta < 0 ) { this.removeDots( -delta ); } }; PageDots.prototype.addDots = function( count ) { let newDots = new Array( count ).fill() .map( ( item, i ) => { let dot = document.createElement('button'); dot.setAttribute( 'type', 'button' ); let num = i + 1 + this.dots.length; dot.className = 'flickity-button flickity-page-dot'; dot.textContent = `View slide ${num}`; return dot; } ); this.holder.append( ...newDots ); this.dots = this.dots.concat( newDots ); }; PageDots.prototype.removeDots = function( count ) { // remove from this.dots collection let removeDots = this.dots.splice( this.dots.length - count, count ); // remove from DOM removeDots.forEach( ( dot ) => dot.remove() ); }; PageDots.prototype.updateSelected = function( index ) { // remove selected class on previous if ( this.selectedDot ) { this.selectedDot.classList.remove('is-selected'); this.selectedDot.removeAttribute('aria-current'); } // don't proceed if no dots if ( !this.dots.length ) return; this.selectedDot = this.dots[ index ]; this.selectedDot.classList.add('is-selected'); this.selectedDot.setAttribute( 'aria-current', 'step' ); }; Flickity.PageDots = PageDots; // -------------------------- Flickity -------------------------- // Object.assign( Flickity.defaults, { pageDots: true, } ); Flickity.create.pageDots = function() { if ( !this.options.pageDots ) return; this.pageDots = new PageDots(); this.handlePageDotsClick = this.onPageDotsClick.bind( this ); // events this.on( 'activate', this.activatePageDots ); this.on( 'select', this.updateSelectedPageDots ); this.on( 'cellChange', this.updatePageDots ); this.on( 'resize', this.updatePageDots ); this.on( 'deactivate', this.deactivatePageDots ); }; let proto = Flickity.prototype; proto.activatePageDots = function() { this.pageDots.setDots( this.slides.length ); this.focusableElems.push( ...this.pageDots.dots ); this.pageDots.holder.addEventListener( 'click', this.handlePageDotsClick ); this.element.insertBefore( this.pageDots.holder, this.viewport ); }; proto.onPageDotsClick = function( event ) { let index = this.pageDots.dots.indexOf( event.target ); if ( index === -1 ) return; // only dot clicks this.uiChange(); this.select( index ); }; proto.updateSelectedPageDots = function() { this.pageDots.updateSelected( this.selectedIndex ); }; proto.updatePageDots = function() { this.pageDots.dots.forEach( ( dot ) => { utils.removeFrom( this.focusableElems, dot ); } ); this.pageDots.setDots( this.slides.length ); this.focusableElems.push( ...this.pageDots.dots ); }; proto.deactivatePageDots = function() { this.pageDots.holder.remove(); this.pageDots.holder.removeEventListener( 'click', this.handlePageDotsClick ); }; // ----- ----- // Flickity.PageDots = PageDots; return Flickity; } ) ); ================================================ FILE: js/player.js ================================================ // player & autoPlay ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core') ); } else { // browser global factory( window.Flickity ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity ) { // -------------------------- Player -------------------------- // function Player( autoPlay, onTick ) { this.autoPlay = autoPlay; this.onTick = onTick; this.state = 'stopped'; // visibility change event handler this.onVisibilityChange = this.visibilityChange.bind( this ); this.onVisibilityPlay = this.visibilityPlay.bind( this ); } // start play Player.prototype.play = function() { if ( this.state === 'playing' ) return; // do not play if page is hidden, start playing when page is visible let isPageHidden = document.hidden; if ( isPageHidden ) { document.addEventListener( 'visibilitychange', this.onVisibilityPlay ); return; } this.state = 'playing'; // listen to visibility change document.addEventListener( 'visibilitychange', this.onVisibilityChange ); // start ticking this.tick(); }; Player.prototype.tick = function() { // do not tick if not playing if ( this.state !== 'playing' ) return; // default to 3 seconds let time = typeof this.autoPlay == 'number' ? this.autoPlay : 3000; // HACK: reset ticks if stopped and started within interval this.clear(); this.timeout = setTimeout( () => { this.onTick(); this.tick(); }, time ); }; Player.prototype.stop = function() { this.state = 'stopped'; this.clear(); // remove visibility change event document.removeEventListener( 'visibilitychange', this.onVisibilityChange ); }; Player.prototype.clear = function() { clearTimeout( this.timeout ); }; Player.prototype.pause = function() { if ( this.state === 'playing' ) { this.state = 'paused'; this.clear(); } }; Player.prototype.unpause = function() { // re-start play if paused if ( this.state === 'paused' ) this.play(); }; // pause if page visibility is hidden, unpause if visible Player.prototype.visibilityChange = function() { let isPageHidden = document.hidden; this[ isPageHidden ? 'pause' : 'unpause' ](); }; Player.prototype.visibilityPlay = function() { this.play(); document.removeEventListener( 'visibilitychange', this.onVisibilityPlay ); }; // -------------------------- Flickity -------------------------- // Object.assign( Flickity.defaults, { pauseAutoPlayOnHover: true, } ); Flickity.create.player = function() { this.player = new Player( this.options.autoPlay, () => { this.next( true ); } ); this.on( 'activate', this.activatePlayer ); this.on( 'uiChange', this.stopPlayer ); this.on( 'pointerDown', this.stopPlayer ); this.on( 'deactivate', this.deactivatePlayer ); }; let proto = Flickity.prototype; proto.activatePlayer = function() { if ( !this.options.autoPlay ) return; this.player.play(); this.element.addEventListener( 'mouseenter', this ); }; // Player API, don't hate the ... thanks I know where the door is proto.playPlayer = function() { this.player.play(); }; proto.stopPlayer = function() { this.player.stop(); }; proto.pausePlayer = function() { this.player.pause(); }; proto.unpausePlayer = function() { this.player.unpause(); }; proto.deactivatePlayer = function() { this.player.stop(); this.element.removeEventListener( 'mouseenter', this ); }; // ----- mouseenter/leave ----- // // pause auto-play on hover proto.onmouseenter = function() { if ( !this.options.pauseAutoPlayOnHover ) return; this.player.pause(); this.element.addEventListener( 'mouseleave', this ); }; // resume auto-play on hover off proto.onmouseleave = function() { this.player.unpause(); this.element.removeEventListener( 'mouseleave', this ); }; // ----- ----- // Flickity.Player = Player; return Flickity; } ) ); ================================================ FILE: js/prev-next-button.js ================================================ // prev/next buttons ( function( window, factory ) { // universal module definition if ( typeof module == 'object' && module.exports ) { // CommonJS module.exports = factory( require('./core') ); } else { // browser global factory( window.Flickity ); } }( typeof window != 'undefined' ? window : this, function factory( Flickity ) { const svgURI = 'http://www.w3.org/2000/svg'; // -------------------------- PrevNextButton -------------------------- // function PrevNextButton( increment, direction, arrowShape ) { this.increment = increment; this.direction = direction; this.isPrevious = increment === 'previous'; this.isLeft = direction === 'left'; this._create( arrowShape ); } PrevNextButton.prototype._create = function( arrowShape ) { // properties let element = this.element = document.createElement('button'); element.className = `flickity-button flickity-prev-next-button ${this.increment}`; let label = this.isPrevious ? 'Previous' : 'Next'; // prevent button from submitting form https://stackoverflow.com/a/10836076/182183 element.setAttribute( 'type', 'button' ); element.setAttribute( 'aria-label', label ); // init as disabled this.disable(); // create arrow let svg = this.createSVG( label, arrowShape ); element.append( svg ); }; PrevNextButton.prototype.createSVG = function( label, arrowShape ) { let svg = document.createElementNS( svgURI, 'svg' ); svg.setAttribute( 'class', 'flickity-button-icon' ); svg.setAttribute( 'viewBox', '0 0 100 100' ); // add title #1189 let title = document.createElementNS( svgURI, 'title' ); title.append( label ); // add path let path = document.createElementNS( svgURI, 'path' ); let pathMovements = getArrowMovements( arrowShape ); path.setAttribute( 'd', pathMovements ); path.setAttribute( 'class', 'arrow' ); // rotate arrow if ( !this.isLeft ) { path.setAttribute( 'transform', 'translate(100, 100) rotate(180)' ); } svg.append( title, path ); return svg; }; // get SVG path movmement function getArrowMovements( shape ) { // use shape as movement if string if ( typeof shape == 'string' ) return shape; let { x0, x1, x2, x3, y1, y2 } = shape; // create movement string return `M ${x0}, 50 L ${x1}, ${y1 + 50} L ${x2}, ${y2 + 50} L ${x3}, 50 L ${x2}, ${50 - y2} L ${x1}, ${50 - y1} Z`; } // ----- ----- // PrevNextButton.prototype.enable = function() { this.element.removeAttribute('disabled'); }; PrevNextButton.prototype.disable = function() { this.element.setAttribute( 'disabled', true ); }; // -------------------------- Flickity prototype -------------------------- // Object.assign( Flickity.defaults, { prevNextButtons: true, arrowShape: { x0: 10, x1: 60, y1: 50, x2: 70, y2: 40, x3: 30, }, } ); Flickity.create.prevNextButtons = function() { if ( !this.options.prevNextButtons ) return; let { rightToLeft, arrowShape } = this.options; let prevDirection = rightToLeft ? 'right' : 'left'; let nextDirection = rightToLeft ? 'left' : 'right'; this.prevButton = new PrevNextButton( 'previous', prevDirection, arrowShape ); this.nextButton = new PrevNextButton( 'next', nextDirection, arrowShape ); this.focusableElems.push( this.prevButton.element ); this.focusableElems.push( this.nextButton.element ); this.handlePrevButtonClick = () => { this.uiChange(); this.previous(); }; this.handleNextButtonClick = () => { this.uiChange(); this.next(); }; this.on( 'activate', this.activatePrevNextButtons ); this.on( 'select', this.updatePrevNextButtons ); }; let proto = Flickity.prototype; proto.updatePrevNextButtons = function() { let lastIndex = this.slides.length ? this.slides.length - 1 : 0; this.updatePrevNextButton( this.prevButton, 0 ); this.updatePrevNextButton( this.nextButton, lastIndex ); }; proto.updatePrevNextButton = function( button, disabledIndex ) { // enable is wrapAround and at least 2 slides if ( this.isWrapping && this.slides.length > 1 ) { button.enable(); return; } let isEnabled = this.selectedIndex !== disabledIndex; button[ isEnabled ? 'enable' : 'disable' ](); // if disabling button that is focused, // shift focus to element to maintain keyboard accessibility let isDisabledFocused = !isEnabled && document.activeElement === button.element; if ( isDisabledFocused ) this.focus(); }; proto.activatePrevNextButtons = function() { this.prevButton.element.addEventListener( 'click', this.handlePrevButtonClick ); this.nextButton.element.addEventListener( 'click', this.handleNextButtonClick ); this.element.prepend( this.prevButton.element, this.nextButton.element ); this.on( 'deactivate', this.deactivatePrevNextButtons ); }; proto.deactivatePrevNextButtons = function() { this.prevButton.element.remove(); this.nextButton.element.remove(); this.prevButton.element.removeEventListener( 'click', this.handlePrevButtonClick ); this.nextButton.element.removeEventListener( 'click', this.handleNextButtonClick ); this.off( 'deactivate', this.deactivatePrevNextButtons ); }; // -------------------------- -------------------------- // Flickity.PrevNextButton = PrevNextButton; return Flickity; } ) ); ================================================ FILE: js/slide.js ================================================ // 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; } ) ); ================================================ FILE: package.json ================================================ { "name": "flickity", "version": "3.0.0", "description": "Touch, responsive, flickable carousels", "main": "js/index.js", "style": "css/flickity.css", "scripts": { "test": "npm run lint && echo \"View test/ in browser\" && exit 1", "lintJs": "npx eslint .", "lintJson": "node bin/lint-json.js", "lintCss": "npx stylelint '**/*.css'", "lint": "npm run lintJson && npm run lintJs && npm run lintCss", "dist": "npm run bundleCss && npm run bundleJs", "bundleCss": "cp css/flickity.css dist/flickity.css && node bin/bundle-css.js", "bundleJs": "node bin/bundle-js.js", "version": "node bin/version.js && npm run dist && git add -A css js dist" }, "dependencies": { "ev-emitter": "^2.1.2", "fizzy-ui-utils": "^3.0.0", "get-size": "^3.0.0", "imagesloaded": "^5.0.0", "unidragger": "^3.0.1" }, "devDependencies": { "clean-css": "^5.2.2", "eslint": "^8.10.0", "eslint-plugin-metafizzy": "^2.0.1", "jquery-bridget": "^3.0.1", "qunit": "^2.17.2", "stylelint": "^14.2.0", "stylelint-config-standard": "^24.0.0", "terser": "^5.10.0" }, "repository": { "type": "git", "url": "git://github.com/metafizzy/flickity.git" }, "keywords": [ "touch", "responsive", "flick", "slider", "carousel", "gallery", "DOM", "browser" ], "author": "Metafizzy", "license": "GPL-3.0", "bugs": { "url": "https://github.com/metafizzy/flickity/issues" }, "homepage": "https://flickity.metafizzy.co", "directories": { "test": "test" } } ================================================ FILE: sandbox/adaptive-height.html ================================================ slides

slides

================================================ FILE: sandbox/add-remove.html ================================================ add/remove cells

add/remove cells

1
2
3
4
5
6

freeScroll

1
2
3
4
5

wrapAround

1
2
3
4
5
6

wrapAround, freeScroll

1
2
3
4
5
6

contain

1
2
3
4
5
6

reposition

1
2
3
4
5
6
7
8
9

prepend with single #492

1

================================================ FILE: sandbox/ajax.html ================================================ ajax

ajax

================================================ FILE: sandbox/basic.html ================================================ basic1

basic1

contain

1
2
3
4
5
6

contain, freeScroll

1
2
3
4
5
6
7
8
9

contain, few

1
2

watch, activate >900px

================================================ FILE: sandbox/freescroll.html ================================================ freescroll

freescroll

================================================ FILE: sandbox/group-cells.html ================================================ group cells

group cells

================================================ FILE: sandbox/jquery.html ================================================ jquery

jquery

1
2
3
4
5
6
================================================ FILE: sandbox/js/add-remove.js ================================================ let cellCount = 6; function getRandom( ary ) { let index = Math.floor( Math.random() * ary.length ); return ary[ index ]; } let widthClasses = [ '', 'w2', 'w3' ]; let nClasses = 'n1 n2 n3 n4 n5 n6'.split(' '); function makeCell() { let cell = document.createElement('div'); cell.className = `cell ${getRandom( widthClasses )} ${getRandom( nClasses )}`; let b = document.createElement('b'); b.textContent = ++cellCount; cell.appendChild( b ); let removeButton = document.createElement('button'); removeButton.className = 'remove-button'; removeButton.textContent = '×'; cell.appendChild( removeButton ); return cell; } function makeCells() { return [ makeCell(), makeCell(), makeCell() ]; } // init [ ...document.querySelectorAll('.demo') ].forEach( ( demo ) => { let container = demo.querySelector('.container'); let flkty = Flickity.data( container ); demo.querySelector('.container').addEventListener( 'click', function( event ) { if ( event.target.matches('.remove-button') ) return; let cellElement = event.target.closest('.cell'); flkty.remove( cellElement ); } ); demo.querySelector('.prepend-button').addEventListener( 'click', function() { flkty.prepend( makeCells() ); } ); demo.querySelector('.insert-button').addEventListener( 'click', function() { flkty.insert( makeCells(), 3 ); } ); demo.querySelector('.append-button').addEventListener( 'click', function() { flkty.append( makeCells() ); } ); } ); // ----- reposition ----- // ( function() { let flkty = new Flickity('#reposition .container'); flkty.on( 'staticClick', function( event, pointer, cellElem ) { if ( !cellElem ) return; cellElem.classList.toggle('w3'); flkty.reposition(); } ); } )(); // ----- prepend single, #492 ----- // ( function() { let demo = document.querySelector('#prepend-single'); let flkty = new Flickity( demo.querySelector('.container') ); demo.querySelector('.prepend-button').addEventListener( 'click', function() { flkty.prepend( makeCell() ); } ); } )(); ================================================ FILE: sandbox/js/basic.js ================================================ let flky = window.flky = new Flickity('#full-width'); // flky.on( 'dragMove', function( event, pointer ) { // console.log( event.type, pointer.pageX, pointer.pageY ); // }); flky.on( 'select', function() { console.log( 'selected', flky.selectedIndex ); } ); flky.on( 'settle', function() { console.log( 'settled', flky.x ); } ); let halfWidthflky = new Flickity( '#half-width', { cellAlign: 'left', } ); halfWidthflky.on( 'staticClick', function( event, pointer, cellIndex, cellElement ) { console.log( cellIndex, cellElement ); } ); new Flickity( '#gallery3', { } ); document.querySelector('#gallery3 button').onclick = function() { console.log('button click'); }; ================================================ FILE: sandbox/js/jquery.js ================================================ /* globals $ */ let $gallery1 = $('#gallery1').flickity(); let flkty = $gallery1.data('flickity'); // $gallery1.on( 'dragMove', function( event, pointer ) { // console.log( event.type, pointer.pageX, pointer.pageY ); // }); $gallery1.on( 'cellSelect.flickity', function( event ) { console.log( 'selected', event.type, `ns:${event.namespace}`, flkty.selectedIndex ); } ); $gallery1.on( 'settle.flickity', function( event ) { console.log( 'settled', flkty.x, event.type ); } ); $gallery1.on( 'staticClick.flickity', function( event, pointer, cellElem, cellIndex ) { console.log( 'staticClick', event.type, cellIndex ); } ); $('#gallery2').flickity({ wrapAround: true, }); ================================================ FILE: sandbox/js/scroll-event.js ================================================ let flkty = new Flickity( '.carousel1', { initialIndex: 2, // groupCells: true, // wrapAround: true, // cellAlign: 'right' } ); let progressBar = document.querySelector('.progress-bar'); flkty.on( 'scroll', function( progress ) { console.log( progress ); let width = Math.max( 0, Math.min( 1, progress ) ); progressBar.style.width = width * 100 + '%'; } ); flkty.reposition(); // ----- ----- // let paraBG = document.querySelector('.parallax__layer--bg'); let paraFG = document.querySelector('.parallax__layer--fg'); let paraFlkty = new Flickity( '.parallax__carousel', { } ); let cellRatio = 0.6; let bgRatio = 0.8; let fgRatio = 1.25; paraFlkty.on( 'scroll', function( progress ) { // console.log( progress ); paraBG.style.left = ( 0.5 - ( 0.5 + progress * 4 ) * cellRatio * bgRatio ) * 100 + '%'; paraFG.style.left = ( 0.5 - ( 0.5 + progress * 4 ) * cellRatio * fgRatio ) * 100 + '%'; } ); paraFlkty.reposition(); // ----- ----- // let imgFlkty = new Flickity( '.image-carousel', { } ); window.onload = function() { imgFlkty.reposition(); }; let imgs = document.querySelectorAll('.image-carousel img'); imgFlkty.on( 'scroll', function() { imgFlkty.slides.forEach( ( slide, i ) => { let img = imgs[i]; let x = ( slide.target + imgFlkty.x ) * -0.333; img.style.transform = `translateX(${x}px)`; } ); } ); ================================================ FILE: sandbox/js/tricky-drag.js ================================================ let nonDragFlkty = new Flickity( '.carousel--non-drag', { draggable: false, } ); function onStaticClick( event, pointer, cellElem, cellIndex ) { console.log( 'staticClick', this.element.className, cellIndex ); } nonDragFlkty.on( 'staticClick', onStaticClick ); let singleCellFlkty = new Flickity('.carousel--single-cell'); singleCellFlkty.on( 'staticClick', onStaticClick ); let groupFlkty = new Flickity( '.carousel--group', { groupCells: true, } ); groupFlkty.on( 'staticClick', function( event ) { let cellElem = event.target.closest('.carousel-cell'); if ( cellElem ) groupFlkty.remove( cellElem ); } ); function makeGroupCell() { let cell = document.createElement('div'); cell.className = 'carousel-cell'; let b = document.createElement('b'); b.textContent = groupFlkty.cells.length + 1; cell.appendChild( b ); return cell; } document.querySelector('.add-group-cell-button').onclick = function() { groupFlkty.append( makeGroupCell() ); }; ================================================ FILE: sandbox/js/v2-sizzle.js ================================================ let flkty = new Flickity( '.carousel', { groupCells: true, adaptiveHeight: true, wrapAround: true, } ); let paraBg = document.querySelector('.parallax-layer--bg'); let paraFg = document.querySelector('.parallax-layer--fg'); let count = flkty.slides.length - 1; flkty.on( 'scroll', function( progress ) { paraBg.style.left = ( 0.5 - ( 0.5 + progress * count ) * 0.7 ) * ( 37/36 ) * 100 + '%'; paraFg.style.left = ( 0.5 - ( 0.5 + progress * count ) * 1.5 ) * ( 37/36 ) * 100 + '%'; } ); flkty.reposition(); ================================================ FILE: sandbox/js/wrap-around.js ================================================ window.flkty = new Flickity( '#gallery1', { wrapAround: true, } ); window.flkty2 = new Flickity( '#gallery2', { } ); window.flkty6 = new Flickity( '#gallery6', { wrapAround: true, cellAlign: 'left', } ); window.flkty4 = new Flickity( '#gallery4', { wrapAround: true, freeScroll: true, } ); window.flky5 = new Flickity( '#gallery5', { freeScroll: true, } ); ================================================ FILE: sandbox/lazyload.html ================================================ lazyload

lazyload

srcset

<picture>

================================================ FILE: sandbox/media.html ================================================ media

media

================================================ FILE: sandbox/right-to-left.html ================================================ right to left

right to left

1
2
3
4
5
6
1
2
3
4
5
6
================================================ FILE: sandbox/sandbox.css ================================================ * { box-sizing: border-box; } body { font-family: sans-serif; color: #333; } .container { border: 1px solid; margin-bottom: 50px; } .container::after { content: ''; display: block; clear: both; } .container:focus { border: 1px blue dotted; } .cell { width: 100%; height: 200px; border: 0 solid white; background: #CCC; } .cell.is-selected { outline: 4px solid hsla(0, 0%, 0%, 25%); outline-offset: -4px; } .cell:nth-child(6n) { background: hsl(0, 80%, 70%); } .cell:nth-child(6n+1) { background: hsl(60, 80%, 70%); } .cell:nth-child(6n+2) { background: hsl(120, 80%, 70%); } .cell:nth-child(6n+3) { background: hsl(180, 80%, 70%); } .cell:nth-child(6n+4) { background: hsl(240, 80%, 70%); } .cell:nth-child(6n+5) { background: hsl(300, 80%, 70%); } .cell.n1 { background: hsl(0, 80%, 70%); } .cell.n2 { background: hsl(60, 80%, 70%); } .cell.n3 { background: hsl(120, 80%, 70%); } .cell.n4 { background: hsl(180, 80%, 70%); } .cell.n5 { background: hsl(240, 80%, 70%); } .cell.n6 { background: hsl(300, 80%, 70%); } .variable-width .cell { width: 20%; margin-right: 3%; } .variable-width .cell.w2 { width: 30%; } .variable-width .cell.w3 { width: 40%; } .fixed-width .cell { width: 200px; margin-right: 20px; } #half-width .cell { width: 48%; margin-right: 4%; } /* big number */ .cell b { display: block; font-size: 100px; color: white; font-weight: bold; position: absolute; left: 10px; top: 10px; } /* ---- couning ---- */ .counting { counter-reset: cell; } .counting .cell::before { counter-increment: cell; content: counter(cell); display: block; font-size: 100px; color: white; font-weight: bold; position: absolute; left: 10px; top: 10px; } ================================================ FILE: sandbox/scroll-event.html ================================================ scroll event

scroll event

================================================ FILE: sandbox/single.html ================================================ single

single

================================================ FILE: sandbox/styles.html ================================================ styles

styles

1
2
3
4
5
1
2
3
4
5
1
2
3
4
5
================================================ FILE: sandbox/tricky-drag.html ================================================ tricky drag

tricky drag

================================================ FILE: sandbox/v2-sizzle.html ================================================ v2 sizzle
Parallax
whoa
Flickity v2
================================================ FILE: sandbox/wrap-around.html ================================================ wrap around

wrap around

1
2
3
4
5
6
1
2
3
4
5
6

Left aligned

1
2
3
4
5
6

Fixed-width cells, pixel-positioning HTML init

1
2
3
4
5
6
7
8

HTML init

1
2
3
4
5
6

freeScroll

1
2
3
4
5
6
1
2
3
4
5
6

no margin for error

1
2
3
4
5

tight wrap #589

1
2
3
4

wrapAround: 'fill', should disabled

1
2
3
4

wrapAround: 'fill', should enabled

1
2
3
4
5
================================================ FILE: stylelint.config.js ================================================ module.exports = { extends: 'stylelint-config-standard', ignoreFiles: [ 'dist/*' ], rules: { 'color-function-notation': 'legacy', 'color-hex-case': 'upper', 'comment-empty-line-before': null, 'declaration-block-no-duplicate-properties': [ true, { ignore: [ 'consecutive-duplicates-with-different-values' ], } ], 'hue-degree-notation': 'number', 'property-no-vendor-prefix': null, 'selector-class-pattern': null, 'string-quotes': 'single', }, }; ================================================ FILE: test/drag.html ================================================ Drag tests

drag and drag with wrapAround tests can be buggy. Try running them again.

drag

drag wrapAround

================================================ FILE: test/index.html ================================================ Flickity tests

Init

cellSelector

empty

getParentCell

position cells

contain

auto-play

prev/next buttons

page dots

getWrapCells

watch

resize

add/remove-cells

destroy

lazyload

lazyload srcset

groupCells

adaptiveHeight

selectCell

change

initialIndex

wrapAround: fill

imagesLoaded

================================================ FILE: test/test.css ================================================ /* stylelint-disable no-descending-specificity */ body { font-family: sans-serif; color: #333; } /* move over qunit window to reduce overlaps */ @media (min-height: 500px) { #qunit { left: 420px; } } .gallery { border: 1px solid; width: 400px; margin-bottom: 40px; } .gallery--imagesloaded img { display: block; height: 140px; } .gallery .cell { width: 100%; height: 100px; background: #F09; font-size: 40px; color: white; } .variable-width .cell { width: 25%; } /* 100px */ .variable-width .cell.width2 { width: 40%; /* 160px */ background: #F90; } .variable-width .cell.width3 { width: 60%; /* 240px */ background: #09F; } #position-cells.percent-margin .cell { margin: 0 2%; } #position-cells.pixel-margin .cell { margin: 0 10px; } .drag .cell { margin-right: 5%; } #watch.has-after::after { content: 'flickity'; display: none; } #lazyload img { display: block; max-height: 100px; } /* ---- group-cells ---- */ #group-cells .cell { width: 100px; } #group-cells .cell--width2 { width: 200px; } #group-cells .cell--width3 { width: 300px; } #group-cells .cell--width4 { width: 400px; } #group-cells.is-expanded { width: 600px; } #group-cells .cell:nth-child(2n) { background: #09F; } /* ---- adaptive-height ---- */ #adaptive-height .cell { width: 33.33%; } #adaptive-height .cell--height2 { height: 200px; } #adaptive-height .cell--height3 { height: 300px; } #adaptive-height .cell--height4 { height: 400px; } #adaptive-height .cell:nth-child(2n) { background: #09F; } /* ---- select-cell ---- */ #select-cell .cell { width: 33.33%; } /* ---- wrap-around-fill ---- */ .variable-width .cell.cell--wrap-around-short { width: 20%; } ================================================ FILE: test/unit/adaptive-height.js ================================================ QUnit.test( 'adaptiveHeight', function( assert ) { let flkty = new Flickity( '#adaptive-height', { adaptiveHeight: true, } ); // 2,1,3, 1,4,2, 1,2,1 function checkSelectHeight( index, height ) { flkty.select( index, false, true ); assert.equal( flkty.viewport.style.height, `${height}px`, `slide ${index}` ); } checkSelectHeight( 0, 200 ); checkSelectHeight( 1, 100 ); checkSelectHeight( 2, 300 ); checkSelectHeight( 3, 100 ); checkSelectHeight( 4, 400 ); checkSelectHeight( 5, 200 ); flkty.options.groupCells = true; flkty.resize(); checkSelectHeight( 0, 300 ); checkSelectHeight( 1, 400 ); checkSelectHeight( 2, 200 ); } ); ================================================ FILE: test/unit/add-remove-cells.js ================================================ QUnit.test( 'add/remove cells', function( assert ) { function makeCellElem() { let cellElem = document.createElement('div'); cellElem.className = 'cell'; return cellElem; } // position values can be off by 0.1% or 1px function isPositionApprox( value, expected ) { value = value.replace( 'translateX(', '' ).replace( ')', '' ); let isPercent = value.indexOf('%') !== -1; value = parseFloat( value ); let diff = Math.abs( expected - value ); return isPercent ? diff < 0.1 : diff <= 1; } let elem = document.querySelector('#add-remove-cells'); let flkty = new Flickity( elem ); let sliderElem = elem.querySelector('.flickity-slider'); function checkCellElem( cellElem, index, message ) { assert.equal( sliderElem.children[ index ], cellElem, message + ' cell element in DOM correct' ); assert.equal( flkty.cells[ index ].element, cellElem, message + ' element added as cell' ); assert.ok( isPositionApprox( cellElem.style.transform, index * 100 ), ` element positioned ${index * 100}` ); } // prepend cell element let cellElem = makeCellElem(); flkty.prepend( cellElem ); checkCellElem( cellElem, 0, 'prepended' ); assert.equal( flkty.selectedIndex, 1, 'selectedIndex +1 after prepend' ); // append cell element cellElem = makeCellElem(); flkty.append( cellElem ); let lastIndex = flkty.cells.length - 1; checkCellElem( cellElem, lastIndex, 'appended' ); assert.equal( flkty.selectedIndex, 1, 'selectedIndex same after prepend' ); // insert single cell element cellElem = makeCellElem(); // this one gets removed first flkty.select( 2 ); flkty.insert( cellElem, 2 ); checkCellElem( cellElem, 2, 'single inserted' ); assert.equal( flkty.selectedIndex, 3, 'selectedIndex +1 after insert before' ); flkty.insert( makeCellElem(), 4 ); assert.equal( flkty.selectedIndex, 3, 'selectedIndex same after insert before' ); // insert multiple cell elements let cellElems = [ makeCellElem(), makeCellElem(), makeCellElem() ]; flkty.insert( cellElems, 3 ); checkCellElem( cellElems[0], 3, 'first multiple inserted' ); checkCellElem( cellElems[1], 4, 'second multiple inserted' ); checkCellElem( cellElems[2], 5, 'third multiple inserted' ); assert.equal( flkty.selectedIndex, 6, 'selectedIndex +6 after 3 insert before' ); function checkCellPositions() { let isGap = false; flkty.cells.forEach( ( cell, i ) => { if ( !isPositionApprox( cell.element.style.transform, i * 100 ) ) { assert.ok( false, `gap in cell position ${i} after removal` ); isGap = true; } } ); assert.ok( !isGap, 'no gaps in cell positions' ); } // remove single cell element that was inserted let len = flkty.cells.length; flkty.remove( cellElem ); assert.equal( len - sliderElem.children.length, 1, 'element removed from DOM' ); assert.equal( len - flkty.cells.length, 1, 'cell removed' ); assert.equal( flkty.selectedIndex, 5, 'selectedIndex -1 after remove before' ); checkCellPositions(); // remove multiple len = flkty.cells.length; flkty.select( 4 ); flkty.remove([ cellElems[2], cellElems[0], cellElems[1] ]); assert.equal( len - sliderElem.children.length, 3, 'elements removed from DOM' ); assert.equal( len - flkty.cells.length, 3, 'cells removed' ); checkCellPositions(); // remove all cells flkty.remove( flkty.getCellElements() ); assert.equal( flkty.cells.length, 0, 'all cells removed' ); flkty.resize(); assert.ok( true, 'resize with zero items didnt freak out' ); } ); ================================================ FILE: test/unit/auto-play.js ================================================ QUnit.test( 'auto play', function( assert ) { let done = assert.async(); let flkty = new Flickity( '#auto-play', { autoPlay: 200, } ); let selectCount = 0; let testDelay = flkty.options.autoPlay + 100; let tests; function nextTest() { if ( tests.length ) { let next = tests.shift(); return next(); } else { flkty.stopPlayer(); done(); } } tests = [ // check that player runs function() { flkty.on( 'select', function onSelect() { selectCount++; if ( selectCount < 5 ) { assert.equal( flkty.selectedIndex, selectCount % flkty.cells.length, `auto-played to ${flkty.selectedIndex}` ); } else if ( selectCount === 5 ) { // HACK do async, should be able to stop after a tick flkty.off( 'select', onSelect ); nextTest(); } } ); }, // pause & unpause function() { function onPauseSelect() { assert.ok( false, 'player ticked during pause' ); } flkty.on( 'select', onPauseSelect ); flkty.pausePlayer(); setTimeout( function() { assert.ok( true, 'player did not tick during pause' ); flkty.off( 'select', onPauseSelect ); flkty.once( 'select', function() { assert.ok( true, 'player resumed after unpausing' ); nextTest(); } ); flkty.unpausePlayer(); }, testDelay ); }, // stopPlayer function() { let ticks = 0; function onSelect() { ticks++; } flkty.stopPlayer(); setTimeout( function() { flkty.off( 'select', onSelect ); assert.equal( ticks, 0, 'no ticks after stopped' ); nextTest(); }, testDelay * 2 ); }, // double playPlayer() function() { let ticks = 0; function onSelect() { ticks++; } flkty.stopPlayer(); flkty.on( 'select', onSelect ); flkty.playPlayer(); flkty.playPlayer(); setTimeout( function() { flkty.off( 'select', onSelect ); assert.equal( ticks, 1, 'only one tick after double playPlayer' ); nextTest(); }, testDelay ); }, ]; nextTest(); } ); ================================================ FILE: test/unit/cell-selector.js ================================================ QUnit.test( 'cellSelector', function( assert ) { let elem = document.querySelector('#cell-selector'); let notCell1 = elem.querySelector('.not-cell1'); let notCell2 = elem.querySelector('.not-cell2'); let flkty = new Flickity( elem, { cellSelector: '.cell', } ); let cellsMatchSelector = true; for ( let cell of flkty.cells ) { let isMatch = cell.element.matches( flkty.options.cellSelector ); cellsMatchSelector = cellsMatchSelector && isMatch; } // getCellElements() let cellElems = flkty.getCellElements(); let queriedCellElems = elem.querySelectorAll( flkty.options.cellSelector ); cellElems.forEach( ( cellElem, i ) => { assert.equal( cellElem, queriedCellElems[i], 'cell element same as queried cell element' ); } ); assert.ok( cellsMatchSelector, 'all cell elements match cellSelector' ); assert.equal( notCell1.parentNode, elem, 'notCell1 parent node is still gallery' ); assert.equal( notCell2.parentNode, elem, 'notCell2 parent node is still gallery' ); } ); ================================================ FILE: test/unit/change.js ================================================ /* eslint-disable no-invalid-this */ QUnit.test( 'change', function( assert ) { let done = assert.async(); function onInitChange() { assert.ok( false, 'change should not trigger on init' ); } new Flickity( '#change', { on: { change: onInitChange, ready: function() { // define events last to first for strict function onChangeC( index ) { assert.equal( index, 0, 'change triggered on select back to 0' ); done(); } function onChangeB() { assert.ok( false, 'change should not trigger on same select' ); } function onSelectB( index ) { assert.equal( index, 1, 'select triggered on same select 1' ); this.off( 'change', onChangeB ); this.once( 'change', onChangeC ); this.select( 0, false, true ); } function onChangeA( index ) { assert.equal( index, 1, 'change triggered, selected 1' ); this.once( 'change', onChangeB ); this.once( 'select', onSelectB ); // select 1 again this.select( 1, false, true ); } // kick off this.off( 'change', onInitChange ); this.once( 'change', onChangeA ); this.select( 1, false, true ); }, }, } ); } ); ================================================ FILE: test/unit/contain.js ================================================ QUnit.test( 'contain', function( assert ) { let flkty = new Flickity( '#contain', { contain: true, } ); assert.equal( Math.round( flkty.x + flkty.cursorPosition ), 0, 'selected at 0, position left edge' ); flkty.select( 1 ); flkty.positionSliderAtSelected(); assert.equal( Math.round( flkty.x + flkty.cursorPosition ), 0, 'selected at 1, position left edge' ); flkty.select( 4 ); flkty.positionSliderAtSelected(); let endLimit = flkty.slideableWidth - flkty.size.innerWidth * ( 1 - flkty.cellAlign ); assert.equal( Math.round( -endLimit ), Math.round( flkty.x ), 'selected at 4, position right edge' ); flkty.select( 5 ); flkty.positionSliderAtSelected(); assert.equal( Math.round( -endLimit ), Math.round( flkty.x ), 'selected at 5, position right edge' ); } ); ================================================ FILE: test/unit/destroy.js ================================================ QUnit.test( 'destroy', function( assert ) { let elem = document.querySelector('#destroy'); let flkty = new Flickity( elem ); let done = assert.async(); // do it async setTimeout( function() { flkty.destroy(); assert.strictEqual( elem.flickityGUID, undefined, 'flickityGUID removed' ); assert.ok( !flkty.isActive, 'not active' ); assert.ok( !Flickity.data( elem ), '.data() returns falsey' ); assert.ok( elem.children[0], '.cell', 'cell is back as first child' ); assert.ok( !elem.matches('.flickity-enabled'), 'flickity-enabled class removed' ); assert.ok( !elem.querySelector('.flickity-prev-next-button'), 'no buttons' ); assert.ok( !elem.querySelector('.flickity-page-dots'), 'no page dots' ); assert.ok( !elem.style.height, 'no height set' ); assert.ok( !elem.children[0].style.left, 'first cell has no left position' ); done(); }, 20 ); } ); ================================================ FILE: test/unit/drag.js ================================================ ( function() { function noop() {} let fakeDrag = window.fakeDrag = function( flkty, positions ) { function fakeEvent( type, pageX ) { return { type: type, pageX: pageX, pageY: 0, preventDefault: noop, target: flkty.viewport, }; } let hasBeenDown = false; function triggerEvent() { let position = positions.shift(); // down or move event if ( !hasBeenDown ) { let downEvent = fakeEvent( 'mousedown', position ); flkty.pointerDown( downEvent, downEvent ); hasBeenDown = true; } else { let moveEvent = fakeEvent( 'mousemove', position ); flkty.pointerMove( moveEvent, moveEvent ); } if ( positions.length ) { // loop next setTimeout( triggerEvent, 40 ); } else { // up event let upEvent = fakeEvent( 'mouseup', position ); flkty.pointerUp( upEvent, upEvent ); } } triggerEvent(); }; let dragTests; // do each drag test one after another function getDoNextDragTest( done ) { return function doNextDragTest() { if ( dragTests.length ) { let dragTest = dragTests.shift(); dragTest(); } else { done(); } }; } // flickity, dragPositions, index, onSettle, message function getFakeDragTest( args ) { let assert = args.assert; let flkty = args.flickity; let msgCell = `'slide[${args.index}]'`; return function fakeDragTest() { let selectMsg = `${args.message ? args.message + '. ' : ''} selected ${msgCell}`; flkty.once( 'select', function() { assert.equal( flkty.selectedIndex, args.index, selectMsg ); } ); let settleMsg = `${args.message ? args.message + '. ' : ''} settled ${msgCell}`; let target = flkty.slides[ args.index ].target; flkty.once( 'settle', function() { assert.equal( Math.round( -flkty.x ), Math.round( target ), settleMsg ); setTimeout( args.onSettle ); } ); fakeDrag( args.flickity, args.dragPositions ); }; } QUnit.test( 'drag', function( assert ) { // async test let done = assert.async(); let flkty = new Flickity('#drag'); let doNextDragTest = getDoNextDragTest( done ); function getDragTest( args ) { args = Object.assign( args, { assert: assert, flickity: flkty, onSettle: doNextDragTest, } ); return getFakeDragTest( args ); } dragTests = [ getDragTest({ message: 'drag to 2nd cell', index: 1, dragPositions: [ 0, -10, -20 ], }), getDragTest({ message: 'drag back to 1st cell', index: 0, dragPositions: [ 0, 10, 20 ], }), getDragTest({ message: 'big flick to 3rd cell', index: 2, dragPositions: [ 0, -10, -80 ], }), // minimal movement to trigger static click function() { flkty.once( 'staticClick', function() { assert.ok( true, 'staticClick fired on non-drag' ); assert.equal( flkty.selectedIndex, 2, 'selected index still at 2 after click' ); setTimeout( doNextDragTest ); } ); fakeDrag( flkty, [ 0, 1, 0, -2, -1 ] ); }, // move out then back to where it started function() { flkty.once( 'settle', function() { assert.equal( flkty.selectedIndex, 2, 'move out then back. same cell' ); setTimeout( doNextDragTest ); } ); fakeDrag( flkty, [ 0, 10, 20, 30, 20 ] ); }, getDragTest({ message: 'drag and try to flick past 6th cell', index: 5, dragPositions: [ 0, -10, -50, -77, -100, -125, -150, -175, -250, -350 ], }), ]; doNextDragTest(); } ); QUnit.test( 'drag with wrapAround', function( assert ) { // async test let done = assert.async(); let flkty = new Flickity( '#drag-wrap-around', { wrapAround: true, } ); let doNextDragTest = getDoNextDragTest( done ); function getDragTest( args ) { args = Object.assign( args, { assert: assert, flickity: flkty, onSettle: doNextDragTest, } ); return getFakeDragTest( args ); } dragTests = [ getDragTest({ message: 'drag to last cell via wrap-around', index: 5, dragPositions: [ 0, 10, 20 ], }), getDragTest({ message: 'drag to first cell via wrap-around', index: 0, dragPositions: [ 0, -10, -20 ], }), getDragTest({ message: 'big flick to 5th cell via wrap-around', index: 4, dragPositions: [ 0, 10, 80 ], }), ]; doNextDragTest(); } ); } )(); ================================================ FILE: test/unit/empty.js ================================================ QUnit.test( 'empty', function( assert ) { let gallery = document.querySelector('#empty'); let flkty = new Flickity( gallery ); assert.ok( true, 'empty gallery ok' ); assert.ok( flkty.prevButton.element.disabled, 'previous button disabled' ); assert.ok( flkty.nextButton.element.disabled, 'next button disabled' ); assert.equal( flkty.pageDots.dots.length, 0, '0 page dots' ); flkty.resize(); assert.ok( true, 'resize with empty gallery ok' ); function makeCellElem() { let cellElem = document.createElement('div'); cellElem.className = 'cell'; return cellElem; } flkty.append( makeCellElem() ); assert.equal( flkty.cells.length, 1, 'added cell to empty gallery' ); assert.ok( flkty.prevButton.element.disabled, 'previous button disabled' ); assert.ok( flkty.nextButton.element.disabled, 'next button disabled' ); assert.equal( flkty.pageDots.dots.length, 1, '1 page dots' ); // destroy and re-init with higher initialIndex flkty.destroy(); flkty = new Flickity( gallery, { initialIndex: 2, } ); // #291 assert.ok( true, 'initializing with initialIndex > cells doesnt throw error' ); } ); ================================================ FILE: test/unit/get-parent-cell.js ================================================ QUnit.test( 'getParentCell', function( assert ) { let gallery = document.querySelector('#get-parent-cell'); let flkty = new Flickity( gallery ); // cell1 let cell = flkty.getParentCell( gallery.querySelector('.cell1') ); assert.ok( cell, 'getParentCell( cell ) ok' ); assert.ok( cell instanceof Flickity.Cell, 'cell is Flickity.Cell' ); let index = flkty.cells.indexOf( cell ); assert.equal( index, 0, 'cell is index 0' ); // cell3 cell = flkty.getParentCell( gallery.querySelector('.cell3') ); assert.ok( cell, 'getParentCell( cell ) ok' ); assert.ok( cell instanceof Flickity.Cell, 'cell is Flickity.Cell' ); index = flkty.cells.indexOf( cell ); assert.equal( index, 2, 'cell is index 2' ); // child1 cell = flkty.getParentCell( gallery.querySelector('.child1') ); assert.ok( cell, 'getParentCell( cell ) ok' ); assert.ok( cell instanceof Flickity.Cell, 'cell is Flickity.Cell' ); index = flkty.cells.indexOf( cell ); assert.equal( index, 0, 'cell is index 0' ); // child2 cell = flkty.getParentCell( gallery.querySelector('.child2') ); assert.ok( cell, 'getParentCell( cell ) ok' ); assert.ok( cell instanceof Flickity.Cell, 'cell is Flickity.Cell' ); index = flkty.cells.indexOf( cell ); assert.equal( index, 1, 'cell is index 1' ); // outside cell = flkty.getParentCell( document.querySelector('.outside') ); assert.ok( !cell, 'getParentCell( notCell ) not ok' ); index = flkty.cells.indexOf( cell ); assert.equal( index, -1, 'not cell is index -1' ); } ); ================================================ FILE: test/unit/get-wrap-cells.js ================================================ QUnit.test( 'getWrapCells', function( assert ) { let flkty = new Flickity( '#get-wrap-cells', { wrapAround: true, } ); // cells are 25% width // center align, 2 cells on each side assert.equal( flkty.beforeShiftCells.length, 2, 'center align, 2 before shift cells' ); assert.equal( flkty.afterShiftCells.length, 2, 'center align, 2 after shift cells' ); flkty.options.cellAlign = 'left'; flkty.resize(); // left align, 0, 4 assert.equal( flkty.beforeShiftCells.length, 0, 'left align, 1 before shift cells' ); assert.equal( flkty.afterShiftCells.length, 4, 'left align, 4 after shift cells' ); flkty.options.cellAlign = 'right'; flkty.resize(); // right align, 4, 0 assert.equal( flkty.beforeShiftCells.length, 4, 'right align, 4 before shift cells' ); assert.equal( flkty.afterShiftCells.length, 0, 'right align, 0 after shift cells' ); } ); ================================================ FILE: test/unit/group-cells.js ================================================ QUnit.test( 'groupCells', function( assert ) { let done = assert.async(); let flkty = new Flickity( '#group-cells', { groupCells: true, } ); function getSlideCellsCount() { let counts = flkty.slides.map( function( slide ) { return slide.cells.length; } ); return counts.join(','); } assert.equal( getSlideCellsCount(), '3,2,2,1,1,3,2', 'groupCells: true' ); let targets = flkty.slides.map( function( slide ) { return slide.target; } ); assert.deepEqual( targets, [ 200, 600, 1000, 1300, 1600, 2000, 2300 ], 'targets' ); flkty.selectCell( 6 ); assert.equal( flkty.selectedIndex, 2, 'selectCell(6) selects 3rd slide' ); flkty.selectCell( flkty.cells[2].element ); assert.equal( flkty.selectedIndex, 0, 'selectCell(3rd elem) selects 1st slide' ); flkty.options.groupCells = 2; flkty.reposition(); assert.equal( getSlideCellsCount(), '2,2,2,2,2,2,2', 'groupCells: 2' ); flkty.options.groupCells = '75%'; flkty.reposition(); assert.equal( getSlideCellsCount(), '2,1,1,2,1,1,1,2,2,1', 'groupCells: 75%' ); flkty.once( 'settle', function() { flkty.element.classList.add('is-expanded'); // 600px wide flkty.options.groupCells = true; flkty.resize(); assert.equal( getSlideCellsCount(), '3,3,2,3,3', 'groupCells: true, container @ 600px' ); done(); } ); } ); ================================================ FILE: test/unit/imagesloaded.js ================================================ /* globals imagesLoaded */ ( function() { // position values can be off by 0.1% or 1px function isPositionApprox( value, expected ) { let isPercent = value.indexOf('%') !== -1; value = parseFloat( value ); let diff = Math.abs( expected - value ); return isPercent ? diff < 0.1 : diff <= 1; } QUnit.test( 'imagesloaded', function( assert ) { let done = assert.async(); let gallery = document.querySelector('#imagesloaded'); let flkty = new Flickity( gallery, { imagesLoaded: true, percentPosition: false, } ); imagesLoaded( gallery, function() { flkty.cells.forEach( ( cell, i ) => { assert.ok( cell.size.width > 10, `cell ${i} has width` ); let transform = cell.element.style.transform; let position = transform.replace( 'translateX(', '' ).replace( ')', '' ); let isApprox = isPositionApprox( position, cell.x ); assert.ok( isApprox, `cell ${i} at proper position` ); } ); assert.equal( flkty.viewport.style.height, '140px', 'gallery height set' ); done(); } ); } ); QUnit.test( 'imagesloaded-in-divs', function( assert ) { let done = assert.async(); let gallery = document.querySelector('#imagesloaded-in-divs'); let flkty = new Flickity( gallery, { imagesLoaded: true, percentPosition: false, } ); imagesLoaded( gallery, function() { flkty.cells.forEach( ( cell, i ) => { assert.ok( cell.size.width > 10, `cell ${i} has width` ); let transform = cell.element.style.transform; let position = transform.replace( 'translateX(', '' ).replace( ')', '' ); let isApprox = isPositionApprox( position, cell.x ); assert.ok( isApprox, `cell ${i} at proper position` ); } ); assert.equal( flkty.viewport.style.height, '140px', 'gallery height set' ); done(); } ); } ); } )(); ================================================ FILE: test/unit/init.js ================================================ ( function() { QUnit.module('Flickity'); let utils = window.fizzyUIUtils; QUnit.test( 'init', function( assert ) { let elem = document.querySelector('#init'); let flkty = new Flickity( elem ); for ( let prop in Flickity.defaults ) { assert.equal( flkty.options[ prop ], Flickity.defaults[ prop ], `${prop} option matches default` ); } assert.equal( flkty.element, elem, '.element is proper element' ); let children = utils.makeArray( flkty.element.children ); assert.notEqual( children.indexOf( flkty.viewport ), -1, 'viewport element is a child element' ); assert.equal( flkty.viewport.children[0], flkty.slider, 'slider is in viewport' ); assert.equal( flkty.viewport.style.height, '100px', 'viewport height set' ); assert.ok( flkty.isActive, 'isActive' ); assert.ok( elem.matches('.flickity-enabled'), 'flickity-enabled class added' ); assert.equal( flkty.cells.length, 6, 'has 6 cells' ); assert.equal( getComputedStyle( flkty.cells[0].element ).left, '0px', 'first cell left: 0px' ); assert.equal( flkty.cells[0].element.style.transform, 'translateX(0%)', 'first cell translateX: 0%' ); assert.equal( flkty.cells[5].element.style.transform, 'translateX(500%)', '6th cell translateX: 500%' ); assert.equal( flkty.selectedIndex, 0, 'selectedIndex = 0' ); assert.equal( flkty.cursorPosition, 200, 'cursorPosition = 200' ); assert.equal( flkty.x + flkty.cursorPosition, 0, 'x + cursorPosition = 0' ); } ); } )(); ================================================ FILE: test/unit/initial-index.js ================================================ QUnit.test( 'initialIndex', function( assert ) { // initialIndex number let flkty = new Flickity( '#initial-index', { initialIndex: 3, } ); assert.equal( flkty.selectedIndex, 3, 'initialIndex number' ); // selectedIndex remains same after reactivation flkty.deactivate(); flkty.activate(); assert.equal( flkty.selectedIndex, 3, 'reactivated selectedIndex stays the same' ); flkty.destroy(); // initialIndex selector string flkty = new Flickity( '#initial-index', { initialIndex: '.cell--initial', } ); assert.equal( flkty.selectedIndex, 4, 'initialIndex selector string' ); flkty.destroy(); // initialIndex selector string with groupCells #881 flkty = new Flickity( '#initial-index', { groupCells: 3, initialIndex: '.cell--initial', } ); assert.equal( flkty.selectedIndex, 1, 'initialIndex selector string with groupCells' ); } ); ================================================ FILE: test/unit/lazyload-srcset.js ================================================ QUnit.test( 'lazyload srcset', function( assert ) { let done = assert.async(); let gallery = document.querySelector('#lazyload-srcset'); let flkty = new Flickity( gallery, { lazyLoad: 1, } ); let loadCount = 0; flkty.on( 'lazyLoad', function( event, cellElem ) { loadCount++; assert.equal( event.type, 'load', 'event.type == load' ); assert.ok( event.target.complete, `img ${loadCount} is complete` ); assert.ok( cellElem, 'cellElement argument there' ); let srcset = event.target.getAttribute('srcset'); assert.ok( srcset, 'srcset attribute set' ); let lazyAttr = event.target.getAttribute('data-flickity-lazyload-srcset'); assert.ok( !lazyAttr, 'data-flickity-lazyload attribute removed' ); // after first 2 have loaded, select 7th cell if ( loadCount === 2 ) { done(); } } ); } ); ================================================ FILE: test/unit/lazyload.js ================================================ QUnit.test( 'lazyload', function( assert ) { let done = assert.async(); let gallery = document.querySelector('#lazyload'); let flkty = new Flickity( gallery, { lazyLoad: 1, } ); let loadCount = 0; flkty.on( 'lazyLoad', function( event, cellElem ) { loadCount++; assert.equal( event.type, 'load', 'event.type == load' ); assert.ok( event.target.complete, `img ${loadCount} is complete` ); assert.ok( cellElem, 'cellElement argument there' ); let lazyAttr = event.target.getAttribute('data-flickity-lazyload'); assert.ok( !lazyAttr, 'data-flickity-lazyload attribute removed' ); // after first 2 have loaded, select 7th cell if ( loadCount === 2 ) { flkty.select( 6 ); } if ( loadCount === 5 ) { let loadedImgs = gallery.querySelectorAll('.flickity-lazyloaded'); assert.equal( loadedImgs.length, '5', 'only 5 images loaded' ); done(); } } ); } ); ================================================ FILE: test/unit/page-dots.js ================================================ QUnit.test( 'pageDots', function( assert ) { let elem = document.querySelector('#page-dots'); let flkty = new Flickity( elem ); let dotsHolder = elem.querySelector('.flickity-page-dots'); let dotsElems = [ ...dotsHolder.querySelectorAll('.flickity-page-dot') ]; assert.ok( dotsHolder, 'dots holder in DOM' ); assert.equal( flkty.pageDots.holder, dotsHolder, 'dots holder element matches flkty.pageDots.holder' ); assert.equal( dotsElems.length, flkty.cells.length, 'number of dots matches number of cells' ); function getSelectedDotIndex() { return dotsElems.indexOf( dotsHolder.querySelector('.is-selected') ); } assert.equal( getSelectedDotIndex(), 0, 'first dot is selected' ); flkty.select( 2 ); assert.equal( getSelectedDotIndex(), 2, '3rd dot is selected' ); // fake click flkty.onPageDotsClick({ target: dotsElems[4] }); assert.equal( flkty.selectedIndex, 4, 'tap dot selects cell' ); } ); ================================================ FILE: test/unit/position-cells.js ================================================ ( function() { // position values can be off by 0.1% or 1px function isPositionApprox( value, expected ) { let isPercent = value.indexOf('%') !== -1; value = parseFloat( value ); let diff = Math.abs( expected - value ); return isPercent ? diff < 0.1 : diff <= 1; } // loop through cells and check position values against expecteds function checkCellPositions( flkty, expecteds ) { let isOK; for ( let i = 0; i < expecteds.length; i++ ) { let expected = expecteds[i]; let cell = flkty.cells[i]; let transform = cell.element.style.transform; let position = transform.replace( 'translateX(', '' ).replace( ')', '' ); isOK = isPositionApprox( position, expected ); if ( !isOK ) { console.error(`wrong cell position, index: ${i}. expected: ${expected}. position: ${position}`); break; } } return isOK; } QUnit.test( 'position cells', function( assert ) { let flkty = new Flickity('#position-cells'); assert.ok( checkCellPositions( flkty, [ 0, 160, 108.3, 312.5, 275, 900 ] ), 'percent cell position' ); // .cell { margin: 0 2%; } flkty.element.classList.add('percent-margin'); flkty.positionCells(); assert.ok( checkCellPositions( flkty, [ 0, 176, 121.67, 342.5, 301.67, 980 ] ), 'percent cell position with margin' ); flkty.element.classList.remove('percent-margin'); // pixel-based position flkty.options.percentPosition = false; flkty.positionCells(); assert.ok( checkCellPositions( flkty, [ 0, 160, 260, 500, 660, 900 ] ), 'pixel cell position' ); // pixel margin, { margin: 0 10px; } flkty.element.classList.add('pixel-margin'); flkty.positionCells(); assert.ok( checkCellPositions( flkty, [ 0, 180, 300, 560, 740, 1000 ] ), 'pixel cell position with margin' ); } ); } )(); ================================================ FILE: test/unit/prev-next-buttons.js ================================================ QUnit.test( 'prev-next-buttons', function( assert ) { let elem = document.querySelector('#prev-next-buttons'); let flkty = new Flickity( elem ); let prevElem = elem.querySelector('.flickity-prev-next-button.previous'); let nextElem = elem.querySelector('.flickity-prev-next-button.next'); assert.ok( prevElem, 'previous button in DOM' ); assert.ok( nextElem, 'next button in DOM' ); assert.equal( flkty.prevButton.element, prevElem, 'previous button element matches prevButton.element' ); assert.equal( flkty.nextButton.element, nextElem, 'next button element matches nextButton.element' ); assert.ok( prevElem.disabled, 'previous button is disabled at first index' ); prevElem.focus(); prevElem.click(); assert.equal( flkty.selectedIndex, 0, 'selectedIndex still at 0' ); nextElem.focus(); nextElem.click(); assert.equal( flkty.selectedIndex, 1, 'next button clicked, selectedIndex at 1' ); prevElem.focus(); prevElem.click(); assert.equal( flkty.selectedIndex, 0, 'previous button clicked, selectedIndex back at 0' ); flkty.select( 5 ); assert.ok( nextElem.disabled, 'next button disabled when at last cell' ); } ); ================================================ FILE: test/unit/resize.js ================================================ QUnit.test( 'resize', function( assert ) { let elem = document.querySelector('#resize'); let flkty = new Flickity( elem, { initialIndex: 2, } ); elem.style.width = '500px'; flkty.resize(); assert.equal( flkty.selectedIndex, 2, 'selectedIndex = 2' ); assert.equal( flkty.cursorPosition, 250, 'cursorPosition = 250' ); assert.equal( flkty.x + flkty.cursorPosition, -1000, 'x + cursorPosition = -1000' ); } ); ================================================ FILE: test/unit/select-cell.js ================================================ QUnit.test( 'selectCell', function( assert ) { let gallery = document.querySelector('#select-cell'); let cellElems = gallery.querySelectorAll('.cell'); let flkty = new Flickity( gallery, { groupCells: true, // groups of 3 } ); flkty.selectCell( 3 ); assert.equal( flkty.selectedIndex, 1, 'selectCell number' ); flkty.selectCell( cellElems[1] ); assert.equal( flkty.selectedIndex, 0, 'selectCell element' ); flkty.selectCell('.select-cell__6'); assert.equal( flkty.selectedIndex, 2, 'selectCell selector string' ); flkty.selectCell('none'); assert.equal( flkty.selectedIndex, 2, 'selectCell bad string is okay' ); } ); ================================================ FILE: test/unit/watch.js ================================================ QUnit.test( 'watch fallback', function( assert ) { let elem = document.querySelector('#watch'); let flkty = new Flickity( elem, { watchCSS: true, } ); assert.ok( !flkty.isActive, 'fallback not active, watchCSS: true' ); } ); ================================================ FILE: test/unit/wrap-around-fill.js ================================================ QUnit.test( 'wrapAround: "fill"', function( assert ) { let elem = document.querySelector('#wrap-around-fill'); let flkty = new Flickity( elem, { wrapAround: 'fill', } ); assert.ok( !flkty.isWrapping, 'total cell width not big enough, not wrapping' ); let shortCell = elem.querySelector('.cell--wrap-around-short'); shortCell.classList.remove('cell--wrap-around-short'); flkty.resize(); assert.ok( flkty.isWrapping, 'cell width big enough, wrapping' ); } );