Initial import.
[yuu.git] / src / ext / hammer.js
1 /*! Hammer.JS - v1.0.10 - 2014-03-28
2 * http://eightmedia.github.io/hammer.js
3 *
4 * Copyright (c) 2014 Jorik Tangelder <j.tangelder@gmail.com>;
5 * Licensed under the MIT license */
6
7 (function(window, undefined) {
8 'use strict';
9
10 /**
11 * Hammer
12 * use this to create instances
13 * @param {HTMLElement} element
14 * @param {Object} options
15 * @returns {Hammer.Instance}
16 * @constructor
17 */
18 var Hammer = function(element, options) {
19 return new Hammer.Instance(element, options || {});
20 };
21
22 Hammer.VERSION = '1.0.10';
23
24 // default settings
25 Hammer.defaults = {
26 // add styles and attributes to the element to prevent the browser from doing
27 // its native behavior. this doesnt prevent the scrolling, but cancels
28 // the contextmenu, tap highlighting etc
29 // set to false to disable this
30 stop_browser_behavior: {
31 // this also triggers onselectstart=false for IE
32 userSelect : 'none',
33 // this makes the element blocking in IE10>, you could experiment with the value
34 // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
35 touchAction : 'none',
36 touchCallout : 'none',
37 contentZooming : 'none',
38 userDrag : 'none',
39 tapHighlightColor: 'rgba(0,0,0,0)'
40 }
41
42 //
43 // more settings are defined per gesture at /gestures
44 //
45 };
46
47
48 // detect touchevents
49 Hammer.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled;
50 Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
51
52 // dont use mouseevents on mobile devices
53 Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i;
54 Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && window.navigator.userAgent.match(Hammer.MOBILE_REGEX);
55
56 // eventtypes per touchevent (start, move, end)
57 // are filled by Event.determineEventTypes on setup
58 Hammer.EVENT_TYPES = {};
59
60 // interval in which Hammer recalculates current velocity in ms
61 Hammer.UPDATE_VELOCITY_INTERVAL = 16;
62
63 // hammer document where the base events are added at
64 Hammer.DOCUMENT = window.document;
65
66 // define these also as vars, for better minification
67 // direction defines
68 var DIRECTION_DOWN = Hammer.DIRECTION_DOWN = 'down';
69 var DIRECTION_LEFT = Hammer.DIRECTION_LEFT = 'left';
70 var DIRECTION_UP = Hammer.DIRECTION_UP = 'up';
71 var DIRECTION_RIGHT = Hammer.DIRECTION_RIGHT = 'right';
72
73 // pointer type
74 var POINTER_MOUSE = Hammer.POINTER_MOUSE = 'mouse';
75 var POINTER_TOUCH = Hammer.POINTER_TOUCH = 'touch';
76 var POINTER_PEN = Hammer.POINTER_PEN = 'pen';
77
78 // touch event defines
79 var EVENT_START = Hammer.EVENT_START = 'start';
80 var EVENT_MOVE = Hammer.EVENT_MOVE = 'move';
81 var EVENT_END = Hammer.EVENT_END = 'end';
82
83
84 // plugins and gestures namespaces
85 Hammer.plugins = Hammer.plugins || {};
86 Hammer.gestures = Hammer.gestures || {};
87
88
89 // if the window events are set...
90 Hammer.READY = false;
91
92
93 /**
94 * setup events to detect gestures on the document
95 */
96 function setup() {
97 if(Hammer.READY) {
98 return;
99 }
100
101 // find what eventtypes we add listeners to
102 Event.determineEventTypes();
103
104 // Register all gestures inside Hammer.gestures
105 Utils.each(Hammer.gestures, function(gesture){
106 Detection.register(gesture);
107 });
108
109 // Add touch events on the document
110 Event.onTouch(Hammer.DOCUMENT, EVENT_MOVE, Detection.detect);
111 Event.onTouch(Hammer.DOCUMENT, EVENT_END, Detection.detect);
112
113 // Hammer is ready...!
114 Hammer.READY = true;
115 }
116
117 var Utils = Hammer.utils = {
118 /**
119 * extend method,
120 * also used for cloning when dest is an empty object
121 * @param {Object} dest
122 * @param {Object} src
123 * @parm {Boolean} merge do a merge
124 * @returns {Object} dest
125 */
126 extend: function extend(dest, src, merge) {
127 for(var key in src) {
128 if(dest[key] !== undefined && merge) {
129 continue;
130 }
131 dest[key] = src[key];
132 }
133 return dest;
134 },
135
136
137 /**
138 * for each
139 * @param obj
140 * @param iterator
141 */
142 each: function each(obj, iterator, context) {
143 var i, o;
144 // native forEach on arrays
145 if ('forEach' in obj) {
146 obj.forEach(iterator, context);
147 }
148 // arrays
149 else if(obj.length !== undefined) {
150 for(i=-1; (o=obj[++i]);) {
151 if (iterator.call(context, o, i, obj) === false) {
152 return;
153 }
154 }
155 }
156 // objects
157 else {
158 for(i in obj) {
159 if(obj.hasOwnProperty(i) &&
160 iterator.call(context, obj[i], i, obj) === false) {
161 return;
162 }
163 }
164 }
165 },
166
167
168 /**
169 * find if a string contains the needle
170 * @param {String} src
171 * @param {String} needle
172 * @returns {Boolean} found
173 */
174 inStr: function inStr(src, needle) {
175 return src.indexOf(needle) > -1;
176 },
177
178
179 /**
180 * find if a node is in the given parent
181 * used for event delegation tricks
182 * @param {HTMLElement} node
183 * @param {HTMLElement} parent
184 * @returns {boolean} has_parent
185 */
186 hasParent: function hasParent(node, parent) {
187 while(node) {
188 if(node == parent) {
189 return true;
190 }
191 node = node.parentNode;
192 }
193 return false;
194 },
195
196
197 /**
198 * get the center of all the touches
199 * @param {Array} touches
200 * @returns {Object} center pageXY clientXY
201 */
202 getCenter: function getCenter(touches) {
203 var pageX = []
204 , pageY = []
205 , clientX = []
206 , clientY = []
207 , min = Math.min
208 , max = Math.max;
209
210 // no need to loop when only one touch
211 if(touches.length === 1) {
212 return {
213 pageX: touches[0].pageX,
214 pageY: touches[0].pageY,
215 clientX: touches[0].clientX,
216 clientY: touches[0].clientY
217 };
218 }
219
220 Utils.each(touches, function(touch) {
221 pageX.push(touch.pageX);
222 pageY.push(touch.pageY);
223 clientX.push(touch.clientX);
224 clientY.push(touch.clientY);
225 });
226
227 return {
228 pageX: (min.apply(Math, pageX) + max.apply(Math, pageX)) / 2,
229 pageY: (min.apply(Math, pageY) + max.apply(Math, pageY)) / 2,
230 clientX: (min.apply(Math, clientX) + max.apply(Math, clientX)) / 2,
231 clientY: (min.apply(Math, clientY) + max.apply(Math, clientY)) / 2
232 };
233 },
234
235
236 /**
237 * calculate the velocity between two points
238 * @param {Number} delta_time
239 * @param {Number} delta_x
240 * @param {Number} delta_y
241 * @returns {Object} velocity
242 */
243 getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
244 return {
245 x: Math.abs(delta_x / delta_time) || 0,
246 y: Math.abs(delta_y / delta_time) || 0
247 };
248 },
249
250
251 /**
252 * calculate the angle between two coordinates
253 * @param {Touch} touch1
254 * @param {Touch} touch2
255 * @returns {Number} angle
256 */
257 getAngle: function getAngle(touch1, touch2) {
258 var x = touch2.clientX - touch1.clientX
259 , y = touch2.clientY - touch1.clientY;
260 return Math.atan2(y, x) * 180 / Math.PI;
261 },
262
263
264 /**
265 * angle to direction define
266 * @param {Touch} touch1
267 * @param {Touch} touch2
268 * @returns {String} direction constant, like DIRECTION_LEFT
269 */
270 getDirection: function getDirection(touch1, touch2) {
271 var x = Math.abs(touch1.clientX - touch2.clientX)
272 , y = Math.abs(touch1.clientY - touch2.clientY);
273 if(x >= y) {
274 return touch1.clientX - touch2.clientX > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
275 }
276 return touch1.clientY - touch2.clientY > 0 ? DIRECTION_UP : DIRECTION_DOWN;
277 },
278
279
280 /**
281 * calculate the distance between two touches
282 * @param {Touch} touch1
283 * @param {Touch} touch2
284 * @returns {Number} distance
285 */
286 getDistance: function getDistance(touch1, touch2) {
287 var x = touch2.clientX - touch1.clientX
288 , y = touch2.clientY - touch1.clientY;
289 return Math.sqrt((x * x) + (y * y));
290 },
291
292
293 /**
294 * calculate the scale factor between two touchLists (fingers)
295 * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
296 * @param {Array} start
297 * @param {Array} end
298 * @returns {Number} scale
299 */
300 getScale: function getScale(start, end) {
301 // need two fingers...
302 if(start.length >= 2 && end.length >= 2) {
303 return this.getDistance(end[0], end[1]) / this.getDistance(start[0], start[1]);
304 }
305 return 1;
306 },
307
308
309 /**
310 * calculate the rotation degrees between two touchLists (fingers)
311 * @param {Array} start
312 * @param {Array} end
313 * @returns {Number} rotation
314 */
315 getRotation: function getRotation(start, end) {
316 // need two fingers
317 if(start.length >= 2 && end.length >= 2) {
318 return this.getAngle(end[1], end[0]) - this.getAngle(start[1], start[0]);
319 }
320 return 0;
321 },
322
323
324 /**
325 * boolean if the direction is vertical
326 * @param {String} direction
327 * @returns {Boolean} is_vertical
328 */
329 isVertical: function isVertical(direction) {
330 return direction == DIRECTION_UP || direction == DIRECTION_DOWN;
331 },
332
333
334 /**
335 * toggle browser default behavior with css props
336 * @param {HtmlElement} element
337 * @param {Object} css_props
338 * @param {Boolean} toggle
339 */
340 toggleDefaultBehavior: function toggleDefaultBehavior(element, css_props, toggle) {
341 if(!css_props || !element || !element.style) {
342 return;
343 }
344
345 // with css properties for modern browsers
346 Utils.each(['webkit', 'moz', 'Moz', 'ms', 'o', ''], function setStyle(vendor) {
347 Utils.each(css_props, function(value, prop) {
348 // vender prefix at the property
349 if(vendor) {
350 prop = vendor + prop.substring(0, 1).toUpperCase() + prop.substring(1);
351 }
352 // set the style
353 if(prop in element.style) {
354 element.style[prop] = !toggle && value;
355 }
356 });
357 });
358
359 var false_fn = function(){ return false; };
360
361 // also the disable onselectstart
362 if(css_props.userSelect == 'none') {
363 element.onselectstart = !toggle && false_fn;
364 }
365 // and disable ondragstart
366 if(css_props.userDrag == 'none') {
367 element.ondragstart = !toggle && false_fn;
368 }
369 }
370 };
371
372
373 /**
374 * create new hammer instance
375 * all methods should return the instance itself, so it is chainable.
376 * @param {HTMLElement} element
377 * @param {Object} [options={}]
378 * @returns {Hammer.Instance}
379 * @constructor
380 */
381 Hammer.Instance = function(element, options) {
382 var self = this;
383
384 // setup HammerJS window events and register all gestures
385 // this also sets up the default options
386 setup();
387
388 this.element = element;
389
390 // start/stop detection option
391 this.enabled = true;
392
393 // merge options
394 this.options = Utils.extend(
395 Utils.extend({}, Hammer.defaults),
396 options || {});
397
398 // add some css to the element to prevent the browser from doing its native behavoir
399 if(this.options.stop_browser_behavior) {
400 Utils.toggleDefaultBehavior(this.element, this.options.stop_browser_behavior, false);
401 }
402
403 // start detection on touchstart
404 this.eventStartHandler = Event.onTouch(element, EVENT_START, function(ev) {
405 if(self.enabled) {
406 Detection.startDetect(self, ev);
407 }
408 });
409
410 // keep a list of user event handlers which needs to be removed when calling 'dispose'
411 this.eventHandlers = [];
412
413 // return instance
414 return this;
415 };
416
417
418 Hammer.Instance.prototype = {
419 /**
420 * bind events to the instance
421 * @param {String} gesture
422 * @param {Function} handler
423 * @returns {Hammer.Instance}
424 */
425 on: function onEvent(gesture, handler) {
426 var gestures = gesture.split(' ');
427 Utils.each(gestures, function(gesture) {
428 this.element.addEventListener(gesture, handler, false);
429 this.eventHandlers.push({ gesture: gesture, handler: handler });
430 }, this);
431 return this;
432 },
433
434
435 /**
436 * unbind events to the instance
437 * @param {String} gesture
438 * @param {Function} handler
439 * @returns {Hammer.Instance}
440 */
441 off: function offEvent(gesture, handler) {
442 var gestures = gesture.split(' ')
443 , i, eh;
444 Utils.each(gestures, function(gesture) {
445 this.element.removeEventListener(gesture, handler, false);
446
447 // remove the event handler from the internal list
448 for(i=-1; (eh=this.eventHandlers[++i]);) {
449 if(eh.gesture === gesture && eh.handler === handler) {
450 this.eventHandlers.splice(i, 1);
451 }
452 }
453 }, this);
454 return this;
455 },
456
457
458 /**
459 * trigger gesture event
460 * @param {String} gesture
461 * @param {Object} [eventData]
462 * @returns {Hammer.Instance}
463 */
464 trigger: function triggerEvent(gesture, eventData) {
465 // optional
466 if(!eventData) {
467 eventData = {};
468 }
469
470 // create DOM event
471 var event = Hammer.DOCUMENT.createEvent('Event');
472 event.initEvent(gesture, true, true);
473 event.gesture = eventData;
474
475 // trigger on the target if it is in the instance element,
476 // this is for event delegation tricks
477 var element = this.element;
478 if(Utils.hasParent(eventData.target, element)) {
479 element = eventData.target;
480 }
481
482 element.dispatchEvent(event);
483 return this;
484 },
485
486
487 /**
488 * enable of disable hammer.js detection
489 * @param {Boolean} state
490 * @returns {Hammer.Instance}
491 */
492 enable: function enable(state) {
493 this.enabled = state;
494 return this;
495 },
496
497
498 /**
499 * dispose this hammer instance
500 * @returns {Hammer.Instance}
501 */
502 dispose: function dispose() {
503 var i, eh;
504
505 // undo all changes made by stop_browser_behavior
506 if(this.options.stop_browser_behavior) {
507 Utils.toggleDefaultBehavior(this.element, this.options.stop_browser_behavior, true);
508 }
509
510 // unbind all custom event handlers
511 for(i=-1; (eh=this.eventHandlers[++i]);) {
512 this.element.removeEventListener(eh.gesture, eh.handler, false);
513 }
514 this.eventHandlers = [];
515
516 // unbind the start event listener
517 Event.unbindDom(this.element, Hammer.EVENT_TYPES[EVENT_START], this.eventStartHandler);
518
519 return null;
520 }
521 };
522
523
524 /**
525 * this holds the last move event,
526 * used to fix empty touchend issue
527 * see the onTouch event for an explanation
528 * @type {Object}
529 */
530 var last_move_event = null;
531
532 /**
533 * when the mouse is hold down, this is true
534 * @type {Boolean}
535 */
536 var should_detect = false;
537
538 /**
539 * when touch events have been fired, this is true
540 * @type {Boolean}
541 */
542 var touch_triggered = false;
543
544
545 var Event = Hammer.event = {
546 /**
547 * simple addEventListener
548 * @param {HTMLElement} element
549 * @param {String} type
550 * @param {Function} handler
551 */
552 bindDom: function(element, type, handler) {
553 var types = type.split(' ');
554 Utils.each(types, function(type){
555 element.addEventListener(type, handler, false);
556 });
557 },
558
559
560 /**
561 * simple removeEventListener
562 * @param {HTMLElement} element
563 * @param {String} type
564 * @param {Function} handler
565 */
566 unbindDom: function(element, type, handler) {
567 var types = type.split(' ');
568 Utils.each(types, function(type){
569 element.removeEventListener(type, handler, false);
570 });
571 },
572
573
574 /**
575 * touch events with mouse fallback
576 * @param {HTMLElement} element
577 * @param {String} eventType like EVENT_MOVE
578 * @param {Function} handler
579 */
580 onTouch: function onTouch(element, eventType, handler) {
581 var self = this;
582
583
584 var bindDomOnTouch = function bindDomOnTouch(ev) {
585 var srcEventType = ev.type.toLowerCase();
586
587 // onmouseup, but when touchend has been fired we do nothing.
588 // this is for touchdevices which also fire a mouseup on touchend
589 if(Utils.inStr(srcEventType, 'mouse') && touch_triggered) {
590 return;
591 }
592
593 // mousebutton must be down or a touch event
594 else if(Utils.inStr(srcEventType, 'touch') || // touch events are always on screen
595 Utils.inStr(srcEventType, 'pointerdown') || // pointerevents touch
596 (Utils.inStr(srcEventType, 'mouse') && ev.which === 1) // mouse is pressed
597 ) {
598 should_detect = true;
599 }
600
601 // mouse isn't pressed
602 else if(Utils.inStr(srcEventType, 'mouse') && !ev.which) {
603 should_detect = false;
604 }
605
606
607 // we are in a touch event, set the touch triggered bool to true,
608 // this for the conflicts that may occur on ios and android
609 if(Utils.inStr(srcEventType, 'touch') || Utils.inStr(srcEventType, 'pointer')) {
610 touch_triggered = true;
611 }
612
613 // count the total touches on the screen
614 var count_touches = 0;
615
616 // when touch has been triggered in this detection session
617 // and we are now handling a mouse event, we stop that to prevent conflicts
618 if(should_detect) {
619 // update pointerevent
620 if(Hammer.HAS_POINTEREVENTS && eventType != EVENT_END) {
621 count_touches = PointerEvent.updatePointer(eventType, ev);
622 }
623 // touch
624 else if(Utils.inStr(srcEventType, 'touch')) {
625 count_touches = ev.touches.length;
626 }
627 // mouse
628 else if(!touch_triggered) {
629 count_touches = Utils.inStr(srcEventType, 'up') ? 0 : 1;
630 }
631
632
633 // if we are in a end event, but when we remove one touch and
634 // we still have enough, set eventType to move
635 if(count_touches > 0 && eventType == EVENT_END) {
636 eventType = EVENT_MOVE;
637 }
638 // no touches, force the end event
639 else if(!count_touches) {
640 eventType = EVENT_END;
641 }
642
643 // store the last move event
644 if(count_touches || last_move_event === null) {
645 last_move_event = ev;
646 }
647
648
649 // trigger the handler
650 handler.call(Detection, self.collectEventData(element, eventType,
651 self.getTouchList(last_move_event, eventType),
652 ev) );
653
654 // remove pointerevent from list
655 if(Hammer.HAS_POINTEREVENTS && eventType == EVENT_END) {
656 count_touches = PointerEvent.updatePointer(eventType, ev);
657 }
658 }
659
660 // on the end we reset everything
661 if(!count_touches) {
662 last_move_event = null;
663 should_detect = false;
664 touch_triggered = false;
665 PointerEvent.reset();
666 }
667 };
668
669 this.bindDom(element, Hammer.EVENT_TYPES[eventType], bindDomOnTouch);
670
671 // return the bound function to be able to unbind it later
672 return bindDomOnTouch;
673 },
674
675
676 /**
677 * we have different events for each device/browser
678 * determine what we need and set them in the Hammer.EVENT_TYPES constant
679 */
680 determineEventTypes: function determineEventTypes() {
681 // determine the eventtype we want to set
682 var types;
683
684 // pointerEvents magic
685 if(Hammer.HAS_POINTEREVENTS) {
686 types = PointerEvent.getEvents();
687 }
688 // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
689 else if(Hammer.NO_MOUSEEVENTS) {
690 types = [
691 'touchstart',
692 'touchmove',
693 'touchend touchcancel'];
694 }
695 // for non pointer events browsers and mixed browsers,
696 // like chrome on windows8 touch laptop
697 else {
698 types = [
699 'touchstart mousedown',
700 'touchmove mousemove',
701 'touchend touchcancel mouseup'];
702 }
703
704 Hammer.EVENT_TYPES[EVENT_START] = types[0];
705 Hammer.EVENT_TYPES[EVENT_MOVE] = types[1];
706 Hammer.EVENT_TYPES[EVENT_END] = types[2];
707 },
708
709
710 /**
711 * create touchlist depending on the event
712 * @param {Object} ev
713 * @param {String} eventType used by the fakemultitouch plugin
714 */
715 getTouchList: function getTouchList(ev/*, eventType*/) {
716 // get the fake pointerEvent touchlist
717 if(Hammer.HAS_POINTEREVENTS) {
718 return PointerEvent.getTouchList();
719 }
720
721 // get the touchlist
722 if(ev.touches) {
723 return ev.touches;
724 }
725
726 // make fake touchlist from mouse position
727 ev.identifier = 1;
728 return [ev];
729 },
730
731
732 /**
733 * collect event data for Hammer js
734 * @param {HTMLElement} element
735 * @param {String} eventType like EVENT_MOVE
736 * @param {Object} eventData
737 */
738 collectEventData: function collectEventData(element, eventType, touches, ev) {
739 // find out pointerType
740 var pointerType = POINTER_TOUCH;
741 if(Utils.inStr(ev.type, 'mouse') || PointerEvent.matchType(POINTER_MOUSE, ev)) {
742 pointerType = POINTER_MOUSE;
743 }
744
745 return {
746 center : Utils.getCenter(touches),
747 timeStamp : Date.now(),
748 target : ev.target,
749 touches : touches,
750 eventType : eventType,
751 pointerType: pointerType,
752 srcEvent : ev,
753
754 /**
755 * prevent the browser default actions
756 * mostly used to disable scrolling of the browser
757 */
758 preventDefault: function() {
759 var srcEvent = this.srcEvent;
760 srcEvent.preventManipulation && srcEvent.preventManipulation();
761 srcEvent.preventDefault && srcEvent.preventDefault();
762 },
763
764 /**
765 * stop bubbling the event up to its parents
766 */
767 stopPropagation: function() {
768 this.srcEvent.stopPropagation();
769 },
770
771 /**
772 * immediately stop gesture detection
773 * might be useful after a swipe was detected
774 * @return {*}
775 */
776 stopDetect: function() {
777 return Detection.stopDetect();
778 }
779 };
780 }
781 };
782
783 var PointerEvent = Hammer.PointerEvent = {
784 /**
785 * holds all pointers
786 * @type {Object}
787 */
788 pointers: {},
789
790 /**
791 * get a list of pointers
792 * @returns {Array} touchlist
793 */
794 getTouchList: function getTouchList() {
795 var touchlist = [];
796 // we can use forEach since pointerEvents only is in IE10
797 Utils.each(this.pointers, function(pointer){
798 touchlist.push(pointer);
799 });
800
801 return touchlist;
802 },
803
804 /**
805 * update the position of a pointer
806 * @param {String} type EVENT_END
807 * @param {Object} pointerEvent
808 */
809 updatePointer: function updatePointer(type, pointerEvent) {
810 if(type == EVENT_END) {
811 delete this.pointers[pointerEvent.pointerId];
812 }
813 else {
814 pointerEvent.identifier = pointerEvent.pointerId;
815 this.pointers[pointerEvent.pointerId] = pointerEvent;
816 }
817
818 // it's save to use Object.keys, since pointerEvents are only in newer browsers
819 return Object.keys(this.pointers).length;
820 },
821
822 /**
823 * check if ev matches pointertype
824 * @param {String} pointerType POINTER_MOUSE
825 * @param {PointerEvent} ev
826 */
827 matchType: function matchType(pointerType, ev) {
828 if(!ev.pointerType) {
829 return false;
830 }
831
832 var pt = ev.pointerType
833 , types = {};
834
835 types[POINTER_MOUSE] = (pt === POINTER_MOUSE);
836 types[POINTER_TOUCH] = (pt === POINTER_TOUCH);
837 types[POINTER_PEN] = (pt === POINTER_PEN);
838 return types[pointerType];
839 },
840
841
842 /**
843 * get events
844 */
845 getEvents: function getEvents() {
846 return [
847 'pointerdown MSPointerDown',
848 'pointermove MSPointerMove',
849 'pointerup pointercancel MSPointerUp MSPointerCancel'
850 ];
851 },
852
853 /**
854 * reset the list
855 */
856 reset: function resetList() {
857 this.pointers = {};
858 }
859 };
860
861
862 var Detection = Hammer.detection = {
863 // contains all registred Hammer.gestures in the correct order
864 gestures: [],
865
866 // data of the current Hammer.gesture detection session
867 current : null,
868
869 // the previous Hammer.gesture session data
870 // is a full clone of the previous gesture.current object
871 previous: null,
872
873 // when this becomes true, no gestures are fired
874 stopped : false,
875
876
877 /**
878 * start Hammer.gesture detection
879 * @param {Hammer.Instance} inst
880 * @param {Object} eventData
881 */
882 startDetect: function startDetect(inst, eventData) {
883 // already busy with a Hammer.gesture detection on an element
884 if(this.current) {
885 return;
886 }
887
888 this.stopped = false;
889
890 // holds current session
891 this.current = {
892 inst : inst, // reference to HammerInstance we're working for
893 startEvent : Utils.extend({}, eventData), // start eventData for distances, timing etc
894 lastEvent : false, // last eventData
895 lastVelocityEvent : false, // last eventData for velocity.
896 velocity : false, // current velocity
897 name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
898 };
899
900 this.detect(eventData);
901 },
902
903
904 /**
905 * Hammer.gesture detection
906 * @param {Object} eventData
907 */
908 detect: function detect(eventData) {
909 if(!this.current || this.stopped) {
910 return;
911 }
912
913 // extend event data with calculations about scale, distance etc
914 eventData = this.extendEventData(eventData);
915
916 // hammer instance and instance options
917 var inst = this.current.inst,
918 inst_options = inst.options;
919
920 // call Hammer.gesture handlers
921 Utils.each(this.gestures, function triggerGesture(gesture) {
922 // only when the instance options have enabled this gesture
923 if(!this.stopped && inst_options[gesture.name] !== false && inst.enabled !== false ) {
924 // if a handler returns false, we stop with the detection
925 if(gesture.handler.call(gesture, eventData, inst) === false) {
926 this.stopDetect();
927 return false;
928 }
929 }
930 }, this);
931
932 // store as previous event event
933 if(this.current) {
934 this.current.lastEvent = eventData;
935 }
936
937 // end event, but not the last touch, so dont stop
938 if(eventData.eventType == EVENT_END && !eventData.touches.length - 1) {
939 this.stopDetect();
940 }
941
942 return eventData;
943 },
944
945
946 /**
947 * clear the Hammer.gesture vars
948 * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
949 * to stop other Hammer.gestures from being fired
950 */
951 stopDetect: function stopDetect() {
952 // clone current data to the store as the previous gesture
953 // used for the double tap gesture, since this is an other gesture detect session
954 this.previous = Utils.extend({}, this.current);
955
956 // reset the current
957 this.current = null;
958
959 // stopped!
960 this.stopped = true;
961 },
962
963
964 /**
965 * calculate velocity
966 * @param {Object} ev
967 * @param {Number} delta_time
968 * @param {Number} delta_x
969 * @param {Number} delta_y
970 */
971 getVelocityData: function getVelocityData(ev, delta_time, delta_x, delta_y) {
972 var cur = this.current
973 , velocityEv = cur.lastVelocityEvent
974 , velocity = cur.velocity;
975
976 // calculate velocity every x ms
977 if (velocityEv && ev.timeStamp - velocityEv.timeStamp > Hammer.UPDATE_VELOCITY_INTERVAL) {
978 velocity = Utils.getVelocity(ev.timeStamp - velocityEv.timeStamp,
979 ev.center.clientX - velocityEv.center.clientX,
980 ev.center.clientY - velocityEv.center.clientY);
981 cur.lastVelocityEvent = ev;
982 }
983 else if(!cur.velocity) {
984 velocity = Utils.getVelocity(delta_time, delta_x, delta_y);
985 cur.lastVelocityEvent = ev;
986 }
987
988 cur.velocity = velocity;
989
990 ev.velocityX = velocity.x;
991 ev.velocityY = velocity.y;
992 },
993
994
995 /**
996 * calculate interim angle and direction
997 * @param {Object} ev
998 */
999 getInterimData: function getInterimData(ev) {
1000 var lastEvent = this.current.lastEvent
1001 , angle
1002 , direction;
1003
1004 // end events (e.g. dragend) don't have useful values for interimDirection & interimAngle
1005 // because the previous event has exactly the same coordinates
1006 // so for end events, take the previous values of interimDirection & interimAngle
1007 // instead of recalculating them and getting a spurious '0'
1008 if(ev.eventType == EVENT_END) {
1009 angle = lastEvent && lastEvent.interimAngle;
1010 direction = lastEvent && lastEvent.interimDirection;
1011 }
1012 else {
1013 angle = lastEvent && Utils.getAngle(lastEvent.center, ev.center);
1014 direction = lastEvent && Utils.getDirection(lastEvent.center, ev.center);
1015 }
1016
1017 ev.interimAngle = angle;
1018 ev.interimDirection = direction;
1019 },
1020
1021
1022 /**
1023 * extend eventData for Hammer.gestures
1024 * @param {Object} evData
1025 * @returns {Object} evData
1026 */
1027 extendEventData: function extendEventData(ev) {
1028 var cur = this.current
1029 , startEv = cur.startEvent;
1030
1031 // if the touches change, set the new touches over the startEvent touches
1032 // this because touchevents don't have all the touches on touchstart, or the
1033 // user must place his fingers at the EXACT same time on the screen, which is not realistic
1034 // but, sometimes it happens that both fingers are touching at the EXACT same time
1035 if(ev.touches.length != startEv.touches.length || ev.touches === startEv.touches) {
1036 // extend 1 level deep to get the touchlist with the touch objects
1037 startEv.touches = [];
1038 Utils.each(ev.touches, function(touch) {
1039 startEv.touches.push(Utils.extend({}, touch));
1040 });
1041 }
1042
1043 var delta_time = ev.timeStamp - startEv.timeStamp
1044 , delta_x = ev.center.clientX - startEv.center.clientX
1045 , delta_y = ev.center.clientY - startEv.center.clientY;
1046
1047 this.getVelocityData(ev, delta_time, delta_x, delta_y);
1048 this.getInterimData(ev);
1049
1050 Utils.extend(ev, {
1051 startEvent: startEv,
1052
1053 deltaTime : delta_time,
1054 deltaX : delta_x,
1055 deltaY : delta_y,
1056
1057 distance : Utils.getDistance(startEv.center, ev.center),
1058 angle : Utils.getAngle(startEv.center, ev.center),
1059 direction : Utils.getDirection(startEv.center, ev.center),
1060
1061 scale : Utils.getScale(startEv.touches, ev.touches),
1062 rotation : Utils.getRotation(startEv.touches, ev.touches)
1063 });
1064
1065 return ev;
1066 },
1067
1068
1069 /**
1070 * register new gesture
1071 * @param {Object} gesture object, see gestures.js for documentation
1072 * @returns {Array} gestures
1073 */
1074 register: function register(gesture) {
1075 // add an enable gesture options if there is no given
1076 var options = gesture.defaults || {};
1077 if(options[gesture.name] === undefined) {
1078 options[gesture.name] = true;
1079 }
1080
1081 // extend Hammer default options with the Hammer.gesture options
1082 Utils.extend(Hammer.defaults, options, true);
1083
1084 // set its index
1085 gesture.index = gesture.index || 1000;
1086
1087 // add Hammer.gesture to the list
1088 this.gestures.push(gesture);
1089
1090 // sort the list by index
1091 this.gestures.sort(function(a, b) {
1092 if(a.index < b.index) { return -1; }
1093 if(a.index > b.index) { return 1; }
1094 return 0;
1095 });
1096
1097 return this.gestures;
1098 }
1099 };
1100
1101
1102 /**
1103 * Drag
1104 * Move with x fingers (default 1) around on the page. Blocking the scrolling when
1105 * moving left and right is a good practice. When all the drag events are blocking
1106 * you disable scrolling on that area.
1107 * @events drag, drapleft, dragright, dragup, dragdown
1108 */
1109 Hammer.gestures.Drag = {
1110 name : 'drag',
1111 index : 50,
1112 defaults : {
1113 drag_min_distance : 10,
1114
1115 // Set correct_for_drag_min_distance to true to make the starting point of the drag
1116 // be calculated from where the drag was triggered, not from where the touch started.
1117 // Useful to avoid a jerk-starting drag, which can make fine-adjustments
1118 // through dragging difficult, and be visually unappealing.
1119 correct_for_drag_min_distance: true,
1120
1121 // set 0 for unlimited, but this can conflict with transform
1122 drag_max_touches : 1,
1123
1124 // prevent default browser behavior when dragging occurs
1125 // be careful with it, it makes the element a blocking element
1126 // when you are using the drag gesture, it is a good practice to set this true
1127 drag_block_horizontal : false,
1128 drag_block_vertical : false,
1129
1130 // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
1131 // It disallows vertical directions if the initial direction was horizontal, and vice versa.
1132 drag_lock_to_axis : false,
1133
1134 // drag lock only kicks in when distance > drag_lock_min_distance
1135 // This way, locking occurs only when the distance has become large enough to reliably determine the direction
1136 drag_lock_min_distance : 25
1137 },
1138
1139 triggered: false,
1140 handler : function dragGesture(ev, inst) {
1141 var cur = Detection.current;
1142
1143 // current gesture isnt drag, but dragged is true
1144 // this means an other gesture is busy. now call dragend
1145 if(cur.name != this.name && this.triggered) {
1146 inst.trigger(this.name + 'end', ev);
1147 this.triggered = false;
1148 return;
1149 }
1150
1151 // max touches
1152 if(inst.options.drag_max_touches > 0 &&
1153 ev.touches.length > inst.options.drag_max_touches) {
1154 return;
1155 }
1156
1157 switch(ev.eventType) {
1158 case EVENT_START:
1159 this.triggered = false;
1160 break;
1161
1162 case EVENT_MOVE:
1163 // when the distance we moved is too small we skip this gesture
1164 // or we can be already in dragging
1165 if(ev.distance < inst.options.drag_min_distance &&
1166 cur.name != this.name) {
1167 return;
1168 }
1169
1170 var startCenter = cur.startEvent.center;
1171
1172 // we are dragging!
1173 if(cur.name != this.name) {
1174 cur.name = this.name;
1175 if(inst.options.correct_for_drag_min_distance && ev.distance > 0) {
1176 // When a drag is triggered, set the event center to drag_min_distance pixels from the original event center.
1177 // Without this correction, the dragged distance would jumpstart at drag_min_distance pixels instead of at 0.
1178 // It might be useful to save the original start point somewhere
1179 var factor = Math.abs(inst.options.drag_min_distance / ev.distance);
1180 startCenter.pageX += ev.deltaX * factor;
1181 startCenter.pageY += ev.deltaY * factor;
1182 startCenter.clientX += ev.deltaX * factor;
1183 startCenter.clientY += ev.deltaY * factor;
1184
1185 // recalculate event data using new start point
1186 ev = Detection.extendEventData(ev);
1187 }
1188 }
1189
1190 // lock drag to axis?
1191 if(cur.lastEvent.drag_locked_to_axis ||
1192 ( inst.options.drag_lock_to_axis &&
1193 inst.options.drag_lock_min_distance <= ev.distance
1194 )) {
1195 ev.drag_locked_to_axis = true;
1196 }
1197 var last_direction = cur.lastEvent.direction;
1198 if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
1199 // keep direction on the axis that the drag gesture started on
1200 if(Utils.isVertical(last_direction)) {
1201 ev.direction = (ev.deltaY < 0) ? DIRECTION_UP : DIRECTION_DOWN;
1202 }
1203 else {
1204 ev.direction = (ev.deltaX < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
1205 }
1206 }
1207
1208 // first time, trigger dragstart event
1209 if(!this.triggered) {
1210 inst.trigger(this.name + 'start', ev);
1211 this.triggered = true;
1212 }
1213
1214 // trigger events
1215 inst.trigger(this.name, ev);
1216 inst.trigger(this.name + ev.direction, ev);
1217
1218 var is_vertical = Utils.isVertical(ev.direction);
1219
1220 // block the browser events
1221 if((inst.options.drag_block_vertical && is_vertical) ||
1222 (inst.options.drag_block_horizontal && !is_vertical)) {
1223 ev.preventDefault();
1224 }
1225 break;
1226
1227 case EVENT_END:
1228 // trigger dragend
1229 if(this.triggered) {
1230 inst.trigger(this.name + 'end', ev);
1231 }
1232
1233 this.triggered = false;
1234 break;
1235 }
1236 }
1237 };
1238
1239 /**
1240 * Hold
1241 * Touch stays at the same place for x time
1242 * @events hold
1243 */
1244 Hammer.gestures.Hold = {
1245 name : 'hold',
1246 index : 10,
1247 defaults: {
1248 hold_timeout : 500,
1249 hold_threshold: 2
1250 },
1251 timer : null,
1252
1253 handler : function holdGesture(ev, inst) {
1254 switch(ev.eventType) {
1255 case EVENT_START:
1256 // clear any running timers
1257 clearTimeout(this.timer);
1258
1259 // set the gesture so we can check in the timeout if it still is
1260 Detection.current.name = this.name;
1261
1262 // set timer and if after the timeout it still is hold,
1263 // we trigger the hold event
1264 this.timer = setTimeout(function() {
1265 if(Detection.current.name == 'hold') {
1266 inst.trigger('hold', ev);
1267 }
1268 }, inst.options.hold_timeout);
1269 break;
1270
1271 // when you move or end we clear the timer
1272 case EVENT_MOVE:
1273 if(ev.distance > inst.options.hold_threshold) {
1274 clearTimeout(this.timer);
1275 }
1276 break;
1277
1278 case EVENT_END:
1279 clearTimeout(this.timer);
1280 break;
1281 }
1282 }
1283 };
1284
1285 /**
1286 * Release
1287 * Called as last, tells the user has released the screen
1288 * @events release
1289 */
1290 Hammer.gestures.Release = {
1291 name : 'release',
1292 index : Infinity,
1293 handler: function releaseGesture(ev, inst) {
1294 if(ev.eventType == EVENT_END) {
1295 inst.trigger(this.name, ev);
1296 }
1297 }
1298 };
1299
1300 /**
1301 * Swipe
1302 * triggers swipe events when the end velocity is above the threshold
1303 * for best usage, set prevent_default (on the drag gesture) to true
1304 * @events swipe, swipeleft, swiperight, swipeup, swipedown
1305 */
1306 Hammer.gestures.Swipe = {
1307 name : 'swipe',
1308 index : 40,
1309 defaults: {
1310 swipe_min_touches: 1,
1311 swipe_max_touches: 1,
1312 swipe_velocity : 0.7
1313 },
1314 handler : function swipeGesture(ev, inst) {
1315 if(ev.eventType == EVENT_END) {
1316 // max touches
1317 if(ev.touches.length < inst.options.swipe_min_touches ||
1318 ev.touches.length > inst.options.swipe_max_touches) {
1319 return;
1320 }
1321
1322 // when the distance we moved is too small we skip this gesture
1323 // or we can be already in dragging
1324 if(ev.velocityX > inst.options.swipe_velocity ||
1325 ev.velocityY > inst.options.swipe_velocity) {
1326 // trigger swipe events
1327 inst.trigger(this.name, ev);
1328 inst.trigger(this.name + ev.direction, ev);
1329 }
1330 }
1331 }
1332 };
1333
1334 /**
1335 * Tap/DoubleTap
1336 * Quick touch at a place or double at the same place
1337 * @events tap, doubletap
1338 */
1339 Hammer.gestures.Tap = {
1340 name : 'tap',
1341 index : 100,
1342 defaults: {
1343 tap_max_touchtime : 250,
1344 tap_max_distance : 10,
1345 tap_always : true,
1346 doubletap_distance: 20,
1347 doubletap_interval: 300
1348 },
1349
1350 has_moved: false,
1351
1352 handler : function tapGesture(ev, inst) {
1353 var prev, since_prev, did_doubletap;
1354
1355 // reset moved state
1356 if(ev.eventType == EVENT_START) {
1357 this.has_moved = false;
1358 }
1359
1360 // Track the distance we've moved. If it's above the max ONCE, remember that (fixes #406).
1361 else if(ev.eventType == EVENT_MOVE && !this.moved) {
1362 this.has_moved = (ev.distance > inst.options.tap_max_distance);
1363 }
1364
1365 else if(ev.eventType == EVENT_END &&
1366 ev.srcEvent.type != 'touchcancel' &&
1367 ev.deltaTime < inst.options.tap_max_touchtime && !this.has_moved) {
1368
1369 // previous gesture, for the double tap since these are two different gesture detections
1370 prev = Detection.previous;
1371 since_prev = prev && prev.lastEvent && ev.timeStamp - prev.lastEvent.timeStamp;
1372 did_doubletap = false;
1373
1374 // check if double tap
1375 if(prev && prev.name == 'tap' &&
1376 (since_prev && since_prev < inst.options.doubletap_interval) &&
1377 ev.distance < inst.options.doubletap_distance) {
1378 inst.trigger('doubletap', ev);
1379 did_doubletap = true;
1380 }
1381
1382 // do a single tap
1383 if(!did_doubletap || inst.options.tap_always) {
1384 Detection.current.name = 'tap';
1385 inst.trigger(Detection.current.name, ev);
1386 }
1387 }
1388 }
1389 };
1390
1391 /**
1392 * Touch
1393 * Called as first, tells the user has touched the screen
1394 * @events touch
1395 */
1396 Hammer.gestures.Touch = {
1397 name : 'touch',
1398 index : -Infinity,
1399 defaults: {
1400 // call preventDefault at touchstart, and makes the element blocking by
1401 // disabling the scrolling of the page, but it improves gestures like
1402 // transforming and dragging.
1403 // be careful with using this, it can be very annoying for users to be stuck
1404 // on the page
1405 prevent_default : false,
1406
1407 // disable mouse events, so only touch (or pen!) input triggers events
1408 prevent_mouseevents: false
1409 },
1410 handler : function touchGesture(ev, inst) {
1411 if(inst.options.prevent_mouseevents &&
1412 ev.pointerType == POINTER_MOUSE) {
1413 ev.stopDetect();
1414 return;
1415 }
1416
1417 if(inst.options.prevent_default) {
1418 ev.preventDefault();
1419 }
1420
1421 if(ev.eventType == EVENT_START) {
1422 inst.trigger(this.name, ev);
1423 }
1424 }
1425 };
1426
1427
1428 /**
1429 * Transform
1430 * User want to scale or rotate with 2 fingers
1431 * @events transform, pinch, pinchin, pinchout, rotate
1432 */
1433 Hammer.gestures.Transform = {
1434 name : 'transform',
1435 index : 45,
1436 defaults : {
1437 // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
1438 transform_min_scale : 0.01,
1439 // rotation in degrees
1440 transform_min_rotation : 1,
1441 // prevent default browser behavior when two touches are on the screen
1442 // but it makes the element a blocking element
1443 // when you are using the transform gesture, it is a good practice to set this true
1444 transform_always_block : false,
1445 // ensures that all touches occurred within the instance element
1446 transform_within_instance: false
1447 },
1448
1449 triggered: false,
1450
1451 handler : function transformGesture(ev, inst) {
1452 // current gesture isnt drag, but dragged is true
1453 // this means an other gesture is busy. now call dragend
1454 if(Detection.current.name != this.name && this.triggered) {
1455 inst.trigger(this.name + 'end', ev);
1456 this.triggered = false;
1457 return;
1458 }
1459
1460 // at least multitouch
1461 if(ev.touches.length < 2) {
1462 return;
1463 }
1464
1465 // prevent default when two fingers are on the screen
1466 if(inst.options.transform_always_block) {
1467 ev.preventDefault();
1468 }
1469
1470 // check if all touches occurred within the instance element
1471 if(inst.options.transform_within_instance) {
1472 for(var i=-1; ev.touches[++i];) {
1473 if(!Utils.hasParent(ev.touches[i].target, inst.element)) {
1474 return;
1475 }
1476 }
1477 }
1478
1479 switch(ev.eventType) {
1480 case EVENT_START:
1481 this.triggered = false;
1482 break;
1483
1484 case EVENT_MOVE:
1485 var scale_threshold = Math.abs(1 - ev.scale);
1486 var rotation_threshold = Math.abs(ev.rotation);
1487
1488 // when the distance we moved is too small we skip this gesture
1489 // or we can be already in dragging
1490 if(scale_threshold < inst.options.transform_min_scale &&
1491 rotation_threshold < inst.options.transform_min_rotation) {
1492 return;
1493 }
1494
1495 // we are transforming!
1496 Detection.current.name = this.name;
1497
1498 // first time, trigger dragstart event
1499 if(!this.triggered) {
1500 inst.trigger(this.name + 'start', ev);
1501 this.triggered = true;
1502 }
1503
1504 inst.trigger(this.name, ev); // basic transform event
1505
1506 // trigger rotate event
1507 if(rotation_threshold > inst.options.transform_min_rotation) {
1508 inst.trigger('rotate', ev);
1509 }
1510
1511 // trigger pinch event
1512 if(scale_threshold > inst.options.transform_min_scale) {
1513 inst.trigger('pinch', ev);
1514 inst.trigger('pinch' + (ev.scale<1 ? 'in' : 'out'), ev);
1515 }
1516 break;
1517
1518 case EVENT_END:
1519 // trigger dragend
1520 if(this.triggered) {
1521 inst.trigger(this.name + 'end', ev);
1522 }
1523
1524 this.triggered = false;
1525 break;
1526 }
1527 }
1528 };
1529
1530 // AMD export
1531 if(typeof define == 'function' && define.amd) {
1532 define(function(){
1533 return Hammer;
1534 });
1535 }
1536 // commonjs export
1537 else if(typeof module == 'object' && module.exports) {
1538 module.exports = Hammer;
1539 }
1540 // browser export
1541 else {
1542 window.Hammer = Hammer;
1543 }
1544
1545 })(window);