/* | |
* Hammer.JS | |
* version 0.4 | |
* author: Eight Media | |
* https://github.com/EightMedia/hammer.js | |
*/ | |
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, | |
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_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; | |
/** | |
* 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; | |
}; | |
/** | |
* 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(!event.touches) { | |
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; | |
} | |
/** | |
* 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(); | |
}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); | |
} | |
}, | |
// 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 scale = event.scale || 1; | |
var rotation = event.rotation || 0; | |
if(countFingers(event) != 2) { | |
return false; | |
} | |
if(_gesture != 'drag' && | |
(_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 | |
}; | |
// 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 { | |
_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': | |
_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); | |
if(options.prevent_default) { | |
cancelEvent(event); | |
} | |
break; | |
case 'mousemove': | |
case 'touchmove': | |
if(!_mousedown) { | |
return false; | |
} | |
_event_move = event; | |
_pos.move = getXYfromEvent(event); | |
if(!gestures.transform(event)) { | |
gestures.drag(event); | |
} | |
break; | |
case 'mouseup': | |
case 'mouseout': | |
case 'touchcancel': | |
case 'touchend': | |
if(!_mousedown || (_gesture != 'transform' && event.touches && event.touches.length > 0)) { | |
return false; | |
} | |
_mousedown = false; | |
_event_end = 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 : event.scale, | |
rotation : event.rotation | |
}); | |
} | |
else { | |
gestures.tap(_event_start); | |
} | |
_prev_gesture = _gesture; | |
// reset vars | |
reset(); | |
break; | |
} | |
} | |
// bind events for touch devices | |
// except for windows phone 7.5, it doesnt support touch events..! | |
if('ontouchstart' in window) { | |
element.addEventListener("touchstart", handleEvents, false); | |
element.addEventListener("touchmove", handleEvents, false); | |
element.addEventListener("touchend", handleEvents, false); | |
element.addEventListener("touchcancel", handleEvents, false); | |
} | |
// for non-touch | |
else { | |
if(element.addEventListener){ // prevent old IE errors | |
element.addEventListener("mouseout", function(event) { | |
if(!isInsideHammer(element, event.relatedTarget)) { | |
handleEvents(event); | |
} | |
}, false); | |
element.addEventListener("mouseup", handleEvents, false); | |
element.addEventListener("mousedown", handleEvents, false); | |
element.addEventListener("mousemove", handleEvents, false); | |
// events for older IE | |
}else if(document.attachEvent){ | |
element.attachEvent("onmouseout", function(event) { | |
if(!isInsideHammer(element, event.relatedTarget)) { | |
handleEvents(event); | |
} | |
}, false); | |
element.attachEvent("onmouseup", handleEvents); | |
element.attachEvent("onmousedown", handleEvents); | |
element.attachEvent("onmousemove", handleEvents); | |
} | |
} | |
/** | |
* 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; | |
} | |
function isFunction( obj ){ | |
return Object.prototype.toString.call( obj ) == "[object Function]"; | |
} | |
} |