├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── History.md ├── LICENSE ├── README.md ├── bower.json ├── component.json ├── example ├── demo.js ├── img │ ├── about1.png │ ├── about2.png │ └── sample.png ├── index.html └── style.css ├── flipsnap.js ├── flipsnap.min.js ├── package.json └── test ├── index.html ├── lib ├── expect.js ├── jquery.js ├── mocha-phantomjs.coffee ├── mocha-phantomjs │ └── core_extensions.js ├── mocha.css ├── mocha.js ├── setup.js └── sinon.js └── tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | gh-pages 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | example/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: phantomjs test/lib/mocha-phantomjs.coffee test/index.html 2 | after_success: 3 | - npm install poncho coveralls 4 | - ./node_modules/.bin/poncho -R lcov test/index.html | ./node_modules/.bin/coveralls 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to flipsnap.js 2 | 3 | * Before you open a ticket or send a pull request, [search](https://github.com/hokaccha/js-flipsnap/issues) for previous discussions about the same feature or issue. Add to the earlier ticket if you find one. 4 | * Before sending a pull request for a feature or bug fix, be sure to have [tests](https://github.com/hokaccha/js-flipsnap/blob/master/test/index.html). 5 | * In your pull request, do not add rebuild the minified flipsnap.min.js file. We'll do that before cutting a new release. 6 | * All pull requests should be made to the `master` branch. 7 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.6.3 / 2015-08-10 2 | ==================== 3 | 4 | * Fix bug of animation #56 (@babatakao) 5 | 6 | 0.6.2 / 2014-01-14 7 | ==================== 8 | 9 | * Fix event handling 10 | * Use touch locus' angle to decide whether to stop the browser default scrolling behavior (@zhouqicf) 11 | 12 | 0.6.1 / 2014-01-09 13 | ==================== 14 | 15 | * Fix bug, touchend event has not fired on Android Chrome #30 #22 16 | * event.stopPropagation cause the events of the upper node can't trigger (@zhouqicf) 17 | 18 | 0.6.0 / 2013-11-18 19 | ==================== 20 | 21 | * Fix Panning broken on Internet Explorer #26 22 | * Fix can't prevent form elements' default behavior (@zhouqicf) 23 | * Add AMD support (@nulltask) 24 | 25 | 0.5.6 / 2013-07-01 26 | ==================== 27 | 28 | * Fix, `refresh` did not work when items is zero. 29 | 30 | 0.5.5 / 2013-06-10 31 | ==================== 32 | 33 | * Fix, Only assign intentionally passed event data values #24 (mattbasta) 34 | 35 | 0.5.4 / 2013-06-05 36 | ==================== 37 | 38 | * Fix bug for Fix bug for Windows8 + IE10 #23 (don't use MSPointer Events) 39 | 40 | 0.5.3 / 2013-03-06 41 | ==================== 42 | 43 | * Fix bug `transitionDuration` when not support cssAnimation 44 | * Fix img draggable in Window8 Chrome and Firefox 45 | 46 | 0.5.2 / 2013-03-06 47 | ==================== 48 | 49 | * Fix bug `transitionDuration` 50 | * Fix, error only load script on not support `addEventlistener` browser 51 | 52 | 0.5.1 / 2013-03-05 53 | ==================== 54 | 55 | * Fix, Mouse event don't work in Window8 Chrome and Firefox. #20 56 | 57 | 0.5.0 / 2013-03-01 58 | ==================== 59 | 60 | * Add touch events, `fstouchstart`, `fstouchmove`, `fstouchend` #19 (chr15m) 61 | * Per transition duration #18 (chr15m) 62 | * `fsmoveend` event rename to `fspointmove` 63 | * Fix maxPoint bug #17 64 | * Fix multi touch bug #5 65 | 66 | 0.4.1 / 2012-12-06 67 | ==================== 68 | 69 | * Fix #14. Thanks to Neotag 70 | * Add `transitionDuration` option. Thanks to Neotag 71 | 72 | 0.4.0 / 2012-11-16 73 | ==================== 74 | 75 | * IE10 for touch device support. Fix #13 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 PixelGrid, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flipsnap.js 2 | 3 | [![Build Status](https://travis-ci.org/hokaccha/js-flipsnap.png?branch=master)](https://travis-ci.org/hokaccha/js-flipsnap) 4 | [![Coverage Status](https://coveralls.io/repos/hokaccha/js-flipsnap/badge.png?branch=master)](https://coveralls.io/r/hokaccha/js-flipsnap?branch=master) 5 | 6 | flipsnap.js is snap scroll for touch device. 7 | 8 | http://hokaccha.github.com/js-flipsnap/ 9 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flipsnap", 3 | "version": "0.6.3", 4 | "homepage": "https://github.com/hokaccha/js-flipsnap", 5 | "authors": [ 6 | "Kazuhito Hokamura " 7 | ], 8 | "description": "Provides UI of snap and slide", 9 | "main": "flipsnap.js", 10 | "keywords": [ 11 | "mobile", 12 | "ui" 13 | ], 14 | "license": "MIT", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "CONTRIBUTING.md", 21 | "example" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flipsnap", 3 | "main": "flipsnap.js", 4 | "repository": "hokaccha/js-flipsnap", 5 | "description": "Prov ides UI of snap and slide", 6 | "version": "0.6.3", 7 | "keywords": [ "mobile", "ui" ], 8 | "scripts": [ "flipsnap.js" ], 9 | "license": "MIT" 10 | } 11 | -------------------------------------------------------------------------------- /example/demo.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | if (!$('.demo').length) return; 4 | 5 | (function simple() { 6 | Flipsnap('#demo-simple .flipsnap'); 7 | })(); 8 | 9 | (function simple() { 10 | Flipsnap('#demo-threshold .flipsnap', { 11 | threshold: 40 12 | }); 13 | })(); 14 | 15 | (function img() { 16 | Flipsnap('#demo-img .flipsnap'); 17 | })(); 18 | 19 | (function distance() { 20 | Flipsnap('#demo-distance .flipsnap', { 21 | distance: 230 22 | }); 23 | })(); 24 | 25 | (function maxPoint() { 26 | Flipsnap('#demo-maxPoint .flipsnap', { 27 | distance: 160, // 80px * 2 28 | maxPoint: 3 // move able 3 times 29 | }); 30 | })(); 31 | 32 | (function transitionDuration() { 33 | Flipsnap('#demo-transitionDuration .flipsnap', { 34 | distance: 230, 35 | transitionDuration: 150 36 | }); 37 | })(); 38 | 39 | (function pointmove() { 40 | var $demo = $('#demo-pointmove'); 41 | var $pointer = $demo.find('.pointer span'); 42 | var flipsnap = Flipsnap('#demo-pointmove .flipsnap', { 43 | distance: 230 44 | }); 45 | flipsnap.element.addEventListener('fspointmove', function() { 46 | $pointer.filter('.current').removeClass('current'); 47 | $pointer.eq(flipsnap.currentPoint).addClass('current'); 48 | }, false); 49 | 50 | var $next = $demo.find(".next").click(function() { 51 | flipsnap.toNext(); 52 | }); 53 | var $prev = $demo.find(".prev").click(function() { 54 | flipsnap.toPrev(); 55 | }); 56 | flipsnap.element.addEventListener('fspointmove', function() { 57 | $next.attr("disabled", !flipsnap.hasNext()); 58 | $prev.attr("disabled", !flipsnap.hasPrev()); 59 | }, false); 60 | })(); 61 | 62 | (function touchevents() { 63 | var $demo = $('#demo-touchevents'); 64 | var $event = $demo.find('.event span'); 65 | var $detail = $demo.find('.detail'); 66 | var flipsnap = Flipsnap('#demo-touchevents .flipsnap', { 67 | distance: 230 68 | }); 69 | flipsnap.element.addEventListener('fstouchstart', function(ev) { 70 | $event.text('fstouchstart'); 71 | }, false); 72 | 73 | flipsnap.element.addEventListener('fstouchmove', function(ev) { 74 | $event.text('fstouchmove'); 75 | $detail.text(JSON.stringify({ 76 | delta: ev.delta, 77 | direction: ev.direction 78 | }, null, 2)); 79 | }, false); 80 | 81 | flipsnap.element.addEventListener('fstouchend', function(ev) { 82 | $event.text('fstouchend'); 83 | $detail.text(JSON.stringify({ 84 | moved: ev.moved, 85 | originalPoint: ev.originalPoint, 86 | newPoint: ev.newPoint, 87 | cancelled: ev.cancelled 88 | }, null, 2)); 89 | }, false); 90 | })(); 91 | 92 | (function cancelmove() { 93 | var $demo = $('#demo-cancelmove'); 94 | var flipsnap = Flipsnap('#demo-cancelmove .flipsnap', { 95 | distance: 230 96 | }); 97 | flipsnap.element.addEventListener('fstouchmove', function(ev) { 98 | if (ev.direction === -1) { 99 | ev.preventDefault(); 100 | } 101 | }, false); 102 | })(); 103 | 104 | (function refresh() { 105 | var $demo = $("#demo-refresh"); 106 | var $flipsnap = $demo.find('.flipsnap'); 107 | var distance = 230; 108 | var padding = 30; 109 | var flipsnap = Flipsnap("#demo-refresh .flipsnap", { 110 | distance: distance 111 | }); 112 | var width = distance + padding; 113 | 114 | // append new item 115 | $demo.find(".add").click(function() { 116 | var newNumber = $flipsnap.find(".item").size() + 1; 117 | var $item = $("
").addClass("item").text(newNumber); 118 | width += distance; 119 | $flipsnap.append($item).width(width); 120 | flipsnap.refresh(); 121 | }); 122 | 123 | // remove last item 124 | $(".remove").click(function() { 125 | var $items = $flipsnap.find(".item"); 126 | if ($items.size() <= 0) return; 127 | width -= distance; 128 | $items.last().remove().width(width); 129 | flipsnap.refresh(); 130 | }); 131 | })(); 132 | 133 | 134 | (function link() { 135 | Flipsnap('#demo-link .flipsnap', { 136 | distance: 230 137 | }); 138 | 139 | var $a = $('#demo-link .item a'); 140 | // click event 141 | $a.eq(1).click(function(e) { 142 | e.preventDefault(); 143 | alert("clicked"); 144 | }); 145 | 146 | // click event and link 147 | $a.eq(2).click(function(e) { 148 | alert("clicked and link to index"); 149 | }); 150 | })(); 151 | 152 | (function nextprev() { 153 | var $demo = $('#demo-nextprev'); 154 | var flipsnap = Flipsnap('#demo-nextprev .flipsnap', { 155 | distance: 230 156 | }); 157 | var $next = $demo.find(".next").click(function() { 158 | flipsnap.toNext(); 159 | }); 160 | var $prev = $demo.find(".prev").click(function() { 161 | flipsnap.toPrev(); 162 | }); 163 | flipsnap.element.addEventListener('fspointmove', function() { 164 | $next.attr("disabled", !flipsnap.hasNext()); 165 | $prev.attr("disabled", !flipsnap.hasPrev()); 166 | }, false); 167 | })(); 168 | 169 | (function moveToPoint() { 170 | var $demo = $('#demo-moveToPoint'); 171 | var flipsnap = Flipsnap('#demo-moveToPoint .flipsnap', { 172 | distance: 230 173 | }); 174 | var $num = $demo.find('.num'); 175 | $demo.find('.go').click(function() { 176 | flipsnap.moveToPoint($num.val() - 1); 177 | }); 178 | })(); 179 | 180 | (function disableTouch() { 181 | var $demo = $('#demo-disableTouch'); 182 | var flipsnap = Flipsnap('#demo-disableTouch .flipsnap', { 183 | distance: 230, 184 | disableTouch: true 185 | }); 186 | 187 | // disable check 188 | $demo.find('.isDisable').change(function() { 189 | flipsnap.disableTouch = $(this).is(':checked'); 190 | }); 191 | 192 | // Go btn 193 | var $num = $demo.find('.num'); 194 | $demo.find('.go').click(function() { 195 | flipsnap.moveToPoint($num.val() - 1); 196 | }); 197 | 198 | // next, prev btn 199 | var $next = $demo.find(".next").click(function() { 200 | flipsnap.toNext(); 201 | }); 202 | var $prev = $demo.find(".prev").click(function() { 203 | flipsnap.toPrev(); 204 | }); 205 | flipsnap.element.addEventListener('fspointmove', function() { 206 | $next.attr("disabled", !flipsnap.hasNext()); 207 | $prev.attr("disabled", !flipsnap.hasPrev()); 208 | }, false); 209 | })(); 210 | 211 | $('.sample a').click(function(e) { 212 | e.preventDefault(); 213 | var $a = $(this); 214 | var $code = $a.parents('.sample').find('pre'); 215 | $code.slideToggle('fast', function() { 216 | $a.text($code.is(':visible') ? 'hide code' : 'show code'); 217 | }); 218 | }); 219 | 220 | }); 221 | -------------------------------------------------------------------------------- /example/img/about1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hokaccha/js-flipsnap/9aabbf722dfdd27ff80cbc665ce33f2cb9a9a0dd/example/img/about1.png -------------------------------------------------------------------------------- /example/img/about2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hokaccha/js-flipsnap/9aabbf722dfdd27ff80cbc665ce33f2cb9a9a0dd/example/img/about2.png -------------------------------------------------------------------------------- /example/img/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hokaccha/js-flipsnap/9aabbf722dfdd27ff80cbc665ce33f2cb9a9a0dd/example/img/sample.png -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Flipsnap Sample 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Flipsnap Example

15 |
16 |

Demo

17 |
18 |

simple

19 |
20 |
21 |
1
22 |
2
23 |
3
24 |
25 |
26 |
27 | 28 |
29 |

threshold

30 |
31 |
32 |
1
33 |
2
34 |
3
35 |
36 |
37 |
38 | 39 |
40 |

img

41 |
42 |
43 |
img
44 |
img
45 |
img
46 |
47 |
48 |
49 | 50 |
51 |

distance

52 |
53 |
54 |
1
55 |
2
56 |
3
57 |
4
58 |
5
59 |
60 |
61 |
62 | 63 |
64 |

maxPoint

65 |
66 |
67 |
1
68 |
2
69 |
3
70 |
4
71 |
5
72 |
6
73 |
7
74 |
8
75 |
9
76 |
10
77 |
78 |
79 |
80 | 81 |
82 |

transitionDuration

83 |
84 |
85 |
1
86 |
2
87 |
3
88 |
4
89 |
5
90 |
91 |
92 |
93 | 94 |
95 |

pointmove event

96 |
97 |
98 |
1
99 |
2
100 |
3
101 |
4
102 |
5
103 |
104 |
105 |
106 | 107 | 108 | 109 | 110 | 111 |
112 |

113 | 114 | 115 |

116 |
117 | 118 |
119 |

touch events

120 |
121 |
122 |
1
123 |
2
124 |
3
125 |
126 |
127 |
128 |
event:
129 |

130 | 		
131 |
132 | 133 |
134 |

cancel move (only next)

135 |
136 |
137 |
1
138 |
2
139 |
3
140 |
141 |
142 |
143 | 144 |
145 |

refresh

146 |
147 |
148 |
1
149 |
150 |
151 |

152 | 153 | 154 |

155 |
156 | 157 | 167 | 168 |
169 |

next, prev

170 |
171 |
172 |
1
173 |
2
174 |
3
175 |
176 |
177 |

178 | 179 | 180 |

181 |
182 | 183 |
184 |

moveToPoint

185 |
186 |
187 |
1
188 |
2
189 |
3
190 |
4
191 |
5
192 |
6
193 |
7
194 |
8
195 |
9
196 |
10
197 |
198 |
199 |

200 | 201 | 202 |

203 |
204 | 205 |
206 |

disableTouch

207 |
208 |
209 |
1
210 |
2
211 |
3
212 |
4
213 |
5
214 |
215 |
216 |

217 | 218 |

219 |

220 | 221 | 222 |

223 |

224 | 225 | 226 |

227 |
228 |
229 | 230 | 231 | -------------------------------------------------------------------------------- /example/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | img { 7 | vertical-align: top; 8 | } 9 | 10 | h1, h2, h3 { 11 | text-align: center; 12 | } 13 | 14 | .viewport { 15 | width: 320px; 16 | overflow: hidden; 17 | margin: 0 auto; 18 | padding: 30px 0; 19 | -webkit-transform: translateZ(0); } 20 | 21 | .flipsnap { 22 | width: 1195px; 23 | /* 230px(item) * 5 + 45px(padding) */ 24 | padding-left: 45px; } 25 | 26 | .flipsnap:after { 27 | content: ''; 28 | display: block; 29 | clear: both; 30 | height: 0; } 31 | 32 | .item { 33 | width: 200px; 34 | margin: 0 10px; 35 | font-size: 50px; 36 | text-align: center; 37 | padding: 50px 0; 38 | background: #EFEFEF; 39 | border: 5px solid #999; 40 | float: left; 41 | color: #666; } 42 | 43 | .pointer { 44 | text-align: center; } 45 | 46 | .pointer span { 47 | display: inline-block; 48 | width: 8px; 49 | height: 8px; 50 | border-radius: 8px; 51 | border: 1px solid #000; } 52 | 53 | .pointer span.current { 54 | background: #FC0; } 55 | 56 | .controls { 57 | text-align: center; 58 | margin: 0 0 15px 0; } 59 | 60 | .controls .num { 61 | width: 60px; } 62 | 63 | #demo-simple .flipsnap, #demo-threshold .flipsnap { 64 | width: 960px; 65 | padding: 0; } 66 | #demo-simple .item, #demo-threshold .item { 67 | width: 310px; 68 | margin: 0; } 69 | 70 | #demo-img .flipsnap { 71 | width: 960px; 72 | padding: 0; } 73 | #demo-img .item { 74 | margin: 0; 75 | padding: 0; 76 | border: none; 77 | width: auto; 78 | height: auto; } 79 | 80 | #demo-maxPoint .flipsnap { 81 | width: 800px; 82 | /* 80px(item) * 10 */ 83 | padding: 0; } 84 | #demo-maxPoint .item { 85 | width: 50px; 86 | font-size: 20px; 87 | padding: 10px 0; } 88 | 89 | #demo-touchevents .log { 90 | width: 280px; 91 | margin: 0 auto; 92 | } 93 | 94 | #demo-touchevents pre.detail { 95 | background: #EFEFEF; 96 | padding: 5px; 97 | border: 1px solid #CCC; 98 | height: 80px; 99 | font-size: 12px; 100 | } 101 | 102 | #demo-refresh .flipsnap { 103 | width: 270px; } 104 | 105 | #demo-link .item { 106 | padding: 0; } 107 | #demo-link .item a { 108 | width: 200px; 109 | height: 180px; 110 | display: table-cell; 111 | vertical-align: middle; 112 | text-align: center; 113 | color: #00F; 114 | font-size: 18px; } 115 | 116 | #demo-moveToPoint .flipsnap { 117 | width: 2345px; } 118 | 119 | .sample p { 120 | text-align: center; } 121 | 122 | .sample pre { 123 | display: none; } 124 | -------------------------------------------------------------------------------- /flipsnap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * flipsnap.js 3 | * 4 | * @version 0.6.3 5 | * @url http://hokaccha.github.com/js-flipsnap/ 6 | * 7 | * Copyright 2011 PixelGrid, Inc. 8 | * Licensed under the MIT License: 9 | * http://www.opensource.org/licenses/mit-license.php 10 | */ 11 | 12 | (function(root, factory) { 13 | if (typeof define === 'function' && define.amd) { 14 | define([], factory); 15 | } else if (typeof exports === 'object') { 16 | module.exports = factory(); 17 | } else { 18 | root.Flipsnap = factory(); 19 | } 20 | })(this, function() { 21 | 22 | var div = document.createElement('div'); 23 | var prefix = ['webkit', 'moz', 'o', 'ms']; 24 | var saveProp = {}; 25 | var support = Flipsnap.support = {}; 26 | var gestureStart = false; 27 | 28 | var DISTANCE_THRESHOLD = 5; 29 | var ANGLE_THREHOLD = 55; 30 | 31 | support.transform3d = hasProp([ 32 | 'perspectiveProperty', 33 | 'WebkitPerspective', 34 | 'MozPerspective', 35 | 'OPerspective', 36 | 'msPerspective' 37 | ]); 38 | 39 | support.transform = hasProp([ 40 | 'transformProperty', 41 | 'WebkitTransform', 42 | 'MozTransform', 43 | 'OTransform', 44 | 'msTransform' 45 | ]); 46 | 47 | support.transition = hasProp([ 48 | 'transitionProperty', 49 | 'WebkitTransitionProperty', 50 | 'MozTransitionProperty', 51 | 'OTransitionProperty', 52 | 'msTransitionProperty' 53 | ]); 54 | 55 | support.addEventListener = 'addEventListener' in window; 56 | support.mspointer = window.navigator.msPointerEnabled; 57 | 58 | support.cssAnimation = (support.transform3d || support.transform) && support.transition; 59 | 60 | var eventTypes = ['touch', 'mouse']; 61 | var events = { 62 | start: { 63 | touch: 'touchstart', 64 | mouse: 'mousedown' 65 | }, 66 | move: { 67 | touch: 'touchmove', 68 | mouse: 'mousemove' 69 | }, 70 | end: { 71 | touch: 'touchend', 72 | mouse: 'mouseup' 73 | } 74 | }; 75 | 76 | if (support.addEventListener) { 77 | document.addEventListener('gesturestart', function() { 78 | gestureStart = true; 79 | }); 80 | 81 | document.addEventListener('gestureend', function() { 82 | gestureStart = false; 83 | }); 84 | } 85 | 86 | function Flipsnap(element, opts) { 87 | return (this instanceof Flipsnap) 88 | ? this.init(element, opts) 89 | : new Flipsnap(element, opts); 90 | } 91 | 92 | Flipsnap.prototype.init = function(element, opts) { 93 | var self = this; 94 | 95 | // set element 96 | self.element = element; 97 | if (typeof element === 'string') { 98 | self.element = document.querySelector(element); 99 | } 100 | 101 | if (!self.element) { 102 | throw new Error('element not found'); 103 | } 104 | 105 | if (support.mspointer) { 106 | self.element.style.msTouchAction = 'pan-y'; 107 | } 108 | 109 | // set opts 110 | opts = opts || {}; 111 | self.distance = opts.distance; 112 | self.maxPoint = opts.maxPoint; 113 | self.disableTouch = (opts.disableTouch === undefined) ? false : opts.disableTouch; 114 | self.disable3d = (opts.disable3d === undefined) ? false : opts.disable3d; 115 | self.transitionDuration = (opts.transitionDuration === undefined) ? '350ms' : opts.transitionDuration + 'ms'; 116 | self.threshold = opts.threshold || 0; 117 | 118 | // set property 119 | self.currentPoint = 0; 120 | self.currentX = 0; 121 | self.animation = false; 122 | self.timerId = null; 123 | self.use3d = support.transform3d; 124 | if (self.disable3d === true) { 125 | self.use3d = false; 126 | } 127 | 128 | // set default style 129 | if (support.cssAnimation) { 130 | self._setStyle({ 131 | transitionProperty: getCSSVal('transform'), 132 | transitionTimingFunction: 'cubic-bezier(0,0,0.25,1)', 133 | transitionDuration: '0ms', 134 | transform: self._getTranslate(0) 135 | }); 136 | } 137 | else { 138 | self._setStyle({ 139 | position: 'relative', 140 | left: '0px' 141 | }); 142 | } 143 | 144 | // initilize 145 | self.refresh(); 146 | 147 | eventTypes.forEach(function(type) { 148 | self.element.addEventListener(events.start[type], self, false); 149 | }); 150 | 151 | return self; 152 | }; 153 | 154 | Flipsnap.prototype.handleEvent = function(event) { 155 | var self = this; 156 | 157 | switch (event.type) { 158 | // start 159 | case events.start.touch: self._touchStart(event, 'touch'); break; 160 | case events.start.mouse: self._touchStart(event, 'mouse'); break; 161 | 162 | // move 163 | case events.move.touch: self._touchMove(event, 'touch'); break; 164 | case events.move.mouse: self._touchMove(event, 'mouse'); break; 165 | 166 | // end 167 | case events.end.touch: self._touchEnd(event, 'touch'); break; 168 | case events.end.mouse: self._touchEnd(event, 'mouse'); break; 169 | 170 | // click 171 | case 'click': self._click(event); break; 172 | } 173 | }; 174 | 175 | Flipsnap.prototype.refresh = function() { 176 | var self = this; 177 | 178 | // setting max point 179 | self._maxPoint = (self.maxPoint === undefined) ? (function() { 180 | var childNodes = self.element.childNodes, 181 | itemLength = -1, 182 | i = 0, 183 | len = childNodes.length, 184 | node; 185 | for(; i < len; i++) { 186 | node = childNodes[i]; 187 | if (node.nodeType === 1) { 188 | itemLength++; 189 | } 190 | } 191 | 192 | return itemLength; 193 | })() : self.maxPoint; 194 | 195 | // setting distance 196 | if (self.distance === undefined) { 197 | if (self._maxPoint < 0) { 198 | self._distance = 0; 199 | } 200 | else { 201 | self._distance = self.element.scrollWidth / (self._maxPoint + 1); 202 | } 203 | } 204 | else { 205 | self._distance = self.distance; 206 | } 207 | 208 | // setting maxX 209 | self._maxX = -self._distance * self._maxPoint; 210 | 211 | self.moveToPoint(); 212 | }; 213 | 214 | Flipsnap.prototype.hasNext = function() { 215 | var self = this; 216 | 217 | return self.currentPoint < self._maxPoint; 218 | }; 219 | 220 | Flipsnap.prototype.hasPrev = function() { 221 | var self = this; 222 | 223 | return self.currentPoint > 0; 224 | }; 225 | 226 | Flipsnap.prototype.toNext = function(transitionDuration) { 227 | var self = this; 228 | 229 | if (!self.hasNext()) { 230 | return; 231 | } 232 | 233 | self.moveToPoint(self.currentPoint + 1, transitionDuration); 234 | }; 235 | 236 | Flipsnap.prototype.toPrev = function(transitionDuration) { 237 | var self = this; 238 | 239 | if (!self.hasPrev()) { 240 | return; 241 | } 242 | 243 | self.moveToPoint(self.currentPoint - 1, transitionDuration); 244 | }; 245 | 246 | Flipsnap.prototype.moveToPoint = function(point, transitionDuration) { 247 | var self = this; 248 | 249 | transitionDuration = transitionDuration === undefined 250 | ? self.transitionDuration : transitionDuration + 'ms'; 251 | 252 | var beforePoint = self.currentPoint; 253 | 254 | // not called from `refresh()` 255 | if (point === undefined) { 256 | point = self.currentPoint; 257 | } 258 | 259 | if (point < 0) { 260 | self.currentPoint = 0; 261 | } 262 | else if (point > self._maxPoint) { 263 | self.currentPoint = self._maxPoint; 264 | } 265 | else { 266 | self.currentPoint = parseInt(point, 10); 267 | } 268 | 269 | if (support.cssAnimation) { 270 | self._setStyle({ transitionDuration: transitionDuration }); 271 | } 272 | else { 273 | self.animation = true; 274 | } 275 | self._setX(- self.currentPoint * self._distance, transitionDuration); 276 | 277 | if (beforePoint !== self.currentPoint) { // is move? 278 | // `fsmoveend` is deprecated 279 | // `fspointmove` is recommend. 280 | self._triggerEvent('fsmoveend', true, false); 281 | self._triggerEvent('fspointmove', true, false); 282 | } 283 | }; 284 | 285 | Flipsnap.prototype._setX = function(x, transitionDuration) { 286 | var self = this; 287 | 288 | self.currentX = x; 289 | if (support.cssAnimation) { 290 | self.element.style[ saveProp.transform ] = self._getTranslate(x); 291 | } 292 | else { 293 | if (self.animation) { 294 | self._animate(x, transitionDuration || self.transitionDuration); 295 | } 296 | else { 297 | self.element.style.left = x + 'px'; 298 | } 299 | } 300 | }; 301 | 302 | Flipsnap.prototype._touchStart = function(event, type) { 303 | var self = this; 304 | 305 | if (self.disableTouch || self.scrolling || gestureStart) { 306 | return; 307 | } 308 | 309 | self.element.addEventListener(events.move[type], self, false); 310 | document.addEventListener(events.end[type], self, false); 311 | 312 | var tagName = event.target.tagName; 313 | if (type === 'mouse' && tagName !== 'SELECT' && tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON') { 314 | event.preventDefault(); 315 | } 316 | 317 | if (support.cssAnimation) { 318 | self._setStyle({ transitionDuration: '0ms' }); 319 | } 320 | else { 321 | self.animation = false; 322 | } 323 | self.scrolling = true; 324 | self.moveReady = false; 325 | self.startPageX = getPage(event, 'pageX'); 326 | self.startPageY = getPage(event, 'pageY'); 327 | self.basePageX = self.startPageX; 328 | self.directionX = 0; 329 | self.startTime = event.timeStamp; 330 | self._triggerEvent('fstouchstart', true, false); 331 | }; 332 | 333 | Flipsnap.prototype._touchMove = function(event, type) { 334 | var self = this; 335 | 336 | if (!self.scrolling || gestureStart) { 337 | return; 338 | } 339 | 340 | var pageX = getPage(event, 'pageX'); 341 | var pageY = getPage(event, 'pageY'); 342 | var distX; 343 | var newX; 344 | 345 | if (self.moveReady) { 346 | event.preventDefault(); 347 | 348 | distX = pageX - self.basePageX; 349 | newX = self.currentX + distX; 350 | if (newX >= 0 || newX < self._maxX) { 351 | newX = Math.round(self.currentX + distX / 3); 352 | } 353 | 354 | // When distX is 0, use one previous value. 355 | // For android firefox. When touchend fired, touchmove also 356 | // fired and distX is certainly set to 0. 357 | self.directionX = 358 | distX === 0 ? self.directionX : 359 | distX > 0 ? -1 : 1; 360 | 361 | // if they prevent us then stop it 362 | var isPrevent = !self._triggerEvent('fstouchmove', true, true, { 363 | delta: distX, 364 | direction: self.directionX 365 | }); 366 | 367 | if (isPrevent) { 368 | self._touchAfter({ 369 | moved: false, 370 | originalPoint: self.currentPoint, 371 | newPoint: self.currentPoint, 372 | cancelled: true 373 | }); 374 | } else { 375 | self._setX(newX); 376 | } 377 | } 378 | else { 379 | // https://github.com/hokaccha/js-flipsnap/pull/36 380 | var triangle = getTriangleSide(self.startPageX, self.startPageY, pageX, pageY); 381 | if (triangle.z > DISTANCE_THRESHOLD) { 382 | if (getAngle(triangle) > ANGLE_THREHOLD) { 383 | event.preventDefault(); 384 | self.moveReady = true; 385 | self.element.addEventListener('click', self, true); 386 | } 387 | else { 388 | self.scrolling = false; 389 | } 390 | } 391 | } 392 | 393 | self.basePageX = pageX; 394 | }; 395 | 396 | Flipsnap.prototype._touchEnd = function(event, type) { 397 | var self = this; 398 | 399 | self.element.removeEventListener(events.move[type], self, false); 400 | document.removeEventListener(events.end[type], self, false); 401 | 402 | if (!self.scrolling) { 403 | return; 404 | } 405 | 406 | var newPoint = -self.currentX / self._distance; 407 | newPoint = 408 | (self.directionX > 0) ? Math.ceil(newPoint) : 409 | (self.directionX < 0) ? Math.floor(newPoint) : 410 | Math.round(newPoint); 411 | 412 | if (newPoint < 0) { 413 | newPoint = 0; 414 | } 415 | else if (newPoint > self._maxPoint) { 416 | newPoint = self._maxPoint; 417 | } 418 | 419 | if (Math.abs(self.startPageX - self.basePageX) < self.threshold) { 420 | newPoint = self.currentPoint; 421 | } 422 | 423 | self._touchAfter({ 424 | moved: newPoint !== self.currentPoint, 425 | originalPoint: self.currentPoint, 426 | newPoint: newPoint, 427 | cancelled: false 428 | }); 429 | 430 | self.moveToPoint(newPoint); 431 | }; 432 | 433 | Flipsnap.prototype._click = function(event) { 434 | var self = this; 435 | 436 | event.stopPropagation(); 437 | event.preventDefault(); 438 | }; 439 | 440 | Flipsnap.prototype._touchAfter = function(params) { 441 | var self = this; 442 | 443 | self.scrolling = false; 444 | self.moveReady = false; 445 | 446 | setTimeout(function() { 447 | self.element.removeEventListener('click', self, true); 448 | }, 200); 449 | 450 | self._triggerEvent('fstouchend', true, false, params); 451 | }; 452 | 453 | Flipsnap.prototype._setStyle = function(styles) { 454 | var self = this; 455 | var style = self.element.style; 456 | 457 | for (var prop in styles) { 458 | setStyle(style, prop, styles[prop]); 459 | } 460 | }; 461 | 462 | Flipsnap.prototype._animate = function(x, transitionDuration) { 463 | var self = this; 464 | 465 | var elem = self.element; 466 | var begin = +new Date(); 467 | var from = parseInt(elem.style.left, 10); 468 | var to = x; 469 | var duration = parseInt(transitionDuration, 10); 470 | var easing = function(time, duration) { 471 | return -(time /= duration) * (time - 2); 472 | }; 473 | 474 | if (self.timerId) { 475 | clearInterval(self.timerId); 476 | } 477 | self.timerId = setInterval(function() { 478 | var time = new Date() - begin; 479 | var pos, now; 480 | if (time > duration) { 481 | clearInterval(self.timerId); 482 | self.timerId = null; 483 | now = to; 484 | } 485 | else { 486 | pos = easing(time, duration); 487 | now = pos * (to - from) + from; 488 | } 489 | elem.style.left = now + "px"; 490 | }, 10); 491 | }; 492 | 493 | Flipsnap.prototype.destroy = function() { 494 | var self = this; 495 | 496 | eventTypes.forEach(function(type) { 497 | self.element.removeEventListener(events.start[type], self, false); 498 | }); 499 | }; 500 | 501 | Flipsnap.prototype._getTranslate = function(x) { 502 | var self = this; 503 | 504 | return self.use3d 505 | ? 'translate3d(' + x + 'px, 0, 0)' 506 | : 'translate(' + x + 'px, 0)'; 507 | }; 508 | 509 | Flipsnap.prototype._triggerEvent = function(type, bubbles, cancelable, data) { 510 | var self = this; 511 | 512 | var ev = document.createEvent('Event'); 513 | ev.initEvent(type, bubbles, cancelable); 514 | 515 | if (data) { 516 | for (var d in data) { 517 | if (data.hasOwnProperty(d)) { 518 | ev[d] = data[d]; 519 | } 520 | } 521 | } 522 | 523 | return self.element.dispatchEvent(ev); 524 | }; 525 | 526 | function getPage(event, page) { 527 | return event.changedTouches ? event.changedTouches[0][page] : event[page]; 528 | } 529 | 530 | function hasProp(props) { 531 | return some(props, function(prop) { 532 | return div.style[ prop ] !== undefined; 533 | }); 534 | } 535 | 536 | function setStyle(style, prop, val) { 537 | var _saveProp = saveProp[ prop ]; 538 | if (_saveProp) { 539 | style[ _saveProp ] = val; 540 | } 541 | else if (style[ prop ] !== undefined) { 542 | saveProp[ prop ] = prop; 543 | style[ prop ] = val; 544 | } 545 | else { 546 | some(prefix, function(_prefix) { 547 | var _prop = ucFirst(_prefix) + ucFirst(prop); 548 | if (style[ _prop ] !== undefined) { 549 | saveProp[ prop ] = _prop; 550 | style[ _prop ] = val; 551 | return true; 552 | } 553 | }); 554 | } 555 | } 556 | 557 | function getCSSVal(prop) { 558 | if (div.style[ prop ] !== undefined) { 559 | return prop; 560 | } 561 | else { 562 | var ret; 563 | some(prefix, function(_prefix) { 564 | var _prop = ucFirst(_prefix) + ucFirst(prop); 565 | if (div.style[ _prop ] !== undefined) { 566 | ret = '-' + _prefix + '-' + prop; 567 | return true; 568 | } 569 | }); 570 | return ret; 571 | } 572 | } 573 | 574 | function ucFirst(str) { 575 | return str.charAt(0).toUpperCase() + str.substr(1); 576 | } 577 | 578 | function some(ary, callback) { 579 | for (var i = 0, len = ary.length; i < len; i++) { 580 | if (callback(ary[i], i)) { 581 | return true; 582 | } 583 | } 584 | return false; 585 | } 586 | 587 | function getTriangleSide(x1, y1, x2, y2) { 588 | var x = Math.abs(x1 - x2); 589 | var y = Math.abs(y1 - y2); 590 | var z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); 591 | 592 | return { 593 | x: x, 594 | y: y, 595 | z: z 596 | }; 597 | } 598 | 599 | function getAngle(triangle) { 600 | var cos = triangle.y / triangle.z; 601 | var radian = Math.acos(cos); 602 | 603 | return 180 / (Math.PI / radian); 604 | } 605 | 606 | return Flipsnap; 607 | 608 | }); 609 | -------------------------------------------------------------------------------- /flipsnap.min.js: -------------------------------------------------------------------------------- 1 | (function(root,factory){if(typeof define==="function"&&define.amd){define([],factory)}else if(typeof exports==="object"){module.exports=factory()}else{root.Flipsnap=factory()}})(this,function(){var div=document.createElement("div");var prefix=["webkit","moz","o","ms"];var saveProp={};var support=Flipsnap.support={};var gestureStart=false;var DISTANCE_THRESHOLD=5;var ANGLE_THREHOLD=55;support.transform3d=hasProp(["perspectiveProperty","WebkitPerspective","MozPerspective","OPerspective","msPerspective"]);support.transform=hasProp(["transformProperty","WebkitTransform","MozTransform","OTransform","msTransform"]);support.transition=hasProp(["transitionProperty","WebkitTransitionProperty","MozTransitionProperty","OTransitionProperty","msTransitionProperty"]);support.addEventListener="addEventListener"in window;support.mspointer=window.navigator.msPointerEnabled;support.cssAnimation=(support.transform3d||support.transform)&&support.transition;var eventTypes=["touch","mouse"];var events={start:{touch:"touchstart",mouse:"mousedown"},move:{touch:"touchmove",mouse:"mousemove"},end:{touch:"touchend",mouse:"mouseup"}};if(support.addEventListener){document.addEventListener("gesturestart",function(){gestureStart=true});document.addEventListener("gestureend",function(){gestureStart=false})}function Flipsnap(element,opts){return this instanceof Flipsnap?this.init(element,opts):new Flipsnap(element,opts)}Flipsnap.prototype.init=function(element,opts){var self=this;self.element=element;if(typeof element==="string"){self.element=document.querySelector(element)}if(!self.element){throw new Error("element not found")}if(support.mspointer){self.element.style.msTouchAction="pan-y"}opts=opts||{};self.distance=opts.distance;self.maxPoint=opts.maxPoint;self.disableTouch=opts.disableTouch===undefined?false:opts.disableTouch;self.disable3d=opts.disable3d===undefined?false:opts.disable3d;self.transitionDuration=opts.transitionDuration===undefined?"350ms":opts.transitionDuration+"ms";self.threshold=opts.threshold||0;self.currentPoint=0;self.currentX=0;self.animation=false;self.timerId=null;self.use3d=support.transform3d;if(self.disable3d===true){self.use3d=false}if(support.cssAnimation){self._setStyle({transitionProperty:getCSSVal("transform"),transitionTimingFunction:"cubic-bezier(0,0,0.25,1)",transitionDuration:"0ms",transform:self._getTranslate(0)})}else{self._setStyle({position:"relative",left:"0px"})}self.refresh();eventTypes.forEach(function(type){self.element.addEventListener(events.start[type],self,false)});return self};Flipsnap.prototype.handleEvent=function(event){var self=this;switch(event.type){case events.start.touch:self._touchStart(event,"touch");break;case events.start.mouse:self._touchStart(event,"mouse");break;case events.move.touch:self._touchMove(event,"touch");break;case events.move.mouse:self._touchMove(event,"mouse");break;case events.end.touch:self._touchEnd(event,"touch");break;case events.end.mouse:self._touchEnd(event,"mouse");break;case"click":self._click(event);break}};Flipsnap.prototype.refresh=function(){var self=this;self._maxPoint=self.maxPoint===undefined?function(){var childNodes=self.element.childNodes,itemLength=-1,i=0,len=childNodes.length,node;for(;i0};Flipsnap.prototype.toNext=function(transitionDuration){var self=this;if(!self.hasNext()){return}self.moveToPoint(self.currentPoint+1,transitionDuration)};Flipsnap.prototype.toPrev=function(transitionDuration){var self=this;if(!self.hasPrev()){return}self.moveToPoint(self.currentPoint-1,transitionDuration)};Flipsnap.prototype.moveToPoint=function(point,transitionDuration){var self=this;transitionDuration=transitionDuration===undefined?self.transitionDuration:transitionDuration+"ms";var beforePoint=self.currentPoint;if(point===undefined){point=self.currentPoint}if(point<0){self.currentPoint=0}else if(point>self._maxPoint){self.currentPoint=self._maxPoint}else{self.currentPoint=parseInt(point,10)}if(support.cssAnimation){self._setStyle({transitionDuration:transitionDuration})}else{self.animation=true}self._setX(-self.currentPoint*self._distance,transitionDuration);if(beforePoint!==self.currentPoint){self._triggerEvent("fsmoveend",true,false);self._triggerEvent("fspointmove",true,false)}};Flipsnap.prototype._setX=function(x,transitionDuration){var self=this;self.currentX=x;if(support.cssAnimation){self.element.style[saveProp.transform]=self._getTranslate(x)}else{if(self.animation){self._animate(x,transitionDuration||self.transitionDuration)}else{self.element.style.left=x+"px"}}};Flipsnap.prototype._touchStart=function(event,type){var self=this;if(self.disableTouch||self.scrolling||gestureStart){return}self.element.addEventListener(events.move[type],self,false);document.addEventListener(events.end[type],self,false);var tagName=event.target.tagName;if(type==="mouse"&&tagName!=="SELECT"&&tagName!=="INPUT"&&tagName!=="TEXTAREA"&&tagName!=="BUTTON"){event.preventDefault()}if(support.cssAnimation){self._setStyle({transitionDuration:"0ms"})}else{self.animation=false}self.scrolling=true;self.moveReady=false;self.startPageX=getPage(event,"pageX");self.startPageY=getPage(event,"pageY");self.basePageX=self.startPageX;self.directionX=0;self.startTime=event.timeStamp;self._triggerEvent("fstouchstart",true,false)};Flipsnap.prototype._touchMove=function(event,type){var self=this;if(!self.scrolling||gestureStart){return}var pageX=getPage(event,"pageX");var pageY=getPage(event,"pageY");var distX;var newX;if(self.moveReady){event.preventDefault();distX=pageX-self.basePageX;newX=self.currentX+distX;if(newX>=0||newX0?-1:1;var isPrevent=!self._triggerEvent("fstouchmove",true,true,{delta:distX,direction:self.directionX});if(isPrevent){self._touchAfter({moved:false,originalPoint:self.currentPoint,newPoint:self.currentPoint,cancelled:true})}else{self._setX(newX)}}else{var triangle=getTriangleSide(self.startPageX,self.startPageY,pageX,pageY);if(triangle.z>DISTANCE_THRESHOLD){if(getAngle(triangle)>ANGLE_THREHOLD){event.preventDefault();self.moveReady=true;self.element.addEventListener("click",self,true)}else{self.scrolling=false}}}self.basePageX=pageX};Flipsnap.prototype._touchEnd=function(event,type){var self=this;self.element.removeEventListener(events.move[type],self,false);document.removeEventListener(events.end[type],self,false);if(!self.scrolling){return}var newPoint=-self.currentX/self._distance;newPoint=self.directionX>0?Math.ceil(newPoint):self.directionX<0?Math.floor(newPoint):Math.round(newPoint);if(newPoint<0){newPoint=0}else if(newPoint>self._maxPoint){newPoint=self._maxPoint}if(Math.abs(self.startPageX-self.basePageX)duration){clearInterval(self.timerId);self.timerId=null;now=to}else{pos=easing(time,duration);now=pos*(to-from)+from}elem.style.left=now+"px"},10)};Flipsnap.prototype.destroy=function(){var self=this;eventTypes.forEach(function(type){self.element.removeEventListener(events.start[type],self,false)})};Flipsnap.prototype._getTranslate=function(x){var self=this;return self.use3d?"translate3d("+x+"px, 0, 0)":"translate("+x+"px, 0)"};Flipsnap.prototype._triggerEvent=function(type,bubbles,cancelable,data){var self=this;var ev=document.createEvent("Event");ev.initEvent(type,bubbles,cancelable);if(data){for(var d in data){if(data.hasOwnProperty(d)){ev[d]=data[d]}}}return self.element.dispatchEvent(ev)};function getPage(event,page){return event.changedTouches?event.changedTouches[0][page]:event[page]}function hasProp(props){return some(props,function(prop){return div.style[prop]!==undefined})}function setStyle(style,prop,val){var _saveProp=saveProp[prop];if(_saveProp){style[_saveProp]=val}else if(style[prop]!==undefined){saveProp[prop]=prop;style[prop]=val}else{some(prefix,function(_prefix){var _prop=ucFirst(_prefix)+ucFirst(prop);if(style[_prop]!==undefined){saveProp[prop]=_prop;style[_prop]=val;return true}})}}function getCSSVal(prop){if(div.style[prop]!==undefined){return prop}else{var ret;some(prefix,function(_prefix){var _prop=ucFirst(_prefix)+ucFirst(prop);if(div.style[_prop]!==undefined){ret="-"+_prefix+"-"+prop;return true}});return ret}}function ucFirst(str){return str.charAt(0).toUpperCase()+str.substr(1)}function some(ary,callback){for(var i=0,len=ary.length;i flipsnap.min.js" 9 | }, 10 | "keywords": [ 11 | "mobile", 12 | "ui", 13 | "browser" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git@github.com/hokaccha/js-flipsnap.git" 18 | }, 19 | "license": "MIT", 20 | "homepage": "https://github.com/hokaccha/js-flipsnap", 21 | "devDependencies": { 22 | "uglify-js": "^2.4.15" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | flipsnap.js Test 7 | 8 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /test/lib/expect.js: -------------------------------------------------------------------------------- 1 | 2 | (function (global, module) { 3 | 4 | if ('undefined' == typeof module) { 5 | var module = { exports: {} } 6 | , exports = module.exports 7 | } 8 | 9 | /** 10 | * Exports. 11 | */ 12 | 13 | module.exports = expect; 14 | expect.Assertion = Assertion; 15 | 16 | /** 17 | * Exports version. 18 | */ 19 | 20 | expect.version = '0.1.2'; 21 | 22 | /** 23 | * Possible assertion flags. 24 | */ 25 | 26 | var flags = { 27 | not: ['to', 'be', 'have', 'include', 'only'] 28 | , to: ['be', 'have', 'include', 'only', 'not'] 29 | , only: ['have'] 30 | , have: ['own'] 31 | , be: ['an'] 32 | }; 33 | 34 | function expect (obj) { 35 | return new Assertion(obj); 36 | } 37 | 38 | /** 39 | * Constructor 40 | * 41 | * @api private 42 | */ 43 | 44 | function Assertion (obj, flag, parent) { 45 | this.obj = obj; 46 | this.flags = {}; 47 | 48 | if (undefined != parent) { 49 | this.flags[flag] = true; 50 | 51 | for (var i in parent.flags) { 52 | if (parent.flags.hasOwnProperty(i)) { 53 | this.flags[i] = true; 54 | } 55 | } 56 | } 57 | 58 | var $flags = flag ? flags[flag] : keys(flags) 59 | , self = this 60 | 61 | if ($flags) { 62 | for (var i = 0, l = $flags.length; i < l; i++) { 63 | // avoid recursion 64 | if (this.flags[$flags[i]]) continue; 65 | 66 | var name = $flags[i] 67 | , assertion = new Assertion(this.obj, name, this) 68 | 69 | if ('function' == typeof Assertion.prototype[name]) { 70 | // clone the function, make sure we dont touch the prot reference 71 | var old = this[name]; 72 | this[name] = function () { 73 | return old.apply(self, arguments); 74 | } 75 | 76 | for (var fn in Assertion.prototype) { 77 | if (Assertion.prototype.hasOwnProperty(fn) && fn != name) { 78 | this[name][fn] = bind(assertion[fn], assertion); 79 | } 80 | } 81 | } else { 82 | this[name] = assertion; 83 | } 84 | } 85 | } 86 | }; 87 | 88 | /** 89 | * Performs an assertion 90 | * 91 | * @api private 92 | */ 93 | 94 | Assertion.prototype.assert = function (truth, msg, error) { 95 | var msg = this.flags.not ? error : msg 96 | , ok = this.flags.not ? !truth : truth; 97 | 98 | if (!ok) { 99 | throw new Error(msg); 100 | } 101 | 102 | this.and = new Assertion(this.obj); 103 | }; 104 | 105 | /** 106 | * Check if the value is truthy 107 | * 108 | * @api public 109 | */ 110 | 111 | Assertion.prototype.ok = function () { 112 | this.assert( 113 | !!this.obj 114 | , 'expected ' + i(this.obj) + ' to be truthy' 115 | , 'expected ' + i(this.obj) + ' to be falsy'); 116 | }; 117 | 118 | /** 119 | * Assert that the function throws. 120 | * 121 | * @param {Function|RegExp} callback, or regexp to match error string against 122 | * @api public 123 | */ 124 | 125 | Assertion.prototype.throwError = 126 | Assertion.prototype.throwException = function (fn) { 127 | expect(this.obj).to.be.a('function'); 128 | 129 | var thrown = false 130 | , not = this.flags.not 131 | 132 | try { 133 | this.obj(); 134 | } catch (e) { 135 | if ('function' == typeof fn) { 136 | fn(e); 137 | } else if ('object' == typeof fn) { 138 | var subject = 'string' == typeof e ? e : e.message; 139 | if (not) { 140 | expect(subject).to.not.match(fn); 141 | } else { 142 | expect(subject).to.match(fn); 143 | } 144 | } 145 | thrown = true; 146 | } 147 | 148 | if ('object' == typeof fn && not) { 149 | // in the presence of a matcher, ensure the `not` only applies to 150 | // the matching. 151 | this.flags.not = false; 152 | } 153 | 154 | var name = this.obj.name || 'fn'; 155 | this.assert( 156 | thrown 157 | , 'expected ' + name + ' to throw an exception' 158 | , 'expected ' + name + ' not to throw an exception'); 159 | }; 160 | 161 | /** 162 | * Checks if the array is empty. 163 | * 164 | * @api public 165 | */ 166 | 167 | Assertion.prototype.empty = function () { 168 | var expectation; 169 | 170 | if ('object' == typeof this.obj && null !== this.obj && !isArray(this.obj)) { 171 | if ('number' == typeof this.obj.length) { 172 | expectation = !this.obj.length; 173 | } else { 174 | expectation = !keys(this.obj).length; 175 | } 176 | } else { 177 | if ('string' != typeof this.obj) { 178 | expect(this.obj).to.be.an('object'); 179 | } 180 | 181 | expect(this.obj).to.have.property('length'); 182 | expectation = !this.obj.length; 183 | } 184 | 185 | this.assert( 186 | expectation 187 | , 'expected ' + i(this.obj) + ' to be empty' 188 | , 'expected ' + i(this.obj) + ' to not be empty'); 189 | return this; 190 | }; 191 | 192 | /** 193 | * Checks if the obj exactly equals another. 194 | * 195 | * @api public 196 | */ 197 | 198 | Assertion.prototype.be = 199 | Assertion.prototype.equal = function (obj) { 200 | this.assert( 201 | obj === this.obj 202 | , 'expected ' + i(this.obj) + ' to equal ' + i(obj) 203 | , 'expected ' + i(this.obj) + ' to not equal ' + i(obj)); 204 | return this; 205 | }; 206 | 207 | /** 208 | * Checks if the obj sortof equals another. 209 | * 210 | * @api public 211 | */ 212 | 213 | Assertion.prototype.eql = function (obj) { 214 | this.assert( 215 | expect.eql(obj, this.obj) 216 | , 'expected ' + i(this.obj) + ' to sort of equal ' + i(obj) 217 | , 'expected ' + i(this.obj) + ' to sort of not equal ' + i(obj)); 218 | return this; 219 | }; 220 | 221 | /** 222 | * Assert within start to finish (inclusive). 223 | * 224 | * @param {Number} start 225 | * @param {Number} finish 226 | * @api public 227 | */ 228 | 229 | Assertion.prototype.within = function (start, finish) { 230 | var range = start + '..' + finish; 231 | this.assert( 232 | this.obj >= start && this.obj <= finish 233 | , 'expected ' + i(this.obj) + ' to be within ' + range 234 | , 'expected ' + i(this.obj) + ' to not be within ' + range); 235 | return this; 236 | }; 237 | 238 | /** 239 | * Assert typeof / instance of 240 | * 241 | * @api public 242 | */ 243 | 244 | Assertion.prototype.a = 245 | Assertion.prototype.an = function (type) { 246 | if ('string' == typeof type) { 247 | // proper english in error msg 248 | var n = /^[aeiou]/.test(type) ? 'n' : ''; 249 | 250 | // typeof with support for 'array' 251 | this.assert( 252 | 'array' == type ? isArray(this.obj) : 253 | 'object' == type 254 | ? 'object' == typeof this.obj && null !== this.obj 255 | : type == typeof this.obj 256 | , 'expected ' + i(this.obj) + ' to be a' + n + ' ' + type 257 | , 'expected ' + i(this.obj) + ' not to be a' + n + ' ' + type); 258 | } else { 259 | // instanceof 260 | var name = type.name || 'supplied constructor'; 261 | this.assert( 262 | this.obj instanceof type 263 | , 'expected ' + i(this.obj) + ' to be an instance of ' + name 264 | , 'expected ' + i(this.obj) + ' not to be an instance of ' + name); 265 | } 266 | 267 | return this; 268 | }; 269 | 270 | /** 271 | * Assert numeric value above _n_. 272 | * 273 | * @param {Number} n 274 | * @api public 275 | */ 276 | 277 | Assertion.prototype.greaterThan = 278 | Assertion.prototype.above = function (n) { 279 | this.assert( 280 | this.obj > n 281 | , 'expected ' + i(this.obj) + ' to be above ' + n 282 | , 'expected ' + i(this.obj) + ' to be below ' + n); 283 | return this; 284 | }; 285 | 286 | /** 287 | * Assert numeric value below _n_. 288 | * 289 | * @param {Number} n 290 | * @api public 291 | */ 292 | 293 | Assertion.prototype.lessThan = 294 | Assertion.prototype.below = function (n) { 295 | this.assert( 296 | this.obj < n 297 | , 'expected ' + i(this.obj) + ' to be below ' + n 298 | , 'expected ' + i(this.obj) + ' to be above ' + n); 299 | return this; 300 | }; 301 | 302 | /** 303 | * Assert string value matches _regexp_. 304 | * 305 | * @param {RegExp} regexp 306 | * @api public 307 | */ 308 | 309 | Assertion.prototype.match = function (regexp) { 310 | this.assert( 311 | regexp.exec(this.obj) 312 | , 'expected ' + i(this.obj) + ' to match ' + regexp 313 | , 'expected ' + i(this.obj) + ' not to match ' + regexp); 314 | return this; 315 | }; 316 | 317 | /** 318 | * Assert property "length" exists and has value of _n_. 319 | * 320 | * @param {Number} n 321 | * @api public 322 | */ 323 | 324 | Assertion.prototype.length = function (n) { 325 | expect(this.obj).to.have.property('length'); 326 | var len = this.obj.length; 327 | this.assert( 328 | n == len 329 | , 'expected ' + i(this.obj) + ' to have a length of ' + n + ' but got ' + len 330 | , 'expected ' + i(this.obj) + ' to not have a length of ' + len); 331 | return this; 332 | }; 333 | 334 | /** 335 | * Assert property _name_ exists, with optional _val_. 336 | * 337 | * @param {String} name 338 | * @param {Mixed} val 339 | * @api public 340 | */ 341 | 342 | Assertion.prototype.property = function (name, val) { 343 | if (this.flags.own) { 344 | this.assert( 345 | Object.prototype.hasOwnProperty.call(this.obj, name) 346 | , 'expected ' + i(this.obj) + ' to have own property ' + i(name) 347 | , 'expected ' + i(this.obj) + ' to not have own property ' + i(name)); 348 | return this; 349 | } 350 | 351 | if (this.flags.not && undefined !== val) { 352 | if (undefined === this.obj[name]) { 353 | throw new Error(i(this.obj) + ' has no property ' + i(name)); 354 | } 355 | } else { 356 | var hasProp; 357 | try { 358 | hasProp = name in this.obj 359 | } catch (e) { 360 | hasProp = undefined !== this.obj[name] 361 | } 362 | 363 | this.assert( 364 | hasProp 365 | , 'expected ' + i(this.obj) + ' to have a property ' + i(name) 366 | , 'expected ' + i(this.obj) + ' to not have a property ' + i(name)); 367 | } 368 | 369 | if (undefined !== val) { 370 | this.assert( 371 | val === this.obj[name] 372 | , 'expected ' + i(this.obj) + ' to have a property ' + i(name) 373 | + ' of ' + i(val) + ', but got ' + i(this.obj[name]) 374 | , 'expected ' + i(this.obj) + ' to not have a property ' + i(name) 375 | + ' of ' + i(val)); 376 | } 377 | 378 | this.obj = this.obj[name]; 379 | return this; 380 | }; 381 | 382 | /** 383 | * Assert that the array contains _obj_ or string contains _obj_. 384 | * 385 | * @param {Mixed} obj|string 386 | * @api public 387 | */ 388 | 389 | Assertion.prototype.string = 390 | Assertion.prototype.contain = function (obj) { 391 | if ('string' == typeof this.obj) { 392 | this.assert( 393 | ~this.obj.indexOf(obj) 394 | , 'expected ' + i(this.obj) + ' to contain ' + i(obj) 395 | , 'expected ' + i(this.obj) + ' to not contain ' + i(obj)); 396 | } else { 397 | this.assert( 398 | ~indexOf(this.obj, obj) 399 | , 'expected ' + i(this.obj) + ' to contain ' + i(obj) 400 | , 'expected ' + i(this.obj) + ' to not contain ' + i(obj)); 401 | } 402 | return this; 403 | }; 404 | 405 | /** 406 | * Assert exact keys or inclusion of keys by using 407 | * the `.own` modifier. 408 | * 409 | * @param {Array|String ...} keys 410 | * @api public 411 | */ 412 | 413 | Assertion.prototype.key = 414 | Assertion.prototype.keys = function ($keys) { 415 | var str 416 | , ok = true; 417 | 418 | $keys = isArray($keys) 419 | ? $keys 420 | : Array.prototype.slice.call(arguments); 421 | 422 | if (!$keys.length) throw new Error('keys required'); 423 | 424 | var actual = keys(this.obj) 425 | , len = $keys.length; 426 | 427 | // Inclusion 428 | ok = every($keys, function (key) { 429 | return ~indexOf(actual, key); 430 | }); 431 | 432 | // Strict 433 | if (!this.flags.not && this.flags.only) { 434 | ok = ok && $keys.length == actual.length; 435 | } 436 | 437 | // Key string 438 | if (len > 1) { 439 | $keys = map($keys, function (key) { 440 | return i(key); 441 | }); 442 | var last = $keys.pop(); 443 | str = $keys.join(', ') + ', and ' + last; 444 | } else { 445 | str = i($keys[0]); 446 | } 447 | 448 | // Form 449 | str = (len > 1 ? 'keys ' : 'key ') + str; 450 | 451 | // Have / include 452 | str = (!this.flags.only ? 'include ' : 'only have ') + str; 453 | 454 | // Assertion 455 | this.assert( 456 | ok 457 | , 'expected ' + i(this.obj) + ' to ' + str 458 | , 'expected ' + i(this.obj) + ' to not ' + str); 459 | 460 | return this; 461 | }; 462 | 463 | /** 464 | * Function bind implementation. 465 | */ 466 | 467 | function bind (fn, scope) { 468 | return function () { 469 | return fn.apply(scope, arguments); 470 | } 471 | } 472 | 473 | /** 474 | * Array every compatibility 475 | * 476 | * @see bit.ly/5Fq1N2 477 | * @api public 478 | */ 479 | 480 | function every (arr, fn, thisObj) { 481 | var scope = thisObj || global; 482 | for (var i = 0, j = arr.length; i < j; ++i) { 483 | if (!fn.call(scope, arr[i], i, arr)) { 484 | return false; 485 | } 486 | } 487 | return true; 488 | }; 489 | 490 | /** 491 | * Array indexOf compatibility. 492 | * 493 | * @see bit.ly/a5Dxa2 494 | * @api public 495 | */ 496 | 497 | function indexOf (arr, o, i) { 498 | if (Array.prototype.indexOf) { 499 | return Array.prototype.indexOf.call(arr, o, i); 500 | } 501 | 502 | if (arr.length === undefined) { 503 | return -1; 504 | } 505 | 506 | for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0 507 | ; i < j && arr[i] !== o; i++); 508 | 509 | return j <= i ? -1 : i; 510 | }; 511 | 512 | /** 513 | * Inspects an object. 514 | * 515 | * @see taken from node.js `util` module (copyright Joyent, MIT license) 516 | * @api private 517 | */ 518 | 519 | function i (obj, showHidden, depth) { 520 | var seen = []; 521 | 522 | function stylize (str) { 523 | return str; 524 | }; 525 | 526 | function format (value, recurseTimes) { 527 | // Provide a hook for user-specified inspect functions. 528 | // Check that value is an object with an inspect function on it 529 | if (value && typeof value.inspect === 'function' && 530 | // Filter out the util module, it's inspect function is special 531 | value !== exports && 532 | // Also filter out any prototype objects using the circular check. 533 | !(value.constructor && value.constructor.prototype === value)) { 534 | return value.inspect(recurseTimes); 535 | } 536 | 537 | // Primitive types cannot have properties 538 | switch (typeof value) { 539 | case 'undefined': 540 | return stylize('undefined', 'undefined'); 541 | 542 | case 'string': 543 | var simple = '\'' + json.stringify(value).replace(/^"|"$/g, '') 544 | .replace(/'/g, "\\'") 545 | .replace(/\\"/g, '"') + '\''; 546 | return stylize(simple, 'string'); 547 | 548 | case 'number': 549 | return stylize('' + value, 'number'); 550 | 551 | case 'boolean': 552 | return stylize('' + value, 'boolean'); 553 | } 554 | // For some reason typeof null is "object", so special case here. 555 | if (value === null) { 556 | return stylize('null', 'null'); 557 | } 558 | 559 | // Look up the keys of the object. 560 | var visible_keys = keys(value); 561 | var $keys = showHidden ? Object.getOwnPropertyNames(value) : visible_keys; 562 | 563 | // Functions without properties can be shortcutted. 564 | if (typeof value === 'function' && $keys.length === 0) { 565 | if (isRegExp(value)) { 566 | return stylize('' + value, 'regexp'); 567 | } else { 568 | var name = value.name ? ': ' + value.name : ''; 569 | return stylize('[Function' + name + ']', 'special'); 570 | } 571 | } 572 | 573 | // Dates without properties can be shortcutted 574 | if (isDate(value) && $keys.length === 0) { 575 | return stylize(value.toUTCString(), 'date'); 576 | } 577 | 578 | var base, type, braces; 579 | // Determine the object type 580 | if (isArray(value)) { 581 | type = 'Array'; 582 | braces = ['[', ']']; 583 | } else { 584 | type = 'Object'; 585 | braces = ['{', '}']; 586 | } 587 | 588 | // Make functions say that they are functions 589 | if (typeof value === 'function') { 590 | var n = value.name ? ': ' + value.name : ''; 591 | base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; 592 | } else { 593 | base = ''; 594 | } 595 | 596 | // Make dates with properties first say the date 597 | if (isDate(value)) { 598 | base = ' ' + value.toUTCString(); 599 | } 600 | 601 | if ($keys.length === 0) { 602 | return braces[0] + base + braces[1]; 603 | } 604 | 605 | if (recurseTimes < 0) { 606 | if (isRegExp(value)) { 607 | return stylize('' + value, 'regexp'); 608 | } else { 609 | return stylize('[Object]', 'special'); 610 | } 611 | } 612 | 613 | seen.push(value); 614 | 615 | var output = map($keys, function (key) { 616 | var name, str; 617 | if (value.__lookupGetter__) { 618 | if (value.__lookupGetter__(key)) { 619 | if (value.__lookupSetter__(key)) { 620 | str = stylize('[Getter/Setter]', 'special'); 621 | } else { 622 | str = stylize('[Getter]', 'special'); 623 | } 624 | } else { 625 | if (value.__lookupSetter__(key)) { 626 | str = stylize('[Setter]', 'special'); 627 | } 628 | } 629 | } 630 | if (indexOf(visible_keys, key) < 0) { 631 | name = '[' + key + ']'; 632 | } 633 | if (!str) { 634 | if (indexOf(seen, value[key]) < 0) { 635 | if (recurseTimes === null) { 636 | str = format(value[key]); 637 | } else { 638 | str = format(value[key], recurseTimes - 1); 639 | } 640 | if (str.indexOf('\n') > -1) { 641 | if (isArray(value)) { 642 | str = map(str.split('\n'), function (line) { 643 | return ' ' + line; 644 | }).join('\n').substr(2); 645 | } else { 646 | str = '\n' + map(str.split('\n'), function (line) { 647 | return ' ' + line; 648 | }).join('\n'); 649 | } 650 | } 651 | } else { 652 | str = stylize('[Circular]', 'special'); 653 | } 654 | } 655 | if (typeof name === 'undefined') { 656 | if (type === 'Array' && key.match(/^\d+$/)) { 657 | return str; 658 | } 659 | name = json.stringify('' + key); 660 | if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { 661 | name = name.substr(1, name.length - 2); 662 | name = stylize(name, 'name'); 663 | } else { 664 | name = name.replace(/'/g, "\\'") 665 | .replace(/\\"/g, '"') 666 | .replace(/(^"|"$)/g, "'"); 667 | name = stylize(name, 'string'); 668 | } 669 | } 670 | 671 | return name + ': ' + str; 672 | }); 673 | 674 | seen.pop(); 675 | 676 | var numLinesEst = 0; 677 | var length = reduce(output, function (prev, cur) { 678 | numLinesEst++; 679 | if (indexOf(cur, '\n') >= 0) numLinesEst++; 680 | return prev + cur.length + 1; 681 | }, 0); 682 | 683 | if (length > 50) { 684 | output = braces[0] + 685 | (base === '' ? '' : base + '\n ') + 686 | ' ' + 687 | output.join(',\n ') + 688 | ' ' + 689 | braces[1]; 690 | 691 | } else { 692 | output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; 693 | } 694 | 695 | return output; 696 | } 697 | return format(obj, (typeof depth === 'undefined' ? 2 : depth)); 698 | }; 699 | 700 | function isArray (ar) { 701 | return Object.prototype.toString.call(ar) == '[object Array]'; 702 | }; 703 | 704 | function isRegExp(re) { 705 | var s = '' + re; 706 | return re instanceof RegExp || // easy case 707 | // duck-type for context-switching evalcx case 708 | typeof(re) === 'function' && 709 | re.constructor.name === 'RegExp' && 710 | re.compile && 711 | re.test && 712 | re.exec && 713 | s.match(/^\/.*\/[gim]{0,3}$/); 714 | }; 715 | 716 | function isDate(d) { 717 | if (d instanceof Date) return true; 718 | return false; 719 | }; 720 | 721 | function keys (obj) { 722 | if (Object.keys) { 723 | return Object.keys(obj); 724 | } 725 | 726 | var keys = []; 727 | 728 | for (var i in obj) { 729 | if (Object.prototype.hasOwnProperty.call(obj, i)) { 730 | keys.push(i); 731 | } 732 | } 733 | 734 | return keys; 735 | } 736 | 737 | function map (arr, mapper, that) { 738 | if (Array.prototype.map) { 739 | return Array.prototype.map.call(arr, mapper, that); 740 | } 741 | 742 | var other= new Array(arr.length); 743 | 744 | for (var i= 0, n = arr.length; i= 2) { 770 | var rv = arguments[1]; 771 | } else { 772 | do { 773 | if (i in this) { 774 | rv = this[i++]; 775 | break; 776 | } 777 | 778 | // if array contains no values, no initial value to return 779 | if (++i >= len) 780 | throw new TypeError(); 781 | } while (true); 782 | } 783 | 784 | for (; i < len; i++) { 785 | if (i in this) 786 | rv = fun.call(null, rv, this[i], i, this); 787 | } 788 | 789 | return rv; 790 | }; 791 | 792 | /** 793 | * Asserts deep equality 794 | * 795 | * @see taken from node.js `assert` module (copyright Joyent, MIT license) 796 | * @api private 797 | */ 798 | 799 | expect.eql = function eql (actual, expected) { 800 | // 7.1. All identical values are equivalent, as determined by ===. 801 | if (actual === expected) { 802 | return true; 803 | } else if ('undefined' != typeof Buffer 804 | && Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { 805 | if (actual.length != expected.length) return false; 806 | 807 | for (var i = 0; i < actual.length; i++) { 808 | if (actual[i] !== expected[i]) return false; 809 | } 810 | 811 | return true; 812 | 813 | // 7.2. If the expected value is a Date object, the actual value is 814 | // equivalent if it is also a Date object that refers to the same time. 815 | } else if (actual instanceof Date && expected instanceof Date) { 816 | return actual.getTime() === expected.getTime(); 817 | 818 | // 7.3. Other pairs that do not both pass typeof value == "object", 819 | // equivalence is determined by ==. 820 | } else if (typeof actual != 'object' && typeof expected != 'object') { 821 | return actual == expected; 822 | 823 | // 7.4. For all other Object pairs, including Array objects, equivalence is 824 | // determined by having the same number of owned properties (as verified 825 | // with Object.prototype.hasOwnProperty.call), the same set of keys 826 | // (although not necessarily the same order), equivalent values for every 827 | // corresponding key, and an identical "prototype" property. Note: this 828 | // accounts for both named and indexed properties on Arrays. 829 | } else { 830 | return objEquiv(actual, expected); 831 | } 832 | } 833 | 834 | function isUndefinedOrNull (value) { 835 | return value === null || value === undefined; 836 | } 837 | 838 | function isArguments (object) { 839 | return Object.prototype.toString.call(object) == '[object Arguments]'; 840 | } 841 | 842 | function objEquiv (a, b) { 843 | if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) 844 | return false; 845 | // an identical "prototype" property. 846 | if (a.prototype !== b.prototype) return false; 847 | //~~~I've managed to break Object.keys through screwy arguments passing. 848 | // Converting to array solves the problem. 849 | if (isArguments(a)) { 850 | if (!isArguments(b)) { 851 | return false; 852 | } 853 | a = pSlice.call(a); 854 | b = pSlice.call(b); 855 | return expect.eql(a, b); 856 | } 857 | try{ 858 | var ka = keys(a), 859 | kb = keys(b), 860 | key, i; 861 | } catch (e) {//happens when one is a string literal and the other isn't 862 | return false; 863 | } 864 | // having the same number of owned properties (keys incorporates hasOwnProperty) 865 | if (ka.length != kb.length) 866 | return false; 867 | //the same set of keys (although not necessarily the same order), 868 | ka.sort(); 869 | kb.sort(); 870 | //~~~cheap key test 871 | for (i = ka.length - 1; i >= 0; i--) { 872 | if (ka[i] != kb[i]) 873 | return false; 874 | } 875 | //equivalent values for every corresponding key, and 876 | //~~~possibly expensive deep test 877 | for (i = ka.length - 1; i >= 0; i--) { 878 | key = ka[i]; 879 | if (!expect.eql(a[key], b[key])) 880 | return false; 881 | } 882 | return true; 883 | } 884 | 885 | var json = (function () { 886 | "use strict"; 887 | 888 | if ('object' == typeof JSON && JSON.parse && JSON.stringify) { 889 | return { 890 | parse: nativeJSON.parse 891 | , stringify: nativeJSON.stringify 892 | } 893 | } 894 | 895 | var JSON = {}; 896 | 897 | function f(n) { 898 | // Format integers to have at least two digits. 899 | return n < 10 ? '0' + n : n; 900 | } 901 | 902 | function date(d, key) { 903 | return isFinite(d.valueOf()) ? 904 | d.getUTCFullYear() + '-' + 905 | f(d.getUTCMonth() + 1) + '-' + 906 | f(d.getUTCDate()) + 'T' + 907 | f(d.getUTCHours()) + ':' + 908 | f(d.getUTCMinutes()) + ':' + 909 | f(d.getUTCSeconds()) + 'Z' : null; 910 | }; 911 | 912 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 913 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 914 | gap, 915 | indent, 916 | meta = { // table of character substitutions 917 | '\b': '\\b', 918 | '\t': '\\t', 919 | '\n': '\\n', 920 | '\f': '\\f', 921 | '\r': '\\r', 922 | '"' : '\\"', 923 | '\\': '\\\\' 924 | }, 925 | rep; 926 | 927 | 928 | function quote(string) { 929 | 930 | // If the string contains no control characters, no quote characters, and no 931 | // backslash characters, then we can safely slap some quotes around it. 932 | // Otherwise we must also replace the offending characters with safe escape 933 | // sequences. 934 | 935 | escapable.lastIndex = 0; 936 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) { 937 | var c = meta[a]; 938 | return typeof c === 'string' ? c : 939 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 940 | }) + '"' : '"' + string + '"'; 941 | } 942 | 943 | 944 | function str(key, holder) { 945 | 946 | // Produce a string from holder[key]. 947 | 948 | var i, // The loop counter. 949 | k, // The member key. 950 | v, // The member value. 951 | length, 952 | mind = gap, 953 | partial, 954 | value = holder[key]; 955 | 956 | // If the value has a toJSON method, call it to obtain a replacement value. 957 | 958 | if (value instanceof Date) { 959 | value = date(key); 960 | } 961 | 962 | // If we were called with a replacer function, then call the replacer to 963 | // obtain a replacement value. 964 | 965 | if (typeof rep === 'function') { 966 | value = rep.call(holder, key, value); 967 | } 968 | 969 | // What happens next depends on the value's type. 970 | 971 | switch (typeof value) { 972 | case 'string': 973 | return quote(value); 974 | 975 | case 'number': 976 | 977 | // JSON numbers must be finite. Encode non-finite numbers as null. 978 | 979 | return isFinite(value) ? String(value) : 'null'; 980 | 981 | case 'boolean': 982 | case 'null': 983 | 984 | // If the value is a boolean or null, convert it to a string. Note: 985 | // typeof null does not produce 'null'. The case is included here in 986 | // the remote chance that this gets fixed someday. 987 | 988 | return String(value); 989 | 990 | // If the type is 'object', we might be dealing with an object or an array or 991 | // null. 992 | 993 | case 'object': 994 | 995 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 996 | // so watch out for that case. 997 | 998 | if (!value) { 999 | return 'null'; 1000 | } 1001 | 1002 | // Make an array to hold the partial results of stringifying this object value. 1003 | 1004 | gap += indent; 1005 | partial = []; 1006 | 1007 | // Is the value an array? 1008 | 1009 | if (Object.prototype.toString.apply(value) === '[object Array]') { 1010 | 1011 | // The value is an array. Stringify every element. Use null as a placeholder 1012 | // for non-JSON values. 1013 | 1014 | length = value.length; 1015 | for (i = 0; i < length; i += 1) { 1016 | partial[i] = str(i, value) || 'null'; 1017 | } 1018 | 1019 | // Join all of the elements together, separated with commas, and wrap them in 1020 | // brackets. 1021 | 1022 | v = partial.length === 0 ? '[]' : gap ? 1023 | '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : 1024 | '[' + partial.join(',') + ']'; 1025 | gap = mind; 1026 | return v; 1027 | } 1028 | 1029 | // If the replacer is an array, use it to select the members to be stringified. 1030 | 1031 | if (rep && typeof rep === 'object') { 1032 | length = rep.length; 1033 | for (i = 0; i < length; i += 1) { 1034 | if (typeof rep[i] === 'string') { 1035 | k = rep[i]; 1036 | v = str(k, value); 1037 | if (v) { 1038 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 1039 | } 1040 | } 1041 | } 1042 | } else { 1043 | 1044 | // Otherwise, iterate through all of the keys in the object. 1045 | 1046 | for (k in value) { 1047 | if (Object.prototype.hasOwnProperty.call(value, k)) { 1048 | v = str(k, value); 1049 | if (v) { 1050 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 1051 | } 1052 | } 1053 | } 1054 | } 1055 | 1056 | // Join all of the member texts together, separated with commas, 1057 | // and wrap them in braces. 1058 | 1059 | v = partial.length === 0 ? '{}' : gap ? 1060 | '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : 1061 | '{' + partial.join(',') + '}'; 1062 | gap = mind; 1063 | return v; 1064 | } 1065 | } 1066 | 1067 | // If the JSON object does not yet have a stringify method, give it one. 1068 | 1069 | JSON.stringify = function (value, replacer, space) { 1070 | 1071 | // The stringify method takes a value and an optional replacer, and an optional 1072 | // space parameter, and returns a JSON text. The replacer can be a function 1073 | // that can replace values, or an array of strings that will select the keys. 1074 | // A default replacer method can be provided. Use of the space parameter can 1075 | // produce text that is more easily readable. 1076 | 1077 | var i; 1078 | gap = ''; 1079 | indent = ''; 1080 | 1081 | // If the space parameter is a number, make an indent string containing that 1082 | // many spaces. 1083 | 1084 | if (typeof space === 'number') { 1085 | for (i = 0; i < space; i += 1) { 1086 | indent += ' '; 1087 | } 1088 | 1089 | // If the space parameter is a string, it will be used as the indent string. 1090 | 1091 | } else if (typeof space === 'string') { 1092 | indent = space; 1093 | } 1094 | 1095 | // If there is a replacer, it must be a function or an array. 1096 | // Otherwise, throw an error. 1097 | 1098 | rep = replacer; 1099 | if (replacer && typeof replacer !== 'function' && 1100 | (typeof replacer !== 'object' || 1101 | typeof replacer.length !== 'number')) { 1102 | throw new Error('JSON.stringify'); 1103 | } 1104 | 1105 | // Make a fake root object containing our value under the key of ''. 1106 | // Return the result of stringifying the value. 1107 | 1108 | return str('', {'': value}); 1109 | }; 1110 | 1111 | // If the JSON object does not yet have a parse method, give it one. 1112 | 1113 | JSON.parse = function (text, reviver) { 1114 | // The parse method takes a text and an optional reviver function, and returns 1115 | // a JavaScript value if the text is a valid JSON text. 1116 | 1117 | var j; 1118 | 1119 | function walk(holder, key) { 1120 | 1121 | // The walk method is used to recursively walk the resulting structure so 1122 | // that modifications can be made. 1123 | 1124 | var k, v, value = holder[key]; 1125 | if (value && typeof value === 'object') { 1126 | for (k in value) { 1127 | if (Object.prototype.hasOwnProperty.call(value, k)) { 1128 | v = walk(value, k); 1129 | if (v !== undefined) { 1130 | value[k] = v; 1131 | } else { 1132 | delete value[k]; 1133 | } 1134 | } 1135 | } 1136 | } 1137 | return reviver.call(holder, key, value); 1138 | } 1139 | 1140 | 1141 | // Parsing happens in four stages. In the first stage, we replace certain 1142 | // Unicode characters with escape sequences. JavaScript handles many characters 1143 | // incorrectly, either silently deleting them, or treating them as line endings. 1144 | 1145 | text = String(text); 1146 | cx.lastIndex = 0; 1147 | if (cx.test(text)) { 1148 | text = text.replace(cx, function (a) { 1149 | return '\\u' + 1150 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 1151 | }); 1152 | } 1153 | 1154 | // In the second stage, we run the text against regular expressions that look 1155 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 1156 | // because they can cause invocation, and '=' because it can cause mutation. 1157 | // But just to be safe, we want to reject all unexpected forms. 1158 | 1159 | // We split the second stage into 4 regexp operations in order to work around 1160 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 1161 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 1162 | // replace all simple value tokens with ']' characters. Third, we delete all 1163 | // open brackets that follow a colon or comma or that begin the text. Finally, 1164 | // we look to see that the remaining characters are only whitespace or ']' or 1165 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 1166 | 1167 | if (/^[\],:{}\s]*$/ 1168 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 1169 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 1170 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 1171 | 1172 | // In the third stage we use the eval function to compile the text into a 1173 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 1174 | // in JavaScript: it can begin a block or an object literal. We wrap the text 1175 | // in parens to eliminate the ambiguity. 1176 | 1177 | j = eval('(' + text + ')'); 1178 | 1179 | // In the optional fourth stage, we recursively walk the new structure, passing 1180 | // each name/value pair to a reviver function for possible transformation. 1181 | 1182 | return typeof reviver === 'function' ? 1183 | walk({'': j}, '') : j; 1184 | } 1185 | 1186 | // If the text is not JSON parseable, then a SyntaxError is thrown. 1187 | 1188 | throw new SyntaxError('JSON.parse'); 1189 | }; 1190 | 1191 | return JSON; 1192 | })(); 1193 | 1194 | if ('undefined' != typeof window) { 1195 | window.expect = module.exports; 1196 | } 1197 | 1198 | })( 1199 | this 1200 | , 'undefined' != typeof module ? module : {} 1201 | , 'undefined' != typeof exports ? exports : {} 1202 | ); 1203 | -------------------------------------------------------------------------------- /test/lib/mocha-phantomjs.coffee: -------------------------------------------------------------------------------- 1 | system = require 'system' 2 | webpage = require 'webpage' 3 | 4 | USAGE = """ 5 | Usage: phantomjs mocha-phantomjs.coffee URL REPORTER 6 | """ 7 | 8 | class Reporter 9 | 10 | constructor: (@reporter) -> 11 | @url = system.args[1] 12 | @columns = parseInt(system.env.COLUMNS or 75) * .75 | 0 13 | @mochaStarted = false 14 | @mochaStartWait = 6000 15 | @fail(USAGE) unless @url 16 | 17 | run: -> 18 | @initPage() 19 | @loadPage() 20 | 21 | # Subclass Hooks 22 | 23 | customizeRunner: (options) -> 24 | undefined 25 | 26 | customizeProcessStdout: (options) -> 27 | undefined 28 | 29 | customizeConsole: (options) -> 30 | undefined 31 | 32 | customizeOptions: -> 33 | columns: @columns 34 | 35 | # Private 36 | 37 | fail: (msg) -> 38 | console.log msg if msg 39 | phantom.exit 1 40 | 41 | finish: -> 42 | phantom.exit @page.evaluate -> mochaPhantomJS.failures 43 | 44 | initPage: -> 45 | @page = webpage.create() 46 | @page.onConsoleMessage = (msg) -> console.log msg 47 | @page.onInitialized = => 48 | @page.evaluate -> 49 | window.mochaPhantomJS = 50 | failures: 0 51 | ended: false 52 | started: false 53 | run: -> 54 | mochaPhantomJS.started = true 55 | 56 | loadPage: -> 57 | @page.open @url 58 | @page.onLoadFinished = (status) => 59 | if status isnt 'success' then @onLoadFailed() else @onLoadSuccess() 60 | 61 | onLoadSuccess: -> 62 | @injectJS() 63 | @waitForRunMocha() 64 | 65 | onLoadFailed: -> 66 | @fail "Failed to load the page. Check the url: #{@url}" 67 | 68 | injectJS: -> 69 | if @page.evaluate(-> window.mocha?) 70 | @page.injectJs 'mocha-phantomjs/core_extensions.js' 71 | @page.evaluate @customizeProcessStdout, @customizeOptions() 72 | @page.evaluate @customizeConsole, @customizeOptions() 73 | else 74 | @fail "Failed to find mocha on the page." 75 | 76 | runMocha: -> 77 | @page.evaluate @runner, @reporter 78 | @mochaStarted = @page.evaluate -> mochaPhantomJS.runner or false 79 | if @mochaStarted 80 | @mochaRunAt = new Date().getTime() 81 | @page.evaluate @customizeRunner, @customizeOptions() 82 | @waitForMocha() 83 | else 84 | @fail "Failed to start mocha." 85 | 86 | waitForMocha: => 87 | ended = @page.evaluate -> mochaPhantomJS.ended 88 | if ended then @finish() else setTimeout @waitForMocha, 100 89 | 90 | waitForRunMocha: => 91 | started = @page.evaluate -> mochaPhantomJS.started 92 | if started then @runMocha() else setTimeout @waitForRunMocha, 100 93 | 94 | runner: (reporter) -> 95 | try 96 | mocha.setup reporter: reporter 97 | mochaPhantomJS.runner = mocha.run() 98 | if mochaPhantomJS.runner 99 | mochaPhantomJS.runner.on 'end', -> 100 | mochaPhantomJS.failures = @failures 101 | mochaPhantomJS.ended = true 102 | catch error 103 | false 104 | 105 | class Spec extends Reporter 106 | 107 | constructor: -> 108 | super 'spec' 109 | 110 | customizeProcessStdout: (options) -> 111 | process.stdout.write = (string) -> 112 | return if string is process.cursor.deleteLine or string is process.cursor.beginningOfLine 113 | console.log string 114 | 115 | customizeConsole: (options) -> 116 | process.cursor.CRMatcher = /\s+◦\s\w/ 117 | process.cursor.CRCleaner = process.cursor.up + process.cursor.deleteLine 118 | origLog = console.log 119 | console.log = -> 120 | string = console.format.apply(console, arguments) 121 | if string.match(process.cursor.CRMatcher) 122 | process.cursor.CRCleanup = true 123 | else if process.cursor.CRCleanup 124 | string = process.cursor.CRCleaner + string 125 | process.cursor.CRCleanup = false 126 | origLog.call console, string 127 | 128 | class Dot extends Reporter 129 | 130 | constructor: -> 131 | super 'dot' 132 | 133 | customizeProcessStdout: (options) -> 134 | process.cursor.margin = 2 135 | process.cursor.CRMatcher = /\u001b\[\d\dm\․\u001b\[0m/ 136 | process.stdout.columns = options.columns 137 | process.stdout.allowedFirstNewLine = false 138 | process.stdout.write = (string) -> 139 | if string is '\n ' 140 | unless process.stdout.allowedFirstNewLine 141 | process.stdout.allowedFirstNewLine = true 142 | else 143 | return 144 | else if string.match(process.cursor.CRMatcher) 145 | if process.cursor.count is process.stdout.columns 146 | process.cursor.count = 0 147 | forward = process.cursor.margin 148 | string = process.cursor.forwardN(forward) + string 149 | else 150 | forward = process.cursor.margin + process.cursor.count 151 | string = process.cursor.up + process.cursor.forwardN(forward) + string 152 | ++process.cursor.count 153 | console.log string 154 | 155 | class Tap extends Reporter 156 | 157 | constructor: -> 158 | super 'tap' 159 | 160 | class List extends Reporter 161 | 162 | constructor: -> 163 | super 'list' 164 | 165 | customizeProcessStdout: (options) -> 166 | process.stdout.write = (string) -> 167 | return if string is process.cursor.deleteLine or string is process.cursor.beginningOfLine 168 | console.log string 169 | 170 | customizeProcessStdout: (options) -> 171 | process.cursor.CRMatcher = /\u001b\[90m.*:\s\u001b\[0m/ 172 | process.cursor.CRCleaner = (string) -> process.cursor.up + process.cursor.deleteLine + string + process.cursor.up + process.cursor.up 173 | origLog = console.log 174 | console.log = -> 175 | string = console.format.apply(console, arguments) 176 | if string.match /\u001b\[32m\s\s-\u001b\[0m/ 177 | string = string 178 | process.cursor.CRCleanup = false 179 | if string.match(process.cursor.CRMatcher) 180 | process.cursor.CRCleanup = true 181 | else if process.cursor.CRCleanup 182 | string = process.cursor.CRCleaner(string) 183 | process.cursor.CRCleanup = false 184 | origLog.call console, string 185 | 186 | class Min extends Reporter 187 | 188 | constructor: -> 189 | super 'min' 190 | 191 | class Doc extends Reporter 192 | 193 | constructor: -> 194 | super 'doc' 195 | 196 | class Teamcity extends Reporter 197 | 198 | constructor: -> 199 | super 'teamcity' 200 | 201 | reporterString = system.args[2] || 'spec' 202 | reporterString = reporterString.charAt(0).toUpperCase() + reporterString.slice(1) 203 | reporterKlass = try 204 | eval(reporterString) 205 | catch error 206 | undefined 207 | 208 | if reporterKlass 209 | reporter = new reporterKlass 210 | reporter.run() 211 | else 212 | console.log "Reporter class not implemented: #{reporterString}" 213 | phantom.exit 1 214 | 215 | 216 | -------------------------------------------------------------------------------- /test/lib/mocha-phantomjs/core_extensions.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | // A shim for non ES5 supporting browsers, like PhantomJS. Lovingly inspired by: 4 | // http://www.angrycoding.com/2011/09/to-bind-or-not-to-bind-that-is-in.html 5 | if (!('bind' in Function.prototype)) { 6 | Function.prototype.bind = function() { 7 | var funcObj = this; 8 | var extraArgs = Array.prototype.slice.call(arguments); 9 | var thisObj = extraArgs.shift(); 10 | return function() { 11 | return funcObj.apply(thisObj, extraArgs.concat(Array.prototype.slice.call(arguments))); 12 | }; 13 | }; 14 | } 15 | 16 | // Mocha needs process.stdout.write in order to change the cursor position. However, 17 | // PhantomJS console.log always puts a new line character when logging and no STDOUT or 18 | // stream access is available, outside writing to /dev/stdout, etc. To work around 19 | // this, runner classes typically override `process.stdout.write` as needed to simulate 20 | // write to standard out using cursor commands. 21 | process.cursor = { 22 | count: 0, 23 | margin: 0, 24 | buffer: '', 25 | CRCleanup: false, 26 | CRMatcher: undefined, 27 | CRCleaner: undefined, 28 | hide: '\u001b[?25l', 29 | show: '\u001b[?25h', 30 | deleteLine: '\u001b[2K', 31 | beginningOfLine: '\u001b[0G', 32 | up: '\u001b[A', 33 | down: '\u001b[B', 34 | forward: '\u001b[C', 35 | forwardN: function(n){ return '\u001b[' + n + 'C'; }, 36 | backward: '\u001b[D', 37 | nextLine: '\u001b[E', 38 | previousLine: '\u001b[F' 39 | } 40 | process.stdout.columns = 0; 41 | process.stdout.write = function(string) { console.log(string); } 42 | 43 | // Mocha needs the formating feature of console.log so copy node's format function and 44 | // monkey-patch it into place. This code is copied from node's, links copyright applies. 45 | // https://github.com/joyent/node/blob/master/lib/util.js 46 | console.format = function(f) { 47 | if (typeof f !== 'string') { 48 | var objects = []; 49 | for (var i = 0; i < arguments.length; i++) { 50 | objects.push(JSON.stringify(arguments[i])); 51 | } 52 | return objects.join(' '); 53 | } 54 | var i = 1; 55 | var args = arguments; 56 | var len = args.length; 57 | var str = String(f).replace(/%[sdj%]/g, function(x) { 58 | if (x === '%%') return '%'; 59 | if (i >= len) return x; 60 | switch (x) { 61 | case '%s': return String(args[i++]); 62 | case '%d': return Number(args[i++]); 63 | case '%j': return JSON.stringify(args[i++]); 64 | default: 65 | return x; 66 | } 67 | }); 68 | for (var x = args[i]; i < len; x = args[++i]) { 69 | if (x === null || typeof x !== 'object') { 70 | str += ' ' + x; 71 | } else { 72 | str += ' ' + JSON.stringify(x); 73 | } 74 | } 75 | return str; 76 | }; 77 | var origError = console.error; 78 | console.error = function(){ origError.call(console, console.format.apply(console, arguments)); }; 79 | var origLog = console.log; 80 | console.log = function(){ origLog.call(console, console.format.apply(console, arguments)); }; 81 | 82 | })(); 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /test/lib/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | padding: 60px 50px; 6 | } 7 | 8 | #mocha ul, #mocha li { 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | #mocha ul { 14 | list-style: none; 15 | } 16 | 17 | #mocha h1, #mocha h2 { 18 | margin: 0; 19 | } 20 | 21 | #mocha h1 { 22 | margin-top: 15px; 23 | font-size: 1em; 24 | font-weight: 200; 25 | } 26 | 27 | #mocha h1 a { 28 | text-decoration: none; 29 | color: inherit; 30 | } 31 | 32 | #mocha h1 a:hover { 33 | text-decoration: underline; 34 | } 35 | 36 | #mocha .suite .suite h1 { 37 | margin-top: 0; 38 | font-size: .8em; 39 | } 40 | 41 | .hidden { 42 | display: none; 43 | } 44 | 45 | #mocha h2 { 46 | font-size: 12px; 47 | font-weight: normal; 48 | cursor: pointer; 49 | } 50 | 51 | #mocha .suite { 52 | margin-left: 15px; 53 | } 54 | 55 | #mocha .test { 56 | margin-left: 15px; 57 | overflow: hidden; 58 | } 59 | 60 | #mocha .test.pending:hover h2::after { 61 | content: '(pending)'; 62 | font-family: arial; 63 | } 64 | 65 | #mocha .test.pass.medium .duration { 66 | background: #C09853; 67 | } 68 | 69 | #mocha .test.pass.slow .duration { 70 | background: #B94A48; 71 | } 72 | 73 | #mocha .test.pass::before { 74 | content: '✓'; 75 | font-size: 12px; 76 | display: block; 77 | float: left; 78 | margin-right: 5px; 79 | color: #00d6b2; 80 | } 81 | 82 | #mocha .test.pass .duration { 83 | font-size: 9px; 84 | margin-left: 5px; 85 | padding: 2px 5px; 86 | color: white; 87 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 88 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 89 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 90 | -webkit-border-radius: 5px; 91 | -moz-border-radius: 5px; 92 | -ms-border-radius: 5px; 93 | -o-border-radius: 5px; 94 | border-radius: 5px; 95 | } 96 | 97 | #mocha .test.pass.fast .duration { 98 | display: none; 99 | } 100 | 101 | #mocha .test.pending { 102 | color: #0b97c4; 103 | } 104 | 105 | #mocha .test.pending::before { 106 | content: '◦'; 107 | color: #0b97c4; 108 | } 109 | 110 | #mocha .test.fail { 111 | color: #c00; 112 | } 113 | 114 | #mocha .test.fail pre { 115 | color: black; 116 | } 117 | 118 | #mocha .test.fail::before { 119 | content: '✖'; 120 | font-size: 12px; 121 | display: block; 122 | float: left; 123 | margin-right: 5px; 124 | color: #c00; 125 | } 126 | 127 | #mocha .test pre.error { 128 | color: #c00; 129 | max-height: 300px; 130 | overflow: auto; 131 | } 132 | 133 | #mocha .test pre { 134 | display: block; 135 | float: left; 136 | clear: left; 137 | font: 12px/1.5 monaco, monospace; 138 | margin: 5px; 139 | padding: 15px; 140 | border: 1px solid #eee; 141 | border-bottom-color: #ddd; 142 | -webkit-border-radius: 3px; 143 | -webkit-box-shadow: 0 1px 3px #eee; 144 | -moz-border-radius: 3px; 145 | -moz-box-shadow: 0 1px 3px #eee; 146 | } 147 | 148 | #mocha .test h2 { 149 | position: relative; 150 | } 151 | 152 | #mocha .test a.replay { 153 | position: absolute; 154 | top: 3px; 155 | right: 0; 156 | text-decoration: none; 157 | vertical-align: middle; 158 | display: block; 159 | width: 15px; 160 | height: 15px; 161 | line-height: 15px; 162 | text-align: center; 163 | background: #eee; 164 | font-size: 15px; 165 | -moz-border-radius: 15px; 166 | border-radius: 15px; 167 | -webkit-transition: opacity 200ms; 168 | -moz-transition: opacity 200ms; 169 | transition: opacity 200ms; 170 | opacity: 0.3; 171 | color: #888; 172 | } 173 | 174 | #mocha .test:hover a.replay { 175 | opacity: 1; 176 | } 177 | 178 | #mocha-report.pass .test.fail { 179 | display: none; 180 | } 181 | 182 | #mocha-report.fail .test.pass { 183 | display: none; 184 | } 185 | 186 | #mocha-error { 187 | color: #c00; 188 | font-size: 1.5 em; 189 | font-weight: 100; 190 | letter-spacing: 1px; 191 | } 192 | 193 | #mocha-stats { 194 | position: fixed; 195 | top: 15px; 196 | right: 10px; 197 | font-size: 12px; 198 | margin: 0; 199 | color: #888; 200 | } 201 | 202 | #mocha-stats .progress { 203 | float: right; 204 | padding-top: 0; 205 | } 206 | 207 | #mocha-stats em { 208 | color: black; 209 | } 210 | 211 | #mocha-stats a { 212 | text-decoration: none; 213 | color: inherit; 214 | } 215 | 216 | #mocha-stats a:hover { 217 | border-bottom: 1px solid #eee; 218 | } 219 | 220 | #mocha-stats li { 221 | display: inline-block; 222 | margin: 0 5px; 223 | list-style: none; 224 | padding-top: 11px; 225 | } 226 | 227 | code .comment { color: #ddd } 228 | code .init { color: #2F6FAD } 229 | code .string { color: #5890AD } 230 | code .keyword { color: #8A6343 } 231 | code .number { color: #2F6FAD } 232 | 233 | @media screen and (max-device-width: 480px) { 234 | body { 235 | padding: 60px 0px; 236 | } 237 | 238 | #mocha-stats { 239 | position: absolute; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /test/lib/setup.js: -------------------------------------------------------------------------------- 1 | mocha.setup({ ui: 'bdd' }); 2 | 3 | $(function() { 4 | if (window.mochaPhantomJS) { 5 | mochaPhantomJS.run(); 6 | } else { 7 | mocha.run(); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | describe('Flipsnap', function() { 2 | var support = Flipsnap.support; 3 | 4 | var html = 5 | '
' + 6 | '
item1
' + 7 | '
item2
' + 8 | '
item3
' + 9 | '
'; 10 | 11 | var $flipsnap; 12 | var f; 13 | beforeEach(function() { 14 | $flipsnap = $(html).appendTo('#sandbox'); 15 | f = Flipsnap($flipsnap.get(0)); 16 | }); 17 | afterEach(function() { 18 | $(f.element).remove(); 19 | }); 20 | 21 | describe('constructor', function() { 22 | context('when set dom element', function() { 23 | it('element should be set', function() { 24 | var div = document.createElement('div'); 25 | var f = Flipsnap(div); 26 | expect(f.element).to.be(div); 27 | }); 28 | }); 29 | 30 | context('when set string(css seclector)', function() { 31 | context('when exist element', function() { 32 | it('element should be search by selector', function() { 33 | var $foo = $('
').appendTo('body'); 34 | var f = Flipsnap('#foo'); 35 | expect(f.element).to.be($foo.get(0)); 36 | $foo.remove(); 37 | }); 38 | }); 39 | 40 | context('when not exist element', function() { 41 | it('should throw error' ,function() { 42 | expect(function() { 43 | Flipsnap('#foo'); 44 | }).to.throwError(/element not found/); 45 | }); 46 | }); 47 | }); 48 | }); 49 | 50 | describe('refresh', function() { 51 | context('has content', function() { 52 | beforeEach(function() { 53 | f.refresh(); 54 | }); 55 | 56 | it('should reset values', function() { 57 | expect(f.currentPoint).to.be(0); 58 | expect(f._maxPoint).to.be(2); 59 | expect(f._distance).to.be(100); 60 | expect(f._maxX).to.be(-200); 61 | }); 62 | }); 63 | 64 | context('has no content', function() { 65 | beforeEach(function() { 66 | $flipsnap.empty(); 67 | f.refresh(); 68 | }); 69 | it('should reset values', function() { 70 | expect(f.currentPoint).to.be(-1); 71 | expect(f._maxPoint).to.be(-1); 72 | expect(f._distance).to.be(0); 73 | expect(f._maxX).to.be(0); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('maxPoint', function() { 79 | context('when set maxPoint to 0', function() { 80 | it('should set _maxPoint to 0', function() { 81 | var f = Flipsnap($(html).get(0), { maxPoint: 0 }); 82 | expect(f._maxPoint).to.be(0); 83 | }); 84 | }); 85 | 86 | context('when set maxPoint to 1', function() { 87 | it('should set _maxPoint to 1', function() { 88 | var f = Flipsnap($(html).get(0), { maxPoint: 1 }); 89 | expect(f._maxPoint).to.be(1); 90 | }); 91 | }); 92 | 93 | context('when dont set maxPoint', function() { 94 | it('should set _maxPoint to element length - 1', function() { 95 | var f = Flipsnap($(html).get(0)); 96 | expect(f._maxPoint).to.be(2); 97 | }); 98 | }); 99 | }); 100 | 101 | describe('#hasNext', function() { 102 | context('when has next element', function() { 103 | it('should return true', function() { 104 | expect(f.hasNext()).to.be(true); 105 | }); 106 | }); 107 | 108 | context('when not has next element', function() { 109 | it('should return false', function() { 110 | f.moveToPoint(2); 111 | expect(f.hasNext()).to.be(false); 112 | }); 113 | }); 114 | }); 115 | 116 | describe('#hasPrev', function() { 117 | context('when has prev element', function() { 118 | it('should return false', function() { 119 | f.moveToPoint(1); 120 | expect(f.hasPrev()).to.be(true); 121 | }); 122 | }); 123 | 124 | context('when not has prev element', function() { 125 | it('should return true', function() { 126 | expect(f.hasPrev()).to.be(false); 127 | }); 128 | }); 129 | }); 130 | 131 | describe('#toNext', function() { 132 | context('when currentPoint is not maxPoint', function() { 133 | it('currentPoint should be increment', function() { 134 | expect(f.currentPoint).to.be(0); 135 | f.toNext(); 136 | expect(f.currentPoint).to.be(1); 137 | }); 138 | 139 | it('should fire fspointmove event', function(done) { 140 | f.element.addEventListener('fspointmove', function() { 141 | expect(f.currentPoint).to.be(1); 142 | done(); 143 | }); 144 | f.toNext(); 145 | }); 146 | }); 147 | 148 | context('when currentPoint is maxPoint', function() { 149 | it('currentPoint should be not increment', function() { 150 | f.moveToPoint(2); 151 | expect(f.currentPoint).to.be(2); 152 | f.toNext(); 153 | expect(f.currentPoint).to.be(2); 154 | }); 155 | 156 | it('should not fire fspointmove event', function() { 157 | f.moveToPoint(2); 158 | f.element.addEventListener('fspointmove', function() { 159 | expect().fail('fire fspointmove event'); 160 | }); 161 | f.toNext(); 162 | }); 163 | }); 164 | }); 165 | 166 | describe('#toPrev', function() { 167 | context('when currentPoint is not 0', function() { 168 | it('currentPoint should be increment', function() { 169 | f.moveToPoint(1); 170 | expect(f.currentPoint).to.be(1); 171 | f.toPrev(); 172 | expect(f.currentPoint).to.be(0); 173 | }); 174 | 175 | it('should fire fspointmove event', function(done) { 176 | f.moveToPoint(1); 177 | f.element.addEventListener('fspointmove', function() { 178 | expect(f.currentPoint).to.be(0); 179 | done(); 180 | }); 181 | f.toPrev(); 182 | }); 183 | }); 184 | 185 | context('when currentPoint is 0', function() { 186 | it('currentPoint should be not increment', function() { 187 | expect(f.currentPoint).to.be(0); 188 | f.toPrev(); 189 | expect(f.currentPoint).to.be(0); 190 | }); 191 | 192 | it('should not fire fspointmove event', function() { 193 | f.moveToPoint(0); 194 | f.element.addEventListener('fspointmove', function() { 195 | expect().fail('fire fspointmove event'); 196 | }); 197 | f.toPrev(); 198 | }); 199 | }); 200 | }); 201 | 202 | describe('#moveToPoint', function() { 203 | context('when argument greater than maxPoint', function() { 204 | it('currentPoint should change to maxPoint', function() { 205 | f.moveToPoint(5); 206 | expect(f.currentPoint).to.be(2); 207 | }); 208 | 209 | it('should fire fspointmove event', function(done) { 210 | f.element.addEventListener('fspointmove', function() { 211 | expect(f.currentPoint).to.be(2); 212 | done(); 213 | }); 214 | f.moveToPoint(5); 215 | }); 216 | }); 217 | 218 | context('when argument less than 0', function() { 219 | it('currentPoint should change to 0', function() { 220 | f.moveToPoint(1); 221 | f.moveToPoint(-1); 222 | expect(f.currentPoint).to.be(0); 223 | }); 224 | }); 225 | 226 | context('when argument betoween 0 and maxPoint', function() { 227 | it('should change currentPoint', function() { 228 | f.moveToPoint(1); 229 | expect(f.currentPoint).to.be(1); 230 | }); 231 | }); 232 | 233 | context('when pass transitionDuration', function() { 234 | context('when no support cssAnimation', function() { 235 | var orig = support.cssAnimation; 236 | beforeEach(function() { 237 | this.spy = sinon.spy(f, '_setStyle'); 238 | support.cssAnimation = true; 239 | }); 240 | afterEach(function() { 241 | support.cssAnimation = orig; 242 | }); 243 | 244 | it('transitionDuration should be string with `ms`', function() { 245 | f.moveToPoint(1, 100); 246 | expect(this.spy.args[0][0]) 247 | .to.have.property('transitionDuration', '100ms'); 248 | }); 249 | it('call moveToPoint while the previous animation is running', function(done) { 250 | f.moveToPoint(1, 200); 251 | f.moveToPoint(2, 0); 252 | setTimeout(function() { 253 | expect(f.currentPoint).to.be(2); 254 | done(); 255 | }, 300); 256 | }); 257 | }); 258 | 259 | context('when no support cssAnimation', function() { 260 | var orig = support.cssAnimation; 261 | beforeEach(function() { 262 | this.spy = sinon.spy(f, '_animate'); 263 | support.cssAnimation = false; 264 | }); 265 | afterEach(function() { 266 | support.cssAnimation = orig; 267 | }); 268 | 269 | it('transitionDuration should pass `_animate`', function() { 270 | f.moveToPoint(1, 100); 271 | expect(this.spy.args[0][1]) 272 | .to.be('100ms'); 273 | }); 274 | it('call moveToPoint while the previous animation is running', function(done) { 275 | f.moveToPoint(1, 200); 276 | f.moveToPoint(2, 0); 277 | setTimeout(function() { 278 | expect(f.currentPoint).to.be(2); 279 | expect(f.element.style.left).to.be('-200px'); 280 | done(); 281 | }, 300); 282 | }); 283 | }); 284 | }); 285 | 286 | }); 287 | 288 | describe('Flip Events', function() { 289 | function trigger(element, eventType, params) { 290 | var ev = document.createEvent('Event'); 291 | ev.initEvent(eventType, true, false); 292 | $.extend(ev, params || {}); 293 | element.dispatchEvent(ev); 294 | } 295 | 296 | function moveEventTest(start, move, end) { 297 | it('should move to next', function() { 298 | trigger(f.element, start, { pageX: 50, pageY: 0 }); 299 | expect(f.currentPoint).to.be(0); 300 | 301 | trigger(f.element, move, { pageX: 40, pageY: 0 }); 302 | trigger(f.element, move, { pageX: 30, pageY: 0 }); 303 | expect(f.currentPoint).to.be(0); 304 | 305 | trigger(document, end); 306 | expect(f.currentPoint).to.be(1); 307 | }); 308 | 309 | it('should move to prev', function() { 310 | trigger(f.element, start, { pageX: 50, pageY: 0 }); 311 | trigger(f.element, move, { pageX: 40, pageY: 0 }); 312 | trigger(f.element, move, { pageX: 30, pageY: 0 }); 313 | trigger(document, end); 314 | expect(f.currentPoint).to.be(1); 315 | 316 | trigger(f.element, start, { pageX: 50, pageY: 0 }); 317 | expect(f.currentPoint).to.be(1); 318 | 319 | trigger(f.element, move, { pageX: 60, pageY: 0 }); 320 | trigger(f.element, move, { pageX: 70, pageY: 0 }); 321 | expect(f.currentPoint).to.be(1); 322 | 323 | trigger(document, end); 324 | expect(f.currentPoint).to.be(0); 325 | }); 326 | } 327 | 328 | context('when fired touch event', function() { 329 | moveEventTest('touchstart', 'touchmove', 'touchend'); 330 | }); 331 | 332 | context('when fired mouse event', function() { 333 | moveEventTest('mousedown', 'mousemove', 'mouseup'); 334 | }); 335 | 336 | context('when fired touchstart and mousedown event', function() { 337 | beforeEach(function() { 338 | this.spy = sinon.spy(f.element, 'addEventListener'); 339 | trigger(f.element, 'touchstart', { pageX: 0, pageY: 0 }); 340 | trigger(f.element, 'mousedown', { pageX: 0, pageY: 0 }); 341 | }); 342 | afterEach(function() { 343 | this.spy.restore(); 344 | }); 345 | 346 | it('move event should bind only first fired event type', function() { 347 | expect(this.spy.callCount).to.be(1); 348 | expect(this.spy.args[0][0]).to.be('touchmove'); 349 | }); 350 | }); 351 | }); 352 | }); 353 | --------------------------------------------------------------------------------