Files
ionic-framework/js/utils/gestures.js
Adam Bradley f5bb023ef7 fix(tap): cancel simulated click w/ hold events
Gesture hold event and ionic’s tap will both fire since the tap has no
strict duration between its start and end (a click will always fire no
matter how long the press is held). If a hold event fires from
ionic.Gesture, then the simulated click which would fire on
touchend/mouseup should be canceled.
2014-06-09 22:00:35 -05:00

1399 lines
45 KiB
JavaScript

/**
* Simple gesture controllers with some common gestures that emit
* gesture events.
*
* Ported from github.com/EightMedia/hammer.js Gestures - thanks!
*/
(function(ionic) {
/**
* ionic.Gestures
* use this to create instances
* @param {HTMLElement} element
* @param {Object} options
* @returns {ionic.Gestures.Instance}
* @constructor
*/
ionic.Gesture = function(element, options) {
return new ionic.Gestures.Instance(element, options || {});
};
ionic.Gestures = {};
// default settings
ionic.Gestures.defaults = {
// add css to the element to prevent the browser from doing
// its native behavior. this doesnt prevent the scrolling,
// but cancels the contextmenu, tap highlighting etc
// set to false to disable this
stop_browser_behavior: 'disable-user-behavior'
};
// detect touchevents
ionic.Gestures.HAS_POINTEREVENTS = window.navigator.pointerEnabled || window.navigator.msPointerEnabled;
ionic.Gestures.HAS_TOUCHEVENTS = ('ontouchstart' in window);
// dont use mouseevents on mobile devices
ionic.Gestures.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android|silk/i;
ionic.Gestures.NO_MOUSEEVENTS = ionic.Gestures.HAS_TOUCHEVENTS && window.navigator.userAgent.match(ionic.Gestures.MOBILE_REGEX);
// eventtypes per touchevent (start, move, end)
// are filled by ionic.Gestures.event.determineEventTypes on setup
ionic.Gestures.EVENT_TYPES = {};
// direction defines
ionic.Gestures.DIRECTION_DOWN = 'down';
ionic.Gestures.DIRECTION_LEFT = 'left';
ionic.Gestures.DIRECTION_UP = 'up';
ionic.Gestures.DIRECTION_RIGHT = 'right';
// pointer type
ionic.Gestures.POINTER_MOUSE = 'mouse';
ionic.Gestures.POINTER_TOUCH = 'touch';
ionic.Gestures.POINTER_PEN = 'pen';
// touch event defines
ionic.Gestures.EVENT_START = 'start';
ionic.Gestures.EVENT_MOVE = 'move';
ionic.Gestures.EVENT_END = 'end';
// hammer document where the base events are added at
ionic.Gestures.DOCUMENT = window.document;
// plugins namespace
ionic.Gestures.plugins = {};
// if the window events are set...
ionic.Gestures.READY = false;
/**
* setup events to detect gestures on the document
*/
function setup() {
if(ionic.Gestures.READY) {
return;
}
// find what eventtypes we add listeners to
ionic.Gestures.event.determineEventTypes();
// Register all gestures inside ionic.Gestures.gestures
for(var name in ionic.Gestures.gestures) {
if(ionic.Gestures.gestures.hasOwnProperty(name)) {
ionic.Gestures.detection.register(ionic.Gestures.gestures[name]);
}
}
// Add touch events on the document
ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_MOVE, ionic.Gestures.detection.detect);
ionic.Gestures.event.onTouch(ionic.Gestures.DOCUMENT, ionic.Gestures.EVENT_END, ionic.Gestures.detection.detect);
// ionic.Gestures is ready...!
ionic.Gestures.READY = true;
}
/**
* create new hammer instance
* all methods should return the instance itself, so it is chainable.
* @param {HTMLElement} element
* @param {Object} [options={}]
* @returns {ionic.Gestures.Instance}
* @name Gesture.Instance
* @constructor
*/
ionic.Gestures.Instance = function(element, options) {
var self = this;
// A null element was passed into the instance, which means
// whatever lookup was done to find this element failed to find it
// so we can't listen for events on it.
if(element === null) {
console.error('Null element passed to gesture (element does not exist). Not listening for gesture');
return;
}
// setup ionic.GesturesJS window events and register all gestures
// this also sets up the default options
setup();
this.element = element;
// start/stop detection option
this.enabled = true;
// merge options
this.options = ionic.Gestures.utils.extend(
ionic.Gestures.utils.extend({}, ionic.Gestures.defaults),
options || {});
// add some css to the element to prevent the browser from doing its native behavoir
if(this.options.stop_browser_behavior) {
ionic.Gestures.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
}
// start detection on touchstart
ionic.Gestures.event.onTouch(element, ionic.Gestures.EVENT_START, function(ev) {
if(self.enabled) {
ionic.Gestures.detection.startDetect(self, ev);
}
});
// return instance
return this;
};
ionic.Gestures.Instance.prototype = {
/**
* bind events to the instance
* @param {String} gesture
* @param {Function} handler
* @returns {ionic.Gestures.Instance}
*/
on: function onEvent(gesture, handler){
var gestures = gesture.split(' ');
for(var t=0; t<gestures.length; t++) {
this.element.addEventListener(gestures[t], handler, false);
}
return this;
},
/**
* unbind events to the instance
* @param {String} gesture
* @param {Function} handler
* @returns {ionic.Gestures.Instance}
*/
off: function offEvent(gesture, handler){
var gestures = gesture.split(' ');
for(var t=0; t<gestures.length; t++) {
this.element.removeEventListener(gestures[t], handler, false);
}
return this;
},
/**
* trigger gesture event
* @param {String} gesture
* @param {Object} eventData
* @returns {ionic.Gestures.Instance}
*/
trigger: function triggerEvent(gesture, eventData){
// create DOM event
var event = ionic.Gestures.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(ionic.Gestures.utils.hasParent(eventData.target, element)) {
element = eventData.target;
}
element.dispatchEvent(event);
return this;
},
/**
* enable of disable hammer.js detection
* @param {Boolean} state
* @returns {ionic.Gestures.Instance}
*/
enable: function enable(state) {
this.enabled = state;
return this;
}
};
/**
* this holds the last move event,
* used to fix empty touchend issue
* see the onTouch event for an explanation
* type {Object}
*/
var last_move_event = null;
/**
* when the mouse is hold down, this is true
* type {Boolean}
*/
var enable_detect = false;
/**
* when touch events have been fired, this is true
* type {Boolean}
*/
var touch_triggered = false;
ionic.Gestures.event = {
/**
* simple addEventListener
* @param {HTMLElement} element
* @param {String} type
* @param {Function} handler
*/
bindDom: function(element, type, handler) {
var types = type.split(' ');
for(var t=0; t<types.length; t++) {
element.addEventListener(types[t], handler, false);
}
},
/**
* touch events with mouse fallback
* @param {HTMLElement} element
* @param {String} eventType like ionic.Gestures.EVENT_MOVE
* @param {Function} handler
*/
onTouch: function onTouch(element, eventType, handler) {
var self = this;
this.bindDom(element, ionic.Gestures.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
var sourceEventType = ev.type.toLowerCase();
// onmouseup, but when touchend has been fired we do nothing.
// this is for touchdevices which also fire a mouseup on touchend
if(sourceEventType.match(/mouse/) && touch_triggered) {
return;
}
// mousebutton must be down or a touch event
else if( sourceEventType.match(/touch/) || // touch events are always on screen
sourceEventType.match(/pointerdown/) || // pointerevents touch
(sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
){
enable_detect = true;
}
// mouse isn't pressed
else if(sourceEventType.match(/mouse/) && ev.which !== 1) {
enable_detect = false;
}
// we are in a touch event, set the touch triggered bool to true,
// this for the conflicts that may occur on ios and android
if(sourceEventType.match(/touch|pointer/)) {
touch_triggered = true;
}
// count the total touches on the screen
var count_touches = 0;
// when touch has been triggered in this detection session
// and we are now handling a mouse event, we stop that to prevent conflicts
if(enable_detect) {
// update pointerevent
if(ionic.Gestures.HAS_POINTEREVENTS && eventType != ionic.Gestures.EVENT_END) {
count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
}
// touch
else if(sourceEventType.match(/touch/)) {
count_touches = ev.touches.length;
}
// mouse
else if(!touch_triggered) {
count_touches = sourceEventType.match(/up/) ? 0 : 1;
}
// if we are in a end event, but when we remove one touch and
// we still have enough, set eventType to move
if(count_touches > 0 && eventType == ionic.Gestures.EVENT_END) {
eventType = ionic.Gestures.EVENT_MOVE;
}
// no touches, force the end event
else if(!count_touches) {
eventType = ionic.Gestures.EVENT_END;
}
// store the last move event
if(count_touches || last_move_event === null) {
last_move_event = ev;
}
// trigger the handler
handler.call(ionic.Gestures.detection, self.collectEventData(element, eventType, self.getTouchList(last_move_event, eventType), ev));
// remove pointerevent from list
if(ionic.Gestures.HAS_POINTEREVENTS && eventType == ionic.Gestures.EVENT_END) {
count_touches = ionic.Gestures.PointerEvent.updatePointer(eventType, ev);
}
}
//debug(sourceEventType +" "+ eventType);
// on the end we reset everything
if(!count_touches) {
last_move_event = null;
enable_detect = false;
touch_triggered = false;
ionic.Gestures.PointerEvent.reset();
}
});
},
/**
* we have different events for each device/browser
* determine what we need and set them in the ionic.Gestures.EVENT_TYPES constant
*/
determineEventTypes: function determineEventTypes() {
// determine the eventtype we want to set
var types;
// pointerEvents magic
if(ionic.Gestures.HAS_POINTEREVENTS) {
types = ionic.Gestures.PointerEvent.getEvents();
}
// on Android, iOS, blackberry, windows mobile we dont want any mouseevents
else if(ionic.Gestures.NO_MOUSEEVENTS) {
types = [
'touchstart',
'touchmove',
'touchend touchcancel'];
}
// for non pointer events browsers and mixed browsers,
// like chrome on windows8 touch laptop
else {
types = [
'touchstart mousedown',
'touchmove mousemove',
'touchend touchcancel mouseup'];
}
ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_START] = types[0];
ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_MOVE] = types[1];
ionic.Gestures.EVENT_TYPES[ionic.Gestures.EVENT_END] = types[2];
},
/**
* create touchlist depending on the event
* @param {Object} ev
* @param {String} eventType used by the fakemultitouch plugin
*/
getTouchList: function getTouchList(ev/*, eventType*/) {
// get the fake pointerEvent touchlist
if(ionic.Gestures.HAS_POINTEREVENTS) {
return ionic.Gestures.PointerEvent.getTouchList();
}
// get the touchlist
else if(ev.touches) {
return ev.touches;
}
// make fake touchlist from mouse position
else {
ev.identifier = 1;
return [ev];
}
},
/**
* collect event data for ionic.Gestures js
* @param {HTMLElement} element
* @param {String} eventType like ionic.Gestures.EVENT_MOVE
* @param {Object} eventData
*/
collectEventData: function collectEventData(element, eventType, touches, ev) {
// find out pointerType
var pointerType = ionic.Gestures.POINTER_TOUCH;
if(ev.type.match(/mouse/) || ionic.Gestures.PointerEvent.matchType(ionic.Gestures.POINTER_MOUSE, ev)) {
pointerType = ionic.Gestures.POINTER_MOUSE;
}
return {
center : ionic.Gestures.utils.getCenter(touches),
timeStamp : new Date().getTime(),
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() {
if(this.srcEvent.preventManipulation) {
this.srcEvent.preventManipulation();
}
if(this.srcEvent.preventDefault) {
//this.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 ionic.Gestures.detection.stopDetect();
}
};
}
};
ionic.Gestures.PointerEvent = {
/**
* holds all pointers
* type {Object}
*/
pointers: {},
/**
* get a list of pointers
* @returns {Array} touchlist
*/
getTouchList: function() {
var self = this;
var touchlist = [];
// we can use forEach since pointerEvents only is in IE10
Object.keys(self.pointers).sort().forEach(function(id) {
touchlist.push(self.pointers[id]);
});
return touchlist;
},
/**
* update the position of a pointer
* @param {String} type ionic.Gestures.EVENT_END
* @param {Object} pointerEvent
*/
updatePointer: function(type, pointerEvent) {
if(type == ionic.Gestures.EVENT_END) {
this.pointers = {};
}
else {
pointerEvent.identifier = pointerEvent.pointerId;
this.pointers[pointerEvent.pointerId] = pointerEvent;
}
return Object.keys(this.pointers).length;
},
/**
* check if ev matches pointertype
* @param {String} pointerType ionic.Gestures.POINTER_MOUSE
* @param {PointerEvent} ev
*/
matchType: function(pointerType, ev) {
if(!ev.pointerType) {
return false;
}
var types = {};
types[ionic.Gestures.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == ionic.Gestures.POINTER_MOUSE);
types[ionic.Gestures.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == ionic.Gestures.POINTER_TOUCH);
types[ionic.Gestures.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == ionic.Gestures.POINTER_PEN);
return types[pointerType];
},
/**
* get events
*/
getEvents: function() {
return [
'pointerdown MSPointerDown',
'pointermove MSPointerMove',
'pointerup pointercancel MSPointerUp MSPointerCancel'
];
},
/**
* reset the list
*/
reset: function() {
this.pointers = {};
}
};
ionic.Gestures.utils = {
/**
* extend method,
* also used for cloning when dest is an empty object
* @param {Object} dest
* @param {Object} src
* @param {Boolean} merge do a merge
* @returns {Object} dest
*/
extend: function extend(dest, src, merge) {
for (var key in src) {
if(dest[key] !== undefined && merge) {
continue;
}
dest[key] = src[key];
}
return dest;
},
/**
* find if a node is in the given parent
* used for event delegation tricks
* @param {HTMLElement} node
* @param {HTMLElement} parent
* @returns {boolean} has_parent
*/
hasParent: function(node, parent) {
while(node){
if(node == parent) {
return true;
}
node = node.parentNode;
}
return false;
},
/**
* get the center of all the touches
* @param {Array} touches
* @returns {Object} center
*/
getCenter: function getCenter(touches) {
var valuesX = [], valuesY = [];
for(var t= 0,len=touches.length; t<len; t++) {
valuesX.push(touches[t].pageX);
valuesY.push(touches[t].pageY);
}
return {
pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
};
},
/**
* calculate the velocity between two points
* @param {Number} delta_time
* @param {Number} delta_x
* @param {Number} delta_y
* @returns {Object} velocity
*/
getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
return {
x: Math.abs(delta_x / delta_time) || 0,
y: Math.abs(delta_y / delta_time) || 0
};
},
/**
* calculate the angle between two coordinates
* @param {Touch} touch1
* @param {Touch} touch2
* @returns {Number} angle
*/
getAngle: function getAngle(touch1, touch2) {
var y = touch2.pageY - touch1.pageY,
x = touch2.pageX - touch1.pageX;
return Math.atan2(y, x) * 180 / Math.PI;
},
/**
* angle to direction define
* @param {Touch} touch1
* @param {Touch} touch2
* @returns {String} direction constant, like ionic.Gestures.DIRECTION_LEFT
*/
getDirection: function getDirection(touch1, touch2) {
var x = Math.abs(touch1.pageX - touch2.pageX),
y = Math.abs(touch1.pageY - touch2.pageY);
if(x >= y) {
return touch1.pageX - touch2.pageX > 0 ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
}
else {
return touch1.pageY - touch2.pageY > 0 ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
}
},
/**
* calculate the distance between two touches
* @param {Touch} touch1
* @param {Touch} touch2
* @returns {Number} distance
*/
getDistance: function getDistance(touch1, touch2) {
var x = touch2.pageX - touch1.pageX,
y = touch2.pageY - touch1.pageY;
return Math.sqrt((x*x) + (y*y));
},
/**
* calculate the scale factor between two touchLists (fingers)
* no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
* @param {Array} start
* @param {Array} end
* @returns {Number} scale
*/
getScale: function getScale(start, end) {
// need two fingers...
if(start.length >= 2 && end.length >= 2) {
return this.getDistance(end[0], end[1]) /
this.getDistance(start[0], start[1]);
}
return 1;
},
/**
* calculate the rotation degrees between two touchLists (fingers)
* @param {Array} start
* @param {Array} end
* @returns {Number} rotation
*/
getRotation: function getRotation(start, end) {
// need two fingers
if(start.length >= 2 && end.length >= 2) {
return this.getAngle(end[1], end[0]) -
this.getAngle(start[1], start[0]);
}
return 0;
},
/**
* boolean if the direction is vertical
* @param {String} direction
* @returns {Boolean} is_vertical
*/
isVertical: function isVertical(direction) {
return (direction == ionic.Gestures.DIRECTION_UP || direction == ionic.Gestures.DIRECTION_DOWN);
},
/**
* stop browser default behavior with css class
* @param {HtmlElement} element
* @param {Object} css_class
*/
stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_class) {
// changed from making many style changes to just adding a preset classname
// less DOM manipulations, less code, and easier to control in the CSS side of things
// hammer.js doesn't come with CSS, but ionic does, which is why we prefer this method
if(element && element.classList) {
element.classList.add(css_class);
element.onselectstart = function() {
return false;
};
}
}
};
ionic.Gestures.detection = {
// contains all registred ionic.Gestures.gestures in the correct order
gestures: [],
// data of the current ionic.Gestures.gesture detection session
current: null,
// the previous ionic.Gestures.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 ionic.Gestures.gesture detection
* @param {ionic.Gestures.Instance} inst
* @param {Object} eventData
*/
startDetect: function startDetect(inst, eventData) {
// already busy with a ionic.Gestures.gesture detection on an element
if(this.current) {
return;
}
this.stopped = false;
this.current = {
inst : inst, // reference to ionic.GesturesInstance we're working for
startEvent : ionic.Gestures.utils.extend({}, eventData), // start eventData for distances, timing etc
lastEvent : false, // last eventData
name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
};
this.detect(eventData);
},
/**
* ionic.Gestures.gesture detection
* @param {Object} eventData
*/
detect: function detect(eventData) {
if(!this.current || this.stopped) {
return;
}
// extend event data with calculations about scale, distance etc
eventData = this.extendEventData(eventData);
// instance options
var inst_options = this.current.inst.options;
// call ionic.Gestures.gesture handlers
for(var g=0,len=this.gestures.length; g<len; g++) {
var gesture = this.gestures[g];
// only when the instance options have enabled this gesture
if(!this.stopped && inst_options[gesture.name] !== false) {
// if a handler returns false, we stop with the detection
if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
this.stopDetect();
break;
}
}
}
// store as previous event event
if(this.current) {
this.current.lastEvent = eventData;
}
// endevent, but not the last touch, so dont stop
if(eventData.eventType == ionic.Gestures.EVENT_END && !eventData.touches.length-1) {
this.stopDetect();
}
return eventData;
},
/**
* clear the ionic.Gestures.gesture vars
* this is called on endDetect, but can also be used when a final ionic.Gestures.gesture has been detected
* to stop other ionic.Gestures.gestures from being fired
*/
stopDetect: function stopDetect() {
// clone current data to the store as the previous gesture
// used for the double tap gesture, since this is an other gesture detect session
this.previous = ionic.Gestures.utils.extend({}, this.current);
// reset the current
this.current = null;
// stopped!
this.stopped = true;
},
/**
* extend eventData for ionic.Gestures.gestures
* @param {Object} ev
* @returns {Object} ev
*/
extendEventData: function extendEventData(ev) {
var startEv = this.current.startEvent;
// if the touches change, set the new touches over the startEvent touches
// this because touchevents don't have all the touches on touchstart, or the
// user must place his fingers at the EXACT same time on the screen, which is not realistic
// but, sometimes it happens that both fingers are touching at the EXACT same time
if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
// extend 1 level deep to get the touchlist with the touch objects
startEv.touches = [];
for(var i=0,len=ev.touches.length; i<len; i++) {
startEv.touches.push(ionic.Gestures.utils.extend({}, ev.touches[i]));
}
}
var delta_time = ev.timeStamp - startEv.timeStamp,
delta_x = ev.center.pageX - startEv.center.pageX,
delta_y = ev.center.pageY - startEv.center.pageY,
velocity = ionic.Gestures.utils.getVelocity(delta_time, delta_x, delta_y);
ionic.Gestures.utils.extend(ev, {
deltaTime : delta_time,
deltaX : delta_x,
deltaY : delta_y,
velocityX : velocity.x,
velocityY : velocity.y,
distance : ionic.Gestures.utils.getDistance(startEv.center, ev.center),
angle : ionic.Gestures.utils.getAngle(startEv.center, ev.center),
direction : ionic.Gestures.utils.getDirection(startEv.center, ev.center),
scale : ionic.Gestures.utils.getScale(startEv.touches, ev.touches),
rotation : ionic.Gestures.utils.getRotation(startEv.touches, ev.touches),
startEvent : startEv
});
return ev;
},
/**
* register new gesture
* @param {Object} gesture object, see gestures.js for documentation
* @returns {Array} gestures
*/
register: function register(gesture) {
// add an enable gesture options if there is no given
var options = gesture.defaults || {};
if(options[gesture.name] === undefined) {
options[gesture.name] = true;
}
// extend ionic.Gestures default options with the ionic.Gestures.gesture options
ionic.Gestures.utils.extend(ionic.Gestures.defaults, options, true);
// set its index
gesture.index = gesture.index || 1000;
// add ionic.Gestures.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;
}
};
ionic.Gestures.gestures = ionic.Gestures.gestures || {};
/**
* Custom gestures
* ==============================
*
* Gesture object
* --------------------
* The object structure of a gesture:
*
* { name: 'mygesture',
* index: 1337,
* defaults: {
* mygesture_option: true
* }
* handler: function(type, ev, inst) {
* // trigger gesture event
* inst.trigger(this.name, ev);
* }
* }
* @param {String} name
* this should be the name of the gesture, lowercase
* it is also being used to disable/enable the gesture per instance config.
*
* @param {Number} [index=1000]
* the index of the gesture, where it is going to be in the stack of gestures detection
* like when you build an gesture that depends on the drag gesture, it is a good
* idea to place it after the index of the drag gesture.
*
* @param {Object} [defaults={}]
* the default settings of the gesture. these are added to the instance settings,
* and can be overruled per instance. you can also add the name of the gesture,
* but this is also added by default (and set to true).
*
* @param {Function} handler
* this handles the gesture detection of your custom gesture and receives the
* following arguments:
*
* @param {Object} eventData
* event data containing the following properties:
* timeStamp {Number} time the event occurred
* target {HTMLElement} target element
* touches {Array} touches (fingers, pointers, mouse) on the screen
* pointerType {String} kind of pointer that was used. matches ionic.Gestures.POINTER_MOUSE|TOUCH
* center {Object} center position of the touches. contains pageX and pageY
* deltaTime {Number} the total time of the touches in the screen
* deltaX {Number} the delta on x axis we haved moved
* deltaY {Number} the delta on y axis we haved moved
* velocityX {Number} the velocity on the x
* velocityY {Number} the velocity on y
* angle {Number} the angle we are moving
* direction {String} the direction we are moving. matches ionic.Gestures.DIRECTION_UP|DOWN|LEFT|RIGHT
* distance {Number} the distance we haved moved
* scale {Number} scaling of the touches, needs 2 touches
* rotation {Number} rotation of the touches, needs 2 touches *
* eventType {String} matches ionic.Gestures.EVENT_START|MOVE|END
* srcEvent {Object} the source event, like TouchStart or MouseDown *
* startEvent {Object} contains the same properties as above,
* but from the first touch. this is used to calculate
* distances, deltaTime, scaling etc
*
* @param {ionic.Gestures.Instance} inst
* the instance we are doing the detection for. you can get the options from
* the inst.options object and trigger the gesture event by calling inst.trigger
*
*
* Handle gestures
* --------------------
* inside the handler you can get/set ionic.Gestures.detectionic.current. This is the current
* detection sessionic. It has the following properties
* @param {String} name
* contains the name of the gesture we have detected. it has not a real function,
* only to check in other gestures if something is detected.
* like in the drag gesture we set it to 'drag' and in the swipe gesture we can
* check if the current gesture is 'drag' by accessing ionic.Gestures.detectionic.current.name
*
* readonly
* @param {ionic.Gestures.Instance} inst
* the instance we do the detection for
*
* readonly
* @param {Object} startEvent
* contains the properties of the first gesture detection in this sessionic.
* Used for calculations about timing, distance, etc.
*
* readonly
* @param {Object} lastEvent
* contains all the properties of the last gesture detect in this sessionic.
*
* after the gesture detection session has been completed (user has released the screen)
* the ionic.Gestures.detectionic.current object is copied into ionic.Gestures.detectionic.previous,
* this is usefull for gestures like doubletap, where you need to know if the
* previous gesture was a tap
*
* options that have been set by the instance can be received by calling inst.options
*
* You can trigger a gesture event by calling inst.trigger("mygesture", event).
* The first param is the name of your gesture, the second the event argument
*
*
* Register gestures
* --------------------
* When an gesture is added to the ionic.Gestures.gestures object, it is auto registered
* at the setup of the first ionic.Gestures instance. You can also call ionic.Gestures.detectionic.register
* manually and pass your gesture object as a param
*
*/
/**
* Hold
* Touch stays at the same place for x time
* events hold
*/
ionic.Gestures.gestures.Hold = {
name: 'hold',
index: 10,
defaults: {
hold_timeout : 500,
hold_threshold : 1
},
timer: null,
handler: function holdGesture(ev, inst) {
switch(ev.eventType) {
case ionic.Gestures.EVENT_START:
// clear any running timers
clearTimeout(this.timer);
// set the gesture so we can check in the timeout if it still is
ionic.Gestures.detection.current.name = this.name;
// set timer and if after the timeout it still is hold,
// we trigger the hold event
this.timer = setTimeout(function() {
if(ionic.Gestures.detection.current.name == 'hold') {
ionic.tap.cancelClick();
inst.trigger('hold', ev);
}
}, inst.options.hold_timeout);
break;
// when you move or end we clear the timer
case ionic.Gestures.EVENT_MOVE:
if(ev.distance > inst.options.hold_threshold) {
clearTimeout(this.timer);
}
break;
case ionic.Gestures.EVENT_END:
clearTimeout(this.timer);
break;
}
}
};
/**
* Tap/DoubleTap
* Quick touch at a place or double at the same place
* events tap, doubletap
*/
ionic.Gestures.gestures.Tap = {
name: 'tap',
index: 100,
defaults: {
tap_max_touchtime : 250,
tap_max_distance : 10,
tap_always : true,
doubletap_distance : 20,
doubletap_interval : 300
},
handler: function tapGesture(ev, inst) {
if(ev.eventType == ionic.Gestures.EVENT_END && ev.srcEvent.type != 'touchcancel') {
// previous gesture, for the double tap since these are two different gesture detections
var prev = ionic.Gestures.detection.previous,
did_doubletap = false;
// when the touchtime is higher then the max touch time
// or when the moving distance is too much
if(ev.deltaTime > inst.options.tap_max_touchtime ||
ev.distance > inst.options.tap_max_distance) {
return;
}
// check if double tap
if(prev && prev.name == 'tap' &&
(ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
ev.distance < inst.options.doubletap_distance) {
inst.trigger('doubletap', ev);
did_doubletap = true;
}
// do a single tap
if(!did_doubletap || inst.options.tap_always) {
ionic.tap.cancelClick();
ionic.Gestures.detection.current.name = 'tap';
inst.trigger('tap', ev);
}
}
}
};
/**
* Swipe
* triggers swipe events when the end velocity is above the threshold
* events swipe, swipeleft, swiperight, swipeup, swipedown
*/
ionic.Gestures.gestures.Swipe = {
name: 'swipe',
index: 40,
defaults: {
// set 0 for unlimited, but this can conflict with transform
swipe_max_touches : 1,
swipe_velocity : 0.7
},
handler: function swipeGesture(ev, inst) {
if(ev.eventType == ionic.Gestures.EVENT_END) {
// max touches
if(inst.options.swipe_max_touches > 0 &&
ev.touches.length > inst.options.swipe_max_touches) {
return;
}
// when the distance we moved is too small we skip this gesture
// or we can be already in dragging
if(ev.velocityX > inst.options.swipe_velocity ||
ev.velocityY > inst.options.swipe_velocity) {
// trigger swipe events
inst.trigger(this.name, ev);
inst.trigger(this.name + ev.direction, ev);
}
}
}
};
/**
* Drag
* Move with x fingers (default 1) around on the page. Blocking the scrolling when
* moving left and right is a good practice. When all the drag events are blocking
* you disable scrolling on that area.
* events drag, drapleft, dragright, dragup, dragdown
*/
ionic.Gestures.gestures.Drag = {
name: 'drag',
index: 50,
defaults: {
drag_min_distance : 10,
// Set correct_for_drag_min_distance to true to make the starting point of the drag
// be calculated from where the drag was triggered, not from where the touch started.
// Useful to avoid a jerk-starting drag, which can make fine-adjustments
// through dragging difficult, and be visually unappealing.
correct_for_drag_min_distance : true,
// set 0 for unlimited, but this can conflict with transform
drag_max_touches : 1,
// prevent default browser behavior when dragging occurs
// be careful with it, it makes the element a blocking element
// when you are using the drag gesture, it is a good practice to set this true
drag_block_horizontal : true,
drag_block_vertical : true,
// drag_lock_to_axis keeps the drag gesture on the axis that it started on,
// It disallows vertical directions if the initial direction was horizontal, and vice versa.
drag_lock_to_axis : false,
// drag lock only kicks in when distance > drag_lock_min_distance
// This way, locking occurs only when the distance has become large enough to reliably determine the direction
drag_lock_min_distance : 25
},
triggered: false,
handler: function dragGesture(ev, inst) {
// current gesture isnt drag, but dragged is true
// this means an other gesture is busy. now call dragend
if(ionic.Gestures.detection.current.name != this.name && this.triggered) {
inst.trigger(this.name +'end', ev);
this.triggered = false;
return;
}
// max touches
if(inst.options.drag_max_touches > 0 &&
ev.touches.length > inst.options.drag_max_touches) {
return;
}
switch(ev.eventType) {
case ionic.Gestures.EVENT_START:
this.triggered = false;
break;
case ionic.Gestures.EVENT_MOVE:
// when the distance we moved is too small we skip this gesture
// or we can be already in dragging
if(ev.distance < inst.options.drag_min_distance &&
ionic.Gestures.detection.current.name != this.name) {
return;
}
// we are dragging!
if(ionic.Gestures.detection.current.name != this.name) {
ionic.Gestures.detection.current.name = this.name;
if (inst.options.correct_for_drag_min_distance) {
// When a drag is triggered, set the event center to drag_min_distance pixels from the original event center.
// Without this correction, the dragged distance would jumpstart at drag_min_distance pixels instead of at 0.
// It might be useful to save the original start point somewhere
var factor = Math.abs(inst.options.drag_min_distance/ev.distance);
ionic.Gestures.detection.current.startEvent.center.pageX += ev.deltaX * factor;
ionic.Gestures.detection.current.startEvent.center.pageY += ev.deltaY * factor;
// recalculate event data using new start point
ev = ionic.Gestures.detection.extendEventData(ev);
}
}
// lock drag to axis?
if(ionic.Gestures.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
ev.drag_locked_to_axis = true;
}
var last_direction = ionic.Gestures.detection.current.lastEvent.direction;
if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
// keep direction on the axis that the drag gesture started on
if(ionic.Gestures.utils.isVertical(last_direction)) {
ev.direction = (ev.deltaY < 0) ? ionic.Gestures.DIRECTION_UP : ionic.Gestures.DIRECTION_DOWN;
}
else {
ev.direction = (ev.deltaX < 0) ? ionic.Gestures.DIRECTION_LEFT : ionic.Gestures.DIRECTION_RIGHT;
}
}
// first time, trigger dragstart event
if(!this.triggered) {
inst.trigger(this.name +'start', ev);
this.triggered = true;
}
// trigger normal event
inst.trigger(this.name, ev);
// direction event, like dragdown
inst.trigger(this.name + ev.direction, ev);
// block the browser events
if( (inst.options.drag_block_vertical && ionic.Gestures.utils.isVertical(ev.direction)) ||
(inst.options.drag_block_horizontal && !ionic.Gestures.utils.isVertical(ev.direction))) {
ev.preventDefault();
}
break;
case ionic.Gestures.EVENT_END:
// trigger dragend
if(this.triggered) {
inst.trigger(this.name +'end', ev);
}
this.triggered = false;
break;
}
}
};
/**
* Transform
* User want to scale or rotate with 2 fingers
* events transform, pinch, pinchin, pinchout, rotate
*/
ionic.Gestures.gestures.Transform = {
name: 'transform',
index: 45,
defaults: {
// factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
transform_min_scale : 0.01,
// rotation in degrees
transform_min_rotation : 1,
// prevent default browser behavior when two touches are on the screen
// but it makes the element a blocking element
// when you are using the transform gesture, it is a good practice to set this true
transform_always_block : false
},
triggered: false,
handler: function transformGesture(ev, inst) {
// current gesture isnt drag, but dragged is true
// this means an other gesture is busy. now call dragend
if(ionic.Gestures.detection.current.name != this.name && this.triggered) {
inst.trigger(this.name +'end', ev);
this.triggered = false;
return;
}
// atleast multitouch
if(ev.touches.length < 2) {
return;
}
// prevent default when two fingers are on the screen
if(inst.options.transform_always_block) {
ev.preventDefault();
}
switch(ev.eventType) {
case ionic.Gestures.EVENT_START:
this.triggered = false;
break;
case ionic.Gestures.EVENT_MOVE:
var scale_threshold = Math.abs(1-ev.scale);
var rotation_threshold = Math.abs(ev.rotation);
// when the distance we moved is too small we skip this gesture
// or we can be already in dragging
if(scale_threshold < inst.options.transform_min_scale &&
rotation_threshold < inst.options.transform_min_rotation) {
return;
}
// we are transforming!
ionic.Gestures.detection.current.name = this.name;
// first time, trigger dragstart event
if(!this.triggered) {
inst.trigger(this.name +'start', ev);
this.triggered = true;
}
inst.trigger(this.name, ev); // basic transform event
// trigger rotate event
if(rotation_threshold > inst.options.transform_min_rotation) {
inst.trigger('rotate', ev);
}
// trigger pinch event
if(scale_threshold > inst.options.transform_min_scale) {
inst.trigger('pinch', ev);
inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
}
break;
case ionic.Gestures.EVENT_END:
// trigger dragend
if(this.triggered) {
inst.trigger(this.name +'end', ev);
}
this.triggered = false;
break;
}
}
};
/**
* Touch
* Called as first, tells the user has touched the screen
* events touch
*/
ionic.Gestures.gestures.Touch = {
name: 'touch',
index: -Infinity,
defaults: {
// call preventDefault at touchstart, and makes the element blocking by
// disabling the scrolling of the page, but it improves gestures like
// transforming and dragging.
// be careful with using this, it can be very annoying for users to be stuck
// on the page
prevent_default: false,
// disable mouse events, so only touch (or pen!) input triggers events
prevent_mouseevents: false
},
handler: function touchGesture(ev, inst) {
if(inst.options.prevent_mouseevents && ev.pointerType == ionic.Gestures.POINTER_MOUSE) {
ev.stopDetect();
return;
}
if(inst.options.prevent_default) {
ev.preventDefault();
}
if(ev.eventType == ionic.Gestures.EVENT_START) {
inst.trigger(this.name, ev);
}
}
};
/**
* Release
* Called as last, tells the user has released the screen
* events release
*/
ionic.Gestures.gestures.Release = {
name: 'release',
index: Infinity,
handler: function releaseGesture(ev, inst) {
if(ev.eventType == ionic.Gestures.EVENT_END) {
inst.trigger(this.name, ev);
}
}
};
})(window.ionic);