2163 lines
No EOL
60 KiB
JavaScript
2163 lines
No EOL
60 KiB
JavaScript
/*! Hammer.JS - v1.1.3 - 2014-05-22
|
|
* http://eightmedia.github.io/hammer.js
|
|
*
|
|
* Copyright (c) 2014 Jorik Tangelder <j.tangelder@gmail.com>;
|
|
* 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
|
|
*/
|
|
/**
|
|
* @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) {
|
|
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 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 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');
|
|
|
|
/**
|
|
* @module hammer
|
|
*/
|
|
|
|
// AMD export
|
|
if(typeof define == 'function' && define.amd) {
|
|
define(function() {
|
|
return Hammer;
|
|
});
|
|
// commonjs export
|
|
} else if(typeof module !== 'undefined' && module.exports) {
|
|
module.exports = Hammer;
|
|
// browser export
|
|
} else {
|
|
window.Hammer = Hammer;
|
|
}
|
|
|
|
})(window); |