Mercurial > hg > Papers > 2013 > sugi-sigos
comparison presen/js/hammer.js @ 10:5c57e35e19b6
add presen
author | sugi |
---|---|
date | Tue, 23 Apr 2013 23:31:26 +0900 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
9:e17bc730af1a | 10:5c57e35e19b6 |
---|---|
1 /* | |
2 * Hammer.JS | |
3 * version 0.4 | |
4 * author: Eight Media | |
5 * https://github.com/EightMedia/hammer.js | |
6 */ | |
7 function Hammer(element, options, undefined) | |
8 { | |
9 var self = this; | |
10 | |
11 var defaults = { | |
12 // prevent the default event or not... might be buggy when false | |
13 prevent_default : false, | |
14 css_hacks : true, | |
15 | |
16 drag : true, | |
17 drag_vertical : true, | |
18 drag_horizontal : true, | |
19 // minimum distance before the drag event starts | |
20 drag_min_distance : 20, // pixels | |
21 | |
22 // pinch zoom and rotation | |
23 transform : true, | |
24 scale_treshold : 0.1, | |
25 rotation_treshold : 15, // degrees | |
26 | |
27 tap : true, | |
28 tap_double : true, | |
29 tap_max_interval : 300, | |
30 tap_double_distance: 20, | |
31 | |
32 hold : true, | |
33 hold_timeout : 500 | |
34 }; | |
35 options = mergeObject(defaults, options); | |
36 | |
37 // some css hacks | |
38 (function() { | |
39 if(!options.css_hacks) { | |
40 return false; | |
41 } | |
42 | |
43 var vendors = ['webkit','moz','ms','o','']; | |
44 var css_props = { | |
45 "userSelect": "none", | |
46 "touchCallout": "none", | |
47 "userDrag": "none", | |
48 "tapHighlightColor": "rgba(0,0,0,0)" | |
49 }; | |
50 | |
51 var prop = ''; | |
52 for(var i = 0; i < vendors.length; i++) { | |
53 for(var p in css_props) { | |
54 prop = p; | |
55 if(vendors[i]) { | |
56 prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); | |
57 } | |
58 element.style[ prop ] = css_props[p]; | |
59 } | |
60 } | |
61 })(); | |
62 | |
63 // holds the distance that has been moved | |
64 var _distance = 0; | |
65 | |
66 // holds the exact angle that has been moved | |
67 var _angle = 0; | |
68 | |
69 // holds the diraction that has been moved | |
70 var _direction = 0; | |
71 | |
72 // holds position movement for sliding | |
73 var _pos = { }; | |
74 | |
75 // how many fingers are on the screen | |
76 var _fingers = 0; | |
77 | |
78 var _first = false; | |
79 | |
80 var _gesture = null; | |
81 var _prev_gesture = null; | |
82 | |
83 var _touch_start_time = null; | |
84 var _prev_tap_pos = {x: 0, y: 0}; | |
85 var _prev_tap_end_time = null; | |
86 | |
87 var _hold_timer = null; | |
88 | |
89 var _offset = {}; | |
90 | |
91 // keep track of the mouse status | |
92 var _mousedown = false; | |
93 | |
94 var _event_start; | |
95 var _event_move; | |
96 var _event_end; | |
97 | |
98 | |
99 /** | |
100 * angle to direction define | |
101 * @param float angle | |
102 * @return string direction | |
103 */ | |
104 this.getDirectionFromAngle = function( angle ) | |
105 { | |
106 var directions = { | |
107 down: angle >= 45 && angle < 135, //90 | |
108 left: angle >= 135 || angle <= -135, //180 | |
109 up: angle < -45 && angle > -135, //270 | |
110 right: angle >= -45 && angle <= 45 //0 | |
111 }; | |
112 | |
113 var direction, key; | |
114 for(key in directions){ | |
115 if(directions[key]){ | |
116 direction = key; | |
117 break; | |
118 } | |
119 } | |
120 return direction; | |
121 }; | |
122 | |
123 | |
124 /** | |
125 * count the number of fingers in the event | |
126 * when no fingers are detected, one finger is returned (mouse pointer) | |
127 * @param event | |
128 * @return int fingers | |
129 */ | |
130 function countFingers( event ) | |
131 { | |
132 // there is a bug on android (until v4?) that touches is always 1, | |
133 // so no multitouch is supported, e.g. no, zoom and rotation... | |
134 return event.touches ? event.touches.length : 1; | |
135 } | |
136 | |
137 | |
138 /** | |
139 * get the x and y positions from the event object | |
140 * @param event | |
141 * @return array [{ x: int, y: int }] | |
142 */ | |
143 function getXYfromEvent( event ) | |
144 { | |
145 event = event || window.event; | |
146 | |
147 // no touches, use the event pageX and pageY | |
148 if(!event.touches) { | |
149 var doc = document, | |
150 body = doc.body; | |
151 | |
152 return [{ | |
153 x: event.pageX || event.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && doc.clientLeft || 0 ), | |
154 y: event.pageY || event.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && doc.clientTop || 0 ) | |
155 }]; | |
156 } | |
157 // multitouch, return array with positions | |
158 else { | |
159 var pos = [], src; | |
160 for(var t=0, len=event.touches.length; t<len; t++) { | |
161 src = event.touches[t]; | |
162 pos.push({ x: src.pageX, y: src.pageY }); | |
163 } | |
164 return pos; | |
165 } | |
166 } | |
167 | |
168 | |
169 /** | |
170 * calculate the angle between two points | |
171 * @param object pos1 { x: int, y: int } | |
172 * @param object pos2 { x: int, y: int } | |
173 */ | |
174 function getAngle( pos1, pos2 ) | |
175 { | |
176 return Math.atan2(pos2.y - pos1.y, pos2.x - pos1.x) * 180 / Math.PI; | |
177 } | |
178 | |
179 /** | |
180 * trigger an event/callback by name with params | |
181 * @param string name | |
182 * @param array params | |
183 */ | |
184 function triggerEvent( eventName, params ) | |
185 { | |
186 // return touches object | |
187 params.touches = getXYfromEvent(params.originalEvent); | |
188 params.type = eventName; | |
189 | |
190 // trigger callback | |
191 if(isFunction(self["on"+ eventName])) { | |
192 self["on"+ eventName].call(self, params); | |
193 } | |
194 } | |
195 | |
196 | |
197 /** | |
198 * cancel event | |
199 * @param object event | |
200 * @return void | |
201 */ | |
202 | |
203 function cancelEvent(event){ | |
204 event = event || window.event; | |
205 if(event.preventDefault){ | |
206 event.preventDefault(); | |
207 }else{ | |
208 event.returnValue = false; | |
209 event.cancelBubble = true; | |
210 } | |
211 } | |
212 | |
213 | |
214 /** | |
215 * reset the internal vars to the start values | |
216 */ | |
217 function reset() | |
218 { | |
219 _pos = {}; | |
220 _first = false; | |
221 _fingers = 0; | |
222 _distance = 0; | |
223 _angle = 0; | |
224 _gesture = null; | |
225 } | |
226 | |
227 | |
228 var gestures = { | |
229 // hold gesture | |
230 // fired on touchstart | |
231 hold : function(event) | |
232 { | |
233 // only when one finger is on the screen | |
234 if(options.hold) { | |
235 _gesture = 'hold'; | |
236 clearTimeout(_hold_timer); | |
237 | |
238 _hold_timer = setTimeout(function() { | |
239 if(_gesture == 'hold') { | |
240 triggerEvent("hold", { | |
241 originalEvent : event, | |
242 position : _pos.start | |
243 }); | |
244 } | |
245 }, options.hold_timeout); | |
246 } | |
247 }, | |
248 | |
249 | |
250 // drag gesture | |
251 // fired on mousemove | |
252 drag : function(event) | |
253 { | |
254 // get the distance we moved | |
255 var _distance_x = _pos.move[0].x - _pos.start[0].x; | |
256 var _distance_y = _pos.move[0].y - _pos.start[0].y; | |
257 _distance = Math.sqrt(_distance_x * _distance_x + _distance_y * _distance_y); | |
258 | |
259 // drag | |
260 // minimal movement required | |
261 if(options.drag && (_distance > options.drag_min_distance) || _gesture == 'drag') { | |
262 // calculate the angle | |
263 _angle = getAngle(_pos.start[0], _pos.move[0]); | |
264 _direction = self.getDirectionFromAngle(_angle); | |
265 | |
266 // check the movement and stop if we go in the wrong direction | |
267 var is_vertical = (_direction == 'up' || _direction == 'down'); | |
268 if(((is_vertical && !options.drag_vertical) || (!is_vertical && !options.drag_horizontal)) | |
269 && (_distance > options.drag_min_distance)) { | |
270 return; | |
271 } | |
272 | |
273 _gesture = 'drag'; | |
274 | |
275 var position = { x: _pos.move[0].x - _offset.left, | |
276 y: _pos.move[0].y - _offset.top }; | |
277 | |
278 var event_obj = { | |
279 originalEvent : event, | |
280 position : position, | |
281 direction : _direction, | |
282 distance : _distance, | |
283 distanceX : _distance_x, | |
284 distanceY : _distance_y, | |
285 angle : _angle | |
286 }; | |
287 | |
288 // on the first time trigger the start event | |
289 if(_first) { | |
290 triggerEvent("dragstart", event_obj); | |
291 | |
292 _first = false; | |
293 } | |
294 | |
295 // normal slide event | |
296 triggerEvent("drag", event_obj); | |
297 | |
298 cancelEvent(event); | |
299 } | |
300 }, | |
301 | |
302 | |
303 // transform gesture | |
304 // fired on touchmove | |
305 transform : function(event) | |
306 { | |
307 if(options.transform) { | |
308 var scale = event.scale || 1; | |
309 var rotation = event.rotation || 0; | |
310 | |
311 if(countFingers(event) != 2) { | |
312 return false; | |
313 } | |
314 | |
315 if(_gesture != 'drag' && | |
316 (_gesture == 'transform' || Math.abs(1-scale) > options.scale_treshold | |
317 || Math.abs(rotation) > options.rotation_treshold)) { | |
318 _gesture = 'transform'; | |
319 | |
320 _pos.center = { x: ((_pos.move[0].x + _pos.move[1].x) / 2) - _offset.left, | |
321 y: ((_pos.move[0].y + _pos.move[1].y) / 2) - _offset.top }; | |
322 | |
323 var event_obj = { | |
324 originalEvent : event, | |
325 position : _pos.center, | |
326 scale : scale, | |
327 rotation : rotation | |
328 }; | |
329 | |
330 // on the first time trigger the start event | |
331 if(_first) { | |
332 triggerEvent("transformstart", event_obj); | |
333 _first = false; | |
334 } | |
335 | |
336 triggerEvent("transform", event_obj); | |
337 | |
338 cancelEvent(event); | |
339 | |
340 return true; | |
341 } | |
342 } | |
343 | |
344 return false; | |
345 }, | |
346 | |
347 | |
348 // tap and double tap gesture | |
349 // fired on touchend | |
350 tap : function(event) | |
351 { | |
352 // compare the kind of gesture by time | |
353 var now = new Date().getTime(); | |
354 var touch_time = now - _touch_start_time; | |
355 | |
356 // dont fire when hold is fired | |
357 if(options.hold && !(options.hold && options.hold_timeout > touch_time)) { | |
358 return; | |
359 } | |
360 | |
361 // when previous event was tap and the tap was max_interval ms ago | |
362 var is_double_tap = (function(){ | |
363 if (_prev_tap_pos && options.tap_double && _prev_gesture == 'tap' && (_touch_start_time - _prev_tap_end_time) < options.tap_max_interval) { | |
364 var x_distance = Math.abs(_prev_tap_pos[0].x - _pos.start[0].x); | |
365 var y_distance = Math.abs(_prev_tap_pos[0].y - _pos.start[0].y); | |
366 return (_prev_tap_pos && _pos.start && Math.max(x_distance, y_distance) < options.tap_double_distance); | |
367 | |
368 } | |
369 return false; | |
370 })(); | |
371 | |
372 if(is_double_tap) { | |
373 _gesture = 'double_tap'; | |
374 _prev_tap_end_time = null; | |
375 | |
376 triggerEvent("doubletap", { | |
377 originalEvent : event, | |
378 position : _pos.start | |
379 }); | |
380 cancelEvent(event); | |
381 } | |
382 | |
383 // single tap is single touch | |
384 else { | |
385 _gesture = 'tap'; | |
386 _prev_tap_end_time = now; | |
387 _prev_tap_pos = _pos.start; | |
388 | |
389 if(options.tap) { | |
390 triggerEvent("tap", { | |
391 originalEvent : event, | |
392 position : _pos.start | |
393 }); | |
394 cancelEvent(event); | |
395 } | |
396 } | |
397 | |
398 } | |
399 | |
400 }; | |
401 | |
402 | |
403 function handleEvents(event) | |
404 { | |
405 switch(event.type) | |
406 { | |
407 case 'mousedown': | |
408 case 'touchstart': | |
409 _pos.start = getXYfromEvent(event); | |
410 _touch_start_time = new Date().getTime(); | |
411 _fingers = countFingers(event); | |
412 _first = true; | |
413 _event_start = event; | |
414 | |
415 // borrowed from jquery offset https://github.com/jquery/jquery/blob/master/src/offset.js | |
416 var box = element.getBoundingClientRect(); | |
417 var clientTop = element.clientTop || document.body.clientTop || 0; | |
418 var clientLeft = element.clientLeft || document.body.clientLeft || 0; | |
419 var scrollTop = window.pageYOffset || element.scrollTop || document.body.scrollTop; | |
420 var scrollLeft = window.pageXOffset || element.scrollLeft || document.body.scrollLeft; | |
421 | |
422 _offset = { | |
423 top: box.top + scrollTop - clientTop, | |
424 left: box.left + scrollLeft - clientLeft | |
425 }; | |
426 | |
427 _mousedown = true; | |
428 | |
429 // hold gesture | |
430 gestures.hold(event); | |
431 | |
432 if(options.prevent_default) { | |
433 cancelEvent(event); | |
434 } | |
435 break; | |
436 | |
437 case 'mousemove': | |
438 case 'touchmove': | |
439 if(!_mousedown) { | |
440 return false; | |
441 } | |
442 _event_move = event; | |
443 _pos.move = getXYfromEvent(event); | |
444 | |
445 if(!gestures.transform(event)) { | |
446 gestures.drag(event); | |
447 } | |
448 break; | |
449 | |
450 case 'mouseup': | |
451 case 'mouseout': | |
452 case 'touchcancel': | |
453 case 'touchend': | |
454 if(!_mousedown || (_gesture != 'transform' && event.touches && event.touches.length > 0)) { | |
455 return false; | |
456 } | |
457 | |
458 _mousedown = false; | |
459 _event_end = event; | |
460 | |
461 // drag gesture | |
462 // dragstart is triggered, so dragend is possible | |
463 if(_gesture == 'drag') { | |
464 triggerEvent("dragend", { | |
465 originalEvent : event, | |
466 direction : _direction, | |
467 distance : _distance, | |
468 angle : _angle | |
469 }); | |
470 } | |
471 | |
472 // transform | |
473 // transformstart is triggered, so transformed is possible | |
474 else if(_gesture == 'transform') { | |
475 triggerEvent("transformend", { | |
476 originalEvent : event, | |
477 position : _pos.center, | |
478 scale : event.scale, | |
479 rotation : event.rotation | |
480 }); | |
481 } | |
482 else { | |
483 gestures.tap(_event_start); | |
484 } | |
485 | |
486 _prev_gesture = _gesture; | |
487 | |
488 // reset vars | |
489 reset(); | |
490 break; | |
491 } | |
492 } | |
493 | |
494 | |
495 // bind events for touch devices | |
496 // except for windows phone 7.5, it doesnt support touch events..! | |
497 if('ontouchstart' in window) { | |
498 element.addEventListener("touchstart", handleEvents, false); | |
499 element.addEventListener("touchmove", handleEvents, false); | |
500 element.addEventListener("touchend", handleEvents, false); | |
501 element.addEventListener("touchcancel", handleEvents, false); | |
502 } | |
503 // for non-touch | |
504 else { | |
505 | |
506 if(element.addEventListener){ // prevent old IE errors | |
507 element.addEventListener("mouseout", function(event) { | |
508 if(!isInsideHammer(element, event.relatedTarget)) { | |
509 handleEvents(event); | |
510 } | |
511 }, false); | |
512 element.addEventListener("mouseup", handleEvents, false); | |
513 element.addEventListener("mousedown", handleEvents, false); | |
514 element.addEventListener("mousemove", handleEvents, false); | |
515 | |
516 // events for older IE | |
517 }else if(document.attachEvent){ | |
518 element.attachEvent("onmouseout", function(event) { | |
519 if(!isInsideHammer(element, event.relatedTarget)) { | |
520 handleEvents(event); | |
521 } | |
522 }, false); | |
523 element.attachEvent("onmouseup", handleEvents); | |
524 element.attachEvent("onmousedown", handleEvents); | |
525 element.attachEvent("onmousemove", handleEvents); | |
526 } | |
527 } | |
528 | |
529 | |
530 /** | |
531 * find if element is (inside) given parent element | |
532 * @param object element | |
533 * @param object parent | |
534 * @return bool inside | |
535 */ | |
536 function isInsideHammer(parent, child) { | |
537 // get related target for IE | |
538 if(!child && window.event && window.event.toElement){ | |
539 child = window.event.toElement; | |
540 } | |
541 | |
542 if(parent === child){ | |
543 return true; | |
544 } | |
545 | |
546 // loop over parentNodes of child until we find hammer element | |
547 if(child){ | |
548 var node = child.parentNode; | |
549 while(node !== null){ | |
550 if(node === parent){ | |
551 return true; | |
552 }; | |
553 node = node.parentNode; | |
554 } | |
555 } | |
556 return false; | |
557 } | |
558 | |
559 | |
560 /** | |
561 * merge 2 objects into a new object | |
562 * @param object obj1 | |
563 * @param object obj2 | |
564 * @return object merged object | |
565 */ | |
566 function mergeObject(obj1, obj2) { | |
567 var output = {}; | |
568 | |
569 if(!obj2) { | |
570 return obj1; | |
571 } | |
572 | |
573 for (var prop in obj1) { | |
574 if (prop in obj2) { | |
575 output[prop] = obj2[prop]; | |
576 } else { | |
577 output[prop] = obj1[prop]; | |
578 } | |
579 } | |
580 return output; | |
581 } | |
582 | |
583 function isFunction( obj ){ | |
584 return Object.prototype.toString.call( obj ) == "[object Function]"; | |
585 } | |
586 } |