├── demo ├── favicon.ico ├── images │ ├── cow1.jpg │ ├── cow2.jpg │ ├── cow3.jpg │ ├── face1.jpg │ ├── face2.jpg │ ├── face3.jpg │ └── face4.jpg ├── styles.css ├── prism.js └── index.html ├── .gitignore ├── .travis.yml ├── .eslintrc ├── src ├── transform.js ├── carousel.css └── carousel.js ├── LICENSE ├── rollup.config.js ├── package.json ├── README.md ├── dist ├── carousel.min.js ├── carousel.es6.js ├── carousel.cjs.js └── carousel.js └── test └── carousel.spec.js /demo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeinc/component-carousel/HEAD/demo/favicon.ico -------------------------------------------------------------------------------- /demo/images/cow1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeinc/component-carousel/HEAD/demo/images/cow1.jpg -------------------------------------------------------------------------------- /demo/images/cow2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeinc/component-carousel/HEAD/demo/images/cow2.jpg -------------------------------------------------------------------------------- /demo/images/cow3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeinc/component-carousel/HEAD/demo/images/cow3.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .DS_Store 3 | /node_modules 4 | npm-debug.log 5 | .idea/ 6 | *sublime* 7 | 8 | -------------------------------------------------------------------------------- /demo/images/face1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeinc/component-carousel/HEAD/demo/images/face1.jpg -------------------------------------------------------------------------------- /demo/images/face2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeinc/component-carousel/HEAD/demo/images/face2.jpg -------------------------------------------------------------------------------- /demo/images/face3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeinc/component-carousel/HEAD/demo/images/face3.jpg -------------------------------------------------------------------------------- /demo/images/face4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugeinc/component-carousel/HEAD/demo/images/face4.jpg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "6.0" 5 | cache: 6 | directories: 7 | - node_modules 8 | script: 9 | - npm run travis -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module" 5 | }, 6 | "root": true, 7 | "globals": { 8 | "window": true, 9 | "document": true, 10 | "module": true 11 | }, 12 | "rules": { 13 | "semi": ["error", "always"], 14 | "space-before-function-paren": ["error", "never"], 15 | "no-multiple-empty-lines": ["error", {"max": 3}], 16 | "arrow-parens": 0 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/transform.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Feature detection: CSS transforms 3 | * @type {Boolean} 4 | */ 5 | 6 | const dummy = document.createElement('div'); 7 | const transform = ['transform', 'webkitTransform', 'MozTransform', 'OTransform', 'msTransform'].find((t) => { 8 | // return (document.body.style[t] !== undefined); // if DOM is not yet ready, let's do: 9 | return (dummy.style[t] !== undefined); 10 | }); 11 | 12 | export default transform; 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013, 2017 wes hatch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/carousel.css: -------------------------------------------------------------------------------- 1 | /* 2 | * flexicarousel 3 | * https://github.com/apathetic/flexicarousel-2 4 | * 5 | * Copyright (c) 2014, 2017 Wes Hatch 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 10 | /* ------------------------------------- 11 | CAROUSEL STYLES 12 | ------------------------------------- */ 13 | 14 | .carousel .animate { 15 | -webkit-transition: all 0.4s ease; 16 | -moz-transition: all 0.4s ease; 17 | -o-transition: all 0.4s ease; 18 | transition: all 0.4s ease !important; 19 | } 20 | 21 | .carousel .wrap { 22 | white-space: nowrap; /* fallback */ 23 | width: 100%; 24 | display: -webkit-box; 25 | display: -webkit-flex; 26 | display: -moz-box; 27 | display: -ms-flexbox; 28 | display: flex; 29 | -webkit-box-wrap: nowrap; 30 | -ms-flexbox-wrap: nowrap; 31 | -moz-box-wrap: nowrap; 32 | flex-wrap: nowrap; 33 | -webkit-user-select: none; 34 | -moz-user-select: none; 35 | -ms-user-select: none; 36 | user-select: none; 37 | padding: 0; 38 | } 39 | 40 | .carousel .wrap > li { 41 | display: inline-block; /* fallback */ 42 | vertical-align: top; /* fallback */ 43 | position: relative; /* fallback */ 44 | width: 100%; /* fallback */ 45 | cursor: move; 46 | -webkit-box-flex: 1 0 100%; 47 | -webkit-flex: 1 0 100%; 48 | -ms-flex: 1 0 100%; 49 | flex: 1 0 100%; 50 | padding: 0; 51 | } 52 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import buble from 'rollup-plugin-buble'; 2 | import uglify from 'rollup-plugin-uglify'; 3 | import { minify } from 'uglify-es'; 4 | import * as fs from 'fs'; 5 | 6 | const license = fs.readFileSync('LICENSE', 'utf8'); 7 | 8 | 9 | export default [{ 10 | input: 'src/carousel.js', 11 | output: [ 12 | { 13 | file: 'dist/carousel.cjs.js', 14 | format: 'cjs', 15 | banner: '/*!\n' + license + '*/' 16 | }, { 17 | file: 'dist/carousel.es6.js', 18 | format: 'es', 19 | banner: '/*!\n' + license + '*/' 20 | }, { 21 | file: 'dist/carousel.js', 22 | format: 'iife', 23 | name: 'Carousel', 24 | banner: '/*!\n' + license + '*/' 25 | }, 26 | ], 27 | plugins: [ 28 | buble() 29 | ] 30 | }, { 31 | input: 'src/carousel.js', 32 | output: [ 33 | { 34 | file: 'dist/carousel.min.js', 35 | format: 'iife', 36 | name: 'Carousel', 37 | banner: '/*!\n' + license + '*/' 38 | } 39 | ], 40 | plugins: [ 41 | buble(), 42 | uglify({ 43 | output: { 44 | comments: function(node, comment) { 45 | var text = comment.value; 46 | var type = comment.type; 47 | if (type == "comment2") { 48 | return /^!/i.test(text); 49 | } 50 | } 51 | } 52 | }, minify) 53 | ] 54 | }]; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apatheticwes/flexicarousel", 3 | "author": "wes hatch", 4 | "license": "MIT", 5 | "version": "0.8.7", 6 | "description": "A micro, responsive, touch-enabled carousel.", 7 | "main": "./dist/carousel.cjs.js", 8 | "browser": "./dist/carousel.js", 9 | "jsnext:main": "./dist/carousel.es6.js", 10 | "module": "./dist/carousel.es6.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:hugeinc/component-carousel.git" 14 | }, 15 | "keywords": [ 16 | "carousel" 17 | ], 18 | "babel": { 19 | "presets": [ 20 | "env" 21 | ] 22 | }, 23 | "scripts": { 24 | "start": "http-server ./ -p 8080 -d", 25 | "clean": "rm -f dist/*.js*", 26 | "build": "npm run clean && npm run lint && rollup -c", 27 | "lint": "eslint source/js/*.js", 28 | "test": "tape -r babel-register test/*.js", 29 | "prepublish": "npm run build && npm test", 30 | "preversion": "npm run build && npm test", 31 | "travis": "npm run lint && npm test" 32 | }, 33 | "devDependencies": { 34 | "babel-cli": "^6.26.0", 35 | "babel-preset-env": "^1.6.1", 36 | "eslint": "^3.1.1", 37 | "http-server": "^0.9.0", 38 | "jsdom": "^11.5.1", 39 | "rollup": "^0.50.0", 40 | "rollup-plugin-buble": "^0.16.0", 41 | "rollup-plugin-uglify": "^2.0.1", 42 | "spy": "^1.0.0", 43 | "tape": "^4.6.0", 44 | "uglify-es": "^3.1.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /demo/styles.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------------- 2 | SAMPLE CAROUSEL STYLES 3 | ------------------------------------- */ 4 | 5 | /* 6 | SEE: https://apathetic.github.io/showcase/assets/css/main.min.css 7 | for other demo styles 8 | */ 9 | 10 | .carousel nav .next.disabled, 11 | .carousel nav .prev.disabled { 12 | opacity: 0.2; 13 | } 14 | 15 | .carousel img { 16 | pointer-events: none; 17 | width: 100%; 18 | height: auto; 19 | border: 2px solid #fff; 20 | display: block; 21 | } 22 | 23 | .carousel h3 { 24 | position: absolute; 25 | z-index: 10; 26 | color: #fff; 27 | padding: 1em; 28 | } 29 | 30 | 31 | /* TEN */ 32 | #ten .wrap { 33 | width: calc(100% + 20px); 34 | margin: -10px; 35 | } 36 | #ten .wrap li { 37 | padding: 10px; 38 | } 39 | 40 | @media(min-width:480px) { 41 | #ten .wrap > li { 42 | -webkit-flex-basis: 100%; 43 | -moz-flex-basis: 100%; 44 | -ms-flex: 1 0 100%; 45 | flex-basis: 100%; 46 | } 47 | } 48 | @media(min-width:800px) { 49 | #ten .wrap > li { 50 | flex-basis: 50%; 51 | } 52 | } 53 | @media(min-width:1120px) { 54 | #ten .wrap li { 55 | flex-basis: 33.3333%; 56 | } 57 | } 58 | 59 | /* BILLION */ 60 | #billion { 61 | max-width: 540px; 62 | overflow: hidden; 63 | } 64 | #billion .wrap { 65 | width: 66.666%; 66 | margin: 0px auto; 67 | } 68 | 69 | /* SEVEN */ 70 | @media screen and (min-width:480px) { 71 | #seven .wrap > li { 72 | flex-basis: calc(1/3 * 100%); /* 33.3333%; */ 73 | } 74 | } 75 | @media screen and (min-width:800px) { 76 | #seven .wrap > li { 77 | flex-basis: 25%; 78 | } 79 | } 80 | @media screen and (min-width:1120px) { 81 | #seven .wrap > li { 82 | flex-basis: 20%; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Carousel 2 | [![NPM Version](https://img.shields.io/npm/v/@apatheticwes/flexicarousel.svg?style=flat-square)](https://www.npmjs.com/package/@apatheticwes/flexicarousel) 3 | [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://raw.githubusercontent.com/apathetic/flexicarousel/master/LICENSE) 4 | 5 | > A carousel that'll use CSS to dynamically adapt its width. Uses transforms for its transitions and is also touch-enabled. 6 | 7 | ## Introduction 8 | 9 | The general idea is that this component should maintain a separation of state and style. That is to say, the Javascript maintains the state of the carousel (which slide, etc), while the CSS should take care of the presentation of this state (ie. transitions between slides, responsive, etc). 10 | 11 | ## Overview 12 | 13 | Features a touch-based interface, simple API, and a very-lightweight footprint. It does the basics well, but that's it. No bloat. 14 | You can swipe to drag a slide yet still use CSS to control how the slide transitions will behave. You can also choose to change slides by 15 | using the exposed API. The carousel works on both desktop and mobile, while only weighing in at 2.5 KB! 16 | 17 | ## Getting Started 18 | There is an ES6 module you may consume however you wish. Alternatively, you can also include the relevant scripts in your web page, and then: 19 | 20 | ```html 21 | 22 | 29 | ``` 30 | 31 | ```javascript 32 | // available options 33 | var options = { 34 | onSlide: someFunction, 35 | activeClass: 'active', 36 | slideWrap: 'ul', 37 | slides: 'li', 38 | infinite: true, 39 | display: 1, 40 | disableDragging: false, 41 | initialIndex: 0 42 | }; 43 | 44 | var container = document.querySelector('.carousel'); 45 | var carousel = new Carousel(container, options); 46 | 47 | ``` 48 | 49 | ## Options 50 | 51 | | name | type | default | description | 52 | | --------------- | -------- | --------- | ----------- | 53 | | onSlide | function | undefined | A function to execute on slide. It is passed _to_ and _from_ indices. | 54 | | slideWrap | string | ul | The selector to use when searching for the slides' container. This is used only to bind touch events to, on mobile. | 55 | | slides | string | li | The selector to use when searching for slides within the slideWrap container. | 56 | | activeClass | string | active | Class to use on the active slide. | 57 | | animateClass | string | animate | Class to use on the wrapper when animating. | 58 | | infinite | boolean | true | Enable an infinitely scrolling carousel or not | 59 | | display | integer | 1 | the maximum # of slides to display at a time. If you want to have prev/next slides visible outside those currently displayed, they'd be included here. | 60 | | disableDragging | boolean | false | if you'd like to disable the touch UI for whatever reason | 61 | | initialIndex | integer | 0 | which slide it's going to start on | 62 | 63 | ## Methods 64 | 65 | | method | description | 66 | | --------- | ----------- | 67 | | next() | Advances carousel to the next slide | 68 | | prev() | Move carousel to the previous slide | 69 | | to(i) | Advance carousel to the ith slide | 70 | | destroy() | Destroy carousel and remove all EventListeners | 71 | 72 | ## Demo 73 | 74 | [Hugeinc Carousel](http://hugeinc.github.io/showcase/components/carousel) 75 | 76 | ## Develop 77 | 78 | After cloning the repo: 79 | ``` 80 | npm i 81 | npm start 82 | ``` 83 | 84 | A server will spin up at ```http://localhost:8080```, where you may play with the various examples. See the "demo" directory. 85 | 86 | ## Support 87 | * IE8+ 88 | * Safari / Chrome 89 | * Firefox 90 | * iOS 91 | * Android 4.0+ 92 | 93 | ## Release History 94 | 95 | ### 0.6 96 | * updated some css 97 | 98 | ### 0.5 99 | * added destroy() 100 | * added some tests 101 | 102 | ### 0.4 103 | * better dragging response 104 | * fixed click bug when dragging 105 | 106 | ### 0.3 107 | * cleaning up cloning logic 108 | * further optimizations 109 | 110 | ### 0.2 111 | * bug fixes, mostly 112 | * updated slide engine 113 | * more robust dragging on mobile 114 | 115 | ### 0.1 116 | * first release 117 | -------------------------------------------------------------------------------- /dist/carousel.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | MIT License 3 | 4 | Copyright (c) 2013, 2017 wes hatch 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | var Carousel=function(){"use strict";var t=function(t,i){var s=this;void 0===i&&(i={}),this.handle=t,this.options={animateClass:"animate",activeClass:"active",slideWrap:"ul",slides:"li",infinite:!0,display:1,disableDragging:!1,initialIndex:0},this.current=0,this.slides=[],this.sliding=!1,this.cloned=0,this.active=!0,this.dragging=!1,this.dragThreshold=50,this.deltaX=0,this.isTouch="ontouchend"in document,["transform","webkitTransform","MozTransform","OTransform","msTransform"].forEach(function(t){void 0!==document.body.style[t]&&(s.transform=t)}),this.options=this._assign(this.options,i),this.init()};return t.prototype.init=function(){var t=this;return this.slideWrap=this.handle.querySelector(this.options.slideWrap),this.slides=this.slideWrap.querySelectorAll(this.options.slides),this.numSlides=this.slides.length,this.current=this.options.initialIndex,!this.slideWrap||!this.slides||this.numSlides=this.numSlides){var e=t<0?this.current+this.numSlides:this.current-this.numSlides;this._slide(-(e*this.width-this.deltaX)),this.slideWrap.offsetHeight}t=this._loop(t),this._slide(-t*this.width,i),s.onSlide&&t!==this.current&&s.onSlide.call(this,t,this.current),this._removeClass(this.slides[this.current],s.activeClass),this._addClass(this.slides[t],s.activeClass),this.current=t}},t.prototype._createBindings=function(){var t=this;this._bindings={touchstart:function(i){t._dragStart(i)},touchmove:function(i){t._drag(i)},touchend:function(i){t._dragEnd(i)},touchcancel:function(i){t._dragEnd(i)},mousedown:function(i){t._dragStart(i)},mousemove:function(i){t._drag(i)},mouseup:function(i){t._dragEnd(i)},mouseleave:function(i){t._dragEnd(i)},click:function(i){t._checkDragThreshold(i)},resize:function(i){t._updateView(i)},orientationchange:function(i){t._updateView(i)}}},t.prototype._checkDragThreshold=function(t){this.dragThresholdMet&&t.preventDefault()},t.prototype._dragStart=function(t){var i;if(this.sliding)return!1;i=void 0!==(t=t.originalEvent||t).touches&&t.touches,this.dragThresholdMet=!1,this.dragging=!0,this.startClientX=i?i[0].pageX:t.clientX,this.startClientY=i?i[0].pageY:t.clientY,this.deltaX=0,this.deltaY=0,"IMG"!==t.target.tagName&&"A"!==t.target.tagName||(t.target.draggable=!1)},t.prototype._drag=function(t){var i;this.dragging&&(i=void 0!==(t=t.originalEvent||t).touches&&t.touches,this.deltaX=(i?i[0].pageX:t.clientX)-this.startClientX,this.deltaY=(i?i[0].pageY:t.clientY)-this.startClientY,this._slide(-(this.current*this.width-this.deltaX)),this.dragThresholdMet=Math.abs(this.deltaX)>this.dragThreshold)},t.prototype._dragEnd=function(t){this.dragging&&(this.dragThresholdMet&&(t.preventDefault(),t.stopPropagation(),t.stopImmediatePropagation()),this.dragging=!1,0!==this.deltaX&&Math.abs(this.deltaX)0?this.prev():this.deltaX<0&&this.next(),this.deltaX=0)},t.prototype._slide=function(t,i){var s=this;t-=this.offset,i&&(this.sliding=!0,this._addClass(this.slideWrap,this.options.animateClass),setTimeout(function(){s.sliding=!1,s.active&&s._removeClass(s.slideWrap,s.options.animateClass)},400)),this.transform?this.slideWrap.style[this.transform]="translate3d("+t+"px, 0, 0)":this.slideWrap.style.left=t+"px"},t.prototype._loop=function(t){return(this.numSlides+t%this.numSlides)%this.numSlides},t.prototype._getDimensions=function(){this.width=this.slides[0].getBoundingClientRect().width,this.offset=this.cloned*this.width},t.prototype._updateView=function(){var t=this;window.innerWidth!==this._viewport&&(this._viewport=window.innerWidth,clearTimeout(this.timer),this.timer=setTimeout(function(){t._getDimensions(),t.go(t.current)},300))},t.prototype._cloneSlides=function(){for(var t,i=this,s=this.options.display,e=Math.max(this.numSlides-s,0),n=Math.min(s,this.numSlides),o=this.numSlides;o>e;o--)(t=i.slides[o-1].cloneNode(!0)).removeAttribute("id"),t.setAttribute("aria-hidden","true"),i._addClass(t,"clone"),i.slideWrap.insertBefore(t,i.slideWrap.firstChild),i.cloned++;for(var r=0;r 10 | 11 | 12 |
13 |
    14 |
  • 15 |
  • 16 |
  • 17 |
18 |
19 | 20 | `; 21 | const badTemplate = ` 22 | 23 | 24 | 25 |
26 |
    27 |
    28 | 29 | `; 30 | 31 | const setup = (opts={}, template = goodTemplate) => { 32 | const dom = new JSDOM(template); 33 | const { window } = dom; 34 | const container = window.document.querySelector('div'); 35 | 36 | global.window = window; 37 | global.document = window.document; 38 | carousel = new Carousel(container, opts); 39 | }; 40 | const teardown = () => { 41 | carousel.destroy(); 42 | // createBindings.reset(); 43 | }; 44 | // const createBindings = spy(Carousel.prototype._createBindings); 45 | 46 | 47 | ///////////////////////////////////////////////////////////////////////////////////// 48 | 49 | 50 | test('Setup and teardown:', function(t) { 51 | 52 | t.test('initializes correctly', function(assert) { 53 | // const createBindings = spy(Carousel.prototype._createBindings); // wrap in spy function 54 | setup(); 55 | 56 | assert.equal(typeof carousel, 'object'); 57 | assert.equal(carousel.handle.nodeName, 'DIV'); 58 | assert.equal(carousel.slideWrap.nodeName, 'UL'); 59 | assert.equal(carousel.slides[0].nodeName, 'LI'); 60 | assert.equal(carousel.numSlides, 3); 61 | // assert.equal(createBindings.called, true); 62 | 63 | teardown(); 64 | assert.end(); 65 | }); 66 | 67 | t.test('requires at least 2 slides (with default display value)', function(assert) { 68 | setup({}, badTemplate); 69 | 70 | // assert.equal(createBindings.called, false); 71 | assert.equal(carousel.active, false); 72 | 73 | teardown(); 74 | assert.end(); 75 | }); 76 | 77 | t.skip('cannot call destroy (or other methods) on carousel which did not init', function(assert) { 78 | setup({}, badTemplate); 79 | // assert: carousel did not init 80 | teardown(); 81 | assert.end(); 82 | 83 | }); 84 | 85 | t.skip('destroys cleanly, removing all references and events', function(assert) { 86 | setup(); 87 | 88 | const destroy = spy(carousel.destroy); 89 | 90 | carousel.destroy(); 91 | 92 | assert.equal(destroy.called, true); 93 | // assert.equal(Object.keys(getEventListeners(carousel.handle)).length, 0); // *sigh* JSDOM cannot do this 94 | // assert.equal(Object.keys(getEventListeners(global.window)).length, 0); // :( 95 | 96 | teardown(); 97 | assert.end(); 98 | }); 99 | 100 | t.test('accepts and applies options correctly', function(assert) { 101 | setup({ 102 | animateClass: 'sample', 103 | activeClass: 'sample', 104 | infinite: false, 105 | display: 2, 106 | disableDragging: true, 107 | initialIndex: 2 108 | }); 109 | 110 | assert.equal(carousel.options.animateClass, 'sample'); 111 | assert.equal(carousel.options.activeClass, 'sample'); 112 | assert.equal(carousel.options.infinite, false); 113 | assert.equal(carousel.options.display, 2); 114 | assert.equal(carousel.options.disableDragging, true); 115 | assert.equal(carousel.options.initialIndex, 2); 116 | assert.equal(carousel.current, 2); 117 | 118 | teardown(); 119 | assert.end(); 120 | }); 121 | 122 | t.test('clones slides if infinite', function(assert) { 123 | setup({ 124 | infinite: true, 125 | }); 126 | 127 | // spyOn(Carousel._cloneSlides).and.callThrough(); 128 | 129 | // assert(carousel._cloneSlides).toHaveBeenCalled(); 130 | 131 | teardown(); 132 | assert.end(); 133 | }); 134 | 135 | t.test('clones slides correctly', function(assert) { 136 | setup({ 137 | infinite: true, 138 | display: 1 139 | }); 140 | 141 | const slides = carousel.slideWrap.children; // this includes cloned nodes, while carousel.slides does not 142 | const begClone = slides[0]; 143 | const endClone = slides[4]; 144 | 145 | assert.equal(slides.length, 5); // 3 slides + 1 begClone + 1 endClone 146 | assert.deepEqual(begClone, slides[3]); // begClone was taken from the end 147 | assert.deepEqual(endClone, slides[1]); // endClone was taken from the beg 148 | // assert: begClone has aria-hidden attr, "clone" class, no id 149 | 150 | teardown(); 151 | assert.end(); 152 | }); 153 | }); 154 | 155 | 156 | test('Navigation:', function(t) { 157 | 158 | t.test('jumps to slide 2 when go(2) is called', function(assert) { 159 | setup(); 160 | 161 | carousel.go(2); 162 | assert.equal(carousel.current, 2); 163 | 164 | teardown(); 165 | assert.end(); 166 | }); 167 | 168 | t.test('goes to the next slide when next() is called', function(assert) { 169 | setup(); 170 | 171 | carousel.next(); 172 | assert.equal(carousel.current, 1); // because 0 is first 173 | 174 | teardown(); 175 | assert.end(); 176 | }); 177 | 178 | t.test('goes to the previous slide when prev() is called', function(assert) { 179 | setup({ initialIndex: 2 }); 180 | 181 | carousel.prev(); 182 | 183 | assert.equal(carousel.current, 1); 184 | 185 | teardown(); 186 | assert.end(); 187 | }); 188 | 189 | t.test('when we are at the last slide in an infinite carousel, goes to the first slide when next() is called', function(assert) { 190 | setup({ initialIndex: 2 }); 191 | 192 | carousel.next(); 193 | 194 | assert.equal(carousel.current, 0); 195 | 196 | teardown(); 197 | assert.end(); 198 | }); 199 | 200 | t.test('when we are at the first slide in an infinite carousel, goes to the last slide when prev() is called', function(assert) { 201 | setup(); 202 | 203 | carousel.prev(); 204 | 205 | assert.equal(carousel.current, 2); 206 | 207 | teardown(); 208 | assert.end(); 209 | }); 210 | }); 211 | 212 | 213 | test('General:', function(t) { 214 | t.skip('will not error out if destroy() called during slide transition', function(assert) { 215 | setup(); 216 | 217 | carousel.next(); 218 | assert.doesNotThrow(carousel.destroy, 'TypeError'); 219 | 220 | teardown(); 221 | assert.end(); 222 | }); 223 | 224 | t.test('updates class on active element', function(assert) { 225 | setup({ 226 | activeClass: 'test', 227 | initialIndex: 1 228 | }); 229 | 230 | const slideClass = carousel.slides[carousel.current].classList[0]; 231 | 232 | assert.equal(slideClass, 'test'); 233 | 234 | teardown(); 235 | assert.end(); 236 | }); 237 | }); -------------------------------------------------------------------------------- /demo/prism.js: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript */ 2 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=_self.Prism={util:{encode:function(e){return e instanceof n?new n(e.type,t.util.encode(e.content),e.alias):"Array"===t.util.type(e)?e.map(t.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(d instanceof a)){u.lastIndex=0;var m=u.exec(d);if(m){c&&(f=m[1].length);var y=m.index-1+f,m=m[0].slice(f),v=m.length,k=y+v,b=d.slice(0,y+1),w=d.slice(k+1),P=[p,1];b&&P.push(b);var A=new a(i,g?t.tokenize(m,g):m,h);P.push(A),w&&P.push(w),Array.prototype.splice.apply(r,P)}}}}}return r},hooks:{all:{},add:function(e,n){var a=t.hooks.all;a[e]=a[e]||[],a[e].push(n)},run:function(e,n){var a=t.hooks.all[e];if(a&&a.length)for(var r,l=0;r=a[l++];)r(n)}}},n=t.Token=function(e,t,n){this.type=e,this.content=t,this.alias=n};if(n.stringify=function(e,a,r){if("string"==typeof e)return e;if("Array"===t.util.type(e))return e.map(function(t){return n.stringify(t,a,e)}).join("");var l={type:e.type,content:n.stringify(e.content,a,r),tag:"span",classes:["token",e.type],attributes:{},language:a,parent:r};if("comment"==l.type&&(l.attributes.spellcheck="true"),e.alias){var i="Array"===t.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(l.classes,i)}t.hooks.run("wrap",l);var o="";for(var s in l.attributes)o+=(o?" ":"")+s+'="'+(l.attributes[s]||"")+'"';return"<"+l.tag+' class="'+l.classes.join(" ")+'" '+o+">"+l.content+""},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var n=JSON.parse(e.data),a=n.language,r=n.code,l=n.immediateClose;_self.postMessage(t.highlight(r,t.languages[a],a)),l&&_self.close()},!1),_self.Prism):_self.Prism;var a=document.getElementsByTagName("script");return a=a[a.length-1],a&&(t.filename=a.src,document.addEventListener&&!a.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 3 | Prism.languages.markup={comment://,prolog:/<\?[\w\W]+?\?>/,doctype://,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=.$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup; 4 | Prism.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},Prism.languages.css.atrule.inside.rest=Prism.util.clone(Prism.languages.css),Prism.languages.markup&&(Prism.languages.insertBefore("markup","tag",{style:{pattern:/()[\w\W]*?(?=<\/style>)/i,lookbehind:!0,inside:Prism.languages.css,alias:"language-css"}}),Prism.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|').*?\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:Prism.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:Prism.languages.css}},alias:"language-css"}},Prism.languages.markup.tag)); 5 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; 6 | Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}}),Prism.languages.insertBefore("javascript","class-name",{"template-string":{pattern:/`(?:\\`|\\?[^`])*`/,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/()[\w\W]*?(?=<\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:"language-javascript"}}),Prism.languages.js=Prism.languages.javascript; 7 | -------------------------------------------------------------------------------- /src/carousel.js: -------------------------------------------------------------------------------- 1 | 2 | export default class Carousel { 3 | 4 | constructor(container, options={}) { 5 | 6 | this.handle = container; 7 | 8 | // default options 9 | this.options = { 10 | animateClass: 'animate', 11 | activeClass: 'active', 12 | slideWrap: 'ul', 13 | slides: 'li', // the slides 14 | infinite: true, // set to true to be able to navigate from last to first slide, and vice versa 15 | display: 1, // the minimum # of slides to display at a time. If you want to have slides 16 | // "hanging" off outside the currently viewable ones, they'd be included here. 17 | disableDragging: false, // set to true if you'd like to only use the API to navigate 18 | initialIndex: 0 // slide index where the carousel should start 19 | }; 20 | 21 | // state vars 22 | this.current = 0; 23 | this.slides = []; 24 | this.sliding = false; 25 | this.cloned = 0; 26 | this.active = true; 27 | 28 | // touch vars 29 | this.dragging = false; 30 | this.dragThreshold = 50; 31 | this.deltaX = 0; 32 | 33 | // feature detection 34 | this.isTouch = 'ontouchend' in document; 35 | ['transform', 'webkitTransform', 'MozTransform', 'OTransform', 'msTransform'].forEach((t) => { 36 | if (document.body.style[t] !== undefined) { this.transform = t; } 37 | }); 38 | 39 | // set up options 40 | this.options = this._assign(this.options, options); 41 | 42 | // engage engines 43 | this.init(); 44 | } 45 | 46 | /** 47 | * Initialize the carousel and set some defaults 48 | * @param {object} options List of key: value options 49 | * @return {void} 50 | */ 51 | init() { 52 | this.slideWrap = this.handle.querySelector(this.options.slideWrap); 53 | this.slides = this.slideWrap.querySelectorAll(this.options.slides); 54 | this.numSlides = this.slides.length; 55 | this.current = this.options.initialIndex; 56 | 57 | if (!this.slideWrap || !this.slides || this.numSlides < this.options.display) { 58 | console.log('Carousel: insufficient # slides'); 59 | return this.active = false; 60 | } 61 | if (this.options.infinite) { this._cloneSlides(); } 62 | 63 | this._createBindings(); 64 | this._getDimensions(); 65 | this.go(this.current, false); 66 | 67 | if (!this.options.disableDragging) { 68 | if (this.isTouch) { 69 | ['touchstart', 'touchmove', 'touchend', 'touchcancel'].map((event) => { 70 | this.handle.addEventListener(event, this._bindings[event]); 71 | }); 72 | } else { 73 | ['mousedown', 'mousemove', 'mouseup', 'mouseleave', 'click'].map((event) => { 74 | this.handle.addEventListener(event, this._bindings[event]); 75 | }); 76 | } 77 | } 78 | 79 | window.addEventListener('resize', this._bindings['resize']); 80 | window.addEventListener('orientationchange', this._bindings['orientationchange']); 81 | 82 | return this; 83 | } 84 | 85 | /** 86 | * Removes all event bindings. 87 | * @returns {Carousel} 88 | */ 89 | destroy() { 90 | if (!this.active) { return; } 91 | 92 | for (let event in this._bindings) { 93 | this.handle.removeEventListener(event, this._bindings[event]); 94 | } 95 | 96 | window.removeEventListener('resize', this._bindings['resize']); 97 | window.removeEventListener('orientationchange', this._bindings['orientationchange']); 98 | 99 | this._bindings = null; 100 | this.options = this.slides = this.slideWrap = this.handle = null; 101 | this.active = false; 102 | 103 | // remove classes ... 104 | // remove clones ... 105 | } 106 | 107 | /** 108 | * Go to the next slide 109 | * @return {void} 110 | */ 111 | next() { 112 | if (this.options.infinite || this.current !== this.numSlides-1) { 113 | this.go(this.current + 1); 114 | } else { 115 | this.go(this.numSlides-1); 116 | } 117 | } 118 | 119 | /** 120 | * Go to the previous slide 121 | * @return {void} 122 | */ 123 | prev() { 124 | if (this.options.infinite || this.current !== 0) { 125 | this.go(this.current - 1); 126 | } else { 127 | this.go(0); // allow the slide to "snap" back if dragging and not infinite 128 | } 129 | } 130 | 131 | /** 132 | * Go to a particular slide. Prime the "to" slide by positioning it, and then calling _slide() if needed 133 | * @param {int} to the slide to go to 134 | * @return {void} 135 | */ 136 | go(to, animate = true) { 137 | const opts = this.options; 138 | 139 | if (this.sliding || !this.active) { return; } 140 | 141 | if (to < 0 || to >= this.numSlides) { // position the carousel if infinite and at end of bounds 142 | let temp = (to < 0) ? this.current + this.numSlides : this.current - this.numSlides; 143 | this._slide( -(temp * this.width - this.deltaX) ); 144 | this.slideWrap.offsetHeight; // force a repaint to actually position "to" slide. *Important* 145 | } 146 | 147 | to = this._loop(to); 148 | this._slide( -(to * this.width), animate ); 149 | 150 | if (opts.onSlide && to !== this.current) { opts.onSlide.call(this, to, this.current); } // note: doesn't check if it's a function 151 | 152 | this._removeClass(this.slides[this.current], opts.activeClass); 153 | this._addClass(this.slides[to], opts.activeClass); 154 | this.current = to; 155 | } 156 | 157 | // ----------------------------------- Event Listeners ----------------------------------- // 158 | 159 | /** 160 | * Create references to all bound Events so that they may be removed upon destroy() 161 | * @return {void} 162 | */ 163 | _createBindings() { 164 | this._bindings = { 165 | // handle 166 | 'touchstart': (e) => { this._dragStart(e); }, 167 | 'touchmove': (e) => { this._drag(e); }, 168 | 'touchend': (e) => { this._dragEnd(e); }, 169 | 'touchcancel': (e) => { this._dragEnd(e); }, 170 | 'mousedown': (e) => { this._dragStart(e); }, 171 | 'mousemove': (e) => { this._drag(e); }, 172 | 'mouseup': (e) => { this._dragEnd(e); }, 173 | 'mouseleave': (e) => { this._dragEnd(e); }, 174 | 'click': (e) => { this._checkDragThreshold(e); }, 175 | 176 | // window 177 | 'resize': (e) => { this._updateView(e); }, 178 | 'orientationchange': (e) => { this._updateView(e); } 179 | }; 180 | } 181 | 182 | // ------------------------------------- Drag Events ------------------------------------- // 183 | 184 | _checkDragThreshold(e) { 185 | if (this.dragThresholdMet) { 186 | e.preventDefault(); 187 | } 188 | } 189 | 190 | /** 191 | * Start dragging (via touch) 192 | * @param {event} e Touch event 193 | * @return {void} 194 | */ 195 | _dragStart(e) { 196 | var touches; 197 | 198 | if (this.sliding) { 199 | return false; 200 | } 201 | 202 | e = e.originalEvent || e; 203 | touches = e.touches !== undefined ? e.touches : false; 204 | 205 | this.dragThresholdMet = false; 206 | this.dragging = true; 207 | this.startClientX = touches ? touches[0].pageX : e.clientX; 208 | this.startClientY = touches ? touches[0].pageY : e.clientY; 209 | this.deltaX = 0; // reset for the case when user does 0,0 touch 210 | this.deltaY = 0; // reset for the case when user does 0,0 touch 211 | 212 | if (e.target.tagName === 'IMG' || e.target.tagName === 'A') { e.target.draggable = false; } 213 | } 214 | 215 | /** 216 | * Update slides positions according to user's touch 217 | * @param {event} e Touch event 218 | * @return {void} 219 | */ 220 | _drag(e) { 221 | var touches; 222 | 223 | if (!this.dragging) { 224 | return; 225 | } 226 | 227 | e = e.originalEvent || e; 228 | touches = e.touches !== undefined ? e.touches : false; 229 | this.deltaX = (touches ? touches[0].pageX : e.clientX) - this.startClientX; 230 | this.deltaY = (touches ? touches[0].pageY : e.clientY) - this.startClientY; 231 | 232 | // drag slide along with cursor 233 | this._slide( -(this.current * this.width - this.deltaX ) ); 234 | 235 | // determine if we should do slide, or cancel and let the event pass through to the page 236 | this.dragThresholdMet = Math.abs(this.deltaX) > this.dragThreshold; 237 | } 238 | 239 | /** 240 | * Drag end, calculate slides' new positions 241 | * @param {event} e Touch event 242 | * @return {void} 243 | */ 244 | _dragEnd(e) { 245 | if (!this.dragging) { 246 | return; 247 | } 248 | 249 | if (this.dragThresholdMet) { 250 | e.preventDefault(); 251 | e.stopPropagation(); 252 | e.stopImmediatePropagation(); 253 | } 254 | 255 | this.dragging = false; 256 | 257 | if ( this.deltaX !== 0 && Math.abs(this.deltaX) < this.dragThreshold ) { 258 | this.go(this.current); 259 | } 260 | else if ( this.deltaX > 0 ) { 261 | // var jump = Math.round(this.deltaX / this.width); // distance-based check to swipe multiple slides 262 | // this.go(this.current - jump); 263 | this.prev(); 264 | } 265 | else if ( this.deltaX < 0 ) { 266 | this.next(); 267 | } 268 | 269 | this.deltaX = 0; 270 | } 271 | 272 | 273 | // ------------------------------------- carousel engine ------------------------------------- // 274 | 275 | 276 | /** 277 | * Applies the slide translation in browser 278 | * @param {number} offset Where to translate the slide to. 279 | * @param {boolean} animate Whether to animation the transition or not. 280 | * @return {void} 281 | */ 282 | _slide(offset, animate) { 283 | var delay = 400; 284 | 285 | offset -= this.offset; 286 | 287 | if (animate) { 288 | this.sliding = true; 289 | this._addClass(this.slideWrap, this.options.animateClass); 290 | 291 | setTimeout(() => { 292 | this.sliding = false; 293 | this.active && this._removeClass(this.slideWrap, this.options.animateClass); 294 | }, delay); 295 | } 296 | 297 | if (this.transform) { 298 | this.slideWrap.style[this.transform] = 'translate3d(' + offset + 'px, 0, 0)'; 299 | } 300 | else { 301 | this.slideWrap.style.left = offset+'px'; 302 | } 303 | } 304 | 305 | 306 | // ------------------------------------- "helper" functions ------------------------------------- // 307 | 308 | 309 | /** 310 | * Helper function. Calculate modulo of a slides position 311 | * @param {int} val Slide's position 312 | * @return {int} the index modulo the # of slides 313 | */ 314 | _loop(val) { 315 | return (this.numSlides + (val % this.numSlides)) % this.numSlides; 316 | } 317 | 318 | /** 319 | * Set the Carousel's width and determine the slide offset. 320 | * @return {void} 321 | */ 322 | _getDimensions() { 323 | this.width = this.slides[0].getBoundingClientRect().width; 324 | this.offset = this.cloned * this.width; 325 | } 326 | 327 | /** 328 | * Update the slides' position on a resize. This is throttled at 300ms 329 | * @return {void} 330 | */ 331 | _updateView() { 332 | // Check if the resize was horizontal. On touch devices, changing scroll 333 | // direction will cause the browser tab bar to appear, which triggers a resize 334 | if (window.innerWidth !== this._viewport) { 335 | this._viewport = window.innerWidth; 336 | clearTimeout(this.timer); 337 | this.timer = setTimeout(() => { 338 | this._getDimensions(); 339 | this.go(this.current); 340 | }, 300); 341 | } 342 | } 343 | 344 | /** 345 | * Duplicate the first and last N slides so that infinite scrolling can work. 346 | * Depends on how many slides are visible at a time, and any outlying slides as well 347 | * @return {void} 348 | */ 349 | _cloneSlides() { 350 | var duplicate; 351 | var display = this.options.display; 352 | var fromEnd = Math.max(this.numSlides - display, 0); 353 | var fromBeg = Math.min(display, this.numSlides); 354 | 355 | // take "display" slides from the end and add to the beginning 356 | for (let i = this.numSlides; i > fromEnd; i--) { 357 | duplicate = this.slides[i-1].cloneNode(true); // cloneNode --> true is deep cloning 358 | duplicate.removeAttribute('id'); 359 | duplicate.setAttribute('aria-hidden', 'true'); 360 | this._addClass(duplicate, 'clone'); 361 | this.slideWrap.insertBefore(duplicate, this.slideWrap.firstChild); // "prependChild" 362 | this.cloned++; 363 | } 364 | 365 | // take "display" slides from the beginning and add to the end 366 | for (let i = 0; i < fromBeg; i++) { 367 | duplicate = this.slides[i].cloneNode(true); 368 | duplicate.removeAttribute('id'); 369 | duplicate.setAttribute('aria-hidden', 'true'); 370 | this._addClass(duplicate, 'clone'); 371 | this.slideWrap.appendChild(duplicate); 372 | } 373 | } 374 | 375 | /** 376 | * Helper function to add a class to an element 377 | * @param {int} i Index of the slide to add a class to 378 | * @param {string} name Class name 379 | * @return {void} 380 | */ 381 | _addClass(el, name) { 382 | if (el.classList) { el.classList.add(name); } 383 | else {el.className += ' ' + name; } 384 | } 385 | 386 | /** 387 | * Helper function to remove a class from an element 388 | * @param {int} i Index of the slide to remove class from 389 | * @param {string} name Class name 390 | * @return {void} 391 | */ 392 | _removeClass(el, name) { 393 | if (el.classList) { el.classList.remove(name); } 394 | else { el.className = el.className.replace(new RegExp('(^|\\b)' + name.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); } 395 | } 396 | 397 | /** 398 | * Shallow Object.assign polyfill 399 | * @param {object} dest The object to copy into 400 | * @param {object} src The object to copy from 401 | */ 402 | _assign(dest, src) { 403 | Object.keys(src).forEach((key) => { 404 | dest[key] = src[key]; 405 | }); 406 | 407 | return dest; 408 | } 409 | }; 410 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Carousel. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 |
    30 |

    Carousel.

    31 |
    32 | 33 | 40 | 41 |
    42 | 43 |
    44 |
    45 |

    About

    46 |

    A flexible carousel that works on both desktop and mobile devices, and is compleletely responsive. Additionally, there is a strict separation 47 | of state and style. That is to say, javascript maintains the state of the carousel, while the CSS is responsible for the presentation and display 48 | of this state

    49 | 50 |

    Implementation

    51 |

    Carousel enables a touch-based interface (ie. sliding slides via dragging) while also respecting the separation of state and behaviour. 52 | You can swipe to drag a slide yet still use CSS to control how the slide transitions will behave. You can also choose to change slides by 53 | using the exposed API. The carousel works on both desktop and mobile, using only CSS to control the look and feel. The result is a basic yet 54 | robust carousel, weighing in at under 2KB

    55 |
    56 |
    57 | 58 |
    59 |
    60 |

    Demo

    61 | 62 |

    Simple demo

    63 | 87 | 88 | 89 |
    90 | 91 | 92 |

    Infinite, overflowing

    93 | 133 | 134 | 135 |
    136 | 137 | 138 |

    Bounded, clipped

    139 | 163 | 164 | 165 |
    166 | 167 | 168 |

    Infinite, scroll by 3

    169 | 208 | 209 |
    210 |
    211 | 212 |
    213 |
    214 |

    Prereqs

    215 |

    none

    216 |
    217 |
    218 | 219 |
    220 |
    221 |

    Compatibility

    222 |
      223 |
    • IE8+
    • 224 |
    • Firefox, Chrome, Opera
    • 225 |
    • iOS, Android
    • 226 |
    227 |
    228 |
    229 | 230 |
    231 |
    232 |

    Code

    233 | 234 |
    
    235 | var container = document.querySelector('#carousel');
    236 | var carousel = new Carousel(container, {
    237 | 	infinite: true,
    238 | 	onSlide: function() { ... }
    239 | });
    240 | 
    241 | container.querySelector('.prev').addEventListener('click', carousel.prev);
    242 | container.querySelector('.next').addEventListener('click', carousel.next);
    243 | 				
    244 | 245 |
    246 |
    247 | 248 |
    249 |
    250 |

    API

    251 | 252 |

    Options

    253 | 254 |
    255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 |
    nametypedefaultdescription
    activeClassstringactiveClass to use on the active slide.
    slideWrapstring.wrapThe selector to use when searching for the slides' container. This is used only to bind touch events to, on mobile.
    slidesstringliThe selector to use when searching for slides within the slideWrap container.
    infinitebooleantrueEnable infinite scrolling or not
    onSlidefunctionundefinedA function to execute on slide. It is passed to and from indices.
    disableDraggingbooleanfalseif you'd like to disable the touch UI for whatever reason
    305 |
    306 | 307 |

    Methods

    308 | 309 |
    310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 |
    namemethoddescription
    Next Slidecarousel.next()Advance carousel to the next slide
    Previous Slidecarousel.prev()Advance carousel to the previous slide
    Go To Slidecarousel.to(i)Advance carousel to the ith slide
    Destroycarousel.destroy()Destroys carousel and removes all Event Listeners
    341 |
    342 | 343 |
    344 |
    345 |
    346 | 347 | 504 | 505 | 506 | 507 | -------------------------------------------------------------------------------- /dist/carousel.es6.js: -------------------------------------------------------------------------------- 1 | /*! 2 | MIT License 3 | 4 | Copyright (c) 2013, 2017 wes hatch 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | var Carousel = function Carousel(container, options) { 25 | var this$1 = this; 26 | if ( options === void 0 ) options={}; 27 | 28 | 29 | this.handle = container; 30 | 31 | // default options 32 | this.options = { 33 | animateClass: 'animate', 34 | activeClass: 'active', 35 | slideWrap: 'ul', 36 | slides: 'li', // the slides 37 | infinite: true, // set to true to be able to navigate from last to first slide, and vice versa 38 | display: 1, // the minimum # of slides to display at a time. If you want to have slides 39 | // "hanging" off outside the currently viewable ones, they'd be included here. 40 | disableDragging: false, // set to true if you'd like to only use the API to navigate 41 | initialIndex: 0 // slide index where the carousel should start 42 | }; 43 | 44 | // state vars 45 | this.current = 0; 46 | this.slides = []; 47 | this.sliding = false; 48 | this.cloned = 0; 49 | this.active = true; 50 | 51 | // touch vars 52 | this.dragging = false; 53 | this.dragThreshold = 50; 54 | this.deltaX = 0; 55 | 56 | // feature detection 57 | this.isTouch = 'ontouchend' in document; 58 | ['transform', 'webkitTransform', 'MozTransform', 'OTransform', 'msTransform'].forEach(function (t) { 59 | if (document.body.style[t] !== undefined) { this$1.transform = t; } 60 | }); 61 | 62 | // set up options 63 | this.options = this._assign(this.options, options); 64 | 65 | // engage engines 66 | this.init(); 67 | }; 68 | 69 | /** 70 | * Initialize the carousel and set some defaults 71 | * @param{object} options List of key: value options 72 | * @return {void} 73 | */ 74 | Carousel.prototype.init = function init () { 75 | var this$1 = this; 76 | 77 | this.slideWrap = this.handle.querySelector(this.options.slideWrap); 78 | this.slides = this.slideWrap.querySelectorAll(this.options.slides); 79 | this.numSlides = this.slides.length; 80 | this.current = this.options.initialIndex; 81 | 82 | if (!this.slideWrap || !this.slides || this.numSlides < this.options.display) { 83 | console.log('Carousel: insufficient # slides'); 84 | return this.active = false; 85 | } 86 | if (this.options.infinite) { this._cloneSlides(); } 87 | 88 | this._createBindings(); 89 | this._getDimensions(); 90 | this.go(this.current, false); 91 | 92 | if (!this.options.disableDragging) { 93 | if (this.isTouch) { 94 | ['touchstart', 'touchmove', 'touchend', 'touchcancel'].map(function (event) { 95 | this$1.handle.addEventListener(event, this$1._bindings[event]); 96 | }); 97 | } else { 98 | ['mousedown', 'mousemove', 'mouseup', 'mouseleave', 'click'].map(function (event) { 99 | this$1.handle.addEventListener(event, this$1._bindings[event]); 100 | }); 101 | } 102 | } 103 | 104 | window.addEventListener('resize', this._bindings['resize']); 105 | window.addEventListener('orientationchange', this._bindings['orientationchange']); 106 | 107 | return this; 108 | }; 109 | 110 | /** 111 | * Removes all event bindings. 112 | * @returns {Carousel} 113 | */ 114 | Carousel.prototype.destroy = function destroy () { 115 | var this$1 = this; 116 | 117 | if (!this.active) { return; } 118 | 119 | for (var event in this$1._bindings) { 120 | this$1.handle.removeEventListener(event, this$1._bindings[event]); 121 | } 122 | 123 | window.removeEventListener('resize', this._bindings['resize']); 124 | window.removeEventListener('orientationchange', this._bindings['orientationchange']); 125 | 126 | this._bindings = null; 127 | this.options = this.slides = this.slideWrap = this.handle = null; 128 | this.active = false; 129 | 130 | // remove classes ... 131 | // remove clones ... 132 | }; 133 | 134 | /** 135 | * Go to the next slide 136 | * @return {void} 137 | */ 138 | Carousel.prototype.next = function next () { 139 | if (this.options.infinite || this.current !== this.numSlides-1) { 140 | this.go(this.current + 1); 141 | } else { 142 | this.go(this.numSlides-1); 143 | } 144 | }; 145 | 146 | /** 147 | * Go to the previous slide 148 | * @return {void} 149 | */ 150 | Carousel.prototype.prev = function prev () { 151 | if (this.options.infinite || this.current !== 0) { 152 | this.go(this.current - 1); 153 | } else { 154 | this.go(0); // allow the slide to "snap" back if dragging and not infinite 155 | } 156 | }; 157 | 158 | /** 159 | * Go to a particular slide. Prime the "to" slide by positioning it, and then calling _slide() if needed 160 | * @param{int} to the slide to go to 161 | * @return {void} 162 | */ 163 | Carousel.prototype.go = function go (to, animate) { 164 | if ( animate === void 0 ) animate = true; 165 | 166 | var opts = this.options; 167 | 168 | if (this.sliding || !this.active) { return; } 169 | 170 | if (to < 0 || to >= this.numSlides) { // position the carousel if infinite and at end of bounds 171 | var temp = (to < 0) ? this.current + this.numSlides : this.current - this.numSlides; 172 | this._slide( -(temp * this.width - this.deltaX) ); 173 | this.slideWrap.offsetHeight; // force a repaint to actually position "to" slide. *Important* 174 | } 175 | 176 | to = this._loop(to); 177 | this._slide( -(to * this.width), animate ); 178 | 179 | if (opts.onSlide && to !== this.current) { opts.onSlide.call(this, to, this.current); }// note: doesn't check if it's a function 180 | 181 | this._removeClass(this.slides[this.current], opts.activeClass); 182 | this._addClass(this.slides[to], opts.activeClass); 183 | this.current = to; 184 | }; 185 | 186 | // ----------------------------------- Event Listeners ----------------------------------- // 187 | 188 | /** 189 | * Create references to all bound Events so that they may be removed upon destroy() 190 | * @return {void} 191 | */ 192 | Carousel.prototype._createBindings = function _createBindings () { 193 | var this$1 = this; 194 | 195 | this._bindings = { 196 | // handle 197 | 'touchstart': function (e) { this$1._dragStart(e); }, 198 | 'touchmove': function (e) { this$1._drag(e); }, 199 | 'touchend': function (e) { this$1._dragEnd(e); }, 200 | 'touchcancel': function (e) { this$1._dragEnd(e); }, 201 | 'mousedown': function (e) { this$1._dragStart(e); }, 202 | 'mousemove': function (e) { this$1._drag(e); }, 203 | 'mouseup': function (e) { this$1._dragEnd(e); }, 204 | 'mouseleave': function (e) { this$1._dragEnd(e); }, 205 | 'click': function (e) { this$1._checkDragThreshold(e); }, 206 | 207 | // window 208 | 'resize': function (e) { this$1._updateView(e); }, 209 | 'orientationchange': function (e) { this$1._updateView(e); } 210 | }; 211 | }; 212 | 213 | // ------------------------------------- Drag Events ------------------------------------- // 214 | 215 | Carousel.prototype._checkDragThreshold = function _checkDragThreshold (e) { 216 | if (this.dragThresholdMet) { 217 | e.preventDefault(); 218 | } 219 | }; 220 | 221 | /** 222 | * Start dragging (via touch) 223 | * @param{event} e Touch event 224 | * @return {void} 225 | */ 226 | Carousel.prototype._dragStart = function _dragStart (e) { 227 | var touches; 228 | 229 | if (this.sliding) { 230 | return false; 231 | } 232 | 233 | e = e.originalEvent || e; 234 | touches = e.touches !== undefined ? e.touches : false; 235 | 236 | this.dragThresholdMet = false; 237 | this.dragging = true; 238 | this.startClientX = touches ? touches[0].pageX : e.clientX; 239 | this.startClientY = touches ? touches[0].pageY : e.clientY; 240 | this.deltaX = 0;// reset for the case when user does 0,0 touch 241 | this.deltaY = 0;// reset for the case when user does 0,0 touch 242 | 243 | if (e.target.tagName === 'IMG' || e.target.tagName === 'A') { e.target.draggable = false; } 244 | }; 245 | 246 | /** 247 | * Update slides positions according to user's touch 248 | * @param{event} e Touch event 249 | * @return {void} 250 | */ 251 | Carousel.prototype._drag = function _drag (e) { 252 | var touches; 253 | 254 | if (!this.dragging) { 255 | return; 256 | } 257 | 258 | e = e.originalEvent || e; 259 | touches = e.touches !== undefined ? e.touches : false; 260 | this.deltaX = (touches ? touches[0].pageX : e.clientX) - this.startClientX; 261 | this.deltaY = (touches ? touches[0].pageY : e.clientY) - this.startClientY; 262 | 263 | // drag slide along with cursor 264 | this._slide( -(this.current * this.width - this.deltaX ) ); 265 | 266 | // determine if we should do slide, or cancel and let the event pass through to the page 267 | this.dragThresholdMet = Math.abs(this.deltaX) > this.dragThreshold; 268 | }; 269 | 270 | /** 271 | * Drag end, calculate slides' new positions 272 | * @param{event} e Touch event 273 | * @return {void} 274 | */ 275 | Carousel.prototype._dragEnd = function _dragEnd (e) { 276 | if (!this.dragging) { 277 | return; 278 | } 279 | 280 | if (this.dragThresholdMet) { 281 | e.preventDefault(); 282 | e.stopPropagation(); 283 | e.stopImmediatePropagation(); 284 | } 285 | 286 | this.dragging = false; 287 | 288 | if ( this.deltaX !== 0 && Math.abs(this.deltaX) < this.dragThreshold ) { 289 | this.go(this.current); 290 | } 291 | else if ( this.deltaX > 0 ) { 292 | // var jump = Math.round(this.deltaX / this.width);// distance-based check to swipe multiple slides 293 | // this.go(this.current - jump); 294 | this.prev(); 295 | } 296 | else if ( this.deltaX < 0 ) { 297 | this.next(); 298 | } 299 | 300 | this.deltaX = 0; 301 | }; 302 | 303 | 304 | // ------------------------------------- carousel engine ------------------------------------- // 305 | 306 | 307 | /** 308 | * Applies the slide translation in browser 309 | * @param{number} offset Where to translate the slide to. 310 | * @param{boolean} animate Whether to animation the transition or not. 311 | * @return {void} 312 | */ 313 | Carousel.prototype._slide = function _slide (offset, animate) { 314 | var this$1 = this; 315 | 316 | var delay = 400; 317 | 318 | offset -= this.offset; 319 | 320 | if (animate) { 321 | this.sliding = true; 322 | this._addClass(this.slideWrap, this.options.animateClass); 323 | 324 | setTimeout(function () { 325 | this$1.sliding = false; 326 | this$1.active && this$1._removeClass(this$1.slideWrap, this$1.options.animateClass); 327 | }, delay); 328 | } 329 | 330 | if (this.transform) { 331 | this.slideWrap.style[this.transform] = 'translate3d(' + offset + 'px, 0, 0)'; 332 | } 333 | else { 334 | this.slideWrap.style.left = offset+'px'; 335 | } 336 | }; 337 | 338 | 339 | // ------------------------------------- "helper" functions ------------------------------------- // 340 | 341 | 342 | /** 343 | * Helper function. Calculate modulo of a slides position 344 | * @param{int} val Slide's position 345 | * @return {int} the index modulo the # of slides 346 | */ 347 | Carousel.prototype._loop = function _loop (val) { 348 | return (this.numSlides + (val % this.numSlides)) % this.numSlides; 349 | }; 350 | 351 | /** 352 | * Set the Carousel's width and determine the slide offset. 353 | * @return {void} 354 | */ 355 | Carousel.prototype._getDimensions = function _getDimensions () { 356 | this.width = this.slides[0].getBoundingClientRect().width; 357 | this.offset = this.cloned * this.width; 358 | }; 359 | 360 | /** 361 | * Update the slides' position on a resize. This is throttled at 300ms 362 | * @return {void} 363 | */ 364 | Carousel.prototype._updateView = function _updateView () { 365 | var this$1 = this; 366 | 367 | // Check if the resize was horizontal. On touch devices, changing scroll 368 | // direction will cause the browser tab bar to appear, which triggers a resize 369 | if (window.innerWidth !== this._viewport) { 370 | this._viewport = window.innerWidth; 371 | clearTimeout(this.timer); 372 | this.timer = setTimeout(function () { 373 | this$1._getDimensions(); 374 | this$1.go(this$1.current); 375 | }, 300); 376 | } 377 | }; 378 | 379 | /** 380 | * Duplicate the first and last N slides so that infinite scrolling can work. 381 | * Depends on how many slides are visible at a time, and any outlying slides as well 382 | * @return {void} 383 | */ 384 | Carousel.prototype._cloneSlides = function _cloneSlides () { 385 | var this$1 = this; 386 | 387 | var duplicate; 388 | var display = this.options.display; 389 | var fromEnd = Math.max(this.numSlides - display, 0); 390 | var fromBeg = Math.min(display, this.numSlides); 391 | 392 | // take "display" slides from the end and add to the beginning 393 | for (var i = this.numSlides; i > fromEnd; i--) { 394 | duplicate = this$1.slides[i-1].cloneNode(true); // cloneNode --> true is deep cloning 395 | duplicate.removeAttribute('id'); 396 | duplicate.setAttribute('aria-hidden', 'true'); 397 | this$1._addClass(duplicate, 'clone'); 398 | this$1.slideWrap.insertBefore(duplicate, this$1.slideWrap.firstChild);// "prependChild" 399 | this$1.cloned++; 400 | } 401 | 402 | // take "display" slides from the beginning and add to the end 403 | for (var i$1 = 0; i$1 < fromBeg; i$1++) { 404 | duplicate = this$1.slides[i$1].cloneNode(true); 405 | duplicate.removeAttribute('id'); 406 | duplicate.setAttribute('aria-hidden', 'true'); 407 | this$1._addClass(duplicate, 'clone'); 408 | this$1.slideWrap.appendChild(duplicate); 409 | } 410 | }; 411 | 412 | /** 413 | * Helper function to add a class to an element 414 | * @param{int} i Index of the slide to add a class to 415 | * @param{string} name Class name 416 | * @return {void} 417 | */ 418 | Carousel.prototype._addClass = function _addClass (el, name) { 419 | if (el.classList) { el.classList.add(name); } 420 | else {el.className += ' ' + name; } 421 | }; 422 | 423 | /** 424 | * Helper function to remove a class from an element 425 | * @param{int} i Index of the slide to remove class from 426 | * @param{string} name Class name 427 | * @return {void} 428 | */ 429 | Carousel.prototype._removeClass = function _removeClass (el, name) { 430 | if (el.classList) { el.classList.remove(name); } 431 | else { el.className = el.className.replace(new RegExp('(^|\\b)' + name.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); } 432 | }; 433 | 434 | /** 435 | * Shallow Object.assign polyfill 436 | * @param {object} dest The object to copy into 437 | * @param {object} srcThe object to copy from 438 | */ 439 | Carousel.prototype._assign = function _assign (dest, src) { 440 | Object.keys(src).forEach(function (key) { 441 | dest[key] = src[key]; 442 | }); 443 | 444 | return dest; 445 | }; 446 | 447 | export default Carousel; 448 | -------------------------------------------------------------------------------- /dist/carousel.cjs.js: -------------------------------------------------------------------------------- 1 | /*! 2 | MIT License 3 | 4 | Copyright (c) 2013, 2017 wes hatch 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 'use strict'; 25 | 26 | var Carousel = function Carousel(container, options) { 27 | var this$1 = this; 28 | if ( options === void 0 ) options={}; 29 | 30 | 31 | this.handle = container; 32 | 33 | // default options 34 | this.options = { 35 | animateClass: 'animate', 36 | activeClass: 'active', 37 | slideWrap: 'ul', 38 | slides: 'li', // the slides 39 | infinite: true, // set to true to be able to navigate from last to first slide, and vice versa 40 | display: 1, // the minimum # of slides to display at a time. If you want to have slides 41 | // "hanging" off outside the currently viewable ones, they'd be included here. 42 | disableDragging: false, // set to true if you'd like to only use the API to navigate 43 | initialIndex: 0 // slide index where the carousel should start 44 | }; 45 | 46 | // state vars 47 | this.current = 0; 48 | this.slides = []; 49 | this.sliding = false; 50 | this.cloned = 0; 51 | this.active = true; 52 | 53 | // touch vars 54 | this.dragging = false; 55 | this.dragThreshold = 50; 56 | this.deltaX = 0; 57 | 58 | // feature detection 59 | this.isTouch = 'ontouchend' in document; 60 | ['transform', 'webkitTransform', 'MozTransform', 'OTransform', 'msTransform'].forEach(function (t) { 61 | if (document.body.style[t] !== undefined) { this$1.transform = t; } 62 | }); 63 | 64 | // set up options 65 | this.options = this._assign(this.options, options); 66 | 67 | // engage engines 68 | this.init(); 69 | }; 70 | 71 | /** 72 | * Initialize the carousel and set some defaults 73 | * @param{object} options List of key: value options 74 | * @return {void} 75 | */ 76 | Carousel.prototype.init = function init () { 77 | var this$1 = this; 78 | 79 | this.slideWrap = this.handle.querySelector(this.options.slideWrap); 80 | this.slides = this.slideWrap.querySelectorAll(this.options.slides); 81 | this.numSlides = this.slides.length; 82 | this.current = this.options.initialIndex; 83 | 84 | if (!this.slideWrap || !this.slides || this.numSlides < this.options.display) { 85 | console.log('Carousel: insufficient # slides'); 86 | return this.active = false; 87 | } 88 | if (this.options.infinite) { this._cloneSlides(); } 89 | 90 | this._createBindings(); 91 | this._getDimensions(); 92 | this.go(this.current, false); 93 | 94 | if (!this.options.disableDragging) { 95 | if (this.isTouch) { 96 | ['touchstart', 'touchmove', 'touchend', 'touchcancel'].map(function (event) { 97 | this$1.handle.addEventListener(event, this$1._bindings[event]); 98 | }); 99 | } else { 100 | ['mousedown', 'mousemove', 'mouseup', 'mouseleave', 'click'].map(function (event) { 101 | this$1.handle.addEventListener(event, this$1._bindings[event]); 102 | }); 103 | } 104 | } 105 | 106 | window.addEventListener('resize', this._bindings['resize']); 107 | window.addEventListener('orientationchange', this._bindings['orientationchange']); 108 | 109 | return this; 110 | }; 111 | 112 | /** 113 | * Removes all event bindings. 114 | * @returns {Carousel} 115 | */ 116 | Carousel.prototype.destroy = function destroy () { 117 | var this$1 = this; 118 | 119 | if (!this.active) { return; } 120 | 121 | for (var event in this$1._bindings) { 122 | this$1.handle.removeEventListener(event, this$1._bindings[event]); 123 | } 124 | 125 | window.removeEventListener('resize', this._bindings['resize']); 126 | window.removeEventListener('orientationchange', this._bindings['orientationchange']); 127 | 128 | this._bindings = null; 129 | this.options = this.slides = this.slideWrap = this.handle = null; 130 | this.active = false; 131 | 132 | // remove classes ... 133 | // remove clones ... 134 | }; 135 | 136 | /** 137 | * Go to the next slide 138 | * @return {void} 139 | */ 140 | Carousel.prototype.next = function next () { 141 | if (this.options.infinite || this.current !== this.numSlides-1) { 142 | this.go(this.current + 1); 143 | } else { 144 | this.go(this.numSlides-1); 145 | } 146 | }; 147 | 148 | /** 149 | * Go to the previous slide 150 | * @return {void} 151 | */ 152 | Carousel.prototype.prev = function prev () { 153 | if (this.options.infinite || this.current !== 0) { 154 | this.go(this.current - 1); 155 | } else { 156 | this.go(0); // allow the slide to "snap" back if dragging and not infinite 157 | } 158 | }; 159 | 160 | /** 161 | * Go to a particular slide. Prime the "to" slide by positioning it, and then calling _slide() if needed 162 | * @param{int} to the slide to go to 163 | * @return {void} 164 | */ 165 | Carousel.prototype.go = function go (to, animate) { 166 | if ( animate === void 0 ) animate = true; 167 | 168 | var opts = this.options; 169 | 170 | if (this.sliding || !this.active) { return; } 171 | 172 | if (to < 0 || to >= this.numSlides) { // position the carousel if infinite and at end of bounds 173 | var temp = (to < 0) ? this.current + this.numSlides : this.current - this.numSlides; 174 | this._slide( -(temp * this.width - this.deltaX) ); 175 | this.slideWrap.offsetHeight; // force a repaint to actually position "to" slide. *Important* 176 | } 177 | 178 | to = this._loop(to); 179 | this._slide( -(to * this.width), animate ); 180 | 181 | if (opts.onSlide && to !== this.current) { opts.onSlide.call(this, to, this.current); }// note: doesn't check if it's a function 182 | 183 | this._removeClass(this.slides[this.current], opts.activeClass); 184 | this._addClass(this.slides[to], opts.activeClass); 185 | this.current = to; 186 | }; 187 | 188 | // ----------------------------------- Event Listeners ----------------------------------- // 189 | 190 | /** 191 | * Create references to all bound Events so that they may be removed upon destroy() 192 | * @return {void} 193 | */ 194 | Carousel.prototype._createBindings = function _createBindings () { 195 | var this$1 = this; 196 | 197 | this._bindings = { 198 | // handle 199 | 'touchstart': function (e) { this$1._dragStart(e); }, 200 | 'touchmove': function (e) { this$1._drag(e); }, 201 | 'touchend': function (e) { this$1._dragEnd(e); }, 202 | 'touchcancel': function (e) { this$1._dragEnd(e); }, 203 | 'mousedown': function (e) { this$1._dragStart(e); }, 204 | 'mousemove': function (e) { this$1._drag(e); }, 205 | 'mouseup': function (e) { this$1._dragEnd(e); }, 206 | 'mouseleave': function (e) { this$1._dragEnd(e); }, 207 | 'click': function (e) { this$1._checkDragThreshold(e); }, 208 | 209 | // window 210 | 'resize': function (e) { this$1._updateView(e); }, 211 | 'orientationchange': function (e) { this$1._updateView(e); } 212 | }; 213 | }; 214 | 215 | // ------------------------------------- Drag Events ------------------------------------- // 216 | 217 | Carousel.prototype._checkDragThreshold = function _checkDragThreshold (e) { 218 | if (this.dragThresholdMet) { 219 | e.preventDefault(); 220 | } 221 | }; 222 | 223 | /** 224 | * Start dragging (via touch) 225 | * @param{event} e Touch event 226 | * @return {void} 227 | */ 228 | Carousel.prototype._dragStart = function _dragStart (e) { 229 | var touches; 230 | 231 | if (this.sliding) { 232 | return false; 233 | } 234 | 235 | e = e.originalEvent || e; 236 | touches = e.touches !== undefined ? e.touches : false; 237 | 238 | this.dragThresholdMet = false; 239 | this.dragging = true; 240 | this.startClientX = touches ? touches[0].pageX : e.clientX; 241 | this.startClientY = touches ? touches[0].pageY : e.clientY; 242 | this.deltaX = 0;// reset for the case when user does 0,0 touch 243 | this.deltaY = 0;// reset for the case when user does 0,0 touch 244 | 245 | if (e.target.tagName === 'IMG' || e.target.tagName === 'A') { e.target.draggable = false; } 246 | }; 247 | 248 | /** 249 | * Update slides positions according to user's touch 250 | * @param{event} e Touch event 251 | * @return {void} 252 | */ 253 | Carousel.prototype._drag = function _drag (e) { 254 | var touches; 255 | 256 | if (!this.dragging) { 257 | return; 258 | } 259 | 260 | e = e.originalEvent || e; 261 | touches = e.touches !== undefined ? e.touches : false; 262 | this.deltaX = (touches ? touches[0].pageX : e.clientX) - this.startClientX; 263 | this.deltaY = (touches ? touches[0].pageY : e.clientY) - this.startClientY; 264 | 265 | // drag slide along with cursor 266 | this._slide( -(this.current * this.width - this.deltaX ) ); 267 | 268 | // determine if we should do slide, or cancel and let the event pass through to the page 269 | this.dragThresholdMet = Math.abs(this.deltaX) > this.dragThreshold; 270 | }; 271 | 272 | /** 273 | * Drag end, calculate slides' new positions 274 | * @param{event} e Touch event 275 | * @return {void} 276 | */ 277 | Carousel.prototype._dragEnd = function _dragEnd (e) { 278 | if (!this.dragging) { 279 | return; 280 | } 281 | 282 | if (this.dragThresholdMet) { 283 | e.preventDefault(); 284 | e.stopPropagation(); 285 | e.stopImmediatePropagation(); 286 | } 287 | 288 | this.dragging = false; 289 | 290 | if ( this.deltaX !== 0 && Math.abs(this.deltaX) < this.dragThreshold ) { 291 | this.go(this.current); 292 | } 293 | else if ( this.deltaX > 0 ) { 294 | // var jump = Math.round(this.deltaX / this.width);// distance-based check to swipe multiple slides 295 | // this.go(this.current - jump); 296 | this.prev(); 297 | } 298 | else if ( this.deltaX < 0 ) { 299 | this.next(); 300 | } 301 | 302 | this.deltaX = 0; 303 | }; 304 | 305 | 306 | // ------------------------------------- carousel engine ------------------------------------- // 307 | 308 | 309 | /** 310 | * Applies the slide translation in browser 311 | * @param{number} offset Where to translate the slide to. 312 | * @param{boolean} animate Whether to animation the transition or not. 313 | * @return {void} 314 | */ 315 | Carousel.prototype._slide = function _slide (offset, animate) { 316 | var this$1 = this; 317 | 318 | var delay = 400; 319 | 320 | offset -= this.offset; 321 | 322 | if (animate) { 323 | this.sliding = true; 324 | this._addClass(this.slideWrap, this.options.animateClass); 325 | 326 | setTimeout(function () { 327 | this$1.sliding = false; 328 | this$1.active && this$1._removeClass(this$1.slideWrap, this$1.options.animateClass); 329 | }, delay); 330 | } 331 | 332 | if (this.transform) { 333 | this.slideWrap.style[this.transform] = 'translate3d(' + offset + 'px, 0, 0)'; 334 | } 335 | else { 336 | this.slideWrap.style.left = offset+'px'; 337 | } 338 | }; 339 | 340 | 341 | // ------------------------------------- "helper" functions ------------------------------------- // 342 | 343 | 344 | /** 345 | * Helper function. Calculate modulo of a slides position 346 | * @param{int} val Slide's position 347 | * @return {int} the index modulo the # of slides 348 | */ 349 | Carousel.prototype._loop = function _loop (val) { 350 | return (this.numSlides + (val % this.numSlides)) % this.numSlides; 351 | }; 352 | 353 | /** 354 | * Set the Carousel's width and determine the slide offset. 355 | * @return {void} 356 | */ 357 | Carousel.prototype._getDimensions = function _getDimensions () { 358 | this.width = this.slides[0].getBoundingClientRect().width; 359 | this.offset = this.cloned * this.width; 360 | }; 361 | 362 | /** 363 | * Update the slides' position on a resize. This is throttled at 300ms 364 | * @return {void} 365 | */ 366 | Carousel.prototype._updateView = function _updateView () { 367 | var this$1 = this; 368 | 369 | // Check if the resize was horizontal. On touch devices, changing scroll 370 | // direction will cause the browser tab bar to appear, which triggers a resize 371 | if (window.innerWidth !== this._viewport) { 372 | this._viewport = window.innerWidth; 373 | clearTimeout(this.timer); 374 | this.timer = setTimeout(function () { 375 | this$1._getDimensions(); 376 | this$1.go(this$1.current); 377 | }, 300); 378 | } 379 | }; 380 | 381 | /** 382 | * Duplicate the first and last N slides so that infinite scrolling can work. 383 | * Depends on how many slides are visible at a time, and any outlying slides as well 384 | * @return {void} 385 | */ 386 | Carousel.prototype._cloneSlides = function _cloneSlides () { 387 | var this$1 = this; 388 | 389 | var duplicate; 390 | var display = this.options.display; 391 | var fromEnd = Math.max(this.numSlides - display, 0); 392 | var fromBeg = Math.min(display, this.numSlides); 393 | 394 | // take "display" slides from the end and add to the beginning 395 | for (var i = this.numSlides; i > fromEnd; i--) { 396 | duplicate = this$1.slides[i-1].cloneNode(true); // cloneNode --> true is deep cloning 397 | duplicate.removeAttribute('id'); 398 | duplicate.setAttribute('aria-hidden', 'true'); 399 | this$1._addClass(duplicate, 'clone'); 400 | this$1.slideWrap.insertBefore(duplicate, this$1.slideWrap.firstChild);// "prependChild" 401 | this$1.cloned++; 402 | } 403 | 404 | // take "display" slides from the beginning and add to the end 405 | for (var i$1 = 0; i$1 < fromBeg; i$1++) { 406 | duplicate = this$1.slides[i$1].cloneNode(true); 407 | duplicate.removeAttribute('id'); 408 | duplicate.setAttribute('aria-hidden', 'true'); 409 | this$1._addClass(duplicate, 'clone'); 410 | this$1.slideWrap.appendChild(duplicate); 411 | } 412 | }; 413 | 414 | /** 415 | * Helper function to add a class to an element 416 | * @param{int} i Index of the slide to add a class to 417 | * @param{string} name Class name 418 | * @return {void} 419 | */ 420 | Carousel.prototype._addClass = function _addClass (el, name) { 421 | if (el.classList) { el.classList.add(name); } 422 | else {el.className += ' ' + name; } 423 | }; 424 | 425 | /** 426 | * Helper function to remove a class from an element 427 | * @param{int} i Index of the slide to remove class from 428 | * @param{string} name Class name 429 | * @return {void} 430 | */ 431 | Carousel.prototype._removeClass = function _removeClass (el, name) { 432 | if (el.classList) { el.classList.remove(name); } 433 | else { el.className = el.className.replace(new RegExp('(^|\\b)' + name.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); } 434 | }; 435 | 436 | /** 437 | * Shallow Object.assign polyfill 438 | * @param {object} dest The object to copy into 439 | * @param {object} srcThe object to copy from 440 | */ 441 | Carousel.prototype._assign = function _assign (dest, src) { 442 | Object.keys(src).forEach(function (key) { 443 | dest[key] = src[key]; 444 | }); 445 | 446 | return dest; 447 | }; 448 | 449 | module.exports = Carousel; 450 | -------------------------------------------------------------------------------- /dist/carousel.js: -------------------------------------------------------------------------------- 1 | /*! 2 | MIT License 3 | 4 | Copyright (c) 2013, 2017 wes hatch 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | var Carousel = (function () { 25 | 'use strict'; 26 | 27 | var Carousel = function Carousel(container, options) { 28 | var this$1 = this; 29 | if ( options === void 0 ) options={}; 30 | 31 | 32 | this.handle = container; 33 | 34 | // default options 35 | this.options = { 36 | animateClass: 'animate', 37 | activeClass: 'active', 38 | slideWrap: 'ul', 39 | slides: 'li', // the slides 40 | infinite: true, // set to true to be able to navigate from last to first slide, and vice versa 41 | display: 1, // the minimum # of slides to display at a time. If you want to have slides 42 | // "hanging" off outside the currently viewable ones, they'd be included here. 43 | disableDragging: false, // set to true if you'd like to only use the API to navigate 44 | initialIndex: 0 // slide index where the carousel should start 45 | }; 46 | 47 | // state vars 48 | this.current = 0; 49 | this.slides = []; 50 | this.sliding = false; 51 | this.cloned = 0; 52 | this.active = true; 53 | 54 | // touch vars 55 | this.dragging = false; 56 | this.dragThreshold = 50; 57 | this.deltaX = 0; 58 | 59 | // feature detection 60 | this.isTouch = 'ontouchend' in document; 61 | ['transform', 'webkitTransform', 'MozTransform', 'OTransform', 'msTransform'].forEach(function (t) { 62 | if (document.body.style[t] !== undefined) { this$1.transform = t; } 63 | }); 64 | 65 | // set up options 66 | this.options = this._assign(this.options, options); 67 | 68 | // engage engines 69 | this.init(); 70 | }; 71 | 72 | /** 73 | * Initialize the carousel and set some defaults 74 | * @param{object} options List of key: value options 75 | * @return {void} 76 | */ 77 | Carousel.prototype.init = function init () { 78 | var this$1 = this; 79 | 80 | this.slideWrap = this.handle.querySelector(this.options.slideWrap); 81 | this.slides = this.slideWrap.querySelectorAll(this.options.slides); 82 | this.numSlides = this.slides.length; 83 | this.current = this.options.initialIndex; 84 | 85 | if (!this.slideWrap || !this.slides || this.numSlides < this.options.display) { 86 | console.log('Carousel: insufficient # slides'); 87 | return this.active = false; 88 | } 89 | if (this.options.infinite) { this._cloneSlides(); } 90 | 91 | this._createBindings(); 92 | this._getDimensions(); 93 | this.go(this.current, false); 94 | 95 | if (!this.options.disableDragging) { 96 | if (this.isTouch) { 97 | ['touchstart', 'touchmove', 'touchend', 'touchcancel'].map(function (event) { 98 | this$1.handle.addEventListener(event, this$1._bindings[event]); 99 | }); 100 | } else { 101 | ['mousedown', 'mousemove', 'mouseup', 'mouseleave', 'click'].map(function (event) { 102 | this$1.handle.addEventListener(event, this$1._bindings[event]); 103 | }); 104 | } 105 | } 106 | 107 | window.addEventListener('resize', this._bindings['resize']); 108 | window.addEventListener('orientationchange', this._bindings['orientationchange']); 109 | 110 | return this; 111 | }; 112 | 113 | /** 114 | * Removes all event bindings. 115 | * @returns {Carousel} 116 | */ 117 | Carousel.prototype.destroy = function destroy () { 118 | var this$1 = this; 119 | 120 | if (!this.active) { return; } 121 | 122 | for (var event in this$1._bindings) { 123 | this$1.handle.removeEventListener(event, this$1._bindings[event]); 124 | } 125 | 126 | window.removeEventListener('resize', this._bindings['resize']); 127 | window.removeEventListener('orientationchange', this._bindings['orientationchange']); 128 | 129 | this._bindings = null; 130 | this.options = this.slides = this.slideWrap = this.handle = null; 131 | this.active = false; 132 | 133 | // remove classes ... 134 | // remove clones ... 135 | }; 136 | 137 | /** 138 | * Go to the next slide 139 | * @return {void} 140 | */ 141 | Carousel.prototype.next = function next () { 142 | if (this.options.infinite || this.current !== this.numSlides-1) { 143 | this.go(this.current + 1); 144 | } else { 145 | this.go(this.numSlides-1); 146 | } 147 | }; 148 | 149 | /** 150 | * Go to the previous slide 151 | * @return {void} 152 | */ 153 | Carousel.prototype.prev = function prev () { 154 | if (this.options.infinite || this.current !== 0) { 155 | this.go(this.current - 1); 156 | } else { 157 | this.go(0); // allow the slide to "snap" back if dragging and not infinite 158 | } 159 | }; 160 | 161 | /** 162 | * Go to a particular slide. Prime the "to" slide by positioning it, and then calling _slide() if needed 163 | * @param{int} to the slide to go to 164 | * @return {void} 165 | */ 166 | Carousel.prototype.go = function go (to, animate) { 167 | if ( animate === void 0 ) animate = true; 168 | 169 | var opts = this.options; 170 | 171 | if (this.sliding || !this.active) { return; } 172 | 173 | if (to < 0 || to >= this.numSlides) { // position the carousel if infinite and at end of bounds 174 | var temp = (to < 0) ? this.current + this.numSlides : this.current - this.numSlides; 175 | this._slide( -(temp * this.width - this.deltaX) ); 176 | this.slideWrap.offsetHeight; // force a repaint to actually position "to" slide. *Important* 177 | } 178 | 179 | to = this._loop(to); 180 | this._slide( -(to * this.width), animate ); 181 | 182 | if (opts.onSlide && to !== this.current) { opts.onSlide.call(this, to, this.current); }// note: doesn't check if it's a function 183 | 184 | this._removeClass(this.slides[this.current], opts.activeClass); 185 | this._addClass(this.slides[to], opts.activeClass); 186 | this.current = to; 187 | }; 188 | 189 | // ----------------------------------- Event Listeners ----------------------------------- // 190 | 191 | /** 192 | * Create references to all bound Events so that they may be removed upon destroy() 193 | * @return {void} 194 | */ 195 | Carousel.prototype._createBindings = function _createBindings () { 196 | var this$1 = this; 197 | 198 | this._bindings = { 199 | // handle 200 | 'touchstart': function (e) { this$1._dragStart(e); }, 201 | 'touchmove': function (e) { this$1._drag(e); }, 202 | 'touchend': function (e) { this$1._dragEnd(e); }, 203 | 'touchcancel': function (e) { this$1._dragEnd(e); }, 204 | 'mousedown': function (e) { this$1._dragStart(e); }, 205 | 'mousemove': function (e) { this$1._drag(e); }, 206 | 'mouseup': function (e) { this$1._dragEnd(e); }, 207 | 'mouseleave': function (e) { this$1._dragEnd(e); }, 208 | 'click': function (e) { this$1._checkDragThreshold(e); }, 209 | 210 | // window 211 | 'resize': function (e) { this$1._updateView(e); }, 212 | 'orientationchange': function (e) { this$1._updateView(e); } 213 | }; 214 | }; 215 | 216 | // ------------------------------------- Drag Events ------------------------------------- // 217 | 218 | Carousel.prototype._checkDragThreshold = function _checkDragThreshold (e) { 219 | if (this.dragThresholdMet) { 220 | e.preventDefault(); 221 | } 222 | }; 223 | 224 | /** 225 | * Start dragging (via touch) 226 | * @param{event} e Touch event 227 | * @return {void} 228 | */ 229 | Carousel.prototype._dragStart = function _dragStart (e) { 230 | var touches; 231 | 232 | if (this.sliding) { 233 | return false; 234 | } 235 | 236 | e = e.originalEvent || e; 237 | touches = e.touches !== undefined ? e.touches : false; 238 | 239 | this.dragThresholdMet = false; 240 | this.dragging = true; 241 | this.startClientX = touches ? touches[0].pageX : e.clientX; 242 | this.startClientY = touches ? touches[0].pageY : e.clientY; 243 | this.deltaX = 0;// reset for the case when user does 0,0 touch 244 | this.deltaY = 0;// reset for the case when user does 0,0 touch 245 | 246 | if (e.target.tagName === 'IMG' || e.target.tagName === 'A') { e.target.draggable = false; } 247 | }; 248 | 249 | /** 250 | * Update slides positions according to user's touch 251 | * @param{event} e Touch event 252 | * @return {void} 253 | */ 254 | Carousel.prototype._drag = function _drag (e) { 255 | var touches; 256 | 257 | if (!this.dragging) { 258 | return; 259 | } 260 | 261 | e = e.originalEvent || e; 262 | touches = e.touches !== undefined ? e.touches : false; 263 | this.deltaX = (touches ? touches[0].pageX : e.clientX) - this.startClientX; 264 | this.deltaY = (touches ? touches[0].pageY : e.clientY) - this.startClientY; 265 | 266 | // drag slide along with cursor 267 | this._slide( -(this.current * this.width - this.deltaX ) ); 268 | 269 | // determine if we should do slide, or cancel and let the event pass through to the page 270 | this.dragThresholdMet = Math.abs(this.deltaX) > this.dragThreshold; 271 | }; 272 | 273 | /** 274 | * Drag end, calculate slides' new positions 275 | * @param{event} e Touch event 276 | * @return {void} 277 | */ 278 | Carousel.prototype._dragEnd = function _dragEnd (e) { 279 | if (!this.dragging) { 280 | return; 281 | } 282 | 283 | if (this.dragThresholdMet) { 284 | e.preventDefault(); 285 | e.stopPropagation(); 286 | e.stopImmediatePropagation(); 287 | } 288 | 289 | this.dragging = false; 290 | 291 | if ( this.deltaX !== 0 && Math.abs(this.deltaX) < this.dragThreshold ) { 292 | this.go(this.current); 293 | } 294 | else if ( this.deltaX > 0 ) { 295 | // var jump = Math.round(this.deltaX / this.width);// distance-based check to swipe multiple slides 296 | // this.go(this.current - jump); 297 | this.prev(); 298 | } 299 | else if ( this.deltaX < 0 ) { 300 | this.next(); 301 | } 302 | 303 | this.deltaX = 0; 304 | }; 305 | 306 | 307 | // ------------------------------------- carousel engine ------------------------------------- // 308 | 309 | 310 | /** 311 | * Applies the slide translation in browser 312 | * @param{number} offset Where to translate the slide to. 313 | * @param{boolean} animate Whether to animation the transition or not. 314 | * @return {void} 315 | */ 316 | Carousel.prototype._slide = function _slide (offset, animate) { 317 | var this$1 = this; 318 | 319 | var delay = 400; 320 | 321 | offset -= this.offset; 322 | 323 | if (animate) { 324 | this.sliding = true; 325 | this._addClass(this.slideWrap, this.options.animateClass); 326 | 327 | setTimeout(function () { 328 | this$1.sliding = false; 329 | this$1.active && this$1._removeClass(this$1.slideWrap, this$1.options.animateClass); 330 | }, delay); 331 | } 332 | 333 | if (this.transform) { 334 | this.slideWrap.style[this.transform] = 'translate3d(' + offset + 'px, 0, 0)'; 335 | } 336 | else { 337 | this.slideWrap.style.left = offset+'px'; 338 | } 339 | }; 340 | 341 | 342 | // ------------------------------------- "helper" functions ------------------------------------- // 343 | 344 | 345 | /** 346 | * Helper function. Calculate modulo of a slides position 347 | * @param{int} val Slide's position 348 | * @return {int} the index modulo the # of slides 349 | */ 350 | Carousel.prototype._loop = function _loop (val) { 351 | return (this.numSlides + (val % this.numSlides)) % this.numSlides; 352 | }; 353 | 354 | /** 355 | * Set the Carousel's width and determine the slide offset. 356 | * @return {void} 357 | */ 358 | Carousel.prototype._getDimensions = function _getDimensions () { 359 | this.width = this.slides[0].getBoundingClientRect().width; 360 | this.offset = this.cloned * this.width; 361 | }; 362 | 363 | /** 364 | * Update the slides' position on a resize. This is throttled at 300ms 365 | * @return {void} 366 | */ 367 | Carousel.prototype._updateView = function _updateView () { 368 | var this$1 = this; 369 | 370 | // Check if the resize was horizontal. On touch devices, changing scroll 371 | // direction will cause the browser tab bar to appear, which triggers a resize 372 | if (window.innerWidth !== this._viewport) { 373 | this._viewport = window.innerWidth; 374 | clearTimeout(this.timer); 375 | this.timer = setTimeout(function () { 376 | this$1._getDimensions(); 377 | this$1.go(this$1.current); 378 | }, 300); 379 | } 380 | }; 381 | 382 | /** 383 | * Duplicate the first and last N slides so that infinite scrolling can work. 384 | * Depends on how many slides are visible at a time, and any outlying slides as well 385 | * @return {void} 386 | */ 387 | Carousel.prototype._cloneSlides = function _cloneSlides () { 388 | var this$1 = this; 389 | 390 | var duplicate; 391 | var display = this.options.display; 392 | var fromEnd = Math.max(this.numSlides - display, 0); 393 | var fromBeg = Math.min(display, this.numSlides); 394 | 395 | // take "display" slides from the end and add to the beginning 396 | for (var i = this.numSlides; i > fromEnd; i--) { 397 | duplicate = this$1.slides[i-1].cloneNode(true); // cloneNode --> true is deep cloning 398 | duplicate.removeAttribute('id'); 399 | duplicate.setAttribute('aria-hidden', 'true'); 400 | this$1._addClass(duplicate, 'clone'); 401 | this$1.slideWrap.insertBefore(duplicate, this$1.slideWrap.firstChild);// "prependChild" 402 | this$1.cloned++; 403 | } 404 | 405 | // take "display" slides from the beginning and add to the end 406 | for (var i$1 = 0; i$1 < fromBeg; i$1++) { 407 | duplicate = this$1.slides[i$1].cloneNode(true); 408 | duplicate.removeAttribute('id'); 409 | duplicate.setAttribute('aria-hidden', 'true'); 410 | this$1._addClass(duplicate, 'clone'); 411 | this$1.slideWrap.appendChild(duplicate); 412 | } 413 | }; 414 | 415 | /** 416 | * Helper function to add a class to an element 417 | * @param{int} i Index of the slide to add a class to 418 | * @param{string} name Class name 419 | * @return {void} 420 | */ 421 | Carousel.prototype._addClass = function _addClass (el, name) { 422 | if (el.classList) { el.classList.add(name); } 423 | else {el.className += ' ' + name; } 424 | }; 425 | 426 | /** 427 | * Helper function to remove a class from an element 428 | * @param{int} i Index of the slide to remove class from 429 | * @param{string} name Class name 430 | * @return {void} 431 | */ 432 | Carousel.prototype._removeClass = function _removeClass (el, name) { 433 | if (el.classList) { el.classList.remove(name); } 434 | else { el.className = el.className.replace(new RegExp('(^|\\b)' + name.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); } 435 | }; 436 | 437 | /** 438 | * Shallow Object.assign polyfill 439 | * @param {object} dest The object to copy into 440 | * @param {object} srcThe object to copy from 441 | */ 442 | Carousel.prototype._assign = function _assign (dest, src) { 443 | Object.keys(src).forEach(function (key) { 444 | dest[key] = src[key]; 445 | }); 446 | 447 | return dest; 448 | }; 449 | 450 | return Carousel; 451 | 452 | }()); 453 | --------------------------------------------------------------------------------