├── .jshintrc ├── README.md ├── attraction-conditional ├── demo.js └── index.html ├── attraction-drag ├── demo.js └── index.html ├── attraction-two-targets ├── demo.js └── index.html ├── attraction ├── demo.js └── index.html ├── basics ├── demo.js └── index.html ├── drag ├── demo.js └── index.html ├── flickity ├── demo.css ├── flickity.css ├── flickity.pkgd.js └── index.html ├── gravity-well ├── demo.js └── index.html ├── live-basics ├── demo.js └── index.html ├── live-bounds ├── demo.js └── index.html ├── live-drag ├── demo.js └── index.html ├── photo-scroller ├── demo.js ├── index.html └── van.jpg ├── resting-position ├── demo.js └── index.html ├── rubber-band-bounds ├── demo.js └── index.html ├── step-graphs ├── graph-bg.png ├── index.html └── step-graphs.js └── wind ├── demo.js └── index.html /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "undef": true, 4 | "unused": true 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Practical UI physics 2 | 3 | Demos for my talk _Practical UI physics_. 4 | 5 | + [Watch the video on YouTube](https://www.youtube.com/watch?v=90oMnMFozEE) 6 | + [Slides on Speakerdeck](https://speakerdeck.com/desandro/practical-ui-physics) 7 | 8 | Adding physics-based motion to UI can make digital interactions feel natural, comfortable, and delightful. So why is it so hard to get right, especially on the web? While mobile device SDK's have physics-based libraries built-in, web developers are missing a straight-forward way to add physics to their sites. This talk aims to solve that. Physics is a huge subject, and physics programming is often intimidating. But for UI developers, we need only to take advantage of its core principles in a practical model. This talk will cover the fundamentals of physics programming, how to develop your own physics model in JavaScript, and how to use that model to make UI feel natural. 9 | 10 | Demos also available on CodePen — [Practial UI physics collection](http://codepen.io/collection/XgYvmv/) 11 | 12 | + [basic](http://codepen.io/desandro/pen/WvWGej) 13 | + [step graphs](http://codepen.io/desandro/pen/zGXKYm) 14 | + [drag](http://codepen.io/desandro/pen/zGXKOJ) 15 | + [attraction](http://codepen.io/desandro/pen/XbQjbP) 16 | + [attraction drag](http://codepen.io/desandro/pen/oXOzXr) 17 | + [attraction conditional](http://codepen.io/desandro/pen/BNELoQ) 18 | + [attraction two targets](http://codepen.io/desandro/pen/VLNKew) 19 | + [gravity well](http://codepen.io/desandro/pen/jPRMWa) 20 | + [wind](http://codepen.io/desandro/pen/doLpMb) 21 | + [resting position](http://codepen.io/desandro/pen/yNraOK) 22 | + [rubber band bounds](http://codepen.io/desandro/pen/QbPKEq) 23 | + [photo scroller](http://codepen.io/desandro/pen/VLNKKY) 24 | 25 | Demo use mouse events to keep code simple, even though I recommend using physics for touch-based devices. Yes, I realize the irony. 26 | 27 | ## Additional resources 28 | 29 | + [Nature of Code](http://natureofcode.com/) by Daniel Shiffman, Chapters 1 & 2 30 | + [Coding Math videos on YouTube](https://www.youtube.com/user/codingmath/) by Keith Peters 31 | -------------------------------------------------------------------------------- /attraction-conditional/demo.js: -------------------------------------------------------------------------------- 1 | // canvas 2 | var canvas = document.querySelector('canvas'); 3 | var ctx = canvas.getContext('2d'); 4 | 5 | // get/set canvas size 6 | var canvasWidth = canvas.width = window.innerWidth - 20; 7 | var canvasHeight = canvas.height; 8 | 9 | // particle properties 10 | var positionX = 400; 11 | var velocityX = 0; 12 | var friction = 0.9; 13 | var isDragging = false; 14 | var dragPositionX = positionX; 15 | var target = 400; 16 | var targetBound = 150; 17 | var attractionStrength = 0.01; 18 | 19 | function animate() { 20 | update(); 21 | render(); 22 | requestAnimationFrame( animate ); 23 | } 24 | 25 | animate(); 26 | 27 | function update() { 28 | // attraction 29 | var distance = target - positionX; 30 | // attract if within bounds 31 | var attraction = Math.abs( distance ) <= targetBound ? 32 | distance * attractionStrength : 0; 33 | applyForce( attraction ); 34 | // drag 35 | applyDragForce(); 36 | // integrate physics 37 | velocityX *= friction; 38 | positionX += velocityX; 39 | } 40 | 41 | function applyForce( force ) { 42 | velocityX += force; 43 | } 44 | 45 | function applyDragForce() { 46 | if ( !isDragging ) { 47 | return; 48 | } 49 | var dragVelocity = dragPositionX - positionX; 50 | var dragForce = dragVelocity - velocityX; 51 | applyForce( dragForce ); 52 | } 53 | 54 | function render() { 55 | ctx.clearRect( 0, 0, canvasWidth, canvasHeight ); 56 | // render target 57 | ctx.fillStyle = 'hsla(210, 100%, 50%, 0.5)'; 58 | circle( target, 200, targetBound, 'fill' ); 59 | // render particle 60 | ctx.fillStyle = 'hsla(0, 100%, 50%, 0.7)'; 61 | circle( positionX, 200, 25, 'fill' ); 62 | } 63 | 64 | function circle( x, y, radius, render ) { 65 | ctx.beginPath(); 66 | ctx.arc( x, y, radius, 0, Math.PI * 2 ); 67 | ctx[ render ](); 68 | ctx.closePath(); 69 | } 70 | 71 | // ----- mouse events ----- // 72 | 73 | var mousedownX; 74 | var dragStartPositionX; 75 | 76 | document.addEventListener( 'mousedown', function( event ) { 77 | isDragging = true; 78 | mousedownX = event.pageX; 79 | dragStartPositionX = positionX; 80 | setDragPosition( event ); 81 | window.addEventListener( 'mousemove', setDragPosition ); 82 | window.addEventListener( 'mouseup', onMouseup ); 83 | }); 84 | 85 | function setDragPosition( event ) { 86 | var moveX = event.pageX - mousedownX; 87 | dragPositionX = dragStartPositionX + moveX; 88 | event.preventDefault(); 89 | } 90 | 91 | function onMouseup() { 92 | isDragging = false; 93 | window.removeEventListener( 'mousemove', setDragPosition ); 94 | window.removeEventListener( 'mouseup', onMouseup ); 95 | } 96 | -------------------------------------------------------------------------------- /attraction-conditional/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | attraction - conditional 8 | 9 | 15 | 16 | 17 | 18 | 19 |

attraction - conditional

20 | 21 |

Drag particle

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /attraction-drag/demo.js: -------------------------------------------------------------------------------- 1 | // canvas 2 | var canvas = document.querySelector('canvas'); 3 | var ctx = canvas.getContext('2d'); 4 | 5 | // get/set canvas size 6 | var canvasWidth = canvas.width = window.innerWidth - 20; 7 | var canvasHeight = canvas.height; 8 | 9 | // particle properties 10 | var positionX = 400; 11 | var velocityX = 0; 12 | var friction = 0.9; 13 | var isDragging = false; 14 | var dragPositionX = positionX; 15 | var target = 400; 16 | var attractionStrength = 0.01; 17 | 18 | function animate() { 19 | update(); 20 | render(); 21 | requestAnimationFrame( animate ); 22 | } 23 | 24 | animate(); 25 | 26 | function update() { 27 | // attraction 28 | var distance = target - positionX; 29 | var attraction = distance * attractionStrength; 30 | applyForce( attraction ); 31 | // drag 32 | applyDragForce(); 33 | // integrate physics 34 | velocityX *= friction; 35 | positionX += velocityX; 36 | } 37 | 38 | function applyForce( force ) { 39 | velocityX += force; 40 | } 41 | 42 | function applyDragForce() { 43 | if ( !isDragging ) { 44 | return; 45 | } 46 | var dragVelocity = dragPositionX - positionX; 47 | var dragForce = dragVelocity - velocityX; 48 | applyForce( dragForce ); 49 | } 50 | 51 | function render() { 52 | ctx.clearRect( 0, 0, canvasWidth, canvasHeight ); 53 | // render target 54 | ctx.fillStyle = 'hsla(210, 100%, 50%, 0.7)'; 55 | circle( target, 100, 15, 'fill' ); 56 | // render particle 57 | ctx.fillStyle = 'hsla(0, 100%, 50%, 0.7)'; 58 | circle( positionX, 100, 25, 'fill' ); 59 | } 60 | 61 | function circle( x, y, radius, render ) { 62 | ctx.beginPath(); 63 | ctx.arc( x, y, radius, 0, Math.PI * 2 ); 64 | ctx[ render ](); 65 | ctx.closePath(); 66 | } 67 | 68 | // ----- mouse events ----- // 69 | 70 | var mousedownX; 71 | var dragStartPositionX; 72 | 73 | document.addEventListener( 'mousedown', function( event ) { 74 | isDragging = true; 75 | mousedownX = event.pageX; 76 | dragStartPositionX = positionX; 77 | setDragPosition( event ); 78 | window.addEventListener( 'mousemove', setDragPosition ); 79 | window.addEventListener( 'mouseup', onMouseup ); 80 | }); 81 | 82 | function setDragPosition( event ) { 83 | var moveX = event.pageX - mousedownX; 84 | dragPositionX = dragStartPositionX + moveX; 85 | event.preventDefault(); 86 | } 87 | 88 | function onMouseup() { 89 | isDragging = false; 90 | window.removeEventListener( 'mousemove', setDragPosition ); 91 | window.removeEventListener( 'mouseup', onMouseup ); 92 | } 93 | -------------------------------------------------------------------------------- /attraction-drag/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | attraction - drag 8 | 9 | 15 | 16 | 17 | 18 | 19 |

attraction - drag

20 | 21 |

Drag particle

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /attraction-two-targets/demo.js: -------------------------------------------------------------------------------- 1 | // canvas 2 | var canvas = document.querySelector('canvas'); 3 | var ctx = canvas.getContext('2d'); 4 | 5 | // get/set canvas size 6 | var canvasWidth = canvas.width = window.innerWidth - 20; 7 | var canvasHeight = canvas.height; 8 | 9 | // particle properties 10 | var positionX = 200; 11 | var velocityX = 0; 12 | var friction = 0.9; 13 | var isDragging = false; 14 | var dragPositionX = positionX; 15 | var targetA = positionX; 16 | var targetB = 700; 17 | var targetBound = 150; 18 | var attractionStrength = 0.01; 19 | 20 | function animate() { 21 | update(); 22 | render(); 23 | requestAnimationFrame( animate ); 24 | } 25 | 26 | animate(); 27 | 28 | function update() { 29 | attract( targetA ); 30 | attract( targetB ); 31 | // drag 32 | applyDragForce(); 33 | // integrate physics 34 | velocityX *= friction; 35 | positionX += velocityX; 36 | } 37 | 38 | function attract( target ) { 39 | // attraction 40 | var distance = target - positionX; 41 | // attract if within bounds 42 | var attraction = Math.abs( distance ) <= targetBound ? 43 | distance * attractionStrength : 0; 44 | applyForce( attraction ); 45 | } 46 | 47 | function applyForce( force ) { 48 | velocityX += force; 49 | } 50 | 51 | function applyDragForce() { 52 | if ( !isDragging ) { 53 | return; 54 | } 55 | var dragVelocity = dragPositionX - positionX; 56 | var dragForce = dragVelocity - velocityX; 57 | applyForce( dragForce ); 58 | } 59 | 60 | function render() { 61 | ctx.clearRect( 0, 0, canvasWidth, canvasHeight ); 62 | // render target 63 | ctx.fillStyle = 'hsla(210, 100%, 50%, 0.5)'; 64 | circle( targetA, 200, targetBound, 'fill' ); 65 | circle( targetB, 200, targetBound, 'fill' ); 66 | // render particle 67 | ctx.fillStyle = 'hsla(0, 100%, 50%, 0.7)'; 68 | circle( positionX, 200, 25, 'fill' ); 69 | } 70 | 71 | function circle( x, y, radius, render ) { 72 | ctx.beginPath(); 73 | ctx.arc( x, y, radius, 0, Math.PI * 2 ); 74 | ctx[ render ](); 75 | ctx.closePath(); 76 | } 77 | 78 | // ----- mouse events ----- // 79 | 80 | var mousedownX; 81 | var dragStartPositionX; 82 | 83 | document.addEventListener( 'mousedown', function( event ) { 84 | isDragging = true; 85 | mousedownX = event.pageX; 86 | dragStartPositionX = positionX; 87 | setDragPosition( event ); 88 | window.addEventListener( 'mousemove', setDragPosition ); 89 | window.addEventListener( 'mouseup', onMouseup ); 90 | }); 91 | 92 | function setDragPosition( event ) { 93 | var moveX = event.pageX - mousedownX; 94 | dragPositionX = dragStartPositionX + moveX; 95 | event.preventDefault(); 96 | } 97 | 98 | function onMouseup() { 99 | isDragging = false; 100 | window.removeEventListener( 'mousemove', setDragPosition ); 101 | window.removeEventListener( 'mouseup', onMouseup ); 102 | } 103 | -------------------------------------------------------------------------------- /attraction-two-targets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | attraction - two targets 8 | 9 | 15 | 16 | 17 | 18 | 19 |

attraction - two targets

20 | 21 |

Drag particle

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /attraction/demo.js: -------------------------------------------------------------------------------- 1 | // canvas 2 | var canvas = document.querySelector('canvas'); 3 | var ctx = canvas.getContext('2d'); 4 | 5 | // get/set canvas size 6 | var canvasWidth = canvas.width = window.innerWidth - 20; 7 | var canvasHeight = canvas.height; 8 | 9 | // particle properties 10 | var positionX = 100; 11 | var velocityX = 0; 12 | var friction = 0.9; 13 | var target = 100; 14 | var attractionStrength = 0.01; 15 | 16 | function animate() { 17 | update(); 18 | render(); 19 | requestAnimationFrame( animate ); 20 | } 21 | 22 | animate(); 23 | 24 | function update() { 25 | // attract particle to target 26 | var distance = target - positionX; 27 | var attraction = distance * attractionStrength; 28 | applyForce( attraction ); 29 | // integrate physics variables 30 | velocityX *= friction; 31 | positionX += velocityX; 32 | } 33 | 34 | function applyForce( force ) { 35 | velocityX += force; 36 | } 37 | 38 | function render() { 39 | ctx.clearRect( 0, 0, canvasWidth, canvasHeight ); 40 | // render target 41 | ctx.fillStyle = 'hsla(210, 100%, 50%, 0.7)'; 42 | circle( target, 100, 15 ); 43 | // render particle 44 | ctx.fillStyle = 'hsla(0, 100%, 50%, 0.7)'; 45 | circle( positionX, 100, 25 ); 46 | } 47 | 48 | function circle( x, y, radius ) { 49 | ctx.beginPath(); 50 | ctx.arc( x, y, radius, 0, Math.PI * 2 ); 51 | ctx.fill(); 52 | ctx.closePath(); 53 | } 54 | 55 | document.addEventListener( 'click', function( event) { 56 | target = event.pageX; 57 | }); 58 | -------------------------------------------------------------------------------- /attraction/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | attraction 8 | 9 | 15 | 16 | 17 | 18 | 19 |

attraction

20 | 21 |

Click to change target. Change attractionStrength and friction

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /basics/demo.js: -------------------------------------------------------------------------------- 1 | // canvas 2 | var canvas = document.querySelector('canvas'); 3 | var ctx = canvas.getContext('2d'); 4 | 5 | // get/set canvas size 6 | var canvasWidth = canvas.width = window.innerWidth - 20; 7 | var canvasHeight = canvas.height; 8 | 9 | // particle properties 10 | var positionX = 100; 11 | var velocityX = 0; 12 | var friction = 0.95; 13 | 14 | function update() { 15 | // integrate physics 16 | velocityX *= friction; 17 | positionX += velocityX; 18 | } 19 | 20 | function animate() { 21 | update(); 22 | render(); 23 | requestAnimationFrame( animate ); 24 | } 25 | 26 | animate(); 27 | 28 | function render() { 29 | ctx.clearRect( 0, 0, canvasWidth, canvasHeight ); 30 | // render particle 31 | ctx.fillStyle = 'red'; 32 | circle( positionX, 100, 25 ); 33 | } 34 | 35 | function circle( x, y, radius ) { 36 | ctx.beginPath(); 37 | ctx.arc( positionX, 100, radius, 0, Math.PI * 2 ); 38 | ctx.fill(); 39 | ctx.closePath(); 40 | } 41 | -------------------------------------------------------------------------------- /basics/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | basics 8 | 9 | 15 | 16 | 17 | 18 | 19 |

basics

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /drag/demo.js: -------------------------------------------------------------------------------- 1 | // canvas 2 | var canvas = document.querySelector('canvas'); 3 | var ctx = canvas.getContext('2d'); 4 | 5 | // get/set canvas size 6 | var canvasWidth = canvas.width = window.innerWidth - 20; 7 | var canvasHeight = canvas.height; 8 | 9 | // particle properties 10 | var positionX = 100; 11 | var velocityX = 0; 12 | var friction = 0.95; 13 | var isDragging = false; 14 | var dragPositionX = positionX; 15 | 16 | function animate() { 17 | update(); 18 | render(); 19 | requestAnimationFrame( animate ); 20 | } 21 | 22 | animate(); 23 | 24 | function update() { 25 | applyDragForce(); 26 | velocityX *= friction; 27 | positionX += velocityX; 28 | } 29 | 30 | function applyForce( force ) { 31 | velocityX += force; 32 | } 33 | 34 | function applyDragForce() { 35 | if ( !isDragging ) { 36 | return; 37 | } 38 | var dragVelocity = dragPositionX - positionX; 39 | var dragForce = dragVelocity - velocityX; 40 | applyForce( dragForce ); 41 | } 42 | 43 | function render() { 44 | ctx.clearRect( 0, 0, canvasWidth, canvasHeight ); 45 | // render particle 46 | ctx.fillStyle = 'hsla(0, 100%, 50%, 0.7)'; 47 | circle( positionX, 100, 25, 'fill' ); 48 | } 49 | 50 | function circle( x, y, radius, render ) { 51 | ctx.beginPath(); 52 | ctx.arc( x, y, radius, 0, Math.PI * 2 ); 53 | ctx[ render ](); 54 | ctx.closePath(); 55 | } 56 | 57 | // ----- mouse events ----- // 58 | 59 | var mousedownX; 60 | var dragStartPositionX; 61 | 62 | document.addEventListener( 'mousedown', function( event ) { 63 | isDragging = true; 64 | mousedownX = event.pageX; 65 | dragStartPositionX = positionX; 66 | setDragPosition( event ); 67 | window.addEventListener( 'mousemove', setDragPosition ); 68 | window.addEventListener( 'mouseup', onMouseup ); 69 | }); 70 | 71 | function setDragPosition( event ) { 72 | var moveX = event.pageX - mousedownX; 73 | dragPositionX = dragStartPositionX + moveX; 74 | event.preventDefault(); 75 | } 76 | 77 | function onMouseup() { 78 | isDragging = false; 79 | window.removeEventListener( 'mousemove', setDragPosition ); 80 | window.removeEventListener( 'mouseup', onMouseup ); 81 | } 82 | -------------------------------------------------------------------------------- /drag/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | drag 8 | 9 | 15 | 16 | 17 | 18 | 19 |

drag

20 | 21 |

Drag particle

22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /flickity/demo.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | box-sizing: border-box; 4 | } 5 | 6 | body { font-family: sans-serif; } 7 | 8 | .gallery { 9 | background: #EEE; 10 | margin-bottom: 100px; 11 | } 12 | 13 | .gallery-cell { 14 | width: 66%; 15 | height: 400px; 16 | margin-right: 10px; 17 | background: #8C8; 18 | border-radius: 10px; 19 | counter-increment: gallery-cell; 20 | } 21 | 22 | /* cell number */ 23 | .gallery-cell:before { 24 | display: block; 25 | text-align: center; 26 | content: counter(gallery-cell); 27 | line-height: 400px; 28 | font-size: 100px; 29 | color: white; 30 | } 31 | 32 | .gallery-cell:nth-child(4n+0) { background: #F90; } 33 | .gallery-cell:nth-child(4n+1) { background: #8C8; } 34 | .gallery-cell:nth-child(4n+2) { background: #F09; } 35 | .gallery-cell:nth-child(4n+3) { background: #09F; } 36 | 37 | .gallery--variable-sizes .gallery-cell:nth-child(4n+1) { width: 25%; } 38 | .gallery--variable-sizes .gallery-cell:nth-child(4n+2) { width: 90%; } 39 | .gallery--variable-sizes .gallery-cell:nth-child(4n+3) { width: 33%; } 40 | 41 | -------------------------------------------------------------------------------- /flickity/flickity.css: -------------------------------------------------------------------------------- 1 | /*! Flickity v1.1.0 2 | http://flickity.metafizzy.co 3 | ---------------------------------------------- */ 4 | 5 | .flickity-enabled { 6 | position: relative; 7 | } 8 | 9 | .flickity-enabled:focus { outline: none; } 10 | 11 | .flickity-viewport { 12 | overflow: hidden; 13 | position: relative; 14 | height: 100%; 15 | } 16 | 17 | .flickity-slider { 18 | position: absolute; 19 | width: 100%; 20 | height: 100%; 21 | } 22 | 23 | /* draggable */ 24 | 25 | .flickity-enabled.is-draggable { 26 | -webkit-tap-highlight-color: transparent; 27 | tap-highlight-color: transparent; 28 | -webkit-user-select: none; 29 | -moz-user-select: none; 30 | -ms-user-select: none; 31 | user-select: none; 32 | } 33 | 34 | .flickity-enabled.is-draggable .flickity-viewport { 35 | cursor: move; 36 | cursor: -webkit-grab; 37 | cursor: grab; 38 | } 39 | 40 | .flickity-enabled.is-draggable .flickity-viewport.is-pointer-down { 41 | cursor: -webkit-grabbing; 42 | cursor: grabbing; 43 | } 44 | 45 | /* ---- previous/next buttons ---- */ 46 | 47 | .flickity-prev-next-button { 48 | position: absolute; 49 | top: 50%; 50 | width: 44px; 51 | height: 44px; 52 | border: none; 53 | border-radius: 50%; 54 | background: white; 55 | background: hsla(0, 0%, 100%, 0.75); 56 | cursor: pointer; 57 | /* vertically center */ 58 | -webkit-transform: translateY(-50%); 59 | -ms-transform: translateY(-50%); 60 | transform: translateY(-50%); 61 | } 62 | 63 | .flickity-prev-next-button:hover { background: white; } 64 | 65 | .flickity-prev-next-button:focus { 66 | outline: none; 67 | box-shadow: 0 0 0 5px #09F; 68 | } 69 | 70 | .flickity-prev-next-button:active { 71 | filter: alpha(opacity=60); /* IE8 */ 72 | opacity: 0.6; 73 | } 74 | 75 | .flickity-prev-next-button.previous { left: 10px; } 76 | .flickity-prev-next-button.next { right: 10px; } 77 | /* right to left */ 78 | .flickity-rtl .flickity-prev-next-button.previous { 79 | left: auto; 80 | right: 10px; 81 | } 82 | .flickity-rtl .flickity-prev-next-button.next { 83 | right: auto; 84 | left: 10px; 85 | } 86 | 87 | .flickity-prev-next-button:disabled { 88 | filter: alpha(opacity=30); /* IE8 */ 89 | opacity: 0.3; 90 | cursor: auto; 91 | } 92 | 93 | .flickity-prev-next-button svg { 94 | position: absolute; 95 | left: 20%; 96 | top: 20%; 97 | width: 60%; 98 | height: 60%; 99 | } 100 | 101 | .flickity-prev-next-button .arrow { 102 | fill: #333; 103 | } 104 | 105 | /* color & size if no SVG - IE8 and Android 2.3 */ 106 | .flickity-prev-next-button.no-svg { 107 | color: #333; 108 | font-size: 26px; 109 | } 110 | 111 | /* ---- page dots ---- */ 112 | 113 | .flickity-page-dots { 114 | position: absolute; 115 | width: 100%; 116 | bottom: -25px; 117 | padding: 0; 118 | margin: 0; 119 | list-style: none; 120 | text-align: center; 121 | line-height: 1; 122 | } 123 | 124 | .flickity-rtl .flickity-page-dots { direction: rtl; } 125 | 126 | .flickity-page-dots .dot { 127 | display: inline-block; 128 | width: 10px; 129 | height: 10px; 130 | margin: 0 8px; 131 | background: #333; 132 | border-radius: 50%; 133 | filter: alpha(opacity=25); /* IE8 */ 134 | opacity: 0.25; 135 | cursor: pointer; 136 | } 137 | 138 | .flickity-page-dots .dot.is-selected { 139 | filter: alpha(opacity=100); /* IE8 */ 140 | opacity: 1; 141 | } 142 | -------------------------------------------------------------------------------- /flickity/flickity.pkgd.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Flickity PACKAGED v1.0.2 3 | * Touch, responsive, flickable galleries 4 | * 5 | * Licensed GPLv3 for open source use 6 | * or Flickity Commercial License for commercial use 7 | * 8 | * http://flickity.metafizzy.co 9 | * Copyright 2015 Metafizzy 10 | */ 11 | 12 | /** 13 | * Bridget makes jQuery widgets 14 | * v1.1.0 15 | * MIT license 16 | */ 17 | 18 | ( function( window ) { 19 | 20 | 21 | 22 | // -------------------------- utils -------------------------- // 23 | 24 | var slice = Array.prototype.slice; 25 | 26 | function noop() {} 27 | 28 | // -------------------------- definition -------------------------- // 29 | 30 | function defineBridget( $ ) { 31 | 32 | // bail if no jQuery 33 | if ( !$ ) { 34 | return; 35 | } 36 | 37 | // -------------------------- addOptionMethod -------------------------- // 38 | 39 | /** 40 | * adds option method -> $().plugin('option', {...}) 41 | * @param {Function} PluginClass - constructor class 42 | */ 43 | function addOptionMethod( PluginClass ) { 44 | // don't overwrite original option method 45 | if ( PluginClass.prototype.option ) { 46 | return; 47 | } 48 | 49 | // option setter 50 | PluginClass.prototype.option = function( opts ) { 51 | // bail out if not an object 52 | if ( !$.isPlainObject( opts ) ){ 53 | return; 54 | } 55 | this.options = $.extend( true, this.options, opts ); 56 | }; 57 | } 58 | 59 | // -------------------------- plugin bridge -------------------------- // 60 | 61 | // helper function for logging errors 62 | // $.error breaks jQuery chaining 63 | var logError = typeof console === 'undefined' ? noop : 64 | function( message ) { 65 | console.error( message ); 66 | }; 67 | 68 | /** 69 | * jQuery plugin bridge, access methods like $elem.plugin('method') 70 | * @param {String} namespace - plugin name 71 | * @param {Function} PluginClass - constructor class 72 | */ 73 | function bridge( namespace, PluginClass ) { 74 | // add to jQuery fn namespace 75 | $.fn[ namespace ] = function( options ) { 76 | if ( typeof options === 'string' ) { 77 | // call plugin method when first argument is a string 78 | // get arguments for method 79 | var args = slice.call( arguments, 1 ); 80 | 81 | for ( var i=0, len = this.length; i < len; i++ ) { 82 | var elem = this[i]; 83 | var instance = $.data( elem, namespace ); 84 | if ( !instance ) { 85 | logError( "cannot call methods on " + namespace + " prior to initialization; " + 86 | "attempted to call '" + options + "'" ); 87 | continue; 88 | } 89 | if ( !$.isFunction( instance[options] ) || options.charAt(0) === '_' ) { 90 | logError( "no such method '" + options + "' for " + namespace + " instance" ); 91 | continue; 92 | } 93 | 94 | // trigger method with arguments 95 | var returnValue = instance[ options ].apply( instance, args ); 96 | 97 | // break look and return first value if provided 98 | if ( returnValue !== undefined ) { 99 | return returnValue; 100 | } 101 | } 102 | // return this if no return value 103 | return this; 104 | } else { 105 | return this.each( function() { 106 | var instance = $.data( this, namespace ); 107 | if ( instance ) { 108 | // apply options & init 109 | instance.option( options ); 110 | instance._init(); 111 | } else { 112 | // initialize new instance 113 | instance = new PluginClass( this, options ); 114 | $.data( this, namespace, instance ); 115 | } 116 | }); 117 | } 118 | }; 119 | 120 | } 121 | 122 | // -------------------------- bridget -------------------------- // 123 | 124 | /** 125 | * converts a Prototypical class into a proper jQuery plugin 126 | * the class must have a ._init method 127 | * @param {String} namespace - plugin name, used in $().pluginName 128 | * @param {Function} PluginClass - constructor class 129 | */ 130 | $.bridget = function( namespace, PluginClass ) { 131 | addOptionMethod( PluginClass ); 132 | bridge( namespace, PluginClass ); 133 | }; 134 | 135 | return $.bridget; 136 | 137 | } 138 | 139 | // transport 140 | if ( typeof define === 'function' && define.amd ) { 141 | // AMD 142 | define( 'jquery-bridget/jquery.bridget',[ 'jquery' ], defineBridget ); 143 | } else if ( typeof exports === 'object' ) { 144 | defineBridget( require('jquery') ); 145 | } else { 146 | // get jquery from browser global 147 | defineBridget( window.jQuery ); 148 | } 149 | 150 | })( window ); 151 | 152 | /*! 153 | * classie v1.0.1 154 | * class helper functions 155 | * from bonzo https://github.com/ded/bonzo 156 | * MIT license 157 | * 158 | * classie.has( elem, 'my-class' ) -> true/false 159 | * classie.add( elem, 'my-new-class' ) 160 | * classie.remove( elem, 'my-unwanted-class' ) 161 | * classie.toggle( elem, 'my-class' ) 162 | */ 163 | 164 | /*jshint browser: true, strict: true, undef: true, unused: true */ 165 | /*global define: false, module: false */ 166 | 167 | ( function( window ) { 168 | 169 | 170 | 171 | // class helper functions from bonzo https://github.com/ded/bonzo 172 | 173 | function classReg( className ) { 174 | return new RegExp("(^|\\s+)" + className + "(\\s+|$)"); 175 | } 176 | 177 | // classList support for class management 178 | // altho to be fair, the api sucks because it won't accept multiple classes at once 179 | var hasClass, addClass, removeClass; 180 | 181 | if ( 'classList' in document.documentElement ) { 182 | hasClass = function( elem, c ) { 183 | return elem.classList.contains( c ); 184 | }; 185 | addClass = function( elem, c ) { 186 | elem.classList.add( c ); 187 | }; 188 | removeClass = function( elem, c ) { 189 | elem.classList.remove( c ); 190 | }; 191 | } 192 | else { 193 | hasClass = function( elem, c ) { 194 | return classReg( c ).test( elem.className ); 195 | }; 196 | addClass = function( elem, c ) { 197 | if ( !hasClass( elem, c ) ) { 198 | elem.className = elem.className + ' ' + c; 199 | } 200 | }; 201 | removeClass = function( elem, c ) { 202 | elem.className = elem.className.replace( classReg( c ), ' ' ); 203 | }; 204 | } 205 | 206 | function toggleClass( elem, c ) { 207 | var fn = hasClass( elem, c ) ? removeClass : addClass; 208 | fn( elem, c ); 209 | } 210 | 211 | var classie = { 212 | // full names 213 | hasClass: hasClass, 214 | addClass: addClass, 215 | removeClass: removeClass, 216 | toggleClass: toggleClass, 217 | // short names 218 | has: hasClass, 219 | add: addClass, 220 | remove: removeClass, 221 | toggle: toggleClass 222 | }; 223 | 224 | // transport 225 | if ( typeof define === 'function' && define.amd ) { 226 | // AMD 227 | define( 'classie/classie',classie ); 228 | } else if ( typeof exports === 'object' ) { 229 | // CommonJS 230 | module.exports = classie; 231 | } else { 232 | // browser global 233 | window.classie = classie; 234 | } 235 | 236 | })( window ); 237 | 238 | /*! 239 | * EventEmitter v4.2.11 - git.io/ee 240 | * Unlicense - http://unlicense.org/ 241 | * Oliver Caldwell - http://oli.me.uk/ 242 | * @preserve 243 | */ 244 | 245 | ;(function () { 246 | 247 | 248 | /** 249 | * Class for managing events. 250 | * Can be extended to provide event functionality in other classes. 251 | * 252 | * @class EventEmitter Manages event registering and emitting. 253 | */ 254 | function EventEmitter() {} 255 | 256 | // Shortcuts to improve speed and size 257 | var proto = EventEmitter.prototype; 258 | var exports = this; 259 | var originalGlobalValue = exports.EventEmitter; 260 | 261 | /** 262 | * Finds the index of the listener for the event in its storage array. 263 | * 264 | * @param {Function[]} listeners Array of listeners to search through. 265 | * @param {Function} listener Method to look for. 266 | * @return {Number} Index of the specified listener, -1 if not found 267 | * @api private 268 | */ 269 | function indexOfListener(listeners, listener) { 270 | var i = listeners.length; 271 | while (i--) { 272 | if (listeners[i].listener === listener) { 273 | return i; 274 | } 275 | } 276 | 277 | return -1; 278 | } 279 | 280 | /** 281 | * Alias a method while keeping the context correct, to allow for overwriting of target method. 282 | * 283 | * @param {String} name The name of the target method. 284 | * @return {Function} The aliased method 285 | * @api private 286 | */ 287 | function alias(name) { 288 | return function aliasClosure() { 289 | return this[name].apply(this, arguments); 290 | }; 291 | } 292 | 293 | /** 294 | * Returns the listener array for the specified event. 295 | * Will initialise the event object and listener arrays if required. 296 | * Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them. 297 | * Each property in the object response is an array of listener functions. 298 | * 299 | * @param {String|RegExp} evt Name of the event to return the listeners from. 300 | * @return {Function[]|Object} All listener functions for the event. 301 | */ 302 | proto.getListeners = function getListeners(evt) { 303 | var events = this._getEvents(); 304 | var response; 305 | var key; 306 | 307 | // Return a concatenated array of all matching events if 308 | // the selector is a regular expression. 309 | if (evt instanceof RegExp) { 310 | response = {}; 311 | for (key in events) { 312 | if (events.hasOwnProperty(key) && evt.test(key)) { 313 | response[key] = events[key]; 314 | } 315 | } 316 | } 317 | else { 318 | response = events[evt] || (events[evt] = []); 319 | } 320 | 321 | return response; 322 | }; 323 | 324 | /** 325 | * Takes a list of listener objects and flattens it into a list of listener functions. 326 | * 327 | * @param {Object[]} listeners Raw listener objects. 328 | * @return {Function[]} Just the listener functions. 329 | */ 330 | proto.flattenListeners = function flattenListeners(listeners) { 331 | var flatListeners = []; 332 | var i; 333 | 334 | for (i = 0; i < listeners.length; i += 1) { 335 | flatListeners.push(listeners[i].listener); 336 | } 337 | 338 | return flatListeners; 339 | }; 340 | 341 | /** 342 | * Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful. 343 | * 344 | * @param {String|RegExp} evt Name of the event to return the listeners from. 345 | * @return {Object} All listener functions for an event in an object. 346 | */ 347 | proto.getListenersAsObject = function getListenersAsObject(evt) { 348 | var listeners = this.getListeners(evt); 349 | var response; 350 | 351 | if (listeners instanceof Array) { 352 | response = {}; 353 | response[evt] = listeners; 354 | } 355 | 356 | return response || listeners; 357 | }; 358 | 359 | /** 360 | * Adds a listener function to the specified event. 361 | * The listener will not be added if it is a duplicate. 362 | * If the listener returns true then it will be removed after it is called. 363 | * If you pass a regular expression as the event name then the listener will be added to all events that match it. 364 | * 365 | * @param {String|RegExp} evt Name of the event to attach the listener to. 366 | * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. 367 | * @return {Object} Current instance of EventEmitter for chaining. 368 | */ 369 | proto.addListener = function addListener(evt, listener) { 370 | var listeners = this.getListenersAsObject(evt); 371 | var listenerIsWrapped = typeof listener === 'object'; 372 | var key; 373 | 374 | for (key in listeners) { 375 | if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) { 376 | listeners[key].push(listenerIsWrapped ? listener : { 377 | listener: listener, 378 | once: false 379 | }); 380 | } 381 | } 382 | 383 | return this; 384 | }; 385 | 386 | /** 387 | * Alias of addListener 388 | */ 389 | proto.on = alias('addListener'); 390 | 391 | /** 392 | * Semi-alias of addListener. It will add a listener that will be 393 | * automatically removed after its first execution. 394 | * 395 | * @param {String|RegExp} evt Name of the event to attach the listener to. 396 | * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. 397 | * @return {Object} Current instance of EventEmitter for chaining. 398 | */ 399 | proto.addOnceListener = function addOnceListener(evt, listener) { 400 | return this.addListener(evt, { 401 | listener: listener, 402 | once: true 403 | }); 404 | }; 405 | 406 | /** 407 | * Alias of addOnceListener. 408 | */ 409 | proto.once = alias('addOnceListener'); 410 | 411 | /** 412 | * Defines an event name. This is required if you want to use a regex to add a listener to multiple events at once. If you don't do this then how do you expect it to know what event to add to? Should it just add to every possible match for a regex? No. That is scary and bad. 413 | * You need to tell it what event names should be matched by a regex. 414 | * 415 | * @param {String} evt Name of the event to create. 416 | * @return {Object} Current instance of EventEmitter for chaining. 417 | */ 418 | proto.defineEvent = function defineEvent(evt) { 419 | this.getListeners(evt); 420 | return this; 421 | }; 422 | 423 | /** 424 | * Uses defineEvent to define multiple events. 425 | * 426 | * @param {String[]} evts An array of event names to define. 427 | * @return {Object} Current instance of EventEmitter for chaining. 428 | */ 429 | proto.defineEvents = function defineEvents(evts) { 430 | for (var i = 0; i < evts.length; i += 1) { 431 | this.defineEvent(evts[i]); 432 | } 433 | return this; 434 | }; 435 | 436 | /** 437 | * Removes a listener function from the specified event. 438 | * When passed a regular expression as the event name, it will remove the listener from all events that match it. 439 | * 440 | * @param {String|RegExp} evt Name of the event to remove the listener from. 441 | * @param {Function} listener Method to remove from the event. 442 | * @return {Object} Current instance of EventEmitter for chaining. 443 | */ 444 | proto.removeListener = function removeListener(evt, listener) { 445 | var listeners = this.getListenersAsObject(evt); 446 | var index; 447 | var key; 448 | 449 | for (key in listeners) { 450 | if (listeners.hasOwnProperty(key)) { 451 | index = indexOfListener(listeners[key], listener); 452 | 453 | if (index !== -1) { 454 | listeners[key].splice(index, 1); 455 | } 456 | } 457 | } 458 | 459 | return this; 460 | }; 461 | 462 | /** 463 | * Alias of removeListener 464 | */ 465 | proto.off = alias('removeListener'); 466 | 467 | /** 468 | * Adds listeners in bulk using the manipulateListeners method. 469 | * If you pass an object as the second argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. You can also pass it an event name and an array of listeners to be added. 470 | * You can also pass it a regular expression to add the array of listeners to all events that match it. 471 | * Yeah, this function does quite a bit. That's probably a bad thing. 472 | * 473 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once. 474 | * @param {Function[]} [listeners] An optional array of listener functions to add. 475 | * @return {Object} Current instance of EventEmitter for chaining. 476 | */ 477 | proto.addListeners = function addListeners(evt, listeners) { 478 | // Pass through to manipulateListeners 479 | return this.manipulateListeners(false, evt, listeners); 480 | }; 481 | 482 | /** 483 | * Removes listeners in bulk using the manipulateListeners method. 484 | * If you pass an object as the second argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. 485 | * You can also pass it an event name and an array of listeners to be removed. 486 | * You can also pass it a regular expression to remove the listeners from all events that match it. 487 | * 488 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once. 489 | * @param {Function[]} [listeners] An optional array of listener functions to remove. 490 | * @return {Object} Current instance of EventEmitter for chaining. 491 | */ 492 | proto.removeListeners = function removeListeners(evt, listeners) { 493 | // Pass through to manipulateListeners 494 | return this.manipulateListeners(true, evt, listeners); 495 | }; 496 | 497 | /** 498 | * Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level. 499 | * The first argument will determine if the listeners are removed (true) or added (false). 500 | * If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. 501 | * You can also pass it an event name and an array of listeners to be added/removed. 502 | * You can also pass it a regular expression to manipulate the listeners of all events that match it. 503 | * 504 | * @param {Boolean} remove True if you want to remove listeners, false if you want to add. 505 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once. 506 | * @param {Function[]} [listeners] An optional array of listener functions to add/remove. 507 | * @return {Object} Current instance of EventEmitter for chaining. 508 | */ 509 | proto.manipulateListeners = function manipulateListeners(remove, evt, listeners) { 510 | var i; 511 | var value; 512 | var single = remove ? this.removeListener : this.addListener; 513 | var multiple = remove ? this.removeListeners : this.addListeners; 514 | 515 | // If evt is an object then pass each of its properties to this method 516 | if (typeof evt === 'object' && !(evt instanceof RegExp)) { 517 | for (i in evt) { 518 | if (evt.hasOwnProperty(i) && (value = evt[i])) { 519 | // Pass the single listener straight through to the singular method 520 | if (typeof value === 'function') { 521 | single.call(this, i, value); 522 | } 523 | else { 524 | // Otherwise pass back to the multiple function 525 | multiple.call(this, i, value); 526 | } 527 | } 528 | } 529 | } 530 | else { 531 | // So evt must be a string 532 | // And listeners must be an array of listeners 533 | // Loop over it and pass each one to the multiple method 534 | i = listeners.length; 535 | while (i--) { 536 | single.call(this, evt, listeners[i]); 537 | } 538 | } 539 | 540 | return this; 541 | }; 542 | 543 | /** 544 | * Removes all listeners from a specified event. 545 | * If you do not specify an event then all listeners will be removed. 546 | * That means every event will be emptied. 547 | * You can also pass a regex to remove all events that match it. 548 | * 549 | * @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed. 550 | * @return {Object} Current instance of EventEmitter for chaining. 551 | */ 552 | proto.removeEvent = function removeEvent(evt) { 553 | var type = typeof evt; 554 | var events = this._getEvents(); 555 | var key; 556 | 557 | // Remove different things depending on the state of evt 558 | if (type === 'string') { 559 | // Remove all listeners for the specified event 560 | delete events[evt]; 561 | } 562 | else if (evt instanceof RegExp) { 563 | // Remove all events matching the regex. 564 | for (key in events) { 565 | if (events.hasOwnProperty(key) && evt.test(key)) { 566 | delete events[key]; 567 | } 568 | } 569 | } 570 | else { 571 | // Remove all listeners in all events 572 | delete this._events; 573 | } 574 | 575 | return this; 576 | }; 577 | 578 | /** 579 | * Alias of removeEvent. 580 | * 581 | * Added to mirror the node API. 582 | */ 583 | proto.removeAllListeners = alias('removeEvent'); 584 | 585 | /** 586 | * Emits an event of your choice. 587 | * When emitted, every listener attached to that event will be executed. 588 | * If you pass the optional argument array then those arguments will be passed to every listener upon execution. 589 | * Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately. 590 | * So they will not arrive within the array on the other side, they will be separate. 591 | * You can also pass a regular expression to emit to all events that match it. 592 | * 593 | * @param {String|RegExp} evt Name of the event to emit and execute listeners for. 594 | * @param {Array} [args] Optional array of arguments to be passed to each listener. 595 | * @return {Object} Current instance of EventEmitter for chaining. 596 | */ 597 | proto.emitEvent = function emitEvent(evt, args) { 598 | var listeners = this.getListenersAsObject(evt); 599 | var listener; 600 | var i; 601 | var key; 602 | var response; 603 | 604 | for (key in listeners) { 605 | if (listeners.hasOwnProperty(key)) { 606 | i = listeners[key].length; 607 | 608 | while (i--) { 609 | // If the listener returns true then it shall be removed from the event 610 | // The function is executed either with a basic call or an apply if there is an args array 611 | listener = listeners[key][i]; 612 | 613 | if (listener.once === true) { 614 | this.removeListener(evt, listener.listener); 615 | } 616 | 617 | response = listener.listener.apply(this, args || []); 618 | 619 | if (response === this._getOnceReturnValue()) { 620 | this.removeListener(evt, listener.listener); 621 | } 622 | } 623 | } 624 | } 625 | 626 | return this; 627 | }; 628 | 629 | /** 630 | * Alias of emitEvent 631 | */ 632 | proto.trigger = alias('emitEvent'); 633 | 634 | /** 635 | * Subtly different from emitEvent in that it will pass its arguments on to the listeners, as opposed to taking a single array of arguments to pass on. 636 | * As with emitEvent, you can pass a regex in place of the event name to emit to all events that match it. 637 | * 638 | * @param {String|RegExp} evt Name of the event to emit and execute listeners for. 639 | * @param {...*} Optional additional arguments to be passed to each listener. 640 | * @return {Object} Current instance of EventEmitter for chaining. 641 | */ 642 | proto.emit = function emit(evt) { 643 | var args = Array.prototype.slice.call(arguments, 1); 644 | return this.emitEvent(evt, args); 645 | }; 646 | 647 | /** 648 | * Sets the current value to check against when executing listeners. If a 649 | * listeners return value matches the one set here then it will be removed 650 | * after execution. This value defaults to true. 651 | * 652 | * @param {*} value The new value to check for when executing listeners. 653 | * @return {Object} Current instance of EventEmitter for chaining. 654 | */ 655 | proto.setOnceReturnValue = function setOnceReturnValue(value) { 656 | this._onceReturnValue = value; 657 | return this; 658 | }; 659 | 660 | /** 661 | * Fetches the current value to check against when executing listeners. If 662 | * the listeners return value matches this one then it should be removed 663 | * automatically. It will return true by default. 664 | * 665 | * @return {*|Boolean} The current value to check for or the default, true. 666 | * @api private 667 | */ 668 | proto._getOnceReturnValue = function _getOnceReturnValue() { 669 | if (this.hasOwnProperty('_onceReturnValue')) { 670 | return this._onceReturnValue; 671 | } 672 | else { 673 | return true; 674 | } 675 | }; 676 | 677 | /** 678 | * Fetches the events object and creates one if required. 679 | * 680 | * @return {Object} The events storage object. 681 | * @api private 682 | */ 683 | proto._getEvents = function _getEvents() { 684 | return this._events || (this._events = {}); 685 | }; 686 | 687 | /** 688 | * Reverts the global {@link EventEmitter} to its previous value and returns a reference to this version. 689 | * 690 | * @return {Function} Non conflicting EventEmitter class. 691 | */ 692 | EventEmitter.noConflict = function noConflict() { 693 | exports.EventEmitter = originalGlobalValue; 694 | return EventEmitter; 695 | }; 696 | 697 | // Expose the class either via AMD, CommonJS or the global object 698 | if (typeof define === 'function' && define.amd) { 699 | define('eventEmitter/EventEmitter',[],function () { 700 | return EventEmitter; 701 | }); 702 | } 703 | else if (typeof module === 'object' && module.exports){ 704 | module.exports = EventEmitter; 705 | } 706 | else { 707 | exports.EventEmitter = EventEmitter; 708 | } 709 | }.call(this)); 710 | 711 | /*! 712 | * eventie v1.0.6 713 | * event binding helper 714 | * eventie.bind( elem, 'click', myFn ) 715 | * eventie.unbind( elem, 'click', myFn ) 716 | * MIT license 717 | */ 718 | 719 | /*jshint browser: true, undef: true, unused: true */ 720 | /*global define: false, module: false */ 721 | 722 | ( function( window ) { 723 | 724 | 725 | 726 | var docElem = document.documentElement; 727 | 728 | var bind = function() {}; 729 | 730 | function getIEEvent( obj ) { 731 | var event = window.event; 732 | // add event.target 733 | event.target = event.target || event.srcElement || obj; 734 | return event; 735 | } 736 | 737 | if ( docElem.addEventListener ) { 738 | bind = function( obj, type, fn ) { 739 | obj.addEventListener( type, fn, false ); 740 | }; 741 | } else if ( docElem.attachEvent ) { 742 | bind = function( obj, type, fn ) { 743 | obj[ type + fn ] = fn.handleEvent ? 744 | function() { 745 | var event = getIEEvent( obj ); 746 | fn.handleEvent.call( fn, event ); 747 | } : 748 | function() { 749 | var event = getIEEvent( obj ); 750 | fn.call( obj, event ); 751 | }; 752 | obj.attachEvent( "on" + type, obj[ type + fn ] ); 753 | }; 754 | } 755 | 756 | var unbind = function() {}; 757 | 758 | if ( docElem.removeEventListener ) { 759 | unbind = function( obj, type, fn ) { 760 | obj.removeEventListener( type, fn, false ); 761 | }; 762 | } else if ( docElem.detachEvent ) { 763 | unbind = function( obj, type, fn ) { 764 | obj.detachEvent( "on" + type, obj[ type + fn ] ); 765 | try { 766 | delete obj[ type + fn ]; 767 | } catch ( err ) { 768 | // can't delete window object properties 769 | obj[ type + fn ] = undefined; 770 | } 771 | }; 772 | } 773 | 774 | var eventie = { 775 | bind: bind, 776 | unbind: unbind 777 | }; 778 | 779 | // ----- module definition ----- // 780 | 781 | if ( typeof define === 'function' && define.amd ) { 782 | // AMD 783 | define( 'eventie/eventie',eventie ); 784 | } else if ( typeof exports === 'object' ) { 785 | // CommonJS 786 | module.exports = eventie; 787 | } else { 788 | // browser global 789 | window.eventie = eventie; 790 | } 791 | 792 | })( window ); 793 | 794 | /*! 795 | * getStyleProperty v1.0.4 796 | * original by kangax 797 | * http://perfectionkills.com/feature-testing-css-properties/ 798 | * MIT license 799 | */ 800 | 801 | /*jshint browser: true, strict: true, undef: true */ 802 | /*global define: false, exports: false, module: false */ 803 | 804 | ( function( window ) { 805 | 806 | 807 | 808 | var prefixes = 'Webkit Moz ms Ms O'.split(' '); 809 | var docElemStyle = document.documentElement.style; 810 | 811 | function getStyleProperty( propName ) { 812 | if ( !propName ) { 813 | return; 814 | } 815 | 816 | // test standard property first 817 | if ( typeof docElemStyle[ propName ] === 'string' ) { 818 | return propName; 819 | } 820 | 821 | // capitalize 822 | propName = propName.charAt(0).toUpperCase() + propName.slice(1); 823 | 824 | // test vendor specific properties 825 | var prefixed; 826 | for ( var i=0, len = prefixes.length; i < len; i++ ) { 827 | prefixed = prefixes[i] + propName; 828 | if ( typeof docElemStyle[ prefixed ] === 'string' ) { 829 | return prefixed; 830 | } 831 | } 832 | } 833 | 834 | // transport 835 | if ( typeof define === 'function' && define.amd ) { 836 | // AMD 837 | define( 'get-style-property/get-style-property',[],function() { 838 | return getStyleProperty; 839 | }); 840 | } else if ( typeof exports === 'object' ) { 841 | // CommonJS for Component 842 | module.exports = getStyleProperty; 843 | } else { 844 | // browser global 845 | window.getStyleProperty = getStyleProperty; 846 | } 847 | 848 | })( window ); 849 | 850 | /*! 851 | * getSize v1.2.2 852 | * measure size of elements 853 | * MIT license 854 | */ 855 | 856 | /*jshint browser: true, strict: true, undef: true, unused: true */ 857 | /*global define: false, exports: false, require: false, module: false, console: false */ 858 | 859 | ( function( window, undefined ) { 860 | 861 | 862 | 863 | // -------------------------- helpers -------------------------- // 864 | 865 | // get a number from a string, not a percentage 866 | function getStyleSize( value ) { 867 | var num = parseFloat( value ); 868 | // not a percent like '100%', and a number 869 | var isValid = value.indexOf('%') === -1 && !isNaN( num ); 870 | return isValid && num; 871 | } 872 | 873 | function noop() {} 874 | 875 | var logError = typeof console === 'undefined' ? noop : 876 | function( message ) { 877 | console.error( message ); 878 | }; 879 | 880 | // -------------------------- measurements -------------------------- // 881 | 882 | var measurements = [ 883 | 'paddingLeft', 884 | 'paddingRight', 885 | 'paddingTop', 886 | 'paddingBottom', 887 | 'marginLeft', 888 | 'marginRight', 889 | 'marginTop', 890 | 'marginBottom', 891 | 'borderLeftWidth', 892 | 'borderRightWidth', 893 | 'borderTopWidth', 894 | 'borderBottomWidth' 895 | ]; 896 | 897 | function getZeroSize() { 898 | var size = { 899 | width: 0, 900 | height: 0, 901 | innerWidth: 0, 902 | innerHeight: 0, 903 | outerWidth: 0, 904 | outerHeight: 0 905 | }; 906 | for ( var i=0, len = measurements.length; i < len; i++ ) { 907 | var measurement = measurements[i]; 908 | size[ measurement ] = 0; 909 | } 910 | return size; 911 | } 912 | 913 | 914 | 915 | function defineGetSize( getStyleProperty ) { 916 | 917 | // -------------------------- setup -------------------------- // 918 | 919 | var isSetup = false; 920 | 921 | var getStyle, boxSizingProp, isBoxSizeOuter; 922 | 923 | /** 924 | * setup vars and functions 925 | * do it on initial getSize(), rather than on script load 926 | * For Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=548397 927 | */ 928 | function setup() { 929 | // setup once 930 | if ( isSetup ) { 931 | return; 932 | } 933 | isSetup = true; 934 | 935 | var getComputedStyle = window.getComputedStyle; 936 | getStyle = ( function() { 937 | var getStyleFn = getComputedStyle ? 938 | function( elem ) { 939 | return getComputedStyle( elem, null ); 940 | } : 941 | function( elem ) { 942 | return elem.currentStyle; 943 | }; 944 | 945 | return function getStyle( elem ) { 946 | var style = getStyleFn( elem ); 947 | if ( !style ) { 948 | logError( 'Style returned ' + style + 949 | '. Are you running this code in a hidden iframe on Firefox? ' + 950 | 'See http://bit.ly/getsizebug1' ); 951 | } 952 | return style; 953 | }; 954 | })(); 955 | 956 | // -------------------------- box sizing -------------------------- // 957 | 958 | boxSizingProp = getStyleProperty('boxSizing'); 959 | 960 | /** 961 | * WebKit measures the outer-width on style.width on border-box elems 962 | * IE & Firefox measures the inner-width 963 | */ 964 | if ( boxSizingProp ) { 965 | var div = document.createElement('div'); 966 | div.style.width = '200px'; 967 | div.style.padding = '1px 2px 3px 4px'; 968 | div.style.borderStyle = 'solid'; 969 | div.style.borderWidth = '1px 2px 3px 4px'; 970 | div.style[ boxSizingProp ] = 'border-box'; 971 | 972 | var body = document.body || document.documentElement; 973 | body.appendChild( div ); 974 | var style = getStyle( div ); 975 | 976 | isBoxSizeOuter = getStyleSize( style.width ) === 200; 977 | body.removeChild( div ); 978 | } 979 | 980 | } 981 | 982 | // -------------------------- getSize -------------------------- // 983 | 984 | function getSize( elem ) { 985 | setup(); 986 | 987 | // use querySeletor if elem is string 988 | if ( typeof elem === 'string' ) { 989 | elem = document.querySelector( elem ); 990 | } 991 | 992 | // do not proceed on non-objects 993 | if ( !elem || typeof elem !== 'object' || !elem.nodeType ) { 994 | return; 995 | } 996 | 997 | var style = getStyle( elem ); 998 | 999 | // if hidden, everything is 0 1000 | if ( style.display === 'none' ) { 1001 | return getZeroSize(); 1002 | } 1003 | 1004 | var size = {}; 1005 | size.width = elem.offsetWidth; 1006 | size.height = elem.offsetHeight; 1007 | 1008 | var isBorderBox = size.isBorderBox = !!( boxSizingProp && 1009 | style[ boxSizingProp ] && style[ boxSizingProp ] === 'border-box' ); 1010 | 1011 | // get all measurements 1012 | for ( var i=0, len = measurements.length; i < len; i++ ) { 1013 | var measurement = measurements[i]; 1014 | var value = style[ measurement ]; 1015 | value = mungeNonPixel( elem, value ); 1016 | var num = parseFloat( value ); 1017 | // any 'auto', 'medium' value will be 0 1018 | size[ measurement ] = !isNaN( num ) ? num : 0; 1019 | } 1020 | 1021 | var paddingWidth = size.paddingLeft + size.paddingRight; 1022 | var paddingHeight = size.paddingTop + size.paddingBottom; 1023 | var marginWidth = size.marginLeft + size.marginRight; 1024 | var marginHeight = size.marginTop + size.marginBottom; 1025 | var borderWidth = size.borderLeftWidth + size.borderRightWidth; 1026 | var borderHeight = size.borderTopWidth + size.borderBottomWidth; 1027 | 1028 | var isBorderBoxSizeOuter = isBorderBox && isBoxSizeOuter; 1029 | 1030 | // overwrite width and height if we can get it from style 1031 | var styleWidth = getStyleSize( style.width ); 1032 | if ( styleWidth !== false ) { 1033 | size.width = styleWidth + 1034 | // add padding and border unless it's already including it 1035 | ( isBorderBoxSizeOuter ? 0 : paddingWidth + borderWidth ); 1036 | } 1037 | 1038 | var styleHeight = getStyleSize( style.height ); 1039 | if ( styleHeight !== false ) { 1040 | size.height = styleHeight + 1041 | // add padding and border unless it's already including it 1042 | ( isBorderBoxSizeOuter ? 0 : paddingHeight + borderHeight ); 1043 | } 1044 | 1045 | size.innerWidth = size.width - ( paddingWidth + borderWidth ); 1046 | size.innerHeight = size.height - ( paddingHeight + borderHeight ); 1047 | 1048 | size.outerWidth = size.width + marginWidth; 1049 | size.outerHeight = size.height + marginHeight; 1050 | 1051 | return size; 1052 | } 1053 | 1054 | // IE8 returns percent values, not pixels 1055 | // taken from jQuery's curCSS 1056 | function mungeNonPixel( elem, value ) { 1057 | // IE8 and has percent value 1058 | if ( window.getComputedStyle || value.indexOf('%') === -1 ) { 1059 | return value; 1060 | } 1061 | var style = elem.style; 1062 | // Remember the original values 1063 | var left = style.left; 1064 | var rs = elem.runtimeStyle; 1065 | var rsLeft = rs && rs.left; 1066 | 1067 | // Put in the new values to get a computed value out 1068 | if ( rsLeft ) { 1069 | rs.left = elem.currentStyle.left; 1070 | } 1071 | style.left = value; 1072 | value = style.pixelLeft; 1073 | 1074 | // Revert the changed values 1075 | style.left = left; 1076 | if ( rsLeft ) { 1077 | rs.left = rsLeft; 1078 | } 1079 | 1080 | return value; 1081 | } 1082 | 1083 | return getSize; 1084 | 1085 | } 1086 | 1087 | // transport 1088 | if ( typeof define === 'function' && define.amd ) { 1089 | // AMD for RequireJS 1090 | define( 'get-size/get-size',[ 'get-style-property/get-style-property' ], defineGetSize ); 1091 | } else if ( typeof exports === 'object' ) { 1092 | // CommonJS for Component 1093 | module.exports = defineGetSize( require('desandro-get-style-property') ); 1094 | } else { 1095 | // browser global 1096 | window.getSize = defineGetSize( window.getStyleProperty ); 1097 | } 1098 | 1099 | })( window ); 1100 | 1101 | /*! 1102 | * docReady v1.0.4 1103 | * Cross browser DOMContentLoaded event emitter 1104 | * MIT license 1105 | */ 1106 | 1107 | /*jshint browser: true, strict: true, undef: true, unused: true*/ 1108 | /*global define: false, require: false, module: false */ 1109 | 1110 | ( function( window ) { 1111 | 1112 | 1113 | 1114 | var document = window.document; 1115 | // collection of functions to be triggered on ready 1116 | var queue = []; 1117 | 1118 | function docReady( fn ) { 1119 | // throw out non-functions 1120 | if ( typeof fn !== 'function' ) { 1121 | return; 1122 | } 1123 | 1124 | if ( docReady.isReady ) { 1125 | // ready now, hit it 1126 | fn(); 1127 | } else { 1128 | // queue function when ready 1129 | queue.push( fn ); 1130 | } 1131 | } 1132 | 1133 | docReady.isReady = false; 1134 | 1135 | // triggered on various doc ready events 1136 | function onReady( event ) { 1137 | // bail if already triggered or IE8 document is not ready just yet 1138 | var isIE8NotReady = event.type === 'readystatechange' && document.readyState !== 'complete'; 1139 | if ( docReady.isReady || isIE8NotReady ) { 1140 | return; 1141 | } 1142 | 1143 | trigger(); 1144 | } 1145 | 1146 | function trigger() { 1147 | docReady.isReady = true; 1148 | // process queue 1149 | for ( var i=0, len = queue.length; i < len; i++ ) { 1150 | var fn = queue[i]; 1151 | fn(); 1152 | } 1153 | } 1154 | 1155 | function defineDocReady( eventie ) { 1156 | // trigger ready if page is ready 1157 | if ( document.readyState === 'complete' ) { 1158 | trigger(); 1159 | } else { 1160 | // listen for events 1161 | eventie.bind( document, 'DOMContentLoaded', onReady ); 1162 | eventie.bind( document, 'readystatechange', onReady ); 1163 | eventie.bind( window, 'load', onReady ); 1164 | } 1165 | 1166 | return docReady; 1167 | } 1168 | 1169 | // transport 1170 | if ( typeof define === 'function' && define.amd ) { 1171 | // AMD 1172 | define( 'doc-ready/doc-ready',[ 'eventie/eventie' ], defineDocReady ); 1173 | } else if ( typeof exports === 'object' ) { 1174 | module.exports = defineDocReady( require('eventie') ); 1175 | } else { 1176 | // browser global 1177 | window.docReady = defineDocReady( window.eventie ); 1178 | } 1179 | 1180 | })( window ); 1181 | 1182 | /** 1183 | * matchesSelector v1.0.3 1184 | * matchesSelector( element, '.selector' ) 1185 | * MIT license 1186 | */ 1187 | 1188 | /*jshint browser: true, strict: true, undef: true, unused: true */ 1189 | /*global define: false, module: false */ 1190 | 1191 | ( function( ElemProto ) { 1192 | 1193 | 1194 | 1195 | var matchesMethod = ( function() { 1196 | // check for the standard method name first 1197 | if ( ElemProto.matches ) { 1198 | return 'matches'; 1199 | } 1200 | // check un-prefixed 1201 | if ( ElemProto.matchesSelector ) { 1202 | return 'matchesSelector'; 1203 | } 1204 | // check vendor prefixes 1205 | var prefixes = [ 'webkit', 'moz', 'ms', 'o' ]; 1206 | 1207 | for ( var i=0, len = prefixes.length; i < len; i++ ) { 1208 | var prefix = prefixes[i]; 1209 | var method = prefix + 'MatchesSelector'; 1210 | if ( ElemProto[ method ] ) { 1211 | return method; 1212 | } 1213 | } 1214 | })(); 1215 | 1216 | // ----- match ----- // 1217 | 1218 | function match( elem, selector ) { 1219 | return elem[ matchesMethod ]( selector ); 1220 | } 1221 | 1222 | // ----- appendToFragment ----- // 1223 | 1224 | function checkParent( elem ) { 1225 | // not needed if already has parent 1226 | if ( elem.parentNode ) { 1227 | return; 1228 | } 1229 | var fragment = document.createDocumentFragment(); 1230 | fragment.appendChild( elem ); 1231 | } 1232 | 1233 | // ----- query ----- // 1234 | 1235 | // fall back to using QSA 1236 | // thx @jonathantneal https://gist.github.com/3062955 1237 | function query( elem, selector ) { 1238 | // append to fragment if no parent 1239 | checkParent( elem ); 1240 | 1241 | // match elem with all selected elems of parent 1242 | var elems = elem.parentNode.querySelectorAll( selector ); 1243 | for ( var i=0, len = elems.length; i < len; i++ ) { 1244 | // return true if match 1245 | if ( elems[i] === elem ) { 1246 | return true; 1247 | } 1248 | } 1249 | // otherwise return false 1250 | return false; 1251 | } 1252 | 1253 | // ----- matchChild ----- // 1254 | 1255 | function matchChild( elem, selector ) { 1256 | checkParent( elem ); 1257 | return match( elem, selector ); 1258 | } 1259 | 1260 | // ----- matchesSelector ----- // 1261 | 1262 | var matchesSelector; 1263 | 1264 | if ( matchesMethod ) { 1265 | // IE9 supports matchesSelector, but doesn't work on orphaned elems 1266 | // check for that 1267 | var div = document.createElement('div'); 1268 | var supportsOrphans = match( div, 'div' ); 1269 | matchesSelector = supportsOrphans ? match : matchChild; 1270 | } else { 1271 | matchesSelector = query; 1272 | } 1273 | 1274 | // transport 1275 | if ( typeof define === 'function' && define.amd ) { 1276 | // AMD 1277 | define( 'matches-selector/matches-selector',[],function() { 1278 | return matchesSelector; 1279 | }); 1280 | } else if ( typeof exports === 'object' ) { 1281 | module.exports = matchesSelector; 1282 | } 1283 | else { 1284 | // browser global 1285 | window.matchesSelector = matchesSelector; 1286 | } 1287 | 1288 | })( Element.prototype ); 1289 | 1290 | /** 1291 | * Fizzy UI utils v1.0.1 1292 | * MIT license 1293 | */ 1294 | 1295 | /*jshint browser: true, undef: true, unused: true, strict: true */ 1296 | 1297 | ( function( window, factory ) { 1298 | /*global define: false, module: false, require: false */ 1299 | 1300 | // universal module definition 1301 | 1302 | if ( typeof define == 'function' && define.amd ) { 1303 | // AMD 1304 | define( 'fizzy-ui-utils/utils',[ 1305 | 'doc-ready/doc-ready', 1306 | 'matches-selector/matches-selector' 1307 | ], function( docReady, matchesSelector ) { 1308 | return factory( window, docReady, matchesSelector ); 1309 | }); 1310 | } else if ( typeof exports == 'object' ) { 1311 | // CommonJS 1312 | module.exports = factory( 1313 | window, 1314 | require('doc-ready'), 1315 | require('desandro-matches-selector') 1316 | ); 1317 | } else { 1318 | // browser global 1319 | window.fizzyUIUtils = factory( 1320 | window, 1321 | window.docReady, 1322 | window.matchesSelector 1323 | ); 1324 | } 1325 | 1326 | }( window, function factory( window, docReady, matchesSelector ) { 1327 | 1328 | 1329 | 1330 | var utils = {}; 1331 | 1332 | // ----- extend ----- // 1333 | 1334 | // extends objects 1335 | utils.extend = function( a, b ) { 1336 | for ( var prop in b ) { 1337 | a[ prop ] = b[ prop ]; 1338 | } 1339 | return a; 1340 | }; 1341 | 1342 | // ----- modulo ----- // 1343 | 1344 | utils.modulo = function( num, div ) { 1345 | return ( ( num % div ) + div ) % div; 1346 | }; 1347 | 1348 | // ----- isArray ----- // 1349 | 1350 | var objToString = Object.prototype.toString; 1351 | utils.isArray = function( obj ) { 1352 | return objToString.call( obj ) == '[object Array]'; 1353 | }; 1354 | 1355 | // ----- makeArray ----- // 1356 | 1357 | // turn element or nodeList into an array 1358 | utils.makeArray = function( obj ) { 1359 | var ary = []; 1360 | if ( utils.isArray( obj ) ) { 1361 | // use object if already an array 1362 | ary = obj; 1363 | } else if ( obj && typeof obj.length == 'number' ) { 1364 | // convert nodeList to array 1365 | for ( var i=0, len = obj.length; i < len; i++ ) { 1366 | ary.push( obj[i] ); 1367 | } 1368 | } else { 1369 | // array of single index 1370 | ary.push( obj ); 1371 | } 1372 | return ary; 1373 | }; 1374 | 1375 | // ----- indexOf ----- // 1376 | 1377 | // index of helper cause IE8 1378 | utils.indexOf = Array.prototype.indexOf ? function( ary, obj ) { 1379 | return ary.indexOf( obj ); 1380 | } : function( ary, obj ) { 1381 | for ( var i=0, len = ary.length; i < len; i++ ) { 1382 | if ( ary[i] === obj ) { 1383 | return i; 1384 | } 1385 | } 1386 | return -1; 1387 | }; 1388 | 1389 | // ----- removeFrom ----- // 1390 | 1391 | utils.removeFrom = function( ary, obj ) { 1392 | var index = utils.indexOf( ary, obj ); 1393 | if ( index != -1 ) { 1394 | ary.splice( index, 1 ); 1395 | } 1396 | }; 1397 | 1398 | // ----- isElement ----- // 1399 | 1400 | // http://stackoverflow.com/a/384380/182183 1401 | utils.isElement = ( typeof HTMLElement == 'function' || typeof HTMLElement == 'object' ) ? 1402 | function isElementDOM2( obj ) { 1403 | return obj instanceof HTMLElement; 1404 | } : 1405 | function isElementQuirky( obj ) { 1406 | return obj && typeof obj == 'object' && 1407 | obj.nodeType == 1 && typeof obj.nodeName == 'string'; 1408 | }; 1409 | 1410 | // ----- setText ----- // 1411 | 1412 | utils.setText = ( function() { 1413 | var setTextProperty; 1414 | function setText( elem, text ) { 1415 | // only check setTextProperty once 1416 | setTextProperty = setTextProperty || ( document.documentElement.textContent !== undefined ? 'textContent' : 'innerText' ); 1417 | elem[ setTextProperty ] = text; 1418 | } 1419 | return setText; 1420 | })(); 1421 | 1422 | // ----- getParent ----- // 1423 | 1424 | utils.getParent = function( elem, selector ) { 1425 | while ( elem != document.body ) { 1426 | elem = elem.parentNode; 1427 | if ( matchesSelector( elem, selector ) ) { 1428 | return elem; 1429 | } 1430 | } 1431 | }; 1432 | 1433 | // ----- getQueryElement ----- // 1434 | 1435 | // use element as selector string 1436 | utils.getQueryElement = function( elem ) { 1437 | if ( typeof elem == 'string' ) { 1438 | return document.querySelector( elem ); 1439 | } 1440 | return elem; 1441 | }; 1442 | 1443 | // ----- handleEvent ----- // 1444 | 1445 | // enable .ontype to trigger from .addEventListener( elem, 'type' ) 1446 | utils.handleEvent = function( event ) { 1447 | var method = 'on' + event.type; 1448 | if ( this[ method ] ) { 1449 | this[ method ]( event ); 1450 | } 1451 | }; 1452 | 1453 | // ----- filterFindElements ----- // 1454 | 1455 | utils.filterFindElements = function( elems, selector ) { 1456 | // make array of elems 1457 | elems = utils.makeArray( elems ); 1458 | var ffElems = []; 1459 | 1460 | for ( var i=0, len = elems.length; i < len; i++ ) { 1461 | var elem = elems[i]; 1462 | // check that elem is an actual element 1463 | if ( !utils.isElement( elem ) ) { 1464 | continue; 1465 | } 1466 | // filter & find items if we have a selector 1467 | if ( selector ) { 1468 | // filter siblings 1469 | if ( matchesSelector( elem, selector ) ) { 1470 | ffElems.push( elem ); 1471 | } 1472 | // find children 1473 | var childElems = elem.querySelectorAll( selector ); 1474 | // concat childElems to filterFound array 1475 | for ( var j=0, jLen = childElems.length; j < jLen; j++ ) { 1476 | ffElems.push( childElems[j] ); 1477 | } 1478 | } else { 1479 | ffElems.push( elem ); 1480 | } 1481 | } 1482 | 1483 | return ffElems; 1484 | }; 1485 | 1486 | // ----- debounceMethod ----- // 1487 | 1488 | utils.debounceMethod = function( _class, methodName, threshold ) { 1489 | // original method 1490 | var method = _class.prototype[ methodName ]; 1491 | var timeoutName = methodName + 'Timeout'; 1492 | 1493 | _class.prototype[ methodName ] = function() { 1494 | var timeout = this[ timeoutName ]; 1495 | if ( timeout ) { 1496 | clearTimeout( timeout ); 1497 | } 1498 | var args = arguments; 1499 | 1500 | var _this = this; 1501 | this[ timeoutName ] = setTimeout( function() { 1502 | method.apply( _this, args ); 1503 | delete _this[ timeoutName ]; 1504 | }, threshold || 100 ); 1505 | }; 1506 | }; 1507 | 1508 | // ----- htmlInit ----- // 1509 | 1510 | // http://jamesroberts.name/blog/2010/02/22/string-functions-for-javascript-trim-to-camel-case-to-dashed-and-to-underscore/ 1511 | utils.toDashed = function( str ) { 1512 | return str.replace( /(.)([A-Z])/g, function( match, $1, $2 ) { 1513 | return $1 + '-' + $2; 1514 | }).toLowerCase(); 1515 | }; 1516 | 1517 | var console = window.console; 1518 | /** 1519 | * allow user to initialize classes via .js-namespace class 1520 | * htmlInit( Widget, 'widgetName' ) 1521 | * options are parsed from data-namespace-option attribute 1522 | */ 1523 | utils.htmlInit = function( WidgetClass, namespace ) { 1524 | docReady( function() { 1525 | var dashedNamespace = utils.toDashed( namespace ); 1526 | var elems = document.querySelectorAll( '.js-' + dashedNamespace ); 1527 | var dataAttr = 'data-' + dashedNamespace + '-options'; 1528 | 1529 | for ( var i=0, len = elems.length; i < len; i++ ) { 1530 | var elem = elems[i]; 1531 | var attr = elem.getAttribute( dataAttr ); 1532 | var options; 1533 | try { 1534 | options = attr && JSON.parse( attr ); 1535 | } catch ( error ) { 1536 | // log error, do not initialize 1537 | if ( console ) { 1538 | console.error( 'Error parsing ' + dataAttr + ' on ' + 1539 | elem.nodeName.toLowerCase() + ( elem.id ? '#' + elem.id : '' ) + ': ' + 1540 | error ); 1541 | } 1542 | continue; 1543 | } 1544 | // initialize 1545 | var instance = new WidgetClass( elem, options ); 1546 | // make available via $().data('layoutname') 1547 | var jQuery = window.jQuery; 1548 | if ( jQuery ) { 1549 | jQuery.data( elem, namespace, instance ); 1550 | } 1551 | } 1552 | }); 1553 | }; 1554 | 1555 | // ----- ----- // 1556 | 1557 | return utils; 1558 | 1559 | })); 1560 | 1561 | ( function( window, factory ) { 1562 | 1563 | // universal module definition 1564 | 1565 | if ( typeof define == 'function' && define.amd ) { 1566 | // AMD 1567 | define( 'flickity/js/cell',[ 1568 | 'get-size/get-size' 1569 | ], function( getSize ) { 1570 | return factory( window, getSize ); 1571 | }); 1572 | } else if ( typeof exports == 'object' ) { 1573 | // CommonJS 1574 | module.exports = factory( 1575 | window, 1576 | require('get-size') 1577 | ); 1578 | } else { 1579 | // browser global 1580 | window.Flickity = window.Flickity || {}; 1581 | window.Flickity.Cell = factory( 1582 | window, 1583 | window.getSize 1584 | ); 1585 | } 1586 | 1587 | }( window, function factory( window, getSize ) { 1588 | 1589 | 1590 | 1591 | function Cell( elem, parent ) { 1592 | this.element = elem; 1593 | this.parent = parent; 1594 | 1595 | this.create(); 1596 | } 1597 | 1598 | var isIE8 = 'attachEvent' in window; 1599 | 1600 | Cell.prototype.create = function() { 1601 | this.element.style.position = 'absolute'; 1602 | // IE8 prevent child from changing focus http://stackoverflow.com/a/17525223/182183 1603 | if ( isIE8 ) { 1604 | this.element.setAttribute( 'unselectable', 'on' ); 1605 | } 1606 | this.x = 0; 1607 | this.shift = 0; 1608 | }; 1609 | 1610 | Cell.prototype.destroy = function() { 1611 | // reset style 1612 | this.element.style.position = ''; 1613 | var side = this.parent.originSide; 1614 | this.element.style[ side ] = ''; 1615 | }; 1616 | 1617 | Cell.prototype.getSize = function() { 1618 | this.size = getSize( this.element ); 1619 | }; 1620 | 1621 | Cell.prototype.setPosition = function( x ) { 1622 | this.x = x; 1623 | this.setDefaultTarget(); 1624 | this.renderPosition( x ); 1625 | }; 1626 | 1627 | Cell.prototype.setDefaultTarget = function() { 1628 | var marginProperty = this.parent.originSide == 'left' ? 'marginLeft' : 'marginRight'; 1629 | this.target = this.x + this.size[ marginProperty ] + 1630 | this.size.width * this.parent.cellAlign; 1631 | }; 1632 | 1633 | Cell.prototype.renderPosition = function( x ) { 1634 | // render position of cell with in slider 1635 | var side = this.parent.originSide; 1636 | this.element.style[ side ] = this.parent.getPositionValue( x ); 1637 | }; 1638 | 1639 | /** 1640 | * @param {Integer} factor - 0, 1, or -1 1641 | **/ 1642 | Cell.prototype.wrapShift = function( shift ) { 1643 | this.shift = shift; 1644 | this.renderPosition( this.x + this.parent.slideableWidth * shift ); 1645 | }; 1646 | 1647 | Cell.prototype.remove = function() { 1648 | this.element.parentNode.removeChild( this.element ); 1649 | }; 1650 | 1651 | return Cell; 1652 | 1653 | })); 1654 | 1655 | ( function( window, factory ) { 1656 | 1657 | // universal module definition 1658 | 1659 | if ( typeof define == 'function' && define.amd ) { 1660 | // AMD 1661 | define( 'flickity/js/animate',[ 1662 | 'get-style-property/get-style-property', 1663 | 'fizzy-ui-utils/utils' 1664 | ], function( getStyleProperty, utils ) { 1665 | return factory( window, getStyleProperty, utils ); 1666 | }); 1667 | } else if ( typeof exports == 'object' ) { 1668 | // CommonJS 1669 | module.exports = factory( 1670 | window, 1671 | require('desandro-get-style-property'), 1672 | require('fizzy-ui-utils') 1673 | ); 1674 | } else { 1675 | // browser global 1676 | window.Flickity = window.Flickity || {}; 1677 | window.Flickity.animatePrototype = factory( 1678 | window, 1679 | window.getStyleProperty, 1680 | window.fizzyUIUtils 1681 | ); 1682 | } 1683 | 1684 | }( window, function factory( window, getStyleProperty, utils ) { 1685 | 1686 | 1687 | 1688 | // -------------------------- requestAnimationFrame -------------------------- // 1689 | 1690 | // https://gist.github.com/1866474 1691 | 1692 | var lastTime = 0; 1693 | var prefixes = 'webkit moz ms o'.split(' '); 1694 | // get unprefixed rAF and cAF, if present 1695 | var requestAnimationFrame = window.requestAnimationFrame; 1696 | var cancelAnimationFrame = window.cancelAnimationFrame; 1697 | // loop through vendor prefixes and get prefixed rAF and cAF 1698 | var prefix; 1699 | for( var i = 0; i < prefixes.length; i++ ) { 1700 | if ( requestAnimationFrame && cancelAnimationFrame ) { 1701 | break; 1702 | } 1703 | prefix = prefixes[i]; 1704 | requestAnimationFrame = requestAnimationFrame || window[ prefix + 'RequestAnimationFrame' ]; 1705 | cancelAnimationFrame = cancelAnimationFrame || window[ prefix + 'CancelAnimationFrame' ] || 1706 | window[ prefix + 'CancelRequestAnimationFrame' ]; 1707 | } 1708 | 1709 | // fallback to setTimeout and clearTimeout if either request/cancel is not supported 1710 | if ( !requestAnimationFrame || !cancelAnimationFrame ) { 1711 | requestAnimationFrame = function( callback ) { 1712 | var currTime = new Date().getTime(); 1713 | var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) ); 1714 | var id = window.setTimeout( function() { 1715 | callback( currTime + timeToCall ); 1716 | }, timeToCall ); 1717 | lastTime = currTime + timeToCall; 1718 | return id; 1719 | }; 1720 | 1721 | cancelAnimationFrame = function( id ) { 1722 | window.clearTimeout( id ); 1723 | }; 1724 | } 1725 | 1726 | // -------------------------- animate -------------------------- // 1727 | 1728 | var proto = {}; 1729 | 1730 | proto.startAnimation = function() { 1731 | if ( this.isAnimating ) { 1732 | return; 1733 | } 1734 | 1735 | this.isAnimating = true; 1736 | this.restingFrames = 0; 1737 | this.animate(); 1738 | }; 1739 | 1740 | proto.animate = function() { 1741 | this.applySelectedAttraction(); 1742 | 1743 | var previousX = this.x; 1744 | 1745 | this.integratePhysics(); 1746 | this.positionSlider(); 1747 | this.settle( previousX ); 1748 | // animate next frame 1749 | if ( this.isAnimating ) { 1750 | var _this = this; 1751 | requestAnimationFrame( function animateFrame() { 1752 | _this.animate(); 1753 | }); 1754 | } 1755 | 1756 | /** / 1757 | // log animation frame rate 1758 | var now = new Date(); 1759 | if ( this.then ) { 1760 | console.log( ~~( 1000 / (now-this.then)) + 'fps' ) 1761 | } 1762 | this.then = now; 1763 | /**/ 1764 | }; 1765 | 1766 | 1767 | var transformProperty = getStyleProperty('transform'); 1768 | var is3d = !!getStyleProperty('perspective'); 1769 | 1770 | proto.positionSlider = function() { 1771 | var x = this.x; 1772 | // wrap position around 1773 | if ( this.options.wrapAround && this.cells.length > 1 ) { 1774 | x = utils.modulo( x, this.slideableWidth ); 1775 | x = x - this.slideableWidth; 1776 | this.shiftWrapCells( x ); 1777 | } 1778 | 1779 | x = x + this.cursorPosition; 1780 | 1781 | // reverse if right-to-left and using transform 1782 | x = this.options.rightToLeft && transformProperty ? -x : x; 1783 | 1784 | var value = this.getPositionValue( x ); 1785 | 1786 | if ( transformProperty ) { 1787 | // use 3D tranforms for hardware acceleration on iOS 1788 | // but use 2D when settled, for better font-rendering 1789 | this.slider.style[ transformProperty ] = is3d && this.isAnimating ? 1790 | 'translate3d(' + value + ',0,0)' : 'translateX(' + value + ')'; 1791 | } else { 1792 | this.slider.style[ this.originSide ] = value; 1793 | } 1794 | }; 1795 | 1796 | proto.positionSliderAtSelected = function() { 1797 | if ( !this.cells.length ) { 1798 | return; 1799 | } 1800 | var selectedCell = this.cells[ this.selectedIndex ]; 1801 | this.x = -selectedCell.target; 1802 | this.positionSlider(); 1803 | }; 1804 | 1805 | proto.getPositionValue = function( position ) { 1806 | if ( this.options.percentPosition ) { 1807 | // percent position, round to 2 digits, like 12.34% 1808 | return ( Math.round( ( position / this.size.innerWidth ) * 10000 ) * 0.01 )+ '%'; 1809 | } else { 1810 | // pixel positioning 1811 | return Math.round( position ) + 'px'; 1812 | } 1813 | }; 1814 | 1815 | proto.settle = function( previousX ) { 1816 | // keep track of frames where x hasn't moved 1817 | if ( !this.isPointerDown && Math.round( this.x * 100 ) == Math.round( previousX * 100 ) ) { 1818 | this.restingFrames++; 1819 | } 1820 | // stop animating if resting for 3 or more frames 1821 | if ( this.restingFrames > 2 ) { 1822 | this.isAnimating = false; 1823 | delete this.isFreeScrolling; 1824 | // render position with translateX when settled 1825 | if ( is3d ) { 1826 | this.positionSlider(); 1827 | } 1828 | this.dispatchEvent('settle'); 1829 | } 1830 | }; 1831 | 1832 | proto.shiftWrapCells = function( x ) { 1833 | // shift before cells 1834 | var beforeGap = this.cursorPosition + x; 1835 | this._shiftCells( this.beforeShiftCells, beforeGap, -1 ); 1836 | // shift after cells 1837 | var afterGap = this.size.innerWidth - ( x + this.slideableWidth + this.cursorPosition ); 1838 | this._shiftCells( this.afterShiftCells, afterGap, 1 ); 1839 | }; 1840 | 1841 | proto._shiftCells = function( cells, gap, shift ) { 1842 | for ( var i=0, len = cells.length; i < len; i++ ) { 1843 | var cell = cells[i]; 1844 | var cellShift = gap > 0 ? shift : 0; 1845 | cell.wrapShift( cellShift ); 1846 | gap -= cell.size.outerWidth; 1847 | } 1848 | }; 1849 | 1850 | proto._unshiftCells = function( cells ) { 1851 | if ( !cells || !cells.length ) { 1852 | return; 1853 | } 1854 | for ( var i=0, len = cells.length; i < len; i++ ) { 1855 | cells[i].wrapShift( 0 ); 1856 | } 1857 | }; 1858 | 1859 | // -------------------------- physics -------------------------- // 1860 | 1861 | proto.integratePhysics = function() { 1862 | this.velocity += this.accel; 1863 | this.x += this.velocity; 1864 | this.velocity *= this.getFrictionFactor(); 1865 | // reset acceleration 1866 | this.accel = 0; 1867 | }; 1868 | 1869 | proto.applyForce = function( force ) { 1870 | this.accel += force; 1871 | }; 1872 | 1873 | proto.getFrictionFactor = function() { 1874 | return 1 - this.options[ this.isFreeScrolling ? 'freeScrollFriction' : 'friction' ]; 1875 | }; 1876 | 1877 | 1878 | proto.getRestingPosition = function() { 1879 | // my thanks to Steven Wittens, who simplified this math greatly 1880 | return this.x + this.velocity / ( 1 - this.getFrictionFactor() ); 1881 | }; 1882 | 1883 | 1884 | proto.applySelectedAttraction = function() { 1885 | // do not attract if pointer down or no cells 1886 | var len = this.cells.length; 1887 | if ( this.isPointerDown || this.isFreeScrolling || !len ) { 1888 | return; 1889 | } 1890 | var cell = this.cells[ this.selectedIndex ]; 1891 | var wrap = this.options.wrapAround && len > 1 ? 1892 | this.slideableWidth * Math.floor( this.selectedIndex / len ) : 0; 1893 | var distance = ( cell.target + wrap ) * -1 - this.x; 1894 | var force = distance * this.options.selectedAttraction; 1895 | this.applyForce( force ); 1896 | }; 1897 | 1898 | return proto; 1899 | 1900 | })); 1901 | 1902 | /*! 1903 | * Flickity v1.0.2 1904 | * Touch, responsive, flickable galleries 1905 | * 1906 | * Licensed GPLv3 for open source use 1907 | * or Flickity Commercial License for commercial use 1908 | * 1909 | * http://flickity.metafizzy.co 1910 | * Copyright 2015 Metafizzy 1911 | */ 1912 | 1913 | ( function( window, factory ) { 1914 | 1915 | // universal module definition 1916 | 1917 | if ( typeof define == 'function' && define.amd ) { 1918 | // AMD 1919 | define( 'flickity/js/flickity',[ 1920 | 'classie/classie', 1921 | 'eventEmitter/EventEmitter', 1922 | 'eventie/eventie', 1923 | 'get-size/get-size', 1924 | 'fizzy-ui-utils/utils', 1925 | './cell', 1926 | './animate' 1927 | ], function( classie, EventEmitter, eventie, getSize, utils, Cell, animatePrototype ) { 1928 | return factory( window, classie, EventEmitter, eventie, getSize, utils, Cell, animatePrototype ); 1929 | }); 1930 | } else if ( typeof exports == 'object' ) { 1931 | // CommonJS 1932 | module.exports = factory( 1933 | window, 1934 | require('desandro-classie'), 1935 | require('wolfy87-eventemitter'), 1936 | require('eventie'), 1937 | require('get-size'), 1938 | require('fizzy-ui-utils'), 1939 | require('./cell'), 1940 | require('./animate') 1941 | ); 1942 | } else { 1943 | // browser global 1944 | var _Flickity = window.Flickity; 1945 | 1946 | window.Flickity = factory( 1947 | window, 1948 | window.classie, 1949 | window.EventEmitter, 1950 | window.eventie, 1951 | window.getSize, 1952 | window.fizzyUIUtils, 1953 | _Flickity.Cell, 1954 | _Flickity.animatePrototype 1955 | ); 1956 | } 1957 | 1958 | }( window, function factory( window, classie, EventEmitter, eventie, getSize, 1959 | utils, Cell, animatePrototype ) { 1960 | 1961 | 1962 | 1963 | // vars 1964 | var jQuery = window.jQuery; 1965 | var getComputedStyle = window.getComputedStyle; 1966 | var console = window.console; 1967 | 1968 | function moveElements( elems, toElem ) { 1969 | elems = utils.makeArray( elems ); 1970 | while ( elems.length ) { 1971 | toElem.appendChild( elems.shift() ); 1972 | } 1973 | } 1974 | 1975 | // -------------------------- Flickity -------------------------- // 1976 | 1977 | // globally unique identifiers 1978 | var GUID = 0; 1979 | // internal store of all Flickity intances 1980 | var instances = {}; 1981 | 1982 | function Flickity( element, options ) { 1983 | var queryElement = utils.getQueryElement( element ); 1984 | if ( !queryElement ) { 1985 | if ( console ) { 1986 | console.error( 'Bad element for Flickity: ' + ( queryElement || element ) ); 1987 | } 1988 | return; 1989 | } 1990 | this.element = queryElement; 1991 | // add jQuery 1992 | if ( jQuery ) { 1993 | this.$element = jQuery( this.element ); 1994 | } 1995 | // options 1996 | this.options = utils.extend( {}, this.constructor.defaults ); 1997 | this.option( options ); 1998 | 1999 | // kick things off 2000 | this._create(); 2001 | } 2002 | 2003 | Flickity.defaults = { 2004 | accessibility: true, 2005 | cellAlign: 'center', 2006 | // cellSelector: undefined, 2007 | // contain: false, 2008 | freeScrollFriction: 0.075, // friction when free-scrolling 2009 | friction: 0.28, // friction when selecting 2010 | // initialIndex: 0, 2011 | percentPosition: true, 2012 | resize: true, 2013 | selectedAttraction: 0.025, 2014 | setGallerySize: true 2015 | // watchCSS: false, 2016 | // wrapAround: false 2017 | }; 2018 | 2019 | // hash of methods triggered on _create() 2020 | Flickity.createMethods = []; 2021 | 2022 | // inherit EventEmitter 2023 | utils.extend( Flickity.prototype, EventEmitter.prototype ); 2024 | 2025 | Flickity.prototype._create = function() { 2026 | // add id for Flickity.data 2027 | var id = this.guid = ++GUID; 2028 | this.element.flickityGUID = id; // expando 2029 | instances[ id ] = this; // associate via id 2030 | // initial properties 2031 | this.selectedIndex = this.options.initialIndex || 0; 2032 | // how many frames slider has been in same position 2033 | this.restingFrames = 0; 2034 | // initial physics properties 2035 | this.x = 0; 2036 | this.velocity = 0; 2037 | this.accel = 0; 2038 | this.originSide = this.options.rightToLeft ? 'right' : 'left'; 2039 | // create viewport & slider 2040 | this.viewport = document.createElement('div'); 2041 | this.viewport.className = 'flickity-viewport'; 2042 | Flickity.setUnselectable( this.viewport ); 2043 | this._createSlider(); 2044 | 2045 | if ( this.options.resize || this.options.watchCSS ) { 2046 | eventie.bind( window, 'resize', this ); 2047 | this.isResizeBound = true; 2048 | } 2049 | 2050 | for ( var i=0, len = Flickity.createMethods.length; i < len; i++ ) { 2051 | var method = Flickity.createMethods[i]; 2052 | this[ method ](); 2053 | } 2054 | 2055 | if ( this.options.watchCSS ) { 2056 | this.watchCSS(); 2057 | } else { 2058 | this.activate(); 2059 | } 2060 | 2061 | }; 2062 | 2063 | /** 2064 | * set options 2065 | * @param {Object} opts 2066 | */ 2067 | Flickity.prototype.option = function( opts ) { 2068 | utils.extend( this.options, opts ); 2069 | }; 2070 | 2071 | Flickity.prototype.activate = function() { 2072 | if ( this.isActive ) { 2073 | return; 2074 | } 2075 | this.isActive = true; 2076 | classie.add( this.element, 'flickity-enabled' ); 2077 | if ( this.options.rightToLeft ) { 2078 | classie.add( this.element, 'flickity-rtl' ); 2079 | } 2080 | 2081 | // move initial cell elements so they can be loaded as cells 2082 | var cellElems = this._filterFindCellElements( this.element.children ); 2083 | moveElements( cellElems, this.slider ); 2084 | this.viewport.appendChild( this.slider ); 2085 | this.element.appendChild( this.viewport ); 2086 | 2087 | this.getSize(); 2088 | // get cells from children 2089 | this.reloadCells(); 2090 | 2091 | if ( this.options.accessibility ) { 2092 | // allow element to focusable 2093 | this.element.tabIndex = 0; 2094 | // listen for key presses 2095 | eventie.bind( this.element, 'keydown', this ); 2096 | } 2097 | 2098 | this.emit('activate'); 2099 | 2100 | this.positionSliderAtSelected(); 2101 | this.select( this.selectedIndex ); 2102 | }; 2103 | 2104 | // slider positions the cells 2105 | Flickity.prototype._createSlider = function() { 2106 | // slider element does all the positioning 2107 | var slider = document.createElement('div'); 2108 | slider.className = 'flickity-slider'; 2109 | slider.style[ this.originSide ] = 0; 2110 | this.slider = slider; 2111 | }; 2112 | 2113 | Flickity.prototype._filterFindCellElements = function( elems ) { 2114 | return utils.filterFindElements( elems, this.options.cellSelector ); 2115 | }; 2116 | 2117 | // goes through all children 2118 | Flickity.prototype.reloadCells = function() { 2119 | // collection of item elements 2120 | this.cells = this._makeCells( this.slider.children ); 2121 | this.positionCells(); 2122 | this._getWrapShiftCells(); 2123 | this.setGallerySize(); 2124 | }; 2125 | 2126 | /** 2127 | * turn elements into Flickity.Cells 2128 | * @param {Array or NodeList or HTMLElement} elems 2129 | * @returns {Array} items - collection of new Flickity Cells 2130 | */ 2131 | Flickity.prototype._makeCells = function( elems ) { 2132 | var cellElems = this._filterFindCellElements( elems ); 2133 | 2134 | // create new Flickity for collection 2135 | var cells = []; 2136 | for ( var i=0, len = cellElems.length; i < len; i++ ) { 2137 | var elem = cellElems[i]; 2138 | var cell = new Cell( elem, this ); 2139 | cells.push( cell ); 2140 | } 2141 | 2142 | return cells; 2143 | }; 2144 | 2145 | Flickity.prototype.getLastCell = function() { 2146 | return this.cells[ this.cells.length - 1 ]; 2147 | }; 2148 | 2149 | // positions all cells 2150 | Flickity.prototype.positionCells = function() { 2151 | // size all cells 2152 | this._sizeCells( this.cells ); 2153 | // position all cells 2154 | this._positionCells( 0 ); 2155 | }; 2156 | 2157 | /** 2158 | * position certain cells 2159 | * @param {Integer} index - which cell to start with 2160 | */ 2161 | Flickity.prototype._positionCells = function( index ) { 2162 | // also measure maxCellHeight 2163 | // start 0 if positioning all cells 2164 | this.maxCellHeight = index ? this.maxCellHeight || 0 : 0; 2165 | var cellX = 0; 2166 | // get cellX 2167 | if ( index > 0 ) { 2168 | var startCell = this.cells[ index - 1 ]; 2169 | cellX = startCell.x + startCell.size.outerWidth; 2170 | } 2171 | var cell; 2172 | for ( var len = this.cells.length, i=index; i < len; i++ ) { 2173 | cell = this.cells[i]; 2174 | cell.setPosition( cellX ); 2175 | cellX += cell.size.outerWidth; 2176 | this.maxCellHeight = Math.max( cell.size.outerHeight, this.maxCellHeight ); 2177 | } 2178 | // keep track of cellX for wrap-around 2179 | this.slideableWidth = cellX; 2180 | // contain cell target 2181 | this._containCells(); 2182 | }; 2183 | 2184 | /** 2185 | * cell.getSize() on multiple cells 2186 | * @param {Array} cells 2187 | */ 2188 | Flickity.prototype._sizeCells = function( cells ) { 2189 | for ( var i=0, len = cells.length; i < len; i++ ) { 2190 | var cell = cells[i]; 2191 | cell.getSize(); 2192 | } 2193 | }; 2194 | 2195 | // alias _init for jQuery plugin .flickity() 2196 | Flickity.prototype._init = 2197 | Flickity.prototype.reposition = function() { 2198 | this.positionCells(); 2199 | this.positionSliderAtSelected(); 2200 | }; 2201 | 2202 | Flickity.prototype.getSize = function() { 2203 | this.size = getSize( this.element ); 2204 | this.setCellAlign(); 2205 | this.cursorPosition = this.size.innerWidth * this.cellAlign; 2206 | }; 2207 | 2208 | var cellAlignShorthands = { 2209 | // cell align, then based on origin side 2210 | center: { 2211 | left: 0.5, 2212 | right: 0.5 2213 | }, 2214 | left: { 2215 | left: 0, 2216 | right: 1 2217 | }, 2218 | right: { 2219 | right: 0, 2220 | left: 1 2221 | } 2222 | }; 2223 | 2224 | Flickity.prototype.setCellAlign = function() { 2225 | var shorthand = cellAlignShorthands[ this.options.cellAlign ]; 2226 | this.cellAlign = shorthand ? shorthand[ this.originSide ] : this.options.cellAlign; 2227 | }; 2228 | 2229 | Flickity.prototype.setGallerySize = function() { 2230 | if ( this.options.setGallerySize ) { 2231 | this.viewport.style.height = this.maxCellHeight + 'px'; 2232 | } 2233 | }; 2234 | 2235 | Flickity.prototype._getWrapShiftCells = function() { 2236 | // only for wrap-around 2237 | if ( !this.options.wrapAround ) { 2238 | return; 2239 | } 2240 | // unshift previous cells 2241 | this._unshiftCells( this.beforeShiftCells ); 2242 | this._unshiftCells( this.afterShiftCells ); 2243 | // get before cells 2244 | // initial gap 2245 | var gapX = this.cursorPosition; 2246 | var cellIndex = this.cells.length - 1; 2247 | this.beforeShiftCells = this._getGapCells( gapX, cellIndex, -1 ); 2248 | // get after cells 2249 | // ending gap between last cell and end of gallery viewport 2250 | gapX = this.size.innerWidth - this.cursorPosition; 2251 | // start cloning at first cell, working forwards 2252 | this.afterShiftCells = this._getGapCells( gapX, 0, 1 ); 2253 | }; 2254 | 2255 | Flickity.prototype._getGapCells = function( gapX, cellIndex, increment ) { 2256 | // keep adding cells until the cover the initial gap 2257 | var cells = []; 2258 | while ( gapX > 0 ) { 2259 | var cell = this.cells[ cellIndex ]; 2260 | if ( !cell ) { 2261 | break; 2262 | } 2263 | cells.push( cell ); 2264 | cellIndex += increment; 2265 | gapX -= cell.size.outerWidth; 2266 | } 2267 | return cells; 2268 | }; 2269 | 2270 | // ----- contain ----- // 2271 | 2272 | // contain cell targets so no excess sliding 2273 | Flickity.prototype._containCells = function() { 2274 | if ( !this.options.contain || this.options.wrapAround || !this.cells.length ) { 2275 | return; 2276 | } 2277 | var startMargin = this.options.rightToLeft ? 'marginRight' : 'marginLeft'; 2278 | var endMargin = this.options.rightToLeft ? 'marginLeft' : 'marginRight'; 2279 | var firstCellStartMargin = this.cells[0].size[ startMargin ]; 2280 | var lastCell = this.getLastCell(); 2281 | var contentWidth = this.slideableWidth - lastCell.size[ endMargin ]; 2282 | var endLimit = contentWidth - this.size.innerWidth * ( 1 - this.cellAlign ); 2283 | // content is less than gallery size 2284 | var isContentSmaller = contentWidth < this.size.innerWidth; 2285 | // contain each cell target 2286 | for ( var i=0, len = this.cells.length; i < len; i++ ) { 2287 | var cell = this.cells[i]; 2288 | // reset default target 2289 | cell.setDefaultTarget(); 2290 | if ( isContentSmaller ) { 2291 | // all cells fit inside gallery 2292 | cell.target = contentWidth * this.cellAlign; 2293 | } else { 2294 | // contain to bounds 2295 | cell.target = Math.max( cell.target, this.cursorPosition + firstCellStartMargin ); 2296 | cell.target = Math.min( cell.target, endLimit ); 2297 | } 2298 | } 2299 | }; 2300 | 2301 | // ----- ----- // 2302 | 2303 | /** 2304 | * emits events via eventEmitter and jQuery events 2305 | * @param {String} type - name of event 2306 | * @param {Event} event - original event 2307 | * @param {Array} args - extra arguments 2308 | */ 2309 | Flickity.prototype.dispatchEvent = function( type, event, args ) { 2310 | var emitArgs = [ event ].concat( args ); 2311 | this.emitEvent( type, emitArgs ); 2312 | 2313 | if ( jQuery && this.$element ) { 2314 | if ( event ) { 2315 | // create jQuery event 2316 | var $event = jQuery.Event( event ); 2317 | $event.type = type; 2318 | this.$element.trigger( $event, args ); 2319 | } else { 2320 | // just trigger with type if no event available 2321 | this.$element.trigger( type, args ); 2322 | } 2323 | } 2324 | }; 2325 | 2326 | // -------------------------- select -------------------------- // 2327 | 2328 | /** 2329 | * @param {Integer} index - index of the cell 2330 | * @param {Boolean} isWrap - will wrap-around to last/first if at the end 2331 | */ 2332 | Flickity.prototype.select = function( index, isWrap ) { 2333 | if ( !this.isActive ) { 2334 | return; 2335 | } 2336 | // wrap position so slider is within normal area 2337 | var len = this.cells.length; 2338 | if ( this.options.wrapAround && len > 1 ) { 2339 | if ( index < 0 ) { 2340 | this.x -= this.slideableWidth; 2341 | } else if ( index >= len ) { 2342 | this.x += this.slideableWidth; 2343 | } 2344 | } 2345 | 2346 | if ( this.options.wrapAround || isWrap ) { 2347 | index = utils.modulo( index, len ); 2348 | } 2349 | 2350 | if ( this.cells[ index ] ) { 2351 | this.selectedIndex = index; 2352 | this.setSelectedCell(); 2353 | this.startAnimation(); 2354 | this.dispatchEvent('cellSelect'); 2355 | } 2356 | }; 2357 | 2358 | Flickity.prototype.previous = function( isWrap ) { 2359 | this.select( this.selectedIndex - 1, isWrap ); 2360 | }; 2361 | 2362 | Flickity.prototype.next = function( isWrap ) { 2363 | this.select( this.selectedIndex + 1, isWrap ); 2364 | }; 2365 | 2366 | Flickity.prototype.setSelectedCell = function() { 2367 | this._removeSelectedCellClass(); 2368 | this.selectedCell = this.cells[ this.selectedIndex ]; 2369 | this.selectedElement = this.selectedCell.element; 2370 | classie.add( this.selectedElement, 'is-selected' ); 2371 | }; 2372 | 2373 | Flickity.prototype._removeSelectedCellClass = function() { 2374 | if ( this.selectedCell ) { 2375 | classie.remove( this.selectedCell.element, 'is-selected' ); 2376 | } 2377 | }; 2378 | 2379 | // -------------------------- get cells -------------------------- // 2380 | 2381 | /** 2382 | * get Flickity.Cell, given an Element 2383 | * @param {Element} elem 2384 | * @returns {Flickity.Cell} item 2385 | */ 2386 | Flickity.prototype.getCell = function( elem ) { 2387 | // loop through cells to get the one that matches 2388 | for ( var i=0, len = this.cells.length; i < len; i++ ) { 2389 | var cell = this.cells[i]; 2390 | if ( cell.element == elem ) { 2391 | return cell; 2392 | } 2393 | } 2394 | }; 2395 | 2396 | /** 2397 | * get collection of Flickity.Cells, given Elements 2398 | * @param {Element, Array, NodeList} elems 2399 | * @returns {Array} cells - Flickity.Cells 2400 | */ 2401 | Flickity.prototype.getCells = function( elems ) { 2402 | elems = utils.makeArray( elems ); 2403 | var cells = []; 2404 | for ( var i=0, len = elems.length; i < len; i++ ) { 2405 | var elem = elems[i]; 2406 | var cell = this.getCell( elem ); 2407 | if ( cell ) { 2408 | cells.push( cell ); 2409 | } 2410 | } 2411 | return cells; 2412 | }; 2413 | 2414 | /** 2415 | * get cell elements 2416 | * @returns {Array} cellElems 2417 | */ 2418 | Flickity.prototype.getCellElements = function() { 2419 | var cellElems = []; 2420 | for ( var i=0, len = this.cells.length; i < len; i++ ) { 2421 | cellElems.push( this.cells[i].element ); 2422 | } 2423 | return cellElems; 2424 | }; 2425 | 2426 | /** 2427 | * get parent cell from an element 2428 | * @param {Element} elem 2429 | * @returns {Flickit.Cell} cell 2430 | */ 2431 | Flickity.prototype.getParentCell = function( elem ) { 2432 | // first check if elem is cell 2433 | var cell = this.getCell( elem ); 2434 | if ( cell ) { 2435 | return cell; 2436 | } 2437 | // try to get parent cell elem 2438 | elem = utils.getParent( elem, '.flickity-slider > *' ); 2439 | return this.getCell( elem ); 2440 | }; 2441 | 2442 | // -------------------------- events -------------------------- // 2443 | 2444 | Flickity.prototype.uiChange = function() { 2445 | this.emit('uiChange'); 2446 | }; 2447 | 2448 | Flickity.prototype.childUIPointerDown = function( event ) { 2449 | this.emitEvent( 'childUIPointerDown', [ event ] ); 2450 | }; 2451 | 2452 | // ----- resize ----- // 2453 | 2454 | Flickity.prototype.onresize = function() { 2455 | this.watchCSS(); 2456 | this.resize(); 2457 | }; 2458 | 2459 | utils.debounceMethod( Flickity, 'onresize', 150 ); 2460 | 2461 | Flickity.prototype.resize = function() { 2462 | if ( !this.isActive ) { 2463 | return; 2464 | } 2465 | this.getSize(); 2466 | // wrap values 2467 | if ( this.options.wrapAround ) { 2468 | this.x = utils.modulo( this.x, this.slideableWidth ); 2469 | } 2470 | this.positionCells(); 2471 | this._getWrapShiftCells(); 2472 | this.setGallerySize(); 2473 | this.positionSliderAtSelected(); 2474 | }; 2475 | 2476 | var supportsConditionalCSS = Flickity.supportsConditionalCSS = ( function() { 2477 | var supports; 2478 | return function checkSupport() { 2479 | if ( supports !== undefined ) { 2480 | return supports; 2481 | } 2482 | if ( !getComputedStyle ) { 2483 | supports = false; 2484 | return; 2485 | } 2486 | // style body's :after and check that 2487 | var style = document.createElement('style'); 2488 | var cssText = document.createTextNode('body:after { content: "foo"; display: none; }'); 2489 | style.appendChild( cssText ); 2490 | document.head.appendChild( style ); 2491 | var afterContent = getComputedStyle( document.body, ':after' ).content; 2492 | // check if able to get :after content 2493 | supports = afterContent.indexOf('foo') != -1; 2494 | document.head.removeChild( style ); 2495 | return supports; 2496 | }; 2497 | })(); 2498 | 2499 | // watches the :after property, activates/deactivates 2500 | Flickity.prototype.watchCSS = function() { 2501 | var watchOption = this.options.watchCSS; 2502 | if ( !watchOption ) { 2503 | return; 2504 | } 2505 | var supports = supportsConditionalCSS(); 2506 | if ( !supports ) { 2507 | // activate if watch option is fallbackOn 2508 | var method = watchOption == 'fallbackOn' ? 'activate' : 'deactivate'; 2509 | this[ method ](); 2510 | return; 2511 | } 2512 | 2513 | var afterContent = getComputedStyle( this.element, ':after' ).content; 2514 | // activate if :after { content: 'flickity' } 2515 | if ( afterContent.indexOf('flickity') != -1 ) { 2516 | this.activate(); 2517 | } else { 2518 | this.deactivate(); 2519 | } 2520 | }; 2521 | 2522 | // ----- keydown ----- // 2523 | 2524 | // go previous/next if left/right keys pressed 2525 | Flickity.prototype.onkeydown = function( event ) { 2526 | // only work if element is in focus 2527 | if ( !this.options.accessibility || 2528 | ( document.activeElement && document.activeElement != this.element ) ) { 2529 | return; 2530 | } 2531 | 2532 | if ( event.keyCode == 37 ) { 2533 | // go left 2534 | var leftMethod = this.options.rightToLeft ? 'next' : 'previous'; 2535 | this.uiChange(); 2536 | this[ leftMethod ](); 2537 | } else if ( event.keyCode == 39 ) { 2538 | // go right 2539 | var rightMethod = this.options.rightToLeft ? 'previous' : 'next'; 2540 | this.uiChange(); 2541 | this[ rightMethod ](); 2542 | } 2543 | }; 2544 | 2545 | // -------------------------- destroy -------------------------- // 2546 | 2547 | // deactivate all Flickity functionality, but keep stuff available 2548 | Flickity.prototype.deactivate = function() { 2549 | if ( !this.isActive ) { 2550 | return; 2551 | } 2552 | classie.remove( this.element, 'flickity-enabled' ); 2553 | classie.remove( this.element, 'flickity-rtl' ); 2554 | // destroy cells 2555 | for ( var i=0, len = this.cells.length; i < len; i++ ) { 2556 | var cell = this.cells[i]; 2557 | cell.destroy(); 2558 | } 2559 | this._removeSelectedCellClass(); 2560 | this.element.removeChild( this.viewport ); 2561 | // move child elements back into element 2562 | moveElements( this.slider.children, this.element ); 2563 | if ( this.options.accessibility ) { 2564 | this.element.removeAttribute('tabIndex'); 2565 | eventie.unbind( this.element, 'keydown', this ); 2566 | } 2567 | // set flags 2568 | this.isActive = false; 2569 | this.emit('deactivate'); 2570 | }; 2571 | 2572 | Flickity.prototype.destroy = function() { 2573 | this.deactivate(); 2574 | if ( this.isResizeBound ) { 2575 | eventie.unbind( window, 'resize', this ); 2576 | } 2577 | this.emit('destroy'); 2578 | if ( jQuery && this.$element ) { 2579 | jQuery.removeData( this.element, 'flickity' ); 2580 | } 2581 | delete this.element.flickityGUID; 2582 | delete instances[ this.guid ]; 2583 | }; 2584 | 2585 | // -------------------------- prototype -------------------------- // 2586 | 2587 | utils.extend( Flickity.prototype, animatePrototype ); 2588 | 2589 | // -------------------------- extras -------------------------- // 2590 | 2591 | // quick check for IE8 2592 | var isIE8 = 'attachEvent' in window; 2593 | 2594 | Flickity.setUnselectable = function( elem ) { 2595 | if ( !isIE8 ) { 2596 | return; 2597 | } 2598 | // IE8 prevent child from changing focus http://stackoverflow.com/a/17525223/182183 2599 | elem.setAttribute( 'unselectable', 'on' ); 2600 | }; 2601 | 2602 | /** 2603 | * get Flickity instance from element 2604 | * @param {Element} elem 2605 | * @returns {Flickity} 2606 | */ 2607 | Flickity.data = function( elem ) { 2608 | elem = utils.getQueryElement( elem ); 2609 | var id = elem && elem.flickityGUID; 2610 | return id && instances[ id ]; 2611 | }; 2612 | 2613 | utils.htmlInit( Flickity, 'flickity' ); 2614 | 2615 | if ( jQuery && jQuery.bridget ) { 2616 | jQuery.bridget( 'flickity', Flickity ); 2617 | } 2618 | 2619 | Flickity.Cell = Cell; 2620 | 2621 | return Flickity; 2622 | 2623 | })); 2624 | 2625 | /*! 2626 | * Unipointer v1.1.0 2627 | * base class for doing one thing with pointer event 2628 | * MIT license 2629 | */ 2630 | 2631 | /*jshint browser: true, undef: true, unused: true, strict: true */ 2632 | /*global define: false, module: false, require: false */ 2633 | 2634 | ( function( window, factory ) { 2635 | 2636 | // universal module definition 2637 | 2638 | if ( typeof define == 'function' && define.amd ) { 2639 | // AMD 2640 | define( 'unipointer/unipointer',[ 2641 | 'eventEmitter/EventEmitter', 2642 | 'eventie/eventie' 2643 | ], function( EventEmitter, eventie ) { 2644 | return factory( window, EventEmitter, eventie ); 2645 | }); 2646 | } else if ( typeof exports == 'object' ) { 2647 | // CommonJS 2648 | module.exports = factory( 2649 | window, 2650 | require('wolfy87-eventemitter'), 2651 | require('eventie') 2652 | ); 2653 | } else { 2654 | // browser global 2655 | window.Unipointer = factory( 2656 | window, 2657 | window.EventEmitter, 2658 | window.eventie 2659 | ); 2660 | } 2661 | 2662 | }( window, function factory( window, EventEmitter, eventie ) { 2663 | 2664 | 2665 | 2666 | function noop() {} 2667 | 2668 | function Unipointer() {} 2669 | 2670 | // inherit EventEmitter 2671 | Unipointer.prototype = new EventEmitter(); 2672 | 2673 | Unipointer.prototype.bindStartEvent = function( elem ) { 2674 | this._bindStartEvent( elem, true ); 2675 | }; 2676 | 2677 | Unipointer.prototype.unbindStartEvent = function( elem ) { 2678 | this._bindStartEvent( elem, false ); 2679 | }; 2680 | 2681 | /** 2682 | * works as unbinder, as you can ._bindStart( false ) to unbind 2683 | * @param {Boolean} isBind - will unbind if falsey 2684 | */ 2685 | Unipointer.prototype._bindStartEvent = function( elem, isBind ) { 2686 | // munge isBind, default to true 2687 | isBind = isBind === undefined ? true : !!isBind; 2688 | var bindMethod = isBind ? 'bind' : 'unbind'; 2689 | 2690 | if ( window.navigator.pointerEnabled ) { 2691 | // W3C Pointer Events, IE11. See https://coderwall.com/p/mfreca 2692 | eventie[ bindMethod ]( elem, 'pointerdown', this ); 2693 | } else if ( window.navigator.msPointerEnabled ) { 2694 | // IE10 Pointer Events 2695 | eventie[ bindMethod ]( elem, 'MSPointerDown', this ); 2696 | } else { 2697 | // listen for both, for devices like Chrome Pixel 2698 | eventie[ bindMethod ]( elem, 'mousedown', this ); 2699 | eventie[ bindMethod ]( elem, 'touchstart', this ); 2700 | } 2701 | }; 2702 | 2703 | // trigger handler methods for events 2704 | Unipointer.prototype.handleEvent = function( event ) { 2705 | var method = 'on' + event.type; 2706 | if ( this[ method ] ) { 2707 | this[ method ]( event ); 2708 | } 2709 | }; 2710 | 2711 | // returns the touch that we're keeping track of 2712 | Unipointer.prototype.getTouch = function( touches ) { 2713 | for ( var i=0, len = touches.length; i < len; i++ ) { 2714 | var touch = touches[i]; 2715 | if ( touch.identifier == this.pointerIdentifier ) { 2716 | return touch; 2717 | } 2718 | } 2719 | }; 2720 | 2721 | // ----- start event ----- // 2722 | 2723 | Unipointer.prototype.onmousedown = function( event ) { 2724 | // dismiss clicks from right or middle buttons 2725 | var button = event.button; 2726 | if ( button && ( button !== 0 && button !== 1 ) ) { 2727 | return; 2728 | } 2729 | this._pointerDown( event, event ); 2730 | }; 2731 | 2732 | Unipointer.prototype.ontouchstart = function( event ) { 2733 | this._pointerDown( event, event.changedTouches[0] ); 2734 | }; 2735 | 2736 | Unipointer.prototype.onMSPointerDown = 2737 | Unipointer.prototype.onpointerdown = function( event ) { 2738 | this._pointerDown( event, event ); 2739 | }; 2740 | 2741 | /** 2742 | * pointer start 2743 | * @param {Event} event 2744 | * @param {Event or Touch} pointer 2745 | */ 2746 | Unipointer.prototype._pointerDown = function( event, pointer ) { 2747 | // dismiss other pointers 2748 | if ( this.isPointerDown ) { 2749 | return; 2750 | } 2751 | 2752 | this.isPointerDown = true; 2753 | // save pointer identifier to match up touch events 2754 | this.pointerIdentifier = pointer.pointerId !== undefined ? 2755 | // pointerId for pointer events, touch.indentifier for touch events 2756 | pointer.pointerId : pointer.identifier; 2757 | 2758 | this.pointerDown( event, pointer ); 2759 | }; 2760 | 2761 | Unipointer.prototype.pointerDown = function( event, pointer ) { 2762 | this._bindPostStartEvents( event ); 2763 | this.emitEvent( 'pointerDown', [ event, pointer ] ); 2764 | }; 2765 | 2766 | // hash of events to be bound after start event 2767 | var postStartEvents = { 2768 | mousedown: [ 'mousemove', 'mouseup' ], 2769 | touchstart: [ 'touchmove', 'touchend', 'touchcancel' ], 2770 | pointerdown: [ 'pointermove', 'pointerup', 'pointercancel' ], 2771 | MSPointerDown: [ 'MSPointerMove', 'MSPointerUp', 'MSPointerCancel' ] 2772 | }; 2773 | 2774 | Unipointer.prototype._bindPostStartEvents = function( event ) { 2775 | if ( !event ) { 2776 | return; 2777 | } 2778 | // get proper events to match start event 2779 | var events = postStartEvents[ event.type ]; 2780 | // IE8 needs to be bound to document 2781 | var node = event.preventDefault ? window : document; 2782 | // bind events to node 2783 | for ( var i=0, len = events.length; i < len; i++ ) { 2784 | var evnt = events[i]; 2785 | eventie.bind( node, evnt, this ); 2786 | } 2787 | // save these arguments 2788 | this._boundPointerEvents = { 2789 | events: events, 2790 | node: node 2791 | }; 2792 | }; 2793 | 2794 | Unipointer.prototype._unbindPostStartEvents = function() { 2795 | var args = this._boundPointerEvents; 2796 | // IE8 can trigger dragEnd twice, check for _boundEvents 2797 | if ( !args || !args.events ) { 2798 | return; 2799 | } 2800 | 2801 | for ( var i=0, len = args.events.length; i < len; i++ ) { 2802 | var event = args.events[i]; 2803 | eventie.unbind( args.node, event, this ); 2804 | } 2805 | delete this._boundPointerEvents; 2806 | }; 2807 | 2808 | // ----- move event ----- // 2809 | 2810 | Unipointer.prototype.onmousemove = function( event ) { 2811 | this._pointerMove( event, event ); 2812 | }; 2813 | 2814 | Unipointer.prototype.onMSPointerMove = 2815 | Unipointer.prototype.onpointermove = function( event ) { 2816 | if ( event.pointerId == this.pointerIdentifier ) { 2817 | this._pointerMove( event, event ); 2818 | } 2819 | }; 2820 | 2821 | Unipointer.prototype.ontouchmove = function( event ) { 2822 | var touch = this.getTouch( event.changedTouches ); 2823 | if ( touch ) { 2824 | this._pointerMove( event, touch ); 2825 | } 2826 | }; 2827 | 2828 | /** 2829 | * pointer move 2830 | * @param {Event} event 2831 | * @param {Event or Touch} pointer 2832 | * @private 2833 | */ 2834 | Unipointer.prototype._pointerMove = function( event, pointer ) { 2835 | this.pointerMove( event, pointer ); 2836 | }; 2837 | 2838 | // public 2839 | Unipointer.prototype.pointerMove = function( event, pointer ) { 2840 | this.emitEvent( 'pointerMove', [ event, pointer ] ); 2841 | }; 2842 | 2843 | // ----- end event ----- // 2844 | 2845 | 2846 | Unipointer.prototype.onmouseup = function( event ) { 2847 | this._pointerUp( event, event ); 2848 | }; 2849 | 2850 | Unipointer.prototype.onMSPointerUp = 2851 | Unipointer.prototype.onpointerup = function( event ) { 2852 | if ( event.pointerId == this.pointerIdentifier ) { 2853 | this._pointerUp( event, event ); 2854 | } 2855 | }; 2856 | 2857 | Unipointer.prototype.ontouchend = function( event ) { 2858 | var touch = this.getTouch( event.changedTouches ); 2859 | if ( touch ) { 2860 | this._pointerUp( event, touch ); 2861 | } 2862 | }; 2863 | 2864 | /** 2865 | * pointer up 2866 | * @param {Event} event 2867 | * @param {Event or Touch} pointer 2868 | * @private 2869 | */ 2870 | Unipointer.prototype._pointerUp = function( event, pointer ) { 2871 | this._pointerDone(); 2872 | this.pointerUp( event, pointer ); 2873 | }; 2874 | 2875 | // public 2876 | Unipointer.prototype.pointerUp = function( event, pointer ) { 2877 | this.emitEvent( 'pointerUp', [ event, pointer ] ); 2878 | }; 2879 | 2880 | // ----- pointer done ----- // 2881 | 2882 | // triggered on pointer up & pointer cancel 2883 | Unipointer.prototype._pointerDone = function() { 2884 | // reset properties 2885 | this.isPointerDown = false; 2886 | delete this.pointerIdentifier; 2887 | // remove events 2888 | this._unbindPostStartEvents(); 2889 | this.pointerDone(); 2890 | }; 2891 | 2892 | Unipointer.prototype.pointerDone = noop; 2893 | 2894 | // ----- pointer cancel ----- // 2895 | 2896 | Unipointer.prototype.onMSPointerCancel = 2897 | Unipointer.prototype.onpointercancel = function( event ) { 2898 | if ( event.pointerId == this.pointerIdentifier ) { 2899 | this._pointerCancel( event, event ); 2900 | } 2901 | }; 2902 | 2903 | Unipointer.prototype.ontouchcancel = function( event ) { 2904 | var touch = this.getTouch( event.changedTouches ); 2905 | if ( touch ) { 2906 | this._pointerCancel( event, touch ); 2907 | } 2908 | }; 2909 | 2910 | /** 2911 | * pointer cancel 2912 | * @param {Event} event 2913 | * @param {Event or Touch} pointer 2914 | * @private 2915 | */ 2916 | Unipointer.prototype._pointerCancel = function( event, pointer ) { 2917 | this._pointerDone(); 2918 | this.pointerCancel( event, pointer ); 2919 | }; 2920 | 2921 | // public 2922 | Unipointer.prototype.pointerCancel = function( event, pointer ) { 2923 | this.emitEvent( 'pointerCancel', [ event, pointer ] ); 2924 | }; 2925 | 2926 | // ----- ----- // 2927 | 2928 | // utility function for getting x/y cooridinates from event, because IE8 2929 | Unipointer.getPointerPoint = function( pointer ) { 2930 | return { 2931 | x: pointer.pageX !== undefined ? pointer.pageX : pointer.clientX, 2932 | y: pointer.pageY !== undefined ? pointer.pageY : pointer.clientY 2933 | }; 2934 | }; 2935 | 2936 | // ----- ----- // 2937 | 2938 | return Unipointer; 2939 | 2940 | })); 2941 | 2942 | /*! 2943 | * Unidragger v1.1.3 2944 | * Draggable base class 2945 | * MIT license 2946 | */ 2947 | 2948 | /*jshint browser: true, unused: true, undef: true, strict: true */ 2949 | 2950 | ( function( window, factory ) { 2951 | /*global define: false, module: false, require: false */ 2952 | 2953 | // universal module definition 2954 | 2955 | if ( typeof define == 'function' && define.amd ) { 2956 | // AMD 2957 | define( 'unidragger/unidragger',[ 2958 | 'eventie/eventie', 2959 | 'unipointer/unipointer' 2960 | ], function( eventie, Unipointer ) { 2961 | return factory( window, eventie, Unipointer ); 2962 | }); 2963 | } else if ( typeof exports == 'object' ) { 2964 | // CommonJS 2965 | module.exports = factory( 2966 | window, 2967 | require('eventie'), 2968 | require('unipointer') 2969 | ); 2970 | } else { 2971 | // browser global 2972 | window.Unidragger = factory( 2973 | window, 2974 | window.eventie, 2975 | window.Unipointer 2976 | ); 2977 | } 2978 | 2979 | }( window, function factory( window, eventie, Unipointer ) { 2980 | 2981 | 2982 | 2983 | // ----- ----- // 2984 | 2985 | function noop() {} 2986 | 2987 | // handle IE8 prevent default 2988 | function preventDefaultEvent( event ) { 2989 | if ( event.preventDefault ) { 2990 | event.preventDefault(); 2991 | } else { 2992 | event.returnValue = false; 2993 | } 2994 | } 2995 | 2996 | // -------------------------- Unidragger -------------------------- // 2997 | 2998 | function Unidragger() {} 2999 | 3000 | // inherit Unipointer & EventEmitter 3001 | Unidragger.prototype = new Unipointer(); 3002 | 3003 | // ----- bind start ----- // 3004 | 3005 | Unidragger.prototype.bindHandles = function() { 3006 | this._bindHandles( true ); 3007 | }; 3008 | 3009 | Unidragger.prototype.unbindHandles = function() { 3010 | this._bindHandles( false ); 3011 | }; 3012 | 3013 | var navigator = window.navigator; 3014 | /** 3015 | * works as unbinder, as you can .bindHandles( false ) to unbind 3016 | * @param {Boolean} isBind - will unbind if falsey 3017 | */ 3018 | Unidragger.prototype._bindHandles = function( isBind ) { 3019 | // munge isBind, default to true 3020 | isBind = isBind === undefined ? true : !!isBind; 3021 | // extra bind logic 3022 | var binderExtra; 3023 | if ( navigator.pointerEnabled ) { 3024 | binderExtra = function( handle ) { 3025 | // disable scrolling on the element 3026 | handle.style.touchAction = isBind ? 'none' : ''; 3027 | }; 3028 | } else if ( navigator.msPointerEnabled ) { 3029 | binderExtra = function( handle ) { 3030 | // disable scrolling on the element 3031 | handle.style.msTouchAction = isBind ? 'none' : ''; 3032 | }; 3033 | } else { 3034 | binderExtra = function() { 3035 | // TODO re-enable img.ondragstart when unbinding 3036 | if ( isBind ) { 3037 | disableImgOndragstart( handle ); 3038 | } 3039 | }; 3040 | } 3041 | // bind each handle 3042 | var bindMethod = isBind ? 'bind' : 'unbind'; 3043 | for ( var i=0, len = this.handles.length; i < len; i++ ) { 3044 | var handle = this.handles[i]; 3045 | this._bindStartEvent( handle, isBind ); 3046 | binderExtra( handle ); 3047 | eventie[ bindMethod ]( handle, 'click', this ); 3048 | } 3049 | }; 3050 | 3051 | // remove default dragging interaction on all images in IE8 3052 | // IE8 does its own drag thing on images, which messes stuff up 3053 | 3054 | function noDragStart() { 3055 | return false; 3056 | } 3057 | 3058 | // TODO replace this with a IE8 test 3059 | var isIE8 = 'attachEvent' in document.documentElement; 3060 | 3061 | // IE8 only 3062 | var disableImgOndragstart = !isIE8 ? noop : function( handle ) { 3063 | 3064 | if ( handle.nodeName == 'IMG' ) { 3065 | handle.ondragstart = noDragStart; 3066 | } 3067 | 3068 | var images = handle.querySelectorAll('img'); 3069 | for ( var i=0, len = images.length; i < len; i++ ) { 3070 | var img = images[i]; 3071 | img.ondragstart = noDragStart; 3072 | } 3073 | }; 3074 | 3075 | // ----- start event ----- // 3076 | 3077 | /** 3078 | * pointer start 3079 | * @param {Event} event 3080 | * @param {Event or Touch} pointer 3081 | */ 3082 | Unidragger.prototype.pointerDown = function( event, pointer ) { 3083 | this._dragPointerDown( event, pointer ); 3084 | // kludge to blur focused inputs in dragger 3085 | var focused = document.activeElement; 3086 | if ( focused && focused.blur ) { 3087 | focused.blur(); 3088 | } 3089 | // bind move and end events 3090 | this._bindPostStartEvents( event ); 3091 | this.emitEvent( 'pointerDown', [ event, pointer ] ); 3092 | }; 3093 | 3094 | // base pointer down logic 3095 | Unidragger.prototype._dragPointerDown = function( event, pointer ) { 3096 | // track to see when dragging starts 3097 | this.pointerDownPoint = Unipointer.getPointerPoint( pointer ); 3098 | 3099 | // prevent default, unless touchstart or s and