hammerjs
Version:
A javascript library for multi-touch gestures
1,545 lines (1,298 loc) • 43.5 kB
JavaScript
/*! Hammer.JS - v1.0.10 - 2014-03-28
* http://eightmedia.github.io/hammer.js
*
* Copyright (c) 2014 Jorik Tangelder <j.tangelder@gmail.com>;
* Licensed under the MIT license */
(function(window, undefined) {
'use strict';
/**
* Hammer
* use this to create instances
* @param {HTMLElement} element
* @param {Object} options
* @returns {Hammer.Instance}
* @constructor
*/
var Hammer = function(element, options) {
return new Hammer.Instance(element, options || {});
};
Hammer.VERSION = '1.0.10';
// default settings
Hammer.defaults = {
// add styles and attributes to the element to prevent the browser from doing
// its native behavior. this doesnt prevent the scrolling, but cancels
// the contextmenu, tap highlighting etc
// set to false to disable this
stop_browser_behavior: {
// this also triggers onselectstart=false for IE
userSelect : 'none',
// this makes the element blocking in IE10>, you could experiment with the value
// see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
touchAction : 'none',
touchCallout : 'none',
contentZooming : 'none',
userDrag : 'none',
tapHighlightColor: 'rgba(0,0,0,0)'
}
//
// more settings are defined per gesture at /gestures
//
};
// detect touchevents
Hammer.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled;
Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
// dont use mouseevents on mobile devices
Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i;
Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && window.navigator.userAgent.match(Hammer.MOBILE_REGEX);
// eventtypes per touchevent (start, move, end)
// are filled by Event.determineEventTypes on setup
Hammer.EVENT_TYPES = {};
// interval in which Hammer recalculates current velocity in ms
Hammer.UPDATE_VELOCITY_INTERVAL = 16;
// hammer document where the base events are added at
Hammer.DOCUMENT = window.document;
// define these also as vars, for better minification
// direction defines
var DIRECTION_DOWN = Hammer.DIRECTION_DOWN = 'down';
var DIRECTION_LEFT = Hammer.DIRECTION_LEFT = 'left';
var DIRECTION_UP = Hammer.DIRECTION_UP = 'up';
var DIRECTION_RIGHT = Hammer.DIRECTION_RIGHT = 'right';
// pointer type
var POINTER_MOUSE = Hammer.POINTER_MOUSE = 'mouse';
var POINTER_TOUCH = Hammer.POINTER_TOUCH = 'touch';
var POINTER_PEN = Hammer.POINTER_PEN = 'pen';
// touch event defines
var EVENT_START = Hammer.EVENT_START = 'start';
var EVENT_MOVE = Hammer.EVENT_MOVE = 'move';
var EVENT_END = Hammer.EVENT_END = 'end';
// plugins and gestures namespaces
Hammer.plugins = Hammer.plugins || {};
Hammer.gestures = Hammer.gestures || {};
// if the window events are set...
Hammer.READY = false;
/**
* setup events to detect gestures on the document
*/
function setup() {
if(Hammer.READY) {
return;
}
// find what eventtypes we add listeners to
Event.determineEventTypes();
// Register all gestures inside Hammer.gestures
Utils.each(Hammer.gestures, function(gesture){
Detection.register(gesture);
});
// Add touch events on the document
Event.onTouch(Hammer.DOCUMENT, EVENT_MOVE, Detection.detect);
Event.onTouch(Hammer.DOCUMENT, EVENT_END, Detection.detect);
// Hammer is ready...!
Hammer.READY = true;
}
var Utils = Hammer.utils = {
/**
* extend method,
* also used for cloning when dest is an empty object
* @param {Object} dest
* @param {Object} src
* @parm {Boolean} merge do a merge
* @returns {Object} dest
*/
extend: function extend(dest, src, merge) {
for(var key in src) {
if(dest[key] !== undefined && merge) {
continue;
}
dest[key] = src[key];
}
return dest;
},
/**
* for each
* @param obj
* @param iterator
*/
each: function each(obj, iterator, context) {
var i, o;
// native forEach on arrays
if ('forEach' in obj) {
obj.forEach(iterator, context);
}
// arrays
else if(obj.length !== undefined) {
for(i=-1; (o=obj[++i]);) {
if (iterator.call(context, o, i, obj) === false) {
return;
}
}
}
// objects
else {
for(i in obj) {
if(obj.hasOwnProperty(i) &&
iterator.call(context, obj[i], i, obj) === false) {
return;
}
}
}
},
/**
* find if a string contains the needle
* @param {String} src
* @param {String} needle
* @returns {Boolean} found
*/
inStr: function inStr(src, needle) {
return src.indexOf(needle) > -1;
},
/**
* find if a node is in the given parent
* used for event delegation tricks
* @param {HTMLElement} node
* @param {HTMLElement} parent
* @returns {boolean} has_parent
*/
hasParent: function hasParent(node, parent) {
while(node) {
if(node == parent) {
return true;
}
node = node.parentNode;
}
return false;
},
/**
* get the center of all the touches
* @param {Array} touches
* @returns {Object} center pageXY clientXY
*/
getCenter: function getCenter(touches) {
var pageX = []
, pageY = []
, clientX = []
, clientY = []
, min = Math.min
, max = Math.max;
// no need to loop when only one touch
if(touches.length === 1) {
return {
pageX: touches[0].pageX,
pageY: touches[0].pageY,
clientX: touches[0].clientX,
clientY: touches[0].clientY
};
}
Utils.each(touches, function(touch) {
pageX.push(touch.pageX);
pageY.push(touch.pageY);
clientX.push(touch.clientX);
clientY.push(touch.clientY);
});
return {
pageX: (min.apply(Math, pageX) + max.apply(Math, pageX)) / 2,
pageY: (min.apply(Math, pageY) + max.apply(Math, pageY)) / 2,
clientX: (min.apply(Math, clientX) + max.apply(Math, clientX)) / 2,
clientY: (min.apply(Math, clientY) + max.apply(Math, clientY)) / 2
};
},
/**
* calculate the velocity between two points
* @param {Number} delta_time
* @param {Number} delta_x
* @param {Number} delta_y
* @returns {Object} velocity
*/
getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
return {
x: Math.abs(delta_x / delta_time) || 0,
y: Math.abs(delta_y / delta_time) || 0
};
},
/**
* calculate the angle between two coordinates
* @param {Touch} touch1
* @param {Touch} touch2
* @returns {Number} angle
*/
getAngle: function getAngle(touch1, touch2) {
var x = touch2.clientX - touch1.clientX
, y = touch2.clientY - touch1.clientY;
return Math.atan2(y, x) * 180 / Math.PI;
},
/**
* angle to direction define
* @param {Touch} touch1
* @param {Touch} touch2
* @returns {String} direction constant, like DIRECTION_LEFT
*/
getDirection: function getDirection(touch1, touch2) {
var x = Math.abs(touch1.clientX - touch2.clientX)
, y = Math.abs(touch1.clientY - touch2.clientY);
if(x >= y) {
return touch1.clientX - touch2.clientX > 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
}
return touch1.clientY - touch2.clientY > 0 ? DIRECTION_UP : DIRECTION_DOWN;
},
/**
* calculate the distance between two touches
* @param {Touch} touch1
* @param {Touch} touch2
* @returns {Number} distance
*/
getDistance: function getDistance(touch1, touch2) {
var x = touch2.clientX - touch1.clientX
, y = touch2.clientY - touch1.clientY;
return Math.sqrt((x * x) + (y * y));
},
/**
* calculate the scale factor between two touchLists (fingers)
* no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
* @param {Array} start
* @param {Array} end
* @returns {Number} scale
*/
getScale: function getScale(start, end) {
// need two fingers...
if(start.length >= 2 && end.length >= 2) {
return this.getDistance(end[0], end[1]) / this.getDistance(start[0], start[1]);
}
return 1;
},
/**
* calculate the rotation degrees between two touchLists (fingers)
* @param {Array} start
* @param {Array} end
* @returns {Number} rotation
*/
getRotation: function getRotation(start, end) {
// need two fingers
if(start.length >= 2 && end.length >= 2) {
return this.getAngle(end[1], end[0]) - this.getAngle(start[1], start[0]);
}
return 0;
},
/**
* boolean if the direction is vertical
* @param {String} direction
* @returns {Boolean} is_vertical
*/
isVertical: function isVertical(direction) {
return direction == DIRECTION_UP || direction == DIRECTION_DOWN;
},
/**
* toggle browser default behavior with css props
* @param {HtmlElement} element
* @param {Object} css_props
* @param {Boolean} toggle
*/
toggleDefaultBehavior: function toggleDefaultBehavior(element, css_props, toggle) {
if(!css_props || !element || !element.style) {
return;
}
// with css properties for modern browsers
Utils.each(['webkit', 'moz', 'Moz', 'ms', 'o', ''], function setStyle(vendor) {
Utils.each(css_props, function(value, prop) {
// vender prefix at the property
if(vendor) {
prop = vendor + prop.substring(0, 1).toUpperCase() + prop.substring(1);
}
// set the style
if(prop in element.style) {
element.style[prop] = !toggle && value;
}
});
});
var false_fn = function(){ return false; };
// also the disable onselectstart
if(css_props.userSelect == 'none') {
element.onselectstart = !toggle && false_fn;
}
// and disable ondragstart
if(css_props.userDrag == 'none') {
element.ondragstart = !toggle && false_fn;
}
}
};
/**
* create new hammer instance
* all methods should return the instance itself, so it is chainable.
* @param {HTMLElement} element
* @param {Object} [options={}]
* @returns {Hammer.Instance}
* @constructor
*/
Hammer.Instance = function(element, options) {
var self = this;
// setup HammerJS window events and register all gestures
// this also sets up the default options
setup();
this.element = element;
// start/stop detection option
this.enabled = true;
// merge options
this.options = Utils.extend(
Utils.extend({}, Hammer.defaults),
options || {});
// add some css to the element to prevent the browser from doing its native behavoir
if(this.options.stop_browser_behavior) {
Utils.toggleDefaultBehavior(this.element, this.options.stop_browser_behavior, false);
}
// start detection on touchstart
this.eventStartHandler = Event.onTouch(element, EVENT_START, function(ev) {
if(self.enabled) {
Detection.startDetect(self, ev);
}
});
// keep a list of user event handlers which needs to be removed when calling 'dispose'
this.eventHandlers = [];
// return instance
return this;
};
Hammer.Instance.prototype = {
/**
* bind events to the instance
* @param {String} gesture
* @param {Function} handler
* @returns {Hammer.Instance}
*/
on: function onEvent(gesture, handler) {
var gestures = gesture.split(' ');
Utils.each(gestures, function(gesture) {
this.element.addEventListener(gesture, handler, false);
this.eventHandlers.push({ gesture: gesture, handler: handler });
}, this);
return this;
},
/**
* unbind events to the instance
* @param {String} gesture
* @param {Function} handler
* @returns {Hammer.Instance}
*/
off: function offEvent(gesture, handler) {
var gestures = gesture.split(' ')
, i, eh;
Utils.each(gestures, function(gesture) {
this.element.removeEventListener(gesture, handler, false);
// remove the event handler from the internal list
for(i=-1; (eh=this.eventHandlers[++i]);) {
if(eh.gesture === gesture && eh.handler === handler) {
this.eventHandlers.splice(i, 1);
}
}
}, this);
return this;
},
/**
* trigger gesture event
* @param {String} gesture
* @param {Object} [eventData]
* @returns {Hammer.Instance}
*/
trigger: function triggerEvent(gesture, eventData) {
// optional
if(!eventData) {
eventData = {};
}
// create DOM event
var event = Hammer.DOCUMENT.createEvent('Event');
event.initEvent(gesture, true, true);
event.gesture = eventData;
// trigger on the target if it is in the instance element,
// this is for event delegation tricks
var element = this.element;
if(Utils.hasParent(eventData.target, element)) {
element = eventData.target;
}
element.dispatchEvent(event);
return this;
},
/**
* enable of disable hammer.js detection
* @param {Boolean} state
* @returns {Hammer.Instance}
*/
enable: function enable(state) {
this.enabled = state;
return this;
},
/**
* dispose this hammer instance
* @returns {Hammer.Instance}
*/
dispose: function dispose() {
var i, eh;
// undo all changes made by stop_browser_behavior
if(this.options.stop_browser_behavior) {
Utils.toggleDefaultBehavior(this.element, this.options.stop_browser_behavior, true);
}
// unbind all custom event handlers
for(i=-1; (eh=this.eventHandlers[++i]);) {
this.element.removeEventListener(eh.gesture, eh.handler, false);
}
this.eventHandlers = [];
// unbind the start event listener
Event.unbindDom(this.element, Hammer.EVENT_TYPES[EVENT_START], this.eventStartHandler);
return null;
}
};
/**
* this holds the last move event,
* used to fix empty touchend issue
* see the onTouch event for an explanation
* @type {Object}
*/
var last_move_event = null;
/**
* when the mouse is hold down, this is true
* @type {Boolean}
*/
var should_detect = false;
/**
* when touch events have been fired, this is true
* @type {Boolean}
*/
var touch_triggered = false;
var Event = Hammer.event = {
/**
* simple addEventListener
* @param {HTMLElement} element
* @param {String} type
* @param {Function} handler
*/
bindDom: function(element, type, handler) {
var types = type.split(' ');
Utils.each(types, function(type){
element.addEventListener(type, handler, false);
});
},
/**
* simple removeEventListener
* @param {HTMLElement} element
* @param {String} type
* @param {Function} handler
*/
unbindDom: function(element, type, handler) {
var types = type.split(' ');
Utils.each(types, function(type){
element.removeEventListener(type, handler, false);
});
},
/**
* touch events with mouse fallback
* @param {HTMLElement} element
* @param {String} eventType like EVENT_MOVE
* @param {Function} handler
*/
onTouch: function onTouch(element, eventType, handler) {
var self = this;
var bindDomOnTouch = function bindDomOnTouch(ev) {
var srcEventType = ev.type.toLowerCase();
// onmouseup, but when touchend has been fired we do nothing.
// this is for touchdevices which also fire a mouseup on touchend
if(Utils.inStr(srcEventType, 'mouse') && touch_triggered) {
return;
}
// mousebutton must be down or a touch event
else if(Utils.inStr(srcEventType, 'touch') || // touch events are always on screen
Utils.inStr(srcEventType, 'pointerdown') || // pointerevents touch
(Utils.inStr(srcEventType, 'mouse') && ev.which === 1) // mouse is pressed
) {
should_detect = true;
}
// mouse isn't pressed
else if(Utils.inStr(srcEventType, 'mouse') && !ev.which) {
should_detect = false;
}
// we are in a touch event, set the touch triggered bool to true,
// this for the conflicts that may occur on ios and android
if(Utils.inStr(srcEventType, 'touch') || Utils.inStr(srcEventType, 'pointer')) {
touch_triggered = true;
}
// count the total touches on the screen
var count_touches = 0;
// when touch has been triggered in this detection session
// and we are now handling a mouse event, we stop that to prevent conflicts
if(should_detect) {
// update pointerevent
if(Hammer.HAS_POINTEREVENTS && eventType != EVENT_END) {
count_touches = PointerEvent.updatePointer(eventType, ev);
}
// touch
else if(Utils.inStr(srcEventType, 'touch')) {
count_touches = ev.touches.length;
}
// mouse
else if(!touch_triggered) {
count_touches = Utils.inStr(srcEventType, 'up') ? 0 : 1;
}
// if we are in a end event, but when we remove one touch and
// we still have enough, set eventType to move
if(count_touches > 0 && eventType == EVENT_END) {
eventType = EVENT_MOVE;
}
// no touches, force the end event
else if(!count_touches) {
eventType = EVENT_END;
}
// store the last move event
if(count_touches || last_move_event === null) {
last_move_event = ev;
}
// trigger the handler
handler.call(Detection, self.collectEventData(element, eventType,
self.getTouchList(last_move_event, eventType),
ev) );
// remove pointerevent from list
if(Hammer.HAS_POINTEREVENTS && eventType == EVENT_END) {
count_touches = PointerEvent.updatePointer(eventType, ev);
}
}
// on the end we reset everything
if(!count_touches) {
last_move_event = null;
should_detect = false;
touch_triggered = false;
PointerEvent.reset();
}
};
this.bindDom(element, Hammer.EVENT_TYPES[eventType], bindDomOnTouch);
// return the bound function to be able to unbind it later
return bindDomOnTouch;
},
/**
* we have different events for each device/browser
* determine what we need and set them in the Hammer.EVENT_TYPES constant
*/
determineEventTypes: function determineEventTypes() {
// determine the eventtype we want to set
var types;
// pointerEvents magic
if(Hammer.HAS_POINTEREVENTS) {
types = PointerEvent.getEvents();
}
// on Android, iOS, blackberry, windows mobile we dont want any mouseevents
else if(Hammer.NO_MOUSEEVENTS) {
types = [
'touchstart',
'touchmove',
'touchend touchcancel'];
}
// for non pointer events browsers and mixed browsers,
// like chrome on windows8 touch laptop
else {
types = [
'touchstart mousedown',
'touchmove mousemove',
'touchend touchcancel mouseup'];
}
Hammer.EVENT_TYPES[EVENT_START] = types[0];
Hammer.EVENT_TYPES[EVENT_MOVE] = types[1];
Hammer.EVENT_TYPES[EVENT_END] = types[2];
},
/**
* create touchlist depending on the event
* @param {Object} ev
* @param {String} eventType used by the fakemultitouch plugin
*/
getTouchList: function getTouchList(ev/*, eventType*/) {
// get the fake pointerEvent touchlist
if(Hammer.HAS_POINTEREVENTS) {
return PointerEvent.getTouchList();
}
// get the touchlist
if(ev.touches) {
return ev.touches;
}
// make fake touchlist from mouse position
ev.identifier = 1;
return [ev];
},
/**
* collect event data for Hammer js
* @param {HTMLElement} element
* @param {String} eventType like EVENT_MOVE
* @param {Object} eventData
*/
collectEventData: function collectEventData(element, eventType, touches, ev) {
// find out pointerType
var pointerType = POINTER_TOUCH;
if(Utils.inStr(ev.type, 'mouse') || PointerEvent.matchType(POINTER_MOUSE, ev)) {
pointerType = POINTER_MOUSE;
}
return {
center : Utils.getCenter(touches),
timeStamp : Date.now(),
target : ev.target,
touches : touches,
eventType : eventType,
pointerType: pointerType,
srcEvent : ev,
/**
* prevent the browser default actions
* mostly used to disable scrolling of the browser
*/
preventDefault: function() {
var srcEvent = this.srcEvent;
srcEvent.preventManipulation && srcEvent.preventManipulation();
srcEvent.preventDefault && srcEvent.preventDefault();
},
/**
* stop bubbling the event up to its parents
*/
stopPropagation: function() {
this.srcEvent.stopPropagation();
},
/**
* immediately stop gesture detection
* might be useful after a swipe was detected
* @return {*}
*/
stopDetect: function() {
return Detection.stopDetect();
}
};
}
};
var PointerEvent = Hammer.PointerEvent = {
/**
* holds all pointers
* @type {Object}
*/
pointers: {},
/**
* get a list of pointers
* @returns {Array} touchlist
*/
getTouchList: function getTouchList() {
var touchlist = [];
// we can use forEach since pointerEvents only is in IE10
Utils.each(this.pointers, function(pointer){
touchlist.push(pointer);
});
return touchlist;
},
/**
* update the position of a pointer
* @param {String} type EVENT_END
* @param {Object} pointerEvent
*/
updatePointer: function updatePointer(type, pointerEvent) {
if(type == EVENT_END) {
delete this.pointers[pointerEvent.pointerId];
}
else {
pointerEvent.identifier = pointerEvent.pointerId;
this.pointers[pointerEvent.pointerId] = pointerEvent;
}
// it's save to use Object.keys, since pointerEvents are only in newer browsers
return Object.keys(this.pointers).length;
},
/**
* check if ev matches pointertype
* @param {String} pointerType POINTER_MOUSE
* @param {PointerEvent} ev
*/
matchType: function matchType(pointerType, ev) {
if(!ev.pointerType) {
return false;
}
var pt = ev.pointerType
, types = {};
types[POINTER_MOUSE] = (pt === POINTER_MOUSE);
types[POINTER_TOUCH] = (pt === POINTER_TOUCH);
types[POINTER_PEN] = (pt === POINTER_PEN);
return types[pointerType];
},
/**
* get events
*/
getEvents: function getEvents() {
return [
'pointerdown MSPointerDown',
'pointermove MSPointerMove',
'pointerup pointercancel MSPointerUp MSPointerCancel'
];
},
/**
* reset the list
*/
reset: function resetList() {
this.pointers = {};
}
};
var Detection = Hammer.detection = {
// contains all registred Hammer.gestures in the correct order
gestures: [],
// data of the current Hammer.gesture detection session
current : null,
// the previous Hammer.gesture session data
// is a full clone of the previous gesture.current object
previous: null,
// when this becomes true, no gestures are fired
stopped : false,
/**
* start Hammer.gesture detection
* @param {Hammer.Instance} inst
* @param {Object} eventData
*/
startDetect: function startDetect(inst, eventData) {
// already busy with a Hammer.gesture detection on an element
if(this.current) {
return;
}
this.stopped = false;
// holds current session
this.current = {
inst : inst, // reference to HammerInstance we're working for
startEvent : Utils.extend({}, eventData), // start eventData for distances, timing etc
lastEvent : false, // last eventData
lastVelocityEvent : false, // last eventData for velocity.
velocity : false, // current velocity
name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
};
this.detect(eventData);
},
/**
* Hammer.gesture detection
* @param {Object} eventData
*/
detect: function detect(eventData) {
if(!this.current || this.stopped) {
return;
}
// extend event data with calculations about scale, distance etc
eventData = this.extendEventData(eventData);
// hammer instance and instance options
var inst = this.current.inst,
inst_options = inst.options;
// call Hammer.gesture handlers
Utils.each(this.gestures, function triggerGesture(gesture) {
// only when the instance options have enabled this gesture
if(!this.stopped && inst_options[gesture.name] !== false && inst.enabled !== false ) {
// if a handler returns false, we stop with the detection
if(gesture.handler.call(gesture, eventData, inst) === false) {
this.stopDetect();
return false;
}
}
}, this);
// store as previous event event
if(this.current) {
this.current.lastEvent = eventData;
}
// end event, but not the last touch, so dont stop
if(eventData.eventType == EVENT_END && !eventData.touches.length - 1) {
this.stopDetect();
}
return eventData;
},
/**
* clear the Hammer.gesture vars
* this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
* to stop other Hammer.gestures from being fired
*/
stopDetect: function stopDetect() {
// clone current data to the store as the previous gesture
// used for the double tap gesture, since this is an other gesture detect session
this.previous = Utils.extend({}, this.current);
// reset the current
this.current = null;
// stopped!
this.stopped = true;
},
/**
* calculate velocity
* @param {Object} ev
* @param {Number} delta_time
* @param {Number} delta_x
* @param {Number} delta_y
*/
getVelocityData: function getVelocityData(ev, delta_time, delta_x, delta_y) {
var cur = this.current
, velocityEv = cur.lastVelocityEvent
, velocity = cur.velocity;
// calculate velocity every x ms
if (velocityEv && ev.timeStamp - velocityEv.timeStamp > Hammer.UPDATE_VELOCITY_INTERVAL) {
velocity = Utils.getVelocity(ev.timeStamp - velocityEv.timeStamp,
ev.center.clientX - velocityEv.center.clientX,
ev.center.clientY - velocityEv.center.clientY);
cur.lastVelocityEvent = ev;
}
else if(!cur.velocity) {
velocity = Utils.getVelocity(delta_time, delta_x, delta_y);
cur.lastVelocityEvent = ev;
}
cur.velocity = velocity;
ev.velocityX = velocity.x;
ev.velocityY = velocity.y;
},
/**
* calculate interim angle and direction
* @param {Object} ev
*/
getInterimData: function getInterimData(ev) {
var lastEvent = this.current.lastEvent
, angle
, direction;
// end events (e.g. dragend) don't have useful values for interimDirection & interimAngle
// because the previous event has exactly the same coordinates
// so for end events, take the previous values of interimDirection & interimAngle
// instead of recalculating them and getting a spurious '0'
if(ev.eventType == EVENT_END) {
angle = lastEvent && lastEvent.interimAngle;
direction = lastEvent && lastEvent.interimDirection;
}
else {
angle = lastEvent && Utils.getAngle(lastEvent.center, ev.center);
direction = lastEvent && Utils.getDirection(lastEvent.center, ev.center);
}
ev.interimAngle = angle;
ev.interimDirection = direction;
},
/**
* extend eventData for Hammer.gestures
* @param {Object} evData
* @returns {Object} evData
*/
extendEventData: function extendEventData(ev) {
var cur = this.current
, startEv = cur.startEvent;
// if the touches change, set the new touches over the startEvent touches
// this because touchevents don't have all the touches on touchstart, or the
// user must place his fingers at the EXACT same time on the screen, which is not realistic
// but, sometimes it happens that both fingers are touching at the EXACT same time
if(ev.touches.length != startEv.touches.length || ev.touches === startEv.touches) {
// extend 1 level deep to get the touchlist with the touch objects
startEv.touches = [];
Utils.each(ev.touches, function(touch) {
startEv.touches.push(Utils.extend({}, touch));
});
}
var delta_time = ev.timeStamp - startEv.timeStamp
, delta_x = ev.center.clientX - startEv.center.clientX
, delta_y = ev.center.clientY - startEv.center.clientY;
this.getVelocityData(ev, delta_time, delta_x, delta_y);
this.getInterimData(ev);
Utils.extend(ev, {
startEvent: startEv,
deltaTime : delta_time,
deltaX : delta_x,
deltaY : delta_y,
distance : Utils.getDistance(startEv.center, ev.center),
angle : Utils.getAngle(startEv.center, ev.center),
direction : Utils.getDirection(startEv.center, ev.center),
scale : Utils.getScale(startEv.touches, ev.touches),
rotation : Utils.getRotation(startEv.touches, ev.touches)
});
return ev;
},
/**
* register new gesture
* @param {Object} gesture object, see gestures.js for documentation
* @returns {Array} gestures
*/
register: function register(gesture) {
// add an enable gesture options if there is no given
var options = gesture.defaults || {};
if(options[gesture.name] === undefined) {
options[gesture.name] = true;
}
// extend Hammer default options with the Hammer.gesture options
Utils.extend(Hammer.defaults, options, true);
// set its index
gesture.index = gesture.index || 1000;
// add Hammer.gesture to the list
this.gestures.push(gesture);
// sort the list by index
this.gestures.sort(function(a, b) {
if(a.index < b.index) { return -1; }
if(a.index > b.index) { return 1; }
return 0;
});
return this.gestures;
}
};
/**
* Drag
* Move with x fingers (default 1) around on the page. Blocking the scrolling when
* moving left and right is a good practice. When all the drag events are blocking
* you disable scrolling on that area.
* @events drag, drapleft, dragright, dragup, dragdown
*/
Hammer.gestures.Drag = {
name : 'drag',
index : 50,
defaults : {
drag_min_distance : 10,
// Set correct_for_drag_min_distance to true to make the starting point of the drag
// be calculated from where the drag was triggered, not from where the touch started.
// Useful to avoid a jerk-starting drag, which can make fine-adjustments
// through dragging difficult, and be visually unappealing.
correct_for_drag_min_distance: true,
// set 0 for unlimited, but this can conflict with transform
drag_max_touches : 1,
// prevent default browser behavior when dragging occurs
// be careful with it, it makes the element a blocking element
// when you are using the drag gesture, it is a good practice to set this true
drag_block_horizontal : false,
drag_block_vertical : false,
// drag_lock_to_axis keeps the drag gesture on the axis that it started on,
// It disallows vertical directions if the initial direction was horizontal, and vice versa.
drag_lock_to_axis : false,
// drag lock only kicks in when distance > drag_lock_min_distance
// This way, locking occurs only when the distance has become large enough to reliably determine the direction
drag_lock_min_distance : 25
},
triggered: false,
handler : function dragGesture(ev, inst) {
var cur = Detection.current;
// current gesture isnt drag, but dragged is true
// this means an other gesture is busy. now call dragend
if(cur.name != this.name && this.triggered) {
inst.trigger(this.name + 'end', ev);
this.triggered = false;
return;
}
// max touches
if(inst.options.drag_max_touches > 0 &&
ev.touches.length > inst.options.drag_max_touches) {
return;
}
switch(ev.eventType) {
case EVENT_START:
this.triggered = false;
break;
case EVENT_MOVE:
// when the distance we moved is too small we skip this gesture
// or we can be already in dragging
if(ev.distance < inst.options.drag_min_distance &&
cur.name != this.name) {
return;
}
var startCenter = cur.startEvent.center;
// we are dragging!
if(cur.name != this.name) {
cur.name = this.name;
if(inst.options.correct_for_drag_min_distance && ev.distance > 0) {
// When a drag is triggered, set the event center to drag_min_distance pixels from the original event center.
// Without this correction, the dragged distance would jumpstart at drag_min_distance pixels instead of at 0.
// It might be useful to save the original start point somewhere
var factor = Math.abs(inst.options.drag_min_distance / ev.distance);
startCenter.pageX += ev.deltaX * factor;
startCenter.pageY += ev.deltaY * factor;
startCenter.clientX += ev.deltaX * factor;
startCenter.clientY += ev.deltaY * factor;
// recalculate event data using new start point
ev = Detection.extendEventData(ev);
}
}
// lock drag to axis?
if(cur.lastEvent.drag_locked_to_axis ||
( inst.options.drag_lock_to_axis &&
inst.options.drag_lock_min_distance <= ev.distance
)) {
ev.drag_locked_to_axis = true;
}
var last_direction = cur.lastEvent.direction;
if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
// keep direction on the axis that the drag gesture started on
if(Utils.isVertical(last_direction)) {
ev.direction = (ev.deltaY < 0) ? DIRECTION_UP : DIRECTION_DOWN;
}
else {
ev.direction = (ev.deltaX < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
}
}
// first time, trigger dragstart event
if(!this.triggered) {
inst.trigger(this.name + 'start', ev);
this.triggered = true;
}
// trigger events
inst.trigger(this.name, ev);
inst.trigger(this.name + ev.direction, ev);
var is_vertical = Utils.isVertical(ev.direction);
// block the browser events
if((inst.options.drag_block_vertical && is_vertical) ||
(inst.options.drag_block_horizontal && !is_vertical)) {
ev.preventDefault();
}
break;
case EVENT_END:
// trigger dragend
if(this.triggered) {
inst.trigger(this.name + 'end', ev);
}
this.triggered = false;
break;
}
}
};
/**
* Hold
* Touch stays at the same place for x time
* @events hold
*/
Hammer.gestures.Hold = {
name : 'hold',
index : 10,
defaults: {
hold_timeout : 500,
hold_threshold: 2
},
timer : null,
handler : function holdGesture(ev, inst) {
switch(ev.eventType) {
case EVENT_START:
// clear any running timers
clearTimeout(this.timer);
// set the gesture so we can check in the timeout if it still is
Detection.current.name = this.name;
// set timer and if after the timeout it still is hold,
// we trigger the hold event
this.timer = setTimeout(function() {
if(Detection.current.name == 'hold') {
inst.trigger('hold', ev);
}
}, inst.options.hold_timeout);
break;
// when you move or end we clear the timer
case EVENT_MOVE:
if(ev.distance > inst.options.hold_threshold) {
clearTimeout(this.timer);
}
break;
case EVENT_END:
clearTimeout(this.timer);
break;
}
}
};
/**
* Release
* Called as last, tells the user has released the screen
* @events release
*/
Hammer.gestures.Release = {
name : 'release',
index : Infinity,
handler: function releaseGesture(ev, inst) {
if(ev.eventType == EVENT_END) {
inst.trigger(this.name, ev);
}
}
};
/**
* Swipe
* triggers swipe events when the end velocity is above the threshold
* for best usage, set prevent_default (on the drag gesture) to true
* @events swipe, swipeleft, swiperight, swipeup, swipedown
*/
Hammer.gestures.Swipe = {
name : 'swipe',
index : 40,
defaults: {
swipe_min_touches: 1,
swipe_max_touches: 1,
swipe_velocity : 0.7
},
handler : function swipeGesture(ev, inst) {
if(ev.eventType == EVENT_END) {
// max touches
if(ev.touches.length < inst.options.swipe_min_touches ||
ev.touches.length > inst.options.swipe_max_touches) {
return;
}
// when the distance we moved is too small we skip this gesture
// or we can be already in dragging
if(ev.velocityX > inst.options.swipe_velocity ||
ev.velocityY > inst.options.swipe_velocity) {
// trigger swipe events
inst.trigger(this.name, ev);
inst.trigger(this.name + ev.direction, ev);
}
}
}
};
/**
* Tap/DoubleTap
* Quick touch at a place or double at the same place
* @events tap, doubletap
*/
Hammer.gestures.Tap = {
name : 'tap',
index : 100,
defaults: {
tap_max_touchtime : 250,
tap_max_distance : 10,
tap_always : true,
doubletap_distance: 20,
doubletap_interval: 300
},
has_moved: false,
handler : function tapGesture(ev, inst) {
var prev, since_prev, did_doubletap;
// reset moved state
if(ev.eventType == EVENT_START) {
this.has_moved = false;
}
// Track the distance we've moved. If it's above the max ONCE, remember that (fixes #406).
else if(ev.eventType == EVENT_MOVE && !this.moved) {
this.has_moved = (ev.distance > inst.options.tap_max_distance);
}
else if(ev.eventType == EVENT_END &&
ev.srcEvent.type != 'touchcancel' &&
ev.deltaTime < inst.options.tap_max_touchtime && !this.has_moved) {
// previous gesture, for the double tap since these are two different gesture detections
prev = Detection.previous;
since_prev = prev && prev.lastEvent && ev.timeStamp - prev.lastEvent.timeStamp;
did_doubletap = false;
// check if double tap
if(prev && prev.name == 'tap' &&
(since_prev && since_prev < inst.options.doubletap_interval) &&
ev.distance < inst.options.doubletap_distance) {
inst.trigger('doubletap', ev);
did_doubletap = true;
}
// do a single tap
if(!did_doubletap || inst.options.tap_always) {
Detection.current.name = 'tap';
inst.trigger(Detection.current.name, ev);
}
}
}
};
/**
* Touch
* Called as first, tells the user has touched the screen
* @events touch
*/
Hammer.gestures.Touch = {
name : 'touch',
index : -Infinity,
defaults: {
// call preventDefault at touchstart, and makes the element blocking by
// disabling the scrolling of the page, but it improves gestures like
// transforming and dragging.
// be careful with using this, it can be very annoying for users to be stuck
// on the page
prevent_default : false,
// disable mouse events, so only touch (or pen!) input triggers events
prevent_mouseevents: false
},
handler : function touchGesture(ev, inst) {
if(inst.options.prevent_mouseevents &&
ev.pointerType == POINTER_MOUSE) {
ev.stopDetect();
return;
}
if(inst.options.prevent_default) {
ev.preventDefault();
}
if(ev.eventType == EVENT_START) {
inst.trigger(this.name, ev);
}
}
};
/**
* Transform
* User want to scale or rotate with 2 fingers
* @events transform, pinch, pinchin, pinchout, rotate
*/
Hammer.gestures.Transform = {
name : 'transform',
index : 45,
defaults : {
// factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
transform_min_scale : 0.01,
// rotation in degrees
transform_min_rotation : 1,
// prevent default browser behavior when two touches are on the screen
// but it makes the element a blocking element
// when you are using the transform gesture, it is a good practice to set this true
transform_always_block : false,
// ensures that all touches occurred within the instance element
transform_within_instance: false
},
triggered: false,
handler : function transformGesture(ev, inst) {
// current gesture isnt drag, but dragged is true
// this means an other gesture is busy. now call dragend
if(Detection.current.name != this.name && this.triggered) {
inst.trigger(this.name + 'end', ev);
this.triggered = false;
return;
}
// at least multitouch
if(ev.touches.length < 2) {
return;
}
// prevent default when two fingers are on the screen
if(inst.options.transform_always_block) {
ev.preventDefault();
}
// check if all touches occurred within the instance element
if(inst.options.transform_within_instance) {
for(var i=-1; ev.touches[++i];) {
if(!Utils.hasParent(ev.touches[i].target, inst.element)) {
return;
}
}
}
switch(ev.eventType) {
case EVENT_START:
this.triggered = false;
break;
case EVENT_MOVE:
var scale_threshold = Math.abs(1 - ev.scale);
var rotation_threshold = Math.abs(ev.rotation);
// when the distance we moved is too small we skip this gesture
// or we can be already in dragging
if(scale_threshold < inst.options.transform_min_scale &&
rotation_threshold < inst.options.transform_min_rotation) {
return;
}
// we are transforming!
Detection.current.name = this.name;
// first time, trigger dragstart event
if(!this.triggered) {
inst.trigger(this.name + 'start', ev);
this.triggered = true;
}
inst.trigger(this.name, ev); // basic transform event
// trigger rotate event
if(rotation_threshold > inst.options.transform_min_rotation) {
inst.trigger('rotate', ev);
}
// trigger pinch event
if(scale_threshold > inst.options.transform_min_scale) {
inst.trigger('pinch', ev);
inst.trigger('pinch' + (ev.scale<1 ? 'in' : 'out'), ev);
}
break;
case EVENT_END:
// trigger dragend
if(this.triggered) {
inst.trigger(this.name + 'end', ev);
}
this.triggered = false;
break;
}
}
};
// AMD export
if(typeof define == 'function' && define.amd) {
define(function(){
return Hammer;
});
}
// commonjs export
else if(typeof module == 'object' && module.exports) {
module.exports = Hammer;
}
// browser export
else {
window.Hammer = Hammer;
}
})(window);