/* * Hammer.JS * version 0.6.4 * 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 direction 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; }; /** * destroy 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 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 }; if(_first) _pos.startCenter = _pos.center; var _distance_x = _pos.center.x - _pos.startCenter.x; var _distance_y = _pos.center.y - _pos.startCenter.y; _distance = Math.sqrt(_distance_x*_distance_x + _distance_y*_distance_y); var event_obj = { originalEvent : event, position : _pos.center, scale : scale, rotation : rotation, distance : _distance, distanceX : _distance_x, distanceY : _distance_y }; // 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' && _pos.start && (_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) { var count; switch(event.type) { case 'mousedown': case 'touchstart': 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(); if(options.prevent_default) { cancelEvent(event); } break; case 'mousemove': case 'touchmove': 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 === 1) { return false; } else if (!_mousedown && count === 2) { _can_tap = false; reset(); _setup(); } _event_move = event; _pos.move = getXYfromEvent(event); if(!gestures.transform(event)) { gestures.drag(event); } break; case 'mouseup': case 'mouseout': case 'touchcancel': case 'touchend': var callReset = true; _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') { // define the transform distance var _distance_x = _pos.center.x - _pos.startCenter.x; var _distance_y = _pos.center.y - _pos.startCenter.y; triggerEvent("transformend", { originalEvent : event, position : _pos.center, scale : calculateScale(_pos.start, _pos.move), rotation : calculateRotation(_pos.start, _pos.move), distance : _distance, distanceX : _distance_x, distanceY : _distance_y }); //If the user goes from transformation to drag there needs to be a //state reset so that way a dragstart/drag/dragend will be properly //fired. if (countFingers(event) === 1) { reset(); _setup(); callReset = false; } } 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 if this was not a transform->drag touch end operation. if (callReset) { 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= 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; }; /** * destroy 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 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 }; if(_first) _pos.startCenter = _pos.center; var _distance_x = _pos.center.x - _pos.startCenter.x; var _distance_y = _pos.center.y - _pos.startCenter.y; _distance = Math.sqrt(_distance_x*_distance_x + _distance_y*_distance_y); var event_obj = { originalEvent : event, position : _pos.center, scale : scale, rotation : rotation, distance : _distance, distanceX : _distance_x, distanceY : _distance_y }; // 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' && _pos.start && (_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) { var count; switch(event.type) { case 'mousedown': case 'touchstart': 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(); if(options.prevent_default) { cancelEvent(event); } break; case 'mousemove': case 'touchmove': 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 === 1) { return false; } else if (!_mousedown && count === 2) { _can_tap = false; reset(); _setup(); } _event_move = event; _pos.move = getXYfromEvent(event); if(!gestures.transform(event)) { gestures.drag(event); } break; case 'mouseup': case 'mouseout': case 'touchcancel': case 'touchend': var callReset = true; _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') { // define the transform distance var _distance_x = _pos.center.x - _pos.startCenter.x; var _distance_y = _pos.center.y - _pos.startCenter.y; triggerEvent("transformend", { originalEvent : event, position : _pos.center, scale : calculateScale(_pos.start, _pos.move), rotation : calculateRotation(_pos.start, _pos.move), distance : _distance, distanceX : _distance_x, distanceY : _distance_y }); //If the user goes from transformation to drag there needs to be a //state reset so that way a dragstart/drag/dragend will be properly //fired. if (countFingers(event) === 1) { reset(); _setup(); callReset = false; } } 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 if this was not a transform->drag touch end operation. if (callReset) { 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