| /* |
| * 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]"; |
| } |
| } |