Created
November 5, 2012 01:43
-
-
Save lazd/4014795 to your computer and use it in GitHub Desktop.
hammer.js pull/89 fixed
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Hammer.JS | |
* version 0.6.3 | |
* author: Eight Media | |
* https://github.com/EightMedia/hammer.js | |
* Licensed under the MIT license. | |
*/ | |
function Hammer(element, options, undefined) | |
{ | |
var self = this; | |
var defaults = { | |
// prevent the default event or not... might be buggy when false | |
prevent_default : false, | |
css_hacks : true, | |
swipe : true, | |
swipe_time : 200, // ms | |
swipe_min_distance : 20, // pixels | |
drag : true, | |
drag_vertical : true, | |
drag_horizontal : true, | |
// minimum distance before the drag event starts | |
drag_min_distance : 20, // pixels | |
// pinch zoom and rotation | |
transform : true, | |
scale_treshold : 0.1, | |
rotation_treshold : 15, // degrees | |
tap : true, | |
tap_double : true, | |
tap_max_interval : 300, | |
tap_max_distance : 10, | |
tap_double_distance: 20, | |
hold : true, | |
hold_timeout : 500 | |
}; | |
options = mergeObject(defaults, options); | |
// some css hacks | |
(function() { | |
if(!options.css_hacks) { | |
return false; | |
} | |
var vendors = ['webkit','moz','ms','o','']; | |
var css_props = { | |
"userSelect": "none", | |
"touchCallout": "none", | |
"userDrag": "none", | |
"tapHighlightColor": "rgba(0,0,0,0)" | |
}; | |
var prop = ''; | |
for(var i = 0; i < vendors.length; i++) { | |
for(var p in css_props) { | |
prop = p; | |
if(vendors[i]) { | |
prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1); | |
} | |
element.style[ prop ] = css_props[p]; | |
} | |
} | |
})(); | |
// holds the distance that has been moved | |
var _distance = 0; | |
// holds the exact angle that has been moved | |
var _angle = 0; | |
// holds the diraction that has been moved | |
var _direction = 0; | |
// holds position movement for sliding | |
var _pos = { }; | |
// how many fingers are on the screen | |
var _fingers = 0; | |
var _first = false; | |
var _gesture = null; | |
var _prev_gesture = null; | |
var _touch_start_time = null; | |
var _prev_tap_pos = {x: 0, y: 0}; | |
var _prev_tap_end_time = null; | |
var _hold_timer = null; | |
var _offset = {}; | |
// keep track of the mouse status | |
var _mousedown = false; | |
var _event_start; | |
var _event_move; | |
var _event_end; | |
var _has_touch = ('ontouchstart' in window); | |
var _can_tap = false; | |
/** | |
* option setter/getter | |
* @param string key | |
* @param mixed value | |
* @return mixed value | |
*/ | |
this.option = function(key, val) { | |
if(val != undefined) { | |
options[key] = val; | |
} | |
return options[key]; | |
}; | |
/** | |
* angle to direction define | |
* @param float angle | |
* @return string direction | |
*/ | |
this.getDirectionFromAngle = function( angle ) { | |
var directions = { | |
down: angle >= 45 && angle < 135, //90 | |
left: angle >= 135 || angle <= -135, //180 | |
up: angle < -45 && angle > -135, //270 | |
right: angle >= -45 && angle <= 45 //0 | |
}; | |
var direction, key; | |
for(key in directions){ | |
if(directions[key]){ | |
direction = key; | |
break; | |
} | |
} | |
return direction; | |
}; | |
/** | |
* destory events | |
* @return void | |
*/ | |
this.destroy = function() { | |
if(_has_touch) { | |
removeEvent(element, "touchstart touchmove touchend touchcancel", handleEvents); | |
} | |
// for non-touch | |
else { | |
removeEvent(element, "mouseup mousedown mousemove", handleEvents); | |
removeEvent(element, "mouseout", handleMouseOut); | |
} | |
}; | |
/** | |
* count the number of fingers in the event | |
* when no fingers are detected, one finger is returned (mouse pointer) | |
* @param event | |
* @return int fingers | |
*/ | |
function countFingers( event ) | |
{ | |
// there is a bug on android (until v4?) that touches is always 1, | |
// so no multitouch is supported, e.g. no, zoom and rotation... | |
return event.touches ? event.touches.length : 1; | |
} | |
/** | |
* get the x and y positions from the event object | |
* @param event | |
* @return array [{ x: int, y: int }] | |
*/ | |
function getXYfromEvent( event ) | |
{ | |
event = event || window.event; | |
// no touches, use the event pageX and pageY | |
if(!_has_touch) { | |
var doc = document, | |
body = doc.body; | |
return [{ | |
x: event.pageX || event.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && doc.clientLeft || 0 ), | |
y: event.pageY || event.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && doc.clientTop || 0 ) | |
}]; | |
} | |
// multitouch, return array with positions | |
else { | |
var pos = [], src; | |
for(var t=0, len=event.touches.length; t<len; t++) { | |
src = event.touches[t]; | |
pos.push({ x: src.pageX, y: src.pageY }); | |
} | |
return pos; | |
} | |
} | |
/** | |
* calculate the angle between two points | |
* @param object pos1 { x: int, y: int } | |
* @param object pos2 { x: int, y: int } | |
*/ | |
function getAngle( pos1, pos2 ) | |
{ | |
return Math.atan2(pos2.y - pos1.y, pos2.x - pos1.x) * 180 / Math.PI; | |
} | |
/** | |
* calculate the distance between two points | |
* @param object pos1 { x: int, y: int } | |
* @param object pos2 { x: int, y: int } | |
*/ | |
function getDistance( pos1, pos2 ) | |
{ | |
var x = pos2.x - pos1.x, y = pos2.y - pos1.y; | |
return Math.sqrt((x * x) + (y * y)); | |
} | |
/** | |
* calculate the scale size between two fingers | |
* @param object pos_start | |
* @param object pos_move | |
* @return float scale | |
*/ | |
function calculateScale(pos_start, pos_move) | |
{ | |
if(pos_start.length == 2 && pos_move.length == 2) { | |
var start_distance = getDistance(pos_start[0], pos_start[1]); | |
var end_distance = getDistance(pos_move[0], pos_move[1]); | |
return end_distance / start_distance; | |
} | |
return 0; | |
} | |
/** | |
* calculate the rotation degrees between two fingers | |
* @param object pos_start | |
* @param object pos_move | |
* @return float rotation | |
*/ | |
function calculateRotation(pos_start, pos_move) | |
{ | |
if(pos_start.length == 2 && pos_move.length == 2) { | |
var start_rotation = getAngle(pos_start[1], pos_start[0]); | |
var end_rotation = getAngle(pos_move[1], pos_move[0]); | |
return end_rotation - start_rotation; | |
} | |
return 0; | |
} | |
/** | |
* trigger an event/callback by name with params | |
* @param string name | |
* @param array params | |
*/ | |
function triggerEvent( eventName, params ) | |
{ | |
// return touches object | |
params.touches = getXYfromEvent(params.originalEvent); | |
params.type = eventName; | |
// trigger callback | |
if(isFunction(self["on"+ eventName])) { | |
self["on"+ eventName].call(self, params); | |
} | |
} | |
/** | |
* cancel event | |
* @param object event | |
* @return void | |
*/ | |
function cancelEvent(event) | |
{ | |
event = event || window.event; | |
if(event.preventDefault){ | |
event.preventDefault(); | |
event.stopPropagation(); | |
}else{ | |
event.returnValue = false; | |
event.cancelBubble = true; | |
} | |
} | |
/** | |
* reset the internal vars to the start values | |
*/ | |
function reset() | |
{ | |
_pos = {}; | |
_first = false; | |
_fingers = 0; | |
_distance = 0; | |
_angle = 0; | |
_gesture = null; | |
} | |
var gestures = { | |
// hold gesture | |
// fired on touchstart | |
hold : function(event) | |
{ | |
// only when one finger is on the screen | |
if(options.hold) { | |
_gesture = 'hold'; | |
clearTimeout(_hold_timer); | |
_hold_timer = setTimeout(function() { | |
if(_gesture == 'hold') { | |
triggerEvent("hold", { | |
originalEvent : event, | |
position : _pos.start | |
}); | |
} | |
}, options.hold_timeout); | |
} | |
}, | |
// swipe gesture | |
// fired on touchend | |
swipe : function(event) | |
{ | |
if(!_pos.move) { | |
return; | |
} | |
// get the distance we moved | |
var _distance_x = _pos.move[0].x - _pos.start[0].x; | |
var _distance_y = _pos.move[0].y - _pos.start[0].y; | |
_distance = Math.sqrt(_distance_x*_distance_x + _distance_y*_distance_y); | |
// compare the kind of gesture by time | |
var now = new Date().getTime(); | |
var touch_time = now - _touch_start_time; | |
if(options.swipe && (options.swipe_time > touch_time) && (_distance > options.swipe_min_distance)) { | |
// calculate the angle | |
_angle = getAngle(_pos.start[0], _pos.move[0]); | |
_direction = self.getDirectionFromAngle(_angle); | |
_gesture = 'swipe'; | |
var position = { x: _pos.move[0].x - _offset.left, | |
y: _pos.move[0].y - _offset.top }; | |
var event_obj = { | |
originalEvent : event, | |
position : position, | |
direction : _direction, | |
distance : _distance, | |
distanceX : _distance_x, | |
distanceY : _distance_y, | |
angle : _angle | |
}; | |
// normal slide event | |
triggerEvent("swipe", event_obj); | |
} | |
}, | |
// drag gesture | |
// fired on mousemove | |
drag : function(event) | |
{ | |
// get the distance we moved | |
var _distance_x = _pos.move[0].x - _pos.start[0].x; | |
var _distance_y = _pos.move[0].y - _pos.start[0].y; | |
_distance = Math.sqrt(_distance_x * _distance_x + _distance_y * _distance_y); | |
// drag | |
// minimal movement required | |
if(options.drag && (_distance > options.drag_min_distance) || _gesture == 'drag') { | |
// calculate the angle | |
_angle = getAngle(_pos.start[0], _pos.move[0]); | |
_direction = self.getDirectionFromAngle(_angle); | |
// check the movement and stop if we go in the wrong direction | |
var is_vertical = (_direction == 'up' || _direction == 'down'); | |
if(((is_vertical && !options.drag_vertical) || (!is_vertical && !options.drag_horizontal)) | |
&& (_distance > options.drag_min_distance)) { | |
return; | |
} | |
_gesture = 'drag'; | |
var position = { x: _pos.move[0].x - _offset.left, | |
y: _pos.move[0].y - _offset.top }; | |
var event_obj = { | |
originalEvent : event, | |
position : position, | |
direction : _direction, | |
distance : _distance, | |
distanceX : _distance_x, | |
distanceY : _distance_y, | |
angle : _angle | |
}; | |
// on the first time trigger the start event | |
if(_first) { | |
triggerEvent("dragstart", event_obj); | |
_first = false; | |
} | |
// normal slide event | |
triggerEvent("drag", event_obj); | |
cancelEvent(event); | |
} | |
}, | |
// transform gesture | |
// fired on touchmove | |
transform : function(event) | |
{ | |
if(options.transform) { | |
var count = countFingers(event); | |
if (count !== 2) { | |
return false; | |
} | |
var rotation = calculateRotation(_pos.start, _pos.move); | |
var scale = calculateScale(_pos.start, _pos.move); | |
if (_gesture === 'transform' || | |
Math.abs(1 - scale) > options.scale_treshold || | |
Math.abs(rotation) > options.rotation_treshold) { | |
_gesture = 'transform'; | |
_pos.center = { | |
x: ((_pos.move[0].x + _pos.move[1].x) / 2) - _offset.left, | |
y: ((_pos.move[0].y + _pos.move[1].y) / 2) - _offset.top | |
}; | |
var event_obj = { | |
originalEvent : event, | |
position : _pos.center, | |
scale : scale, | |
rotation : rotation, | |
continuation : !_can_tap // The user has gone from drag to zoom | |
}; | |
// on the first time trigger the start event | |
if (_first) { | |
triggerEvent("transformstart", event_obj); | |
_first = false; | |
} | |
triggerEvent("transform", event_obj); | |
cancelEvent(event); | |
return true; | |
} | |
} | |
return false; | |
}, | |
// tap and double tap gesture | |
// fired on touchend | |
tap : function(event) | |
{ | |
// compare the kind of gesture by time | |
var now = new Date().getTime(); | |
var touch_time = now - _touch_start_time; | |
// dont fire when hold is fired | |
if(options.hold && !(options.hold && options.hold_timeout > touch_time)) { | |
return; | |
} | |
// when previous event was tap and the tap was max_interval ms ago | |
var is_double_tap = (function(){ | |
if (_prev_tap_pos && | |
options.tap_double && | |
_prev_gesture == 'tap' && | |
(_touch_start_time - _prev_tap_end_time) < options.tap_max_interval) | |
{ | |
var x_distance = Math.abs(_prev_tap_pos[0].x - _pos.start[0].x); | |
var y_distance = Math.abs(_prev_tap_pos[0].y - _pos.start[0].y); | |
return (_prev_tap_pos && _pos.start && Math.max(x_distance, y_distance) < options.tap_double_distance); | |
} | |
return false; | |
})(); | |
if(is_double_tap) { | |
_gesture = 'double_tap'; | |
_prev_tap_end_time = null; | |
triggerEvent("doubletap", { | |
originalEvent : event, | |
position : _pos.start | |
}); | |
cancelEvent(event); | |
} | |
// single tap is single touch | |
else { | |
var x_distance = (_pos.move) ? Math.abs(_pos.move[0].x - _pos.start[0].x) : 0; | |
var y_distance = (_pos.move) ? Math.abs(_pos.move[0].y - _pos.start[0].y) : 0; | |
_distance = Math.max(x_distance, y_distance); | |
if(_distance < options.tap_max_distance) { | |
_gesture = 'tap'; | |
_prev_tap_end_time = now; | |
_prev_tap_pos = _pos.start; | |
if(options.tap) { | |
triggerEvent("tap", { | |
originalEvent : event, | |
position : _pos.start | |
}); | |
cancelEvent(event); | |
} | |
} | |
} | |
} | |
}; | |
function handleEvents(event) | |
{ | |
switch(event.type) | |
{ | |
case 'mousedown': | |
case 'touchstart': | |
var count = countFingers(event); | |
_can_tap = count === 1; | |
//We were dragging and now we are zooming. | |
if (count === 2 && _gesture === "drag") { | |
//The user needs to have the dragend to be fired to ensure that | |
//there is proper cleanup from the drag and move onto transforming. | |
triggerEvent("dragend", { | |
originalEvent : event, | |
direction : _direction, | |
distance : _distance, | |
angle : _angle | |
}); | |
} | |
_setup(); | |
_mousedown = true; | |
if(options.prevent_default) { | |
cancelEvent(event); | |
} | |
break; | |
case 'mousemove': | |
case 'touchmove': | |
var count = countFingers(event); | |
//The user has gone from transforming to dragging. The | |
//user needs to have the proper cleanup of the state and | |
//setup with the new "start" points. | |
if (!_mousedown && count <= 2) { | |
_can_tap = false; | |
reset(); | |
_setup(); | |
} | |
_event_move = event; | |
_pos.move = getXYfromEvent(event); | |
// If they're dragging on touch, then the "mouse" is down | |
if (_has_touch) | |
_mousedown = true; | |
if(!gestures.transform(event) && _mousedown) { | |
gestures.drag(event); | |
} | |
break; | |
case 'mouseup': | |
case 'mouseout': | |
case 'touchcancel': | |
case 'touchend': | |
_mousedown = false; | |
_event_end = event; | |
// swipe gesture | |
gestures.swipe(event); | |
// drag gesture | |
// dragstart is triggered, so dragend is possible | |
if(_gesture == 'drag') { | |
triggerEvent("dragend", { | |
originalEvent : event, | |
direction : _direction, | |
distance : _distance, | |
angle : _angle | |
}); | |
} | |
// transform | |
// transformstart is triggered, so transformed is possible | |
else if(_gesture == 'transform') { | |
triggerEvent("transformend", { | |
originalEvent : event, | |
position : _pos.center, | |
scale : calculateScale(_pos.start, _pos.move), | |
rotation : calculateRotation(_pos.start, _pos.move) | |
}); | |
} else if (_can_tap) { | |
gestures.tap(_event_start); | |
} | |
_prev_gesture = _gesture; | |
// trigger release event | |
// "release" by default doesn't return the co-ords where your | |
// finger was released. "position" will return "the last touched co-ords" | |
triggerEvent("release", { | |
originalEvent : event, | |
gesture : _gesture, | |
position : _pos.move || _pos.start | |
}); | |
// reset vars | |
reset(); | |
break; | |
} // end switch | |
/** | |
* Performs a blank setup. | |
* @private | |
*/ | |
function _setup() { | |
_pos.start = getXYfromEvent(event); | |
_touch_start_time = new Date().getTime(); | |
_fingers = countFingers(event); | |
_first = true; | |
_event_start = event; | |
// borrowed from jquery offset https://github.com/jquery/jquery/blob/master/src/offset.js | |
var box = element.getBoundingClientRect(); | |
var clientTop = element.clientTop || document.body.clientTop || 0; | |
var clientLeft = element.clientLeft || document.body.clientLeft || 0; | |
var scrollTop = window.pageYOffset || element.scrollTop || document.body.scrollTop; | |
var scrollLeft = window.pageXOffset || element.scrollLeft || document.body.scrollLeft; | |
_offset = { | |
top: box.top + scrollTop - clientTop, | |
left: box.left + scrollLeft - clientLeft | |
}; | |
//_mousedown = true; | |
// hold gesture | |
gestures.hold(event); | |
} | |
} | |
function handleMouseOut(event) { | |
if(!isInsideHammer(element, event.relatedTarget)) { | |
handleEvents(event); | |
} | |
} | |
// bind events for touch devices | |
// except for windows phone 7.5, it doesnt support touch events..! | |
if(_has_touch) { | |
addEvent(element, "touchstart touchmove touchend touchcancel", handleEvents); | |
} | |
// for non-touch | |
else { | |
addEvent(element, "mouseup mousedown mousemove", handleEvents); | |
addEvent(element, "mouseout", handleMouseOut); | |
} | |
/** | |
* find if element is (inside) given parent element | |
* @param object element | |
* @param object parent | |
* @return bool inside | |
*/ | |
function isInsideHammer(parent, child) { | |
// get related target for IE | |
if(!child && window.event && window.event.toElement){ | |
child = window.event.toElement; | |
} | |
if(parent === child){ | |
return true; | |
} | |
// loop over parentNodes of child until we find hammer element | |
if(child){ | |
var node = child.parentNode; | |
while(node !== null){ | |
if(node === parent){ | |
return true; | |
}; | |
node = node.parentNode; | |
} | |
} | |
return false; | |
} | |
/** | |
* merge 2 objects into a new object | |
* @param object obj1 | |
* @param object obj2 | |
* @return object merged object | |
*/ | |
function mergeObject(obj1, obj2) { | |
var output = {}; | |
if(!obj2) { | |
return obj1; | |
} | |
for (var prop in obj1) { | |
if (prop in obj2) { | |
output[prop] = obj2[prop]; | |
} else { | |
output[prop] = obj1[prop]; | |
} | |
} | |
return output; | |
} | |
/** | |
* check if object is a function | |
* @param object obj | |
* @return bool is function | |
*/ | |
function isFunction( obj ){ | |
return Object.prototype.toString.call( obj ) == "[object Function]"; | |
} | |
/** | |
* attach event | |
* @param node element | |
* @param string types | |
* @param object callback | |
*/ | |
function addEvent(element, types, callback) { | |
types = types.split(" "); | |
for(var t= 0,len=types.length; t<len; t++) { | |
if(element.addEventListener){ | |
element.addEventListener(types[t], callback, false); | |
} | |
else if(document.attachEvent){ | |
element.attachEvent("on"+ types[t], callback); | |
} | |
} | |
} | |
/** | |
* detach event | |
* @param node element | |
* @param string types | |
* @param object callback | |
*/ | |
function removeEvent(element, types, callback) { | |
types = types.split(" "); | |
for(var t= 0,len=types.length; t<len; t++) { | |
if(element.removeEventListener){ | |
element.removeEventListener(types[t], callback, false); | |
} | |
else if(document.detachEvent){ | |
element.detachEvent("on"+ types[t], callback); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment