0
|
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 } |