/*! Hammer.JS - v1.0.11 - 2014-05-20 * http://eightmedia.github.io/hammer.js * * Copyright (c) 2014 Jorik Tangelder ; * 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.11'; // 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> and Chrome 35>, you could experiment with the value // see for more options the wiki: https://github.com/EightMedia/hammer.js/wiki touchAction : 'pan-y', 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);