/*! jQuery plugin for Hammer.JS - v1.1.3 - 2014-05-20 * http://eightmedia.github.com/hammer.js * * Copyright (c) 2014 Jorik Tangelder ; * Licensed under the MIT license */ (function(window, undefined) { 'use strict'; /** * @main * @module hammer * * @class Hammer * @static */ /** * Hammer, use this to create instances * ```` * var hammertime = new Hammer(myElement); * ```` * * @method Hammer * @param {HTMLElement} element * @param {Object} [options={}] * @return {Hammer.Instance} */ var Hammer = function Hammer(element, options) { return new Hammer.Instance(element, options || {}); }; /** * version, as defined in package.json * the value will be set at each build * @property VERSION * @final * @type {String} */ Hammer.VERSION = '1.1.3'; /** * default settings. * more settings are defined per gesture at `/gestures`. Each gesture can be disabled/enabled * by setting it's name (like `swipe`) to false. * You can set the defaults for all instances by changing this object before creating an instance. * @example * ```` * Hammer.defaults.drag = false; * Hammer.defaults.behavior.touchAction = 'pan-y'; * delete Hammer.defaults.behavior.userSelect; * ```` * @property defaults * @type {Object} */ Hammer.defaults = { /** * this setting object adds styles and attributes to the element to prevent the browser from doing * its native behavior. The css properties are auto prefixed for the browsers when needed. * @property defaults.behavior * @type {Object} */ behavior: { /** * Disables text selection to improve the dragging gesture. When the value is `none` it also sets * `onselectstart=false` for IE on the element. Mainly for desktop browsers. * @property defaults.behavior.userSelect * @type {String} * @default 'none' */ userSelect: 'none', /** * Specifies whether and how a given region can be manipulated by the user (for instance, by panning or zooming). * Used by Chrome 35> and IE10>. By default this makes the element blocking any touch event. * @property defaults.behavior.touchAction * @type {String} * @default: 'pan-y' */ touchAction: 'pan-y', /** * Disables the default callout shown when you touch and hold a touch target. * On iOS, when you touch and hold a touch target such as a link, Safari displays * a callout containing information about the link. This property allows you to disable that callout. * @property defaults.behavior.touchCallout * @type {String} * @default 'none' */ touchCallout: 'none', /** * Specifies whether zooming is enabled. Used by IE10> * @property defaults.behavior.contentZooming * @type {String} * @default 'none' */ contentZooming: 'none', /** * Specifies that an entire element should be draggable instead of its contents. * Mainly for desktop browsers. * @property defaults.behavior.userDrag * @type {String} * @default 'none' */ userDrag: 'none', /** * Overrides the highlight color shown when the user taps a link or a JavaScript * clickable element in Safari on iPhone. This property obeys the alpha value, if specified. * * If you don't specify an alpha value, Safari on iPhone applies a default alpha value * to the color. To disable tap highlighting, set the alpha value to 0 (invisible). * If you set the alpha value to 1.0 (opaque), the element is not visible when tapped. * @property defaults.behavior.tapHighlightColor * @type {String} * @default 'rgba(0,0,0,0)' */ tapHighlightColor: 'rgba(0,0,0,0)' } }; /** * hammer document where the base events are added at * @property DOCUMENT * @type {HTMLElement} * @default window.document */ Hammer.DOCUMENT = document; /** * detect support for pointer events * @property HAS_POINTEREVENTS * @type {Boolean} */ Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled; /** * detect support for touch events * @property HAS_TOUCHEVENTS * @type {Boolean} */ Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window); /** * detect mobile browsers * @property IS_MOBILE * @type {Boolean} */ Hammer.IS_MOBILE = /mobile|tablet|ip(ad|hone|od)|android|silk/i.test(navigator.userAgent); /** * detect if we want to support mouseevents at all * @property NO_MOUSEEVENTS * @type {Boolean} */ Hammer.NO_MOUSEEVENTS = (Hammer.HAS_TOUCHEVENTS && Hammer.IS_MOBILE) || Hammer.HAS_POINTEREVENTS; /** * interval in which Hammer recalculates current velocity/direction/angle in ms * @property CALCULATE_INTERVAL * @type {Number} * @default 25 */ Hammer.CALCULATE_INTERVAL = 25; /** * eventtypes per touchevent (start, move, end) are filled by `Event.determineEventTypes` on `setup` * the object contains the DOM event names per type (`EVENT_START`, `EVENT_MOVE`, `EVENT_END`) * @property EVENT_TYPES * @private * @writeOnce * @type {Object} */ var EVENT_TYPES = {}; /** * direction strings, for safe comparisons * @property DIRECTION_DOWN|LEFT|UP|RIGHT * @final * @type {String} * @default 'down' 'left' 'up' 'right' */ 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'; /** * pointertype strings, for safe comparisons * @property POINTER_MOUSE|TOUCH|PEN * @final * @type {String} * @default 'mouse' 'touch' 'pen' */ var POINTER_MOUSE = Hammer.POINTER_MOUSE = 'mouse'; var POINTER_TOUCH = Hammer.POINTER_TOUCH = 'touch'; var POINTER_PEN = Hammer.POINTER_PEN = 'pen'; /** * eventtypes * @property EVENT_START|MOVE|END|RELEASE|TOUCH * @final * @type {String} * @default 'start' 'change' 'move' 'end' 'release' 'touch' */ var EVENT_START = Hammer.EVENT_START = 'start'; var EVENT_MOVE = Hammer.EVENT_MOVE = 'move'; var EVENT_END = Hammer.EVENT_END = 'end'; var EVENT_RELEASE = Hammer.EVENT_RELEASE = 'release'; var EVENT_TOUCH = Hammer.EVENT_TOUCH = 'touch'; /** * if the window events are set... * @property READY * @writeOnce * @type {Boolean} * @default false */ Hammer.READY = false; /** * plugins namespace * @property plugins * @type {Object} */ Hammer.plugins = Hammer.plugins || {}; /** * gestures namespace * see `/gestures` for the definitions * @property gestures * @type {Object} */ Hammer.gestures = Hammer.gestures || {}; /** * setup events to detect gestures on the document * this function is called when creating an new instance * @private */ 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; } /** * @module hammer * * @class Utils * @static */ var Utils = Hammer.utils = { /** * extend method, could also be used for cloning when `dest` is an empty object. * changes the dest object * @method extend * @param {Object} dest * @param {Object} src * @param {Boolean} [merge=false] do a merge * @return {Object} dest */ extend: function extend(dest, src, merge) { for(var key in src) { if(!src.hasOwnProperty(key) || (dest[key] !== undefined && merge)) { continue; } dest[key] = src[key]; } return dest; }, /** * simple addEventListener wrapper * @method on * @param {HTMLElement} element * @param {String} type * @param {Function} handler */ on: function on(element, type, handler) { element.addEventListener(type, handler, false); }, /** * simple removeEventListener wrapper * @method off * @param {HTMLElement} element * @param {String} type * @param {Function} handler */ off: function off(element, type, handler) { element.removeEventListener(type, handler, false); }, /** * forEach over arrays and objects * @method each * @param {Object|Array} obj * @param {Function} iterator * @param {any} iterator.item * @param {Number} iterator.index * @param {Object|Array} iterator.obj the source object * @param {Object} context value to use as `this` in the iterator */ each: function each(obj, iterator, context) { var i, len; // native forEach on arrays if('forEach' in obj) { obj.forEach(iterator, context); // arrays } else if(obj.length !== undefined) { for(i = 0, len = obj.length; i < len; i++) { if(iterator.call(context, obj[i], 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 string using indexOf * @method inStr * @param {String} src * @param {String} find * @return {Boolean} found */ inStr: function inStr(src, find) { return src.indexOf(find) > -1; }, /** * find if a array contains the object using indexOf or a simple polyfill * @method inArray * @param {String} src * @param {String} find * @return {Boolean|Number} false when not found, or the index */ inArray: function inArray(src, find) { if(src.indexOf) { var index = src.indexOf(find); return (index === -1) ? false : index; } else { for(var i = 0, len = src.length; i < len; i++) { if(src[i] === find) { return i; } } return false; } }, /** * convert an array-like object (`arguments`, `touchlist`) to an array * @method toArray * @param {Object} obj * @return {Array} */ toArray: function toArray(obj) { return Array.prototype.slice.call(obj, 0); }, /** * find if a node is in the given parent * @method hasParent * @param {HTMLElement} node * @param {HTMLElement} parent * @return {Boolean} found */ hasParent: function hasParent(node, parent) { while(node) { if(node == parent) { return true; } node = node.parentNode; } return false; }, /** * get the center of all the touches * @method getCenter * @param {Array} touches * @return {Object} center contains `pageX`, `pageY`, `clientX` and `clientY` properties */ 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. unit is in px per ms. * @method getVelocity * @param {Number} deltaTime * @param {Number} deltaX * @param {Number} deltaY * @return {Object} velocity `x` and `y` */ getVelocity: function getVelocity(deltaTime, deltaX, deltaY) { return { x: Math.abs(deltaX / deltaTime) || 0, y: Math.abs(deltaY / deltaTime) || 0 }; }, /** * calculate the angle between two coordinates * @method getAngle * @param {Touch} touch1 * @param {Touch} touch2 * @return {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; }, /** * do a small comparision to get the direction between two touches. * @method getDirection * @param {Touch} touch1 * @param {Touch} touch2 * @return {String} direction matches `DIRECTION_LEFT|RIGHT|UP|DOWN` */ 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 * @method getDistance * @param {Touch}touch1 * @param {Touch} touch2 * @return {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 * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out * @method getScale * @param {Array} start array of touches * @param {Array} end array of touches * @return {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 * @method getRotation * @param {Array} start array of touches * @param {Array} end array of touches * @return {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; }, /** * find out if the direction is vertical * * @method isVertical * @param {String} direction matches `DIRECTION_UP|DOWN` * @return {Boolean} is_vertical */ isVertical: function isVertical(direction) { return direction == DIRECTION_UP || direction == DIRECTION_DOWN; }, /** * set css properties with their prefixes * @param {HTMLElement} element * @param {String} prop * @param {String} value * @param {Boolean} [toggle=true] * @return {Boolean} */ setPrefixedCss: function setPrefixedCss(element, prop, value, toggle) { var prefixes = ['', 'Webkit', 'Moz', 'O', 'ms']; prop = Utils.toCamelCase(prop); for(var i = 0; i < prefixes.length; i++) { var p = prop; // prefixes if(prefixes[i]) { p = prefixes[i] + p.slice(0, 1).toUpperCase() + p.slice(1); } // test the style if(p in element.style) { element.style[p] = (toggle == null || toggle) && value || ''; break; } } }, /** * toggle browser default behavior by setting css properties. * `userSelect='none'` also sets `element.onselectstart` to false * `userDrag='none'` also sets `element.ondragstart` to false * * @method toggleBehavior * @param {HtmlElement} element * @param {Object} props * @param {Boolean} [toggle=true] */ toggleBehavior: function toggleBehavior(element, props, toggle) { if(!props || !element || !element.style) { return; } // set the css properties Utils.each(props, function(value, prop) { Utils.setPrefixedCss(element, prop, value, toggle); }); var falseFn = toggle && function() { return false; }; // also the disable onselectstart if(props.userSelect == 'none') { element.onselectstart = falseFn; } // and disable ondragstart if(props.userDrag == 'none') { element.ondragstart = falseFn; } }, /** * convert a string with underscores to camelCase * so prevent_default becomes preventDefault * @param {String} str * @return {String} camelCaseStr */ toCamelCase: function toCamelCase(str) { return str.replace(/[_-]([a-z])/g, function(s) { return s[1].toUpperCase(); }); } }; /** * @module hammer */ /** * create new hammer instance * all methods should return the instance itself, so it is chainable. * * @class Instance * @constructor * @param {HTMLElement} element * @param {Object} [options={}] options are merged with `Hammer.defaults` * @return {Hammer.Instance} */ Hammer.Instance = function(element, options) { var self = this; // setup HammerJS window events and register all gestures // this also sets up the default options setup(); /** * @property element * @type {HTMLElement} */ this.element = element; /** * @property enabled * @type {Boolean} * @protected */ this.enabled = true; /** * options, merged with the defaults * options with an _ are converted to camelCase * @property options * @type {Object} */ Utils.each(options, function(value, name) { delete options[name]; options[Utils.toCamelCase(name)] = value; }); 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.behavior) { Utils.toggleBehavior(this.element, this.options.behavior, true); } /** * event start handler on the element to start the detection * @property eventStartHandler * @type {Object} */ this.eventStartHandler = Event.onTouch(element, EVENT_START, function(ev) { if(self.enabled && ev.eventType == EVENT_START) { Detection.startDetect(self, ev); } else if(ev.eventType == EVENT_TOUCH) { Detection.detect(ev); } }); /** * keep a list of user event handlers which needs to be removed when calling 'dispose' * @property eventHandlers * @type {Array} */ this.eventHandlers = []; }; Hammer.Instance.prototype = { /** * bind events to the instance * @method on * @chainable * @param {String} gestures multiple gestures by splitting with a space * @param {Function} handler * @param {Object} handler.ev event object */ on: function onEvent(gestures, handler) { var self = this; Event.on(self.element, gestures, handler, function(type) { self.eventHandlers.push({ gesture: type, handler: handler }); }); return self; }, /** * unbind events to the instance * @method off * @chainable * @param {String} gestures * @param {Function} handler */ off: function offEvent(gestures, handler) { var self = this; Event.off(self.element, gestures, handler, function(type) { var index = Utils.inArray({ gesture: type, handler: handler }); if(index !== false) { self.eventHandlers.splice(index, 1); } }); return self; }, /** * trigger gesture event * @method trigger * @chainable * @param {String} gesture * @param {Object} [eventData] */ 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 * @method enable * @chainable * @param {Boolean} state */ enable: function enable(state) { this.enabled = state; return this; }, /** * dispose this hammer instance * @method dispose * @return {Null} */ dispose: function dispose() { var i, eh; // undo all changes made by stop_browser_behavior Utils.toggleBehavior(this.element, this.options.behavior, false); // unbind all custom event handlers for(i = -1; (eh = this.eventHandlers[++i]);) { Utils.off(this.element, eh.gesture, eh.handler); } this.eventHandlers = []; // unbind the start event listener Event.off(this.element, EVENT_TYPES[EVENT_START], this.eventStartHandler); return null; } }; /** * @module hammer */ /** * @class Event * @static */ var Event = Hammer.event = { /** * when touch events have been fired, this is true * this is used to stop mouse events * @property prevent_mouseevents * @private * @type {Boolean} */ preventMouseEvents: false, /** * if EVENT_START has been fired * @property started * @private * @type {Boolean} */ started: false, /** * when the mouse is hold down, this is true * @property should_detect * @private * @type {Boolean} */ shouldDetect: false, /** * simple event binder with a hook and support for multiple types * @method on * @param {HTMLElement} element * @param {String} type * @param {Function} handler * @param {Function} [hook] * @param {Object} hook.type */ on: function on(element, type, handler, hook) { var types = type.split(' '); Utils.each(types, function(type) { Utils.on(element, type, handler); hook && hook(type); }); }, /** * simple event unbinder with a hook and support for multiple types * @method off * @param {HTMLElement} element * @param {String} type * @param {Function} handler * @param {Function} [hook] * @param {Object} hook.type */ off: function off(element, type, handler, hook) { var types = type.split(' '); Utils.each(types, function(type) { Utils.off(element, type, handler); hook && hook(type); }); }, /** * the core touch event handler. * this finds out if we should to detect gestures * @method onTouch * @param {HTMLElement} element * @param {String} eventType matches `EVENT_START|MOVE|END` * @param {Function} handler * @return onTouchHandler {Function} the core event handler */ onTouch: function onTouch(element, eventType, handler) { var self = this; var onTouchHandler = function onTouchHandler(ev) { var srcType = ev.type.toLowerCase(), isPointer = Hammer.HAS_POINTEREVENTS, isMouse = Utils.inStr(srcType, 'mouse'), triggerType; // if we are in a mouseevent, but there has been a touchevent triggered in this session // we want to do nothing. simply break out of the event. if(isMouse && self.preventMouseEvents) { return; // mousebutton must be down } else if(isMouse && eventType == EVENT_START && ev.button === 0) { self.preventMouseEvents = false; self.shouldDetect = true; } else if(isPointer && eventType == EVENT_START) { self.shouldDetect = (ev.buttons === 1 || PointerEvent.matchType(POINTER_TOUCH, ev)); // just a valid start event, but no mouse } else if(!isMouse && eventType == EVENT_START) { self.preventMouseEvents = true; self.shouldDetect = true; } // update the pointer event before entering the detection if(isPointer && eventType != EVENT_END) { PointerEvent.updatePointer(eventType, ev); } // we are in a touch/down state, so allowed detection of gestures if(self.shouldDetect) { triggerType = self.doDetect.call(self, ev, eventType, element, handler); } // ...and we are done with the detection // so reset everything to start each detection totally fresh if(triggerType == EVENT_END) { self.preventMouseEvents = false; self.shouldDetect = false; PointerEvent.reset(); // update the pointerevent object after the detection } if(isPointer && eventType == EVENT_END) { PointerEvent.updatePointer(eventType, ev); } }; this.on(element, EVENT_TYPES[eventType], onTouchHandler); return onTouchHandler; }, /** * the core detection method * this finds out what hammer-touch-events to trigger * @method doDetect * @param {Object} ev * @param {String} eventType matches `EVENT_START|MOVE|END` * @param {HTMLElement} element * @param {Function} handler * @return {String} triggerType matches `EVENT_START|MOVE|END` */ doDetect: function doDetect(ev, eventType, element, handler) { var touchList = this.getTouchList(ev, eventType); var touchListLength = touchList.length; var triggerType = eventType; var triggerChange = touchList.trigger; // used by fakeMultitouch plugin var changedLength = touchListLength; // at each touchstart-like event we want also want to trigger a TOUCH event... if(eventType == EVENT_START) { triggerChange = EVENT_TOUCH; // ...the same for a touchend-like event } else if(eventType == EVENT_END) { triggerChange = EVENT_RELEASE; // keep track of how many touches have been removed changedLength = touchList.length - ((ev.changedTouches) ? ev.changedTouches.length : 1); } // after there are still touches on the screen, // we just want to trigger a MOVE event. so change the START or END to a MOVE // but only after detection has been started, the first time we actualy want a START if(changedLength > 0 && this.started) { triggerType = EVENT_MOVE; } // detection has been started, we keep track of this, see above this.started = true; // generate some event data, some basic information var evData = this.collectEventData(element, triggerType, touchList, ev); // trigger the triggerType event before the change (TOUCH, RELEASE) events // but the END event should be at last if(eventType != EVENT_END) { handler.call(Detection, evData); } // trigger a change (TOUCH, RELEASE) event, this means the length of the touches changed if(triggerChange) { evData.changedLength = changedLength; evData.eventType = triggerChange; handler.call(Detection, evData); evData.eventType = triggerType; delete evData.changedLength; } // trigger the END event if(triggerType == EVENT_END) { handler.call(Detection, evData); // ...and we are done with the detection // so reset everything to start each detection totally fresh this.started = false; } return triggerType; }, /** * we have different events for each device/browser * determine what we need and set them in the EVENT_TYPES constant * the `onTouch` method is bind to these properties. * @method determineEventTypes * @return {Object} events */ determineEventTypes: function determineEventTypes() { var types; if(Hammer.HAS_POINTEREVENTS) { if(window.PointerEvent) { types = [ 'pointerdown', 'pointermove', 'pointerup pointercancel lostpointercapture' ]; } else { types = [ 'MSPointerDown', 'MSPointerMove', 'MSPointerUp MSPointerCancel MSLostPointerCapture' ]; } } else if(Hammer.NO_MOUSEEVENTS) { types = [ 'touchstart', 'touchmove', 'touchend touchcancel' ]; } else { types = [ 'touchstart mousedown', 'touchmove mousemove', 'touchend touchcancel mouseup' ]; } EVENT_TYPES[EVENT_START] = types[0]; EVENT_TYPES[EVENT_MOVE] = types[1]; EVENT_TYPES[EVENT_END] = types[2]; return EVENT_TYPES; }, /** * create touchList depending on the event * @method getTouchList * @param {Object} ev * @param {String} eventType * @return {Array} touches */ getTouchList: function getTouchList(ev, eventType) { // get the fake pointerEvent touchlist if(Hammer.HAS_POINTEREVENTS) { return PointerEvent.getTouchList(); } // get the touchlist if(ev.touches) { if(eventType == EVENT_MOVE) { return ev.touches; } var identifiers = []; var concat = [].concat(Utils.toArray(ev.touches), Utils.toArray(ev.changedTouches)); var touchList = []; Utils.each(concat, function(touch) { if(Utils.inArray(identifiers, touch.identifier) === false) { touchList.push(touch); } identifiers.push(touch.identifier); }); return touchList; } // make fake touchList from mouse position ev.identifier = 1; return [ev]; }, /** * collect basic event data * @method collectEventData * @param {HTMLElement} element * @param {String} eventType matches `EVENT_START|MOVE|END` * @param {Array} touches * @param {Object} ev * @return {Object} ev */ 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; } else if(PointerEvent.matchType(POINTER_PEN, ev)) { pointerType = POINTER_PEN; } 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(); } }; } }; /** * @module hammer * * @class PointerEvent * @static */ var PointerEvent = Hammer.PointerEvent = { /** * holds all pointers, by `identifier` * @property pointers * @type {Object} */ pointers: {}, /** * get the pointers as an array * @method getTouchList * @return {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 * @method updatePointer * @param {String} eventType matches `EVENT_START|MOVE|END` * @param {Object} pointerEvent */ updatePointer: function updatePointer(eventType, pointerEvent) { if(eventType == EVENT_END || (eventType != EVENT_END && pointerEvent.buttons !== 1)) { delete this.pointers[pointerEvent.pointerId]; } else { pointerEvent.identifier = pointerEvent.pointerId; this.pointers[pointerEvent.pointerId] = pointerEvent; } }, /** * check if ev matches pointertype * @method matchType * @param {String} pointerType matches `POINTER_MOUSE|TOUCH|PEN` * @param {PointerEvent} ev */ matchType: function matchType(pointerType, ev) { if(!ev.pointerType) { return false; } var pt = ev.pointerType, types = {}; types[POINTER_MOUSE] = (pt === (ev.MSPOINTER_TYPE_MOUSE || POINTER_MOUSE)); types[POINTER_TOUCH] = (pt === (ev.MSPOINTER_TYPE_TOUCH || POINTER_TOUCH)); types[POINTER_PEN] = (pt === (ev.MSPOINTER_TYPE_PEN || POINTER_PEN)); return types[pointerType]; }, /** * reset the stored pointers * @method reset */ reset: function resetList() { this.pointers = {}; } }; /** * @module hammer * * @class Detection * @static */ 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 * @method startDetect * @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 lastCalcEvent: false, // last eventData for calculations. futureCalcEvent: false, // last eventData for calculations. lastCalcData: {}, // last lastCalcData name: '' // current gesture we're in/detected, can be 'tap', 'hold' etc }; this.detect(eventData); }, /** * Hammer.gesture detection * @method detect * @param {Object} eventData * @return {any} */ 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, instOptions = 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.enabled && instOptions[gesture.name]) { gesture.handler.call(gesture, eventData, inst); } }, this); // store as previous event event if(this.current) { this.current.lastEvent = eventData; } if(eventData.eventType == EVENT_END) { 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 * @method stopDetect */ 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; this.stopped = true; }, /** * calculate velocity, angle and direction * @method getVelocityData * @param {Object} ev * @param {Object} center * @param {Number} deltaTime * @param {Number} deltaX * @param {Number} deltaY */ getCalculatedData: function getCalculatedData(ev, center, deltaTime, deltaX, deltaY) { var cur = this.current, recalc = false, calcEv = cur.lastCalcEvent, calcData = cur.lastCalcData; if(calcEv && ev.timeStamp - calcEv.timeStamp > Hammer.CALCULATE_INTERVAL) { center = calcEv.center; deltaTime = ev.timeStamp - calcEv.timeStamp; deltaX = ev.center.clientX - calcEv.center.clientX; deltaY = ev.center.clientY - calcEv.center.clientY; recalc = true; } if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) { cur.futureCalcEvent = ev; } if(!cur.lastCalcEvent || recalc) { calcData.velocity = Utils.getVelocity(deltaTime, deltaX, deltaY); calcData.angle = Utils.getAngle(center, ev.center); calcData.direction = Utils.getDirection(center, ev.center); cur.lastCalcEvent = cur.futureCalcEvent || ev; cur.futureCalcEvent = ev; } ev.velocityX = calcData.velocity.x; ev.velocityY = calcData.velocity.y; ev.interimAngle = calcData.angle; ev.interimDirection = calcData.direction; }, /** * extend eventData for Hammer.gestures * @method extendEventData * @param {Object} ev * @return {Object} ev */ extendEventData: function extendEventData(ev) { var cur = this.current, startEv = cur.startEvent, lastEv = cur.lastEvent || startEv; // update the start touchlist to calculate the scale/rotation if(ev.eventType == EVENT_TOUCH || ev.eventType == EVENT_RELEASE) { startEv.touches = []; Utils.each(ev.touches, function(touch) { startEv.touches.push({ clientX: touch.clientX, clientY: touch.clientY }); }); } var deltaTime = ev.timeStamp - startEv.timeStamp, deltaX = ev.center.clientX - startEv.center.clientX, deltaY = ev.center.clientY - startEv.center.clientY; this.getCalculatedData(ev, lastEv.center, deltaTime, deltaX, deltaY); Utils.extend(ev, { startEvent: startEv, deltaTime: deltaTime, deltaX: deltaX, deltaY: deltaY, 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 * @method register * @param {Object} gesture object, see `gestures/` for documentation * @return {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; } }; /** * @module gestures */ /** * Move with x fingers (default 1) around on the page. * Preventing the default browser behavior is a good way to improve feel and working. * ```` * hammertime.on("drag", function(ev) { * console.log(ev); * ev.gesture.preventDefault(); * }); * ```` * * @class Drag * @static */ /** * @event drag * @param {Object} ev */ /** * @event dragstart * @param {Object} ev */ /** * @event dragend * @param {Object} ev */ /** * @event drapleft * @param {Object} ev */ /** * @event dragright * @param {Object} ev */ /** * @event dragup * @param {Object} ev */ /** * @event dragdown * @param {Object} ev */ /** * @param {String} name */ (function(name) { var triggered = false; function dragGesture(ev, inst) { var cur = Detection.current; // max touches if(inst.options.dragMaxTouches > 0 && ev.touches.length > inst.options.dragMaxTouches) { return; } switch(ev.eventType) { case EVENT_START: 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.dragMinDistance && cur.name != name) { return; } var startCenter = cur.startEvent.center; // we are dragging! if(cur.name != name) { cur.name = name; if(inst.options.dragDistanceCorrection && ev.distance > 0) { // When a drag is triggered, set the event center to dragMinDistance pixels from the original event center. // Without this correction, the dragged distance would jumpstart at dragMinDistance pixels instead of at 0. // It might be useful to save the original start point somewhere var factor = Math.abs(inst.options.dragMinDistance / 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.dragLockToAxis || ( inst.options.dragLockToAxis && inst.options.dragLockMinDistance <= ev.distance )) { ev.dragLockToAxis = true; } // keep direction on the axis that the drag gesture started on var lastDirection = cur.lastEvent.direction; if(ev.dragLockToAxis && lastDirection !== ev.direction) { if(Utils.isVertical(lastDirection)) { 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(!triggered) { inst.trigger(name + 'start', ev); triggered = true; } // trigger events inst.trigger(name, ev); inst.trigger(name + ev.direction, ev); var isVertical = Utils.isVertical(ev.direction); // block the browser events if((inst.options.dragBlockVertical && isVertical) || (inst.options.dragBlockHorizontal && !isVertical)) { ev.preventDefault(); } break; case EVENT_RELEASE: if(triggered && ev.changedLength <= inst.options.dragMaxTouches) { inst.trigger(name + 'end', ev); triggered = false; } break; case EVENT_END: triggered = false; break; } } Hammer.gestures.Drag = { name: name, index: 50, handler: dragGesture, defaults: { /** * minimal movement that have to be made before the drag event gets triggered * @property dragMinDistance * @type {Number} * @default 10 */ dragMinDistance: 10, /** * Set dragDistanceCorrection 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. * @property dragDistanceCorrection * @type {Boolean} * @default true */ dragDistanceCorrection: true, /** * set 0 for unlimited, but this can conflict with transform * @property dragMaxTouches * @type {Number} * @default 1 */ dragMaxTouches: 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 * @property dragBlockHorizontal * @type {Boolean} * @default false */ dragBlockHorizontal: false, /** * same as `dragBlockHorizontal`, but for vertical movement * @property dragBlockVertical * @type {Boolean} * @default false */ dragBlockVertical: false, /** * dragLockToAxis keeps the drag gesture on the axis that it started on, * It disallows vertical directions if the initial direction was horizontal, and vice versa. * @property dragLockToAxis * @type {Boolean} * @default false */ dragLockToAxis: false, /** * drag lock only kicks in when distance > dragLockMinDistance * This way, locking occurs only when the distance has become large enough to reliably determine the direction * @property dragLockMinDistance * @type {Number} * @default 25 */ dragLockMinDistance: 25 } }; })('drag'); /** * @module gestures */ /** * trigger a simple gesture event, so you can do anything in your handler. * only usable if you know what your doing... * * @class Gesture * @static */ /** * @event gesture * @param {Object} ev */ Hammer.gestures.Gesture = { name: 'gesture', index: 1337, handler: function releaseGesture(ev, inst) { inst.trigger(this.name, ev); } }; /** * @module gestures */ /** * Touch stays at the same place for x time * * @class Hold * @static */ /** * @event hold * @param {Object} ev */ /** * @param {String} name */ (function(name) { var timer; function holdGesture(ev, inst) { var options = inst.options, current = Detection.current; switch(ev.eventType) { case EVENT_START: clearTimeout(timer); // set the gesture so we can check in the timeout if it still is current.name = name; // set timer and if after the timeout it still is hold, // we trigger the hold event timer = setTimeout(function() { if(current && current.name == name) { inst.trigger(name, ev); } }, options.holdTimeout); break; case EVENT_MOVE: if(ev.distance > options.holdThreshold) { clearTimeout(timer); } break; case EVENT_RELEASE: clearTimeout(timer); break; } } Hammer.gestures.Hold = { name: name, index: 10, defaults: { /** * @property holdTimeout * @type {Number} * @default 500 */ holdTimeout: 500, /** * movement allowed while holding * @property holdThreshold * @type {Number} * @default 2 */ holdThreshold: 2 }, handler: holdGesture }; })('hold'); /** * @module gestures */ /** * when a touch is being released from the page * * @class Release * @static */ /** * @event release * @param {Object} ev */ Hammer.gestures.Release = { name: 'release', index: Infinity, handler: function releaseGesture(ev, inst) { if(ev.eventType == EVENT_RELEASE) { inst.trigger(this.name, ev); } } }; /** * @module gestures */ /** * triggers swipe events when the end velocity is above the threshold * for best usage, set `preventDefault` (on the drag gesture) to `true` * ```` * hammertime.on("dragleft swipeleft", function(ev) { * console.log(ev); * ev.gesture.preventDefault(); * }); * ```` * * @class Swipe * @static */ /** * @event swipe * @param {Object} ev */ /** * @event swipeleft * @param {Object} ev */ /** * @event swiperight * @param {Object} ev */ /** * @event swipeup * @param {Object} ev */ /** * @event swipedown * @param {Object} ev */ Hammer.gestures.Swipe = { name: 'swipe', index: 40, defaults: { /** * @property swipeMinTouches * @type {Number} * @default 1 */ swipeMinTouches: 1, /** * @property swipeMaxTouches * @type {Number} * @default 1 */ swipeMaxTouches: 1, /** * horizontal swipe velocity * @property swipeVelocityX * @type {Number} * @default 0.6 */ swipeVelocityX: 0.6, /** * vertical swipe velocity * @property swipeVelocityY * @type {Number} * @default 0.6 */ swipeVelocityY: 0.6 }, handler: function swipeGesture(ev, inst) { if(ev.eventType == EVENT_RELEASE) { var touches = ev.touches.length, options = inst.options; // max touches if(touches < options.swipeMinTouches || touches > options.swipeMaxTouches) { return; } // when the distance we moved is too small we skip this gesture // or we can be already in dragging if(ev.velocityX > options.swipeVelocityX || ev.velocityY > options.swipeVelocityY) { // trigger swipe events inst.trigger(this.name, ev); inst.trigger(this.name + ev.direction, ev); } } } }; /** * @module gestures */ /** * Single tap and a double tap on a place * * @class Tap * @static */ /** * @event tap * @param {Object} ev */ /** * @event doubletap * @param {Object} ev */ /** * @param {String} name */ (function(name) { var hasMoved = false; function tapGesture(ev, inst) { var options = inst.options, current = Detection.current, prev = Detection.previous, sincePrev, didDoubleTap; switch(ev.eventType) { case EVENT_START: hasMoved = false; break; case EVENT_MOVE: hasMoved = hasMoved || (ev.distance > options.tapMaxDistance); break; case EVENT_END: if(!Utils.inStr(ev.srcEvent.type, 'cancel') && ev.deltaTime < options.tapMaxTime && !hasMoved) { // previous gesture, for the double tap since these are two different gesture detections sincePrev = prev && prev.lastEvent && ev.timeStamp - prev.lastEvent.timeStamp; didDoubleTap = false; // check if double tap if(prev && prev.name == name && (sincePrev && sincePrev < options.doubleTapInterval) && ev.distance < options.doubleTapDistance) { inst.trigger('doubletap', ev); didDoubleTap = true; } // do a single tap if(!didDoubleTap || options.tapAlways) { current.name = name; inst.trigger(current.name, ev); } } break; } } Hammer.gestures.Tap = { name: name, index: 100, handler: tapGesture, defaults: { /** * max time of a tap, this is for the slow tappers * @property tapMaxTime * @type {Number} * @default 250 */ tapMaxTime: 250, /** * max distance of movement of a tap, this is for the slow tappers * @property tapMaxDistance * @type {Number} * @default 10 */ tapMaxDistance: 10, /** * always trigger the `tap` event, even while double-tapping * @property tapAlways * @type {Boolean} * @default true */ tapAlways: true, /** * max distance between two taps * @property doubleTapDistance * @type {Number} * @default 20 */ doubleTapDistance: 20, /** * max time between two taps * @property doubleTapInterval * @type {Number} * @default 300 */ doubleTapInterval: 300 } }; })('tap'); /** * @module gestures */ /** * when a touch is being touched at the page * * @class Touch * @static */ /** * @event touch * @param {Object} ev */ 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 * @property preventDefault * @type {Boolean} * @default false */ preventDefault: false, /** * disable mouse events, so only touch (or pen!) input triggers events * @property preventMouse * @type {Boolean} * @default false */ preventMouse: false }, handler: function touchGesture(ev, inst) { if(inst.options.preventMouse && ev.pointerType == POINTER_MOUSE) { ev.stopDetect(); return; } if(inst.options.preventDefault) { ev.preventDefault(); } if(ev.eventType == EVENT_TOUCH) { inst.trigger('touch', ev); } } }; /** * @module gestures */ /** * User want to scale or rotate with 2 fingers * Preventing the default browser behavior is a good way to improve feel and working. This can be done with the * `preventDefault` option. * * @class Transform * @static */ /** * @event transform * @param {Object} ev */ /** * @event transformstart * @param {Object} ev */ /** * @event transformend * @param {Object} ev */ /** * @event pinchin * @param {Object} ev */ /** * @event pinchout * @param {Object} ev */ /** * @event rotate * @param {Object} ev */ /** * @param {String} name */ (function(name) { var triggered = false; function transformGesture(ev, inst) { switch(ev.eventType) { case EVENT_START: triggered = false; break; case EVENT_MOVE: // at least multitouch if(ev.touches.length < 2) { return; } var scaleThreshold = Math.abs(1 - ev.scale); var rotationThreshold = Math.abs(ev.rotation); // when the distance we moved is too small we skip this gesture // or we can be already in dragging if(scaleThreshold < inst.options.transformMinScale && rotationThreshold < inst.options.transformMinRotation) { return; } // we are transforming! Detection.current.name = name; // first time, trigger dragstart event if(!triggered) { inst.trigger(name + 'start', ev); triggered = true; } inst.trigger(name, ev); // basic transform event // trigger rotate event if(rotationThreshold > inst.options.transformMinRotation) { inst.trigger('rotate', ev); } // trigger pinch event if(scaleThreshold > inst.options.transformMinScale) { inst.trigger('pinch', ev); inst.trigger('pinch' + (ev.scale < 1 ? 'in' : 'out'), ev); } break; case EVENT_RELEASE: if(triggered && ev.changedLength < 2) { inst.trigger(name + 'end', ev); triggered = false; } break; } } Hammer.gestures.Transform = { name: name, index: 45, defaults: { /** * minimal scale factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1 * @property transformMinScale * @type {Number} * @default 0.01 */ transformMinScale: 0.01, /** * rotation in degrees * @property transformMinRotation * @type {Number} * @default 1 */ transformMinRotation: 1 }, handler: transformGesture }; })('transform'); window.Hammer = Hammer; if(typeof module !== 'undefined' && module.exports) { module.exports = Hammer; } function setupPlugin(Hammer, $) { // provide polyfill for Date.now() // browser support: http://kangax.github.io/es5-compat-table/#Date.now if(!Date.now) { Date.now = function now() { return new Date().getTime(); }; } /** * the methods on/off are called by the instance, but with the jquery plugin * we use the jquery event methods instead. * @this {Hammer.Instance} * @return {jQuery} */ Hammer.utils.each(['on', 'off'], function(method) { Hammer.utils[method] = function(element, type, handler) { $(element)[method](type, function($ev) { // append the jquery fixed properties/methods var data = $.extend({}, $ev.originalEvent, $ev); if(data.button === undefined) { data.button = $ev.which - 1; } handler.call(this, data); }); }; }); /** * trigger events * this is called by the gestures to trigger an event like 'tap' * @this {Hammer.Instance} * @param {String} gesture * @param {Object} eventData * @return {jQuery} */ Hammer.Instance.prototype.trigger = function(gesture, eventData) { var el = $(this.element); if(el.has(eventData.target).length) { el = $(eventData.target); } return el.trigger({ type: gesture, gesture: eventData }); }; /** * jQuery plugin * create instance of Hammer and watch for gestures, * and when called again you can change the options * @param {Object} [options={}] * @return {jQuery} */ $.fn.hammer = function(options) { return this.each(function() { var el = $(this); var inst = el.data('hammer'); // start new hammer instance if(!inst) { el.data('hammer', new Hammer(this, options || {})); // change the options } else if(inst && options) { Hammer.utils.extend(inst.options, options); } }); }; } // AMD if(typeof define == 'function' && define.amd) { define(['jquery'], function($) { return setupPlugin(window.Hammer, $); }); } else { setupPlugin(window.Hammer, window.jQuery || window.Zepto); } })(window);