toloframework
Version:
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
1,588 lines (1,372 loc) • 84.9 kB
JavaScript
'use strict';
/**
* ! Hammer.JS - v2.0.8 - 2016-04-23
* http://hammerjs.github.io/
*
* Copyright (c) 2016 Jorik Tangelder;
* Licensed under the MIT license */
/* eslint max-statements: ["error", 20, { "ignoreTopLevelFunctions": true }] */
/* eslint wrap-iife: 0 */
( function ( window, document, exportName, undefined ) {
const
VENDOR_PREFIXES = [ '', 'webkit', 'Moz', 'MS', 'ms', 'o' ],
TEST_ELEMENT = document.createElement( 'div' ),
TYPE_FUNCTION = 'function';
const
round = Math.round,
abs = Math.abs,
now = Date.now;
/**
* set a timeout with a given scope
* @param {Function} fn
* @param {Number} timeout
* @param {Object} context
* @returns {number}
*/
function setTimeoutContext( fn, timeout, context ) {
return setTimeout( bindFn( fn, context ), timeout );
}
/**
* if the argument is an array, we want to execute the fn on each entry
* if it aint an array we don't want to do a thing.
* this is used by all the methods that accept a single and array argument.
* @param {*|Array} arg
* @param {String} fn
* @param {Object} [context]
* @returns {Boolean}
*/
function invokeArrayArg( arg, fn, context ) {
if ( Array.isArray( arg ) ) {
each( arg, context[ fn ], context );
return true;
}
return false;
}
/**
* walk objects and arrays
* @param {Object} obj
* @param {Function} iterator
* @param {Object} context
*/
function each( obj, iterator, context ) {
var i;
if ( !obj ) {
return;
}
if ( obj.forEach ) {
obj.forEach( iterator, context );
} else if ( obj.length !== undefined ) {
i = 0;
while ( i < obj.length ) {
iterator.call( context, obj[ i ], i, obj );
i++;
}
} else {
for ( i in obj ) {
obj.hasOwnProperty( i ) && iterator.call( context, obj[ i ], i, obj );
}
}
}
/**
* wrap a method with a deprecation warning and stack trace
* @param {Function} method
* @param {String} name
* @param {String} message
* @returns {Function} A new function wrapping the supplied method.
*/
function deprecate( method, name, message ) {
var deprecationMessage = 'DEPRECATED METHOD: ' + name + '\n' + message + ' AT \n';
return function () {
var e = new Error( 'get-stack-trace' );
var stack = e && e.stack ? e.stack.replace( /^[^\(]+?[\n$]/gm, '' )
.replace( /^\s+at\s+/gm, '' )
.replace( /^Object.<anonymous>\s*\(/gm, '{anonymous}()@' ) : 'Unknown Stack Trace';
var log = window.console && ( window.console.warn || window.console.log );
if ( log ) {
log.call( window.console, deprecationMessage, stack );
}
return method.apply( this, arguments );
};
}
/**
* extend object.
* means that properties in dest will be overwritten by the ones in src.
* @param {Object} target
* @param {...Object} objects_to_assign
* @returns {Object} target
*/
let assign;
if ( typeof Object.assign !== 'function' ) {
assign = function assign( target ) {
if ( target === undefined || target === null ) {
throw new TypeError( 'Cannot convert undefined or null to object' );
}
var output = Object( target );
for ( var index = 1; index < arguments.length; index++ ) {
var source = arguments[ index ];
if ( source !== undefined && source !== null ) {
for ( var nextKey in source ) {
if ( source.hasOwnProperty( nextKey ) ) {
output[ nextKey ] = source[ nextKey ];
}
}
}
}
return output;
};
} else {
assign = Object.assign;
}
/**
* extend object.
* means that properties in dest will be overwritten by the ones in src.
* @param {Object} dest
* @param {Object} src
* @param {Boolean} [merge=false]
* @returns {Object} dest
*/
var extend = deprecate( function extend( dest, src, merge ) {
var keys = Object.keys( src );
var i = 0;
while ( i < keys.length ) {
if ( !merge || ( merge && dest[ keys[ i ] ] === undefined ) ) {
dest[ keys[ i ] ] = src[ keys[ i ] ];
}
i++;
}
return dest;
}, 'extend', 'Use `assign`.' );
/**
* merge the values from src in the dest.
* means that properties that exist in dest will not be overwritten by src
* @param {Object} dest
* @param {Object} src
* @returns {Object} dest
*/
var merge = deprecate( function merge( dest, src ) {
return extend( dest, src, true );
}, 'merge', 'Use `assign`.' );
/**
* simple class inheritance
* @param {Function} child
* @param {Function} base
* @param {Object} [properties]
*/
function inherit( child, base, properties ) {
const
baseP = base.prototype,
childP = child.prototype = Object.create( baseP );
childP.constructor = child;
childP._super = baseP;
if ( properties ) {
assign( childP, properties );
}
}
/**
* simple function bind
* @param {Function} fn
* @param {Object} context
* @returns {Function}
*/
function bindFn( fn, context ) {
return function boundFn() {
return fn.apply( context, arguments );
};
}
/**
* let a boolean value also be a function that must return a boolean
* this first item in args will be used as the context
* @param {Boolean|Function} val
* @param {Array} [args]
* @returns {Boolean}
*/
function boolOrFn( val, args ) {
if ( typeof val == TYPE_FUNCTION ) {
return val.apply( args ? args[ 0 ] || undefined : undefined, args );
}
return val;
}
/**
* use the val2 when val1 is undefined
* @param {*} val1
* @param {*} val2
* @returns {*}
*/
function ifUndefined( val1, val2 ) {
return ( val1 === undefined ) ? val2 : val1;
}
/**
* addEventListener with multiple events at once
* @param {EventTarget} target
* @param {String} types
* @param {Function} handler
*/
function addEventListeners( target, types, handler ) {
each( splitStr( types ), function ( type ) {
target.addEventListener( type, handler, false );
} );
}
/**
* removeEventListener with multiple events at once
* @param {EventTarget} target
* @param {String} types
* @param {Function} handler
*/
function removeEventListeners( target, types, handler ) {
each( splitStr( types ), function ( type ) {
target.removeEventListener( type, handler, false );
} );
}
/**
* find if a node is in the given parent
* @method hasParent
* @param {HTMLElement} node
* @param {HTMLElement} parent
* @return {Boolean} found
*/
function hasParent( node, parent ) {
while ( node ) {
if ( node == parent ) {
return true;
}
node = node.parentNode;
}
return false;
}
/**
* small indexOf wrapper
* @param {String} str
* @param {String} find
* @returns {Boolean} found
*/
function inStr( str, find ) {
return str.indexOf( find ) > -1;
}
/**
* split string on whitespace
* @param {String} str
* @returns {Array} words
*/
function splitStr( str ) {
return str.trim().split( /\s+/g );
}
/**
* find if a array contains the object using indexOf or a simple polyFill
* @param {Array} src
* @param {String} find
* @param {String} [findByKey]
* @return {Boolean|Number} false when not found, or the index
*/
function inArray( src, find, findByKey ) {
if ( src.indexOf && !findByKey ) {
return src.indexOf( find );
} else {
var i = 0;
while ( i < src.length ) {
if ( ( findByKey && src[ i ][ findByKey ] == find ) || ( !findByKey && src[ i ] === find ) ) {
return i;
}
i++;
}
return -1;
}
}
/**
* convert array-like objects to real arrays
* @param {Object} obj
* @returns {Array}
*/
function toArray( obj ) {
return Array.prototype.slice.call( obj, 0 );
}
/**
* unique array with objects based on a key (like 'id') or just by the array's value
* @param {Array} src [{id:1},{id:2},{id:1}]
* @param {String} [key]
* @param {Boolean} [sort=False]
* @returns {Array} [{id:1},{id:2}]
*/
function uniqueArray( src, key, sort ) {
var results = [];
var values = [];
var i = 0;
while ( i < src.length ) {
var val = key ? src[ i ][ key ] : src[ i ];
if ( inArray( values, val ) < 0 ) {
results.push( src[ i ] );
}
values[ i ] = val;
i++;
}
if ( sort ) {
if ( !key ) {
results = results.sort();
} else {
results = results.sort( function sortUniqueArray( a, b ) {
return a[ key ] > b[ key ];
} );
}
}
return results;
}
/**
* get the prefixed property
* @param {Object} obj
* @param {String} property
* @returns {String|Undefined} prefixed
*/
function prefixed( obj, property ) {
const camelProp = property[ 0 ].toUpperCase() + property.slice( 1 );
let i = 0;
while ( i < VENDOR_PREFIXES.length ) {
const
prefix = VENDOR_PREFIXES[ i ],
prop = ( prefix ) ? prefix + camelProp : property;
if ( prop in obj ) {
return prop;
}
i++;
}
return undefined;
}
/**
* get a unique id
* @returns {number} uniqueId
*/
let _uniqueId = 1;
function uniqueId() {
return _uniqueId++;
}
/**
* get the window object of an element
* @param {HTMLElement} element
* @returns {DocumentView|Window}
*/
function getWindowForElement( element ) {
var doc = element.ownerDocument || element;
return ( doc.defaultView || doc.parentWindow || window );
}
const MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
const
SUPPORT_TOUCH = ( 'ontouchstart' in window ),
SUPPORT_POINTER_EVENTS = prefixed( window, 'PointerEvent' ) !== undefined,
SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test( navigator.userAgent );
const
INPUT_TYPE_TOUCH = 'touch',
INPUT_TYPE_PEN = 'pen',
INPUT_TYPE_MOUSE = 'mouse',
INPUT_TYPE_KINECT = 'kinect';
const COMPUTE_INTERVAL = 25;
const
INPUT_START = 1,
INPUT_MOVE = 2,
INPUT_END = 4,
INPUT_CANCEL = 8;
const
DIRECTION_NONE = 1,
DIRECTION_LEFT = 2,
DIRECTION_RIGHT = 4,
DIRECTION_UP = 8,
DIRECTION_DOWN = 16;
const
DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT,
DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN,
DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
const
PROPS_XY = [ 'x', 'y' ],
PROPS_CLIENT_XY = [ 'clientX', 'clientY' ];
/**
* create new input type manager
* @param {Manager} manager
* @param {Function} callback
* @returns {Input}
* @constructor
*/
function Input( manager, callback ) {
const that = this;
this.manager = manager;
this.callback = callback;
this.element = manager.element;
this.target = manager.options.inputTarget;
// smaller wrapper around the handler, for the scope and the enabled state of the manager,
// so when disabled the input events are completely bypassed.
this.domHandler = function ( ev ) {
if ( boolOrFn( manager.options.enable, [ manager ] ) ) {
that.handler( ev );
}
};
this.init();
}
Input.prototype = {
/**
* should handle the inputEvent data and trigger the callback
* @virtual
*/
handler() {},
/**
* bind the events
*/
init() {
this.evEl && addEventListeners( this.element, this.evEl, this.domHandler );
this.evTarget && addEventListeners( this.target, this.evTarget, this.domHandler );
this.evWin && addEventListeners( getWindowForElement( this.element ), this.evWin, this.domHandler );
},
/**
* unbind the events
*/
destroy() {
this.evEl && removeEventListeners( this.element, this.evEl, this.domHandler );
this.evTarget && removeEventListeners( this.target, this.evTarget, this.domHandler );
this.evWin && removeEventListeners( getWindowForElement( this.element ), this.evWin, this.domHandler );
}
};
/**
* create new input type manager
* called by the Manager constructor
* @param {Hammer} manager
* @returns {Input}
*/
function createInputInstance( manager ) {
var Type;
var inputClass = manager.options.inputClass;
if ( inputClass ) {
Type = inputClass;
} else if ( SUPPORT_POINTER_EVENTS ) {
Type = PointerEventInput;
} else if ( SUPPORT_ONLY_TOUCH ) {
Type = TouchInput;
} else if ( !SUPPORT_TOUCH ) {
Type = MouseInput;
} else {
Type = TouchMouseInput;
}
return new( Type )( manager, inputHandler );
}
/**
* handle input events
* @param {Manager} manager
* @param {String} eventType
* @param {Object} input
*/
function inputHandler( manager, eventType, input ) {
var pointersLen = input.pointers.length;
var changedPointersLen = input.changedPointers.length;
var isFirst = ( eventType & INPUT_START && ( pointersLen - changedPointersLen === 0 ) );
var isFinal = ( eventType & ( INPUT_END | INPUT_CANCEL ) && ( pointersLen - changedPointersLen === 0 ) );
input.isFirst = !!isFirst;
input.isFinal = !!isFinal;
if ( isFirst ) {
manager.session = {};
}
// source event is the normalized value of the domEvents
// like 'touchstart, mouseup, pointerdown'
input.eventType = eventType;
// compute scale, rotation etc
computeInputData( manager, input );
// emit secret event
manager.emit( 'hammer.input', input );
manager.recognize( input );
manager.session.prevInput = input;
}
/**
* extend the data with some usable properties like scale, rotate, velocity etc
* @param {Object} manager
* @param {Object} input
*/
function computeInputData( manager, input ) {
const
session = manager.session,
pointers = input.pointers,
pointersLength = pointers.length;
// store the first input to calculate the distance and direction
if ( !session.firstInput ) {
session.firstInput = simpleCloneInputData( input );
}
// to compute scale and rotation we need to store the multiple touches
if ( pointersLength > 1 && !session.firstMultiple ) {
session.firstMultiple = simpleCloneInputData( input );
} else if ( pointersLength === 1 ) {
session.firstMultiple = false;
}
const
firstInput = session.firstInput,
firstMultiple = session.firstMultiple,
offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center;
const center = input.center = getCenter( pointers );
input.timeStamp = now();
input.deltaTime = input.timeStamp - firstInput.timeStamp;
input.angle = getAngle( offsetCenter, center );
input.distance = getDistance( offsetCenter, center );
computeDeltaXY( session, input );
input.offsetDirection = getDirection( input.deltaX, input.deltaY );
const overallVelocity = getVelocity( input.deltaTime, input.deltaX, input.deltaY );
input.overallVelocityX = overallVelocity.x;
input.overallVelocityY = overallVelocity.y;
input.overallVelocity = ( abs( overallVelocity.x ) > abs( overallVelocity.y ) ) ? overallVelocity.x : overallVelocity.y;
input.scale = firstMultiple ? getScale( firstMultiple.pointers, pointers ) : 1;
input.rotation = firstMultiple ? getRotation( firstMultiple.pointers, pointers ) : 0;
input.maxPointers = !session.prevInput ? input.pointers.length : ( ( input.pointers.length >
session.prevInput.maxPointers ) ? input.pointers.length : session.prevInput.maxPointers );
computeIntervalInputData( session, input );
// find the correct target
var target = manager.element;
if ( hasParent( input.srcEvent.target, target ) ) {
target = input.srcEvent.target;
}
input.target = target;
}
function computeDeltaXY( session, input ) {
let
center = input.center,
offset = session.offsetDelta || {},
prevDelta = session.prevDelta || {},
prevInput = session.prevInput || {};
if ( input.eventType === INPUT_START || prevInput.eventType === INPUT_END ) {
prevDelta = session.prevDelta = {
x: prevInput.deltaX || 0,
y: prevInput.deltaY || 0
};
offset = session.offsetDelta = {
x: center.x,
y: center.y
};
}
input.deltaX = prevDelta.x + ( center.x - offset.x );
input.deltaY = prevDelta.y + ( center.y - offset.y );
}
/**
* velocity is calculated every x ms
* @param {Object} session
* @param {Object} input
*/
function computeIntervalInputData( session, input ) {
const
last = session.lastInterval || input,
deltaTime = input.timeStamp - last.timeStamp;
let velocity, velocityX, velocityY, direction;
if ( input.eventType !== INPUT_CANCEL && ( deltaTime > COMPUTE_INTERVAL || last.velocity === undefined ) ) {
const
deltaX = input.deltaX - last.deltaX,
deltaY = input.deltaY - last.deltaY,
v = getVelocity( deltaTime, deltaX, deltaY );
velocityX = v.x;
velocityY = v.y;
velocity = ( abs( v.x ) > abs( v.y ) ) ? v.x : v.y;
direction = getDirection( deltaX, deltaY );
session.lastInterval = input;
} else {
// use latest velocity info if it doesn't overtake a minimum period
velocity = last.velocity;
velocityX = last.velocityX;
velocityY = last.velocityY;
direction = last.direction;
}
input.velocity = velocity;
input.velocityX = velocityX;
input.velocityY = velocityY;
input.direction = direction;
}
/**
* create a simple clone from the input used for storage of firstInput and firstMultiple
* @param {Object} input
* @returns {Object} clonedInputData
*/
function simpleCloneInputData( input ) {
// make a simple copy of the pointers because we will get a reference if we don't
// we only need clientXY for the calculations
var pointers = [];
var i = 0;
while ( i < input.pointers.length ) {
pointers[ i ] = {
clientX: round( input.pointers[ i ].clientX ),
clientY: round( input.pointers[ i ].clientY )
};
i++;
}
return {
timeStamp: now(),
pointers: pointers,
center: getCenter( pointers ),
deltaX: input.deltaX,
deltaY: input.deltaY
};
}
/**
* get the center of all the pointers
* @param {Array} pointers
* @return {Object} center contains `x` and `y` properties
*/
function getCenter( pointers ) {
var pointersLength = pointers.length;
// no need to loop when only one touch
if ( pointersLength === 1 ) {
return {
x: round( pointers[ 0 ].clientX ),
y: round( pointers[ 0 ].clientY )
};
}
var x = 0,
y = 0,
i = 0;
while ( i < pointersLength ) {
x += pointers[ i ].clientX;
y += pointers[ i ].clientY;
i++;
}
return {
x: round( x / pointersLength ),
y: round( y / pointersLength )
};
}
/**
* calculate the velocity between two points. unit is in px per ms.
* @param {Number} deltaTime
* @param {Number} x
* @param {Number} y
* @return {Object} velocity `x` and `y`
*/
function getVelocity( deltaTime, x, y ) {
return {
x: x / deltaTime || 0,
y: y / deltaTime || 0
};
}
/**
* get the direction between two points
* @param {Number} x
* @param {Number} y
* @return {Number} direction
*/
function getDirection( x, y ) {
if ( x === y ) {
return DIRECTION_NONE;
}
if ( abs( x ) >= abs( y ) ) {
return x < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
}
return y < 0 ? DIRECTION_UP : DIRECTION_DOWN;
}
/**
* calculate the absolute distance between two points
* @param {Object} p1 {x, y}
* @param {Object} p2 {x, y}
* @param {Array} [props] containing x and y keys
* @return {Number} distance
*/
function getDistance( p1, p2, props ) {
if ( !props ) {
props = PROPS_XY;
}
var x = p2[ props[ 0 ] ] - p1[ props[ 0 ] ],
y = p2[ props[ 1 ] ] - p1[ props[ 1 ] ];
return Math.sqrt( ( x * x ) + ( y * y ) );
}
/**
* calculate the angle between two coordinates
* @param {Object} p1
* @param {Object} p2
* @param {Array} [props] containing x and y keys
* @return {Number} angle
*/
function getAngle( p1, p2, props ) {
if ( !props ) {
props = PROPS_XY;
}
var x = p2[ props[ 0 ] ] - p1[ props[ 0 ] ],
y = p2[ props[ 1 ] ] - p1[ props[ 1 ] ];
return Math.atan2( y, x ) * 180 / Math.PI;
}
/**
* calculate the rotation degrees between two pointersets
* @param {Array} start array of pointers
* @param {Array} end array of pointers
* @return {Number} rotation
*/
function getRotation( start, end ) {
return getAngle( end[ 1 ], end[ 0 ], PROPS_CLIENT_XY ) + getAngle( start[ 1 ], start[ 0 ], PROPS_CLIENT_XY );
}
/**
* calculate the scale factor between two pointersets
* no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
* @param {Array} start array of pointers
* @param {Array} end array of pointers
* @return {Number} scale
*/
function getScale( start, end ) {
return getDistance( end[ 0 ], end[ 1 ], PROPS_CLIENT_XY ) / getDistance( start[ 0 ], start[ 1 ], PROPS_CLIENT_XY );
}
var MOUSE_INPUT_MAP = {
mousedown: INPUT_START,
mousemove: INPUT_MOVE,
mouseup: INPUT_END
};
var MOUSE_ELEMENT_EVENTS = 'mousedown';
var MOUSE_WINDOW_EVENTS = 'mousemove mouseup';
/**
* Mouse events input
* @constructor
* @extends Input
*/
function MouseInput() {
this.evEl = MOUSE_ELEMENT_EVENTS;
this.evWin = MOUSE_WINDOW_EVENTS;
this.pressed = false; // mousedown state
Input.apply( this, arguments );
}
inherit( MouseInput, Input, {
/**
* handle mouse events
* @param {Object} ev
*/
handler: function MEhandler( ev ) {
var eventType = MOUSE_INPUT_MAP[ ev.type ];
// on start we want to have the left mouse button down
if ( eventType & INPUT_START && ev.button !== 2 ) {
this.pressed = true;
}
if ( eventType & INPUT_MOVE && ev.which !== 1 ) {
eventType = INPUT_END;
}
// mouse must be down
if ( !this.pressed ) {
return;
}
if ( eventType & INPUT_END ) {
this.pressed = false;
}
this.callback( this.manager, eventType, {
pointers: [ ev ],
changedPointers: [ ev ],
pointerType: INPUT_TYPE_MOUSE,
srcEvent: ev
} );
}
} );
var POINTER_INPUT_MAP = {
pointerdown: INPUT_START,
pointermove: INPUT_MOVE,
pointerup: INPUT_END,
pointercancel: INPUT_CANCEL,
pointerout: INPUT_CANCEL
};
// in IE10 the pointer types is defined as an enum
var IE10_POINTER_TYPE_ENUM = {
2: INPUT_TYPE_TOUCH,
3: INPUT_TYPE_PEN,
4: INPUT_TYPE_MOUSE,
5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816
};
var POINTER_ELEMENT_EVENTS = 'pointerdown';
var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel';
// IE10 has prefixed support, and case-sensitive
if ( window.MSPointerEvent && !window.PointerEvent ) {
POINTER_ELEMENT_EVENTS = 'MSPointerDown';
POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel';
}
/**
* Pointer events input
* @constructor
* @extends Input
*/
function PointerEventInput() {
this.evEl = POINTER_ELEMENT_EVENTS;
this.evWin = POINTER_WINDOW_EVENTS;
Input.apply( this, arguments );
this.store = ( this.manager.session.pointerEvents = [] );
}
inherit( PointerEventInput, Input, {
/**
* handle mouse events
* @param {Object} ev
*/
handler: function PEhandler( ev ) {
var store = this.store;
var removePointer = false;
var eventTypeNormalized = ev.type.toLowerCase().replace( 'ms', '' );
var eventType = POINTER_INPUT_MAP[ eventTypeNormalized ];
var pointerType = IE10_POINTER_TYPE_ENUM[ ev.pointerType ] || ev.pointerType;
var isTouch = ( pointerType == INPUT_TYPE_TOUCH );
// get index of the event in the store
var storeIndex = inArray( store, ev.pointerId, 'pointerId' );
// start and mouse must be down
if ( eventType & INPUT_START && ( ev.button !== 2 || isTouch ) ) {
if ( storeIndex < 0 ) {
store.push( ev );
storeIndex = store.length - 1;
}
} else if ( eventType & ( INPUT_END | INPUT_CANCEL ) ) {
removePointer = true;
}
// it not found, so the pointer hasn't been down (so it's probably a hover)
if ( storeIndex < 0 ) {
return;
}
// update the event in the store
store[ storeIndex ] = ev;
this.callback( this.manager, eventType, {
pointers: store,
changedPointers: [ ev ],
pointerType: pointerType,
srcEvent: ev
} );
if ( removePointer ) {
// remove from the store
store.splice( storeIndex, 1 );
}
}
} );
var SINGLE_TOUCH_INPUT_MAP = {
touchstart: INPUT_START,
touchmove: INPUT_MOVE,
touchend: INPUT_END,
touchcancel: INPUT_CANCEL
};
var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart';
var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel';
/**
* Touch events input
* @constructor
* @extends Input
*/
function SingleTouchInput() {
this.evTarget = SINGLE_TOUCH_TARGET_EVENTS;
this.evWin = SINGLE_TOUCH_WINDOW_EVENTS;
this.started = false;
Input.apply( this, arguments );
}
inherit( SingleTouchInput, Input, {
handler: function TEhandler( ev ) {
var type = SINGLE_TOUCH_INPUT_MAP[ ev.type ];
// should we handle the touch events?
if ( type === INPUT_START ) {
this.started = true;
}
if ( !this.started ) {
return;
}
var touches = normalizeSingleTouches.call( this, ev, type );
// when done, reset the started state
if ( type & ( INPUT_END | INPUT_CANCEL ) && touches[ 0 ].length - touches[ 1 ].length === 0 ) {
this.started = false;
}
this.callback( this.manager, type, {
pointers: touches[ 0 ],
changedPointers: touches[ 1 ],
pointerType: INPUT_TYPE_TOUCH,
srcEvent: ev
} );
}
} );
/**
* @this {TouchInput}
* @param {Object} ev
* @param {Number} type flag
* @returns {undefined|Array} [all, changed]
*/
function normalizeSingleTouches( ev, type ) {
var all = toArray( ev.touches );
var changed = toArray( ev.changedTouches );
if ( type & ( INPUT_END | INPUT_CANCEL ) ) {
all = uniqueArray( all.concat( changed ), 'identifier', true );
}
return [ all, changed ];
}
var TOUCH_INPUT_MAP = {
touchstart: INPUT_START,
touchmove: INPUT_MOVE,
touchend: INPUT_END,
touchcancel: INPUT_CANCEL
};
var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel';
/**
* Multi-user touch events input
* @constructor
* @extends Input
*/
function TouchInput() {
this.evTarget = TOUCH_TARGET_EVENTS;
this.targetIds = {};
Input.apply( this, arguments );
}
inherit( TouchInput, Input, {
handler: function MTEhandler( ev ) {
var type = TOUCH_INPUT_MAP[ ev.type ];
var touches = getTouches.call( this, ev, type );
if ( !touches ) {
return;
}
this.callback( this.manager, type, {
pointers: touches[ 0 ],
changedPointers: touches[ 1 ],
pointerType: INPUT_TYPE_TOUCH,
srcEvent: ev
} );
}
} );
/**
* @this {TouchInput}
* @param {Object} ev
* @param {Number} type flag
* @returns {undefined|Array} [all, changed]
*/
function getTouches( ev, type ) {
var allTouches = toArray( ev.touches );
var targetIds = this.targetIds;
// when there is only one touch, the process can be simplified
if ( type & ( INPUT_START | INPUT_MOVE ) && allTouches.length === 1 ) {
targetIds[ allTouches[ 0 ].identifier ] = true;
return [ allTouches, allTouches ];
}
var i,
targetTouches,
changedTouches = toArray( ev.changedTouches ),
changedTargetTouches = [],
target = this.target;
// get target touches from touches
targetTouches = allTouches.filter( function ( touch ) {
return hasParent( touch.target, target );
} );
// collect touches
if ( type === INPUT_START ) {
i = 0;
while ( i < targetTouches.length ) {
targetIds[ targetTouches[ i ].identifier ] = true;
i++;
}
}
// filter changed touches to only contain touches that exist in the collected target ids
i = 0;
while ( i < changedTouches.length ) {
if ( targetIds[ changedTouches[ i ].identifier ] ) {
changedTargetTouches.push( changedTouches[ i ] );
}
// cleanup removed touches
if ( type & ( INPUT_END | INPUT_CANCEL ) ) {
delete targetIds[ changedTouches[ i ].identifier ];
}
i++;
}
if ( !changedTargetTouches.length ) {
return;
}
return [
// merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel'
uniqueArray( targetTouches.concat( changedTargetTouches ), 'identifier', true ),
changedTargetTouches
];
}
/**
* Combined touch and mouse input
*
* Touch has a higher priority then mouse, and while touching no mouse events are allowed.
* This because touch devices also emit mouse events while doing a touch.
*
* @constructor
* @extends Input
*/
var DEDUP_TIMEOUT = 2500;
var DEDUP_DISTANCE = 25;
function TouchMouseInput() {
Input.apply( this, arguments );
var handler = bindFn( this.handler, this );
this.touch = new TouchInput( this.manager, handler );
this.mouse = new MouseInput( this.manager, handler );
this.primaryTouch = null;
this.lastTouches = [];
}
inherit( TouchMouseInput, Input, {
/**
* handle mouse and touch events
* @param {Hammer} manager
* @param {String} inputEvent
* @param {Object} inputData
*/
handler: function TMEhandler( manager, inputEvent, inputData ) {
var isTouch = ( inputData.pointerType == INPUT_TYPE_TOUCH ),
isMouse = ( inputData.pointerType == INPUT_TYPE_MOUSE );
if ( isMouse && inputData.sourceCapabilities && inputData.sourceCapabilities.firesTouchEvents ) {
return;
}
// when we're in a touch event, record touches to de-dupe synthetic mouse event
if ( isTouch ) {
recordTouches.call( this, inputEvent, inputData );
} else if ( isMouse && isSyntheticEvent.call( this, inputData ) ) {
return;
}
this.callback( manager, inputEvent, inputData );
},
/**
* remove the event listeners
*/
destroy: function destroy() {
this.touch.destroy();
this.mouse.destroy();
}
} );
function recordTouches( eventType, eventData ) {
if ( eventType & INPUT_START ) {
this.primaryTouch = eventData.changedPointers[ 0 ].identifier;
setLastTouch.call( this, eventData );
} else if ( eventType & ( INPUT_END | INPUT_CANCEL ) ) {
setLastTouch.call( this, eventData );
}
}
function setLastTouch( eventData ) {
var touch = eventData.changedPointers[ 0 ];
if ( touch.identifier === this.primaryTouch ) {
var lastTouch = { x: touch.clientX, y: touch.clientY };
this.lastTouches.push( lastTouch );
var lts = this.lastTouches;
var removeLastTouch = function () {
var i = lts.indexOf( lastTouch );
if ( i > -1 ) {
lts.splice( i, 1 );
}
};
setTimeout( removeLastTouch, DEDUP_TIMEOUT );
}
}
function isSyntheticEvent( eventData ) {
var x = eventData.srcEvent.clientX,
y = eventData.srcEvent.clientY;
for ( var i = 0; i < this.lastTouches.length; i++ ) {
var t = this.lastTouches[ i ];
var dx = Math.abs( x - t.x ),
dy = Math.abs( y - t.y );
if ( dx <= DEDUP_DISTANCE && dy <= DEDUP_DISTANCE ) {
return true;
}
}
return false;
}
var PREFIXED_TOUCH_ACTION = prefixed( TEST_ELEMENT.style, 'touchAction' );
var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined;
// magical touchAction value
var TOUCH_ACTION_COMPUTE = 'compute';
var TOUCH_ACTION_AUTO = 'auto';
var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented
var TOUCH_ACTION_NONE = 'none';
var TOUCH_ACTION_PAN_X = 'pan-x';
var TOUCH_ACTION_PAN_Y = 'pan-y';
var TOUCH_ACTION_MAP = getTouchActionProps();
/**
* Touch Action
* sets the touchAction property or uses the js alternative
* @param {Manager} manager
* @param {String} value
* @constructor
*/
function TouchAction( manager, value ) {
this.manager = manager;
this.set( value );
}
TouchAction.prototype = {
/**
* set the touchAction value on the element or enable the polyfill
* @param {String} value
*/
set: function ( value ) {
// find out the touch-action by the event handlers
if ( value == TOUCH_ACTION_COMPUTE ) {
value = this.compute();
}
if ( NATIVE_TOUCH_ACTION && this.manager.element.style && TOUCH_ACTION_MAP[ value ] ) {
this.manager.element.style[ PREFIXED_TOUCH_ACTION ] = value;
}
this.actions = value.toLowerCase().trim();
},
/**
* just re-set the touchAction value
*/
update: function () {
this.set( this.manager.options.touchAction );
},
/**
* compute the value for the touchAction property based on the recognizer's settings
* @returns {String} value
*/
compute: function () {
var actions = [];
each( this.manager.recognizers, function ( recognizer ) {
if ( boolOrFn( recognizer.options.enable, [ recognizer ] ) ) {
actions = actions.concat( recognizer.getTouchAction() );
}
} );
return cleanTouchActions( actions.join( ' ' ) );
},
/**
* this method is called on each input cycle and provides the preventing of the browser behavior
* @param {Object} input
*/
preventDefaults: function ( input ) {
var srcEvent = input.srcEvent;
var direction = input.offsetDirection;
// if the touch action did prevented once this session
if ( this.manager.session.prevented ) {
srcEvent.preventDefault();
return;
}
var actions = this.actions;
var hasNone = inStr( actions, TOUCH_ACTION_NONE ) && !TOUCH_ACTION_MAP[ TOUCH_ACTION_NONE ];
var hasPanY = inStr( actions, TOUCH_ACTION_PAN_Y ) && !TOUCH_ACTION_MAP[ TOUCH_ACTION_PAN_Y ];
var hasPanX = inStr( actions, TOUCH_ACTION_PAN_X ) && !TOUCH_ACTION_MAP[ TOUCH_ACTION_PAN_X ];
if ( hasNone ) {
//do not prevent defaults if this is a tap gesture
var isTapPointer = input.pointers.length === 1;
var isTapMovement = input.distance < 2;
var isTapTouchTime = input.deltaTime < 250;
if ( isTapPointer && isTapMovement && isTapTouchTime ) {
return;
}
}
if ( hasPanX && hasPanY ) {
// `pan-x pan-y` means browser handles all scrolling/panning, do not prevent
return;
}
if ( hasNone ||
( hasPanY && direction & DIRECTION_HORIZONTAL ) ||
( hasPanX && direction & DIRECTION_VERTICAL ) ) {
return this.preventSrc( srcEvent );
}
},
/**
* call preventDefault to prevent the browser's default behavior (scrolling in most cases)
* @param {Object} srcEvent
*/
preventSrc: function ( srcEvent ) {
this.manager.session.prevented = true;
srcEvent.preventDefault();
}
};
/**
* when the touchActions are collected they are not a valid value, so we need to clean things up. *
* @param {String} actions
* @returns {*}
*/
function cleanTouchActions( actions ) {
// none
if ( inStr( actions, TOUCH_ACTION_NONE ) ) {
return TOUCH_ACTION_NONE;
}
var hasPanX = inStr( actions, TOUCH_ACTION_PAN_X );
var hasPanY = inStr( actions, TOUCH_ACTION_PAN_Y );
// if both pan-x and pan-y are set (different recognizers
// for different directions, e.g. horizontal pan but vertical swipe?)
// we need none (as otherwise with pan-x pan-y combined none of these
// recognizers will work, since the browser would handle all panning
if ( hasPanX && hasPanY ) {
return TOUCH_ACTION_NONE;
}
// pan-x OR pan-y
if ( hasPanX || hasPanY ) {
return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y;
}
// manipulation
if ( inStr( actions, TOUCH_ACTION_MANIPULATION ) ) {
return TOUCH_ACTION_MANIPULATION;
}
return TOUCH_ACTION_AUTO;
}
function getTouchActionProps() {
if ( !NATIVE_TOUCH_ACTION ) {
return false;
}
var touchMap = {};
var cssSupports = window.CSS && window.CSS.supports;
[ 'auto', 'manipulation', 'pan-y', 'pan-x', 'pan-x pan-y', 'none' ].forEach( function ( val ) {
// If css.supports is not supported but there is native touch-action assume it supports
// all values. This is the case for IE 10 and 11.
touchMap[ val ] = cssSupports ? window.CSS.supports( 'touch-action', val ) : true;
} );
return touchMap;
}
/**
* Recognizer flow explained; *
* All recognizers have the initial state of POSSIBLE when a input session starts.
* The definition of a input session is from the first input until the last input, with all it's movement in it. *
* Example session for mouse-input: mousedown -> mousemove -> mouseup
*
* On each recognizing cycle (see Manager.recognize) the .recognize() method is executed
* which determines with state it should be.
*
* If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to
* POSSIBLE to give it another change on the next cycle.
*
* Possible
* |
* +-----+---------------+
* | |
* +-----+-----+ |
* | | |
* Failed Cancelled |
* +-------+------+
* | |
* Recognized Began
* |
* Changed
* |
* Ended/Recognized
*/
var STATE_POSSIBLE = 1;
var STATE_BEGAN = 2;
var STATE_CHANGED = 4;
var STATE_ENDED = 8;
var STATE_RECOGNIZED = STATE_ENDED;
var STATE_CANCELLED = 16;
var STATE_FAILED = 32;
/**
* Recognizer
* Every recognizer needs to extend from this class.
* @constructor
* @param {Object} options
*/
function Recognizer( options ) {
this.options = assign( {}, this.defaults, options || {} );
this.id = uniqueId();
this.manager = null;
// default is enable true
this.options.enable = ifUndefined( this.options.enable, true );
this.state = STATE_POSSIBLE;
this.simultaneous = {};
this.requireFail = [];
}
Recognizer.prototype = {
/**
* @virtual
* @type {Object}
*/
defaults: {},
/**
* set options
* @param {Object} options
* @return {Recognizer}
*/
set: function ( options ) {
assign( this.options, options );
// also update the touchAction, in case something changed about the directions/enabled state
this.manager && this.manager.touchAction.update();
return this;
},
/**
* recognize simultaneous with an other recognizer.
* @param {Recognizer} otherRecognizer
* @returns {Recognizer} this
*/
recognizeWith: function ( otherRecognizer ) {
if ( invokeArrayArg( otherRecognizer, 'recognizeWith', this ) ) {
return this;
}
var simultaneous = this.simultaneous;
otherRecognizer = getRecognizerByNameIfManager( otherRecognizer, this );
if ( !simultaneous[ otherRecognizer.id ] ) {
simultaneous[ otherRecognizer.id ] = otherRecognizer;
otherRecognizer.recognizeWith( this );
}
return this;
},
/**
* drop the simultaneous link. it doesnt remove the link on the other recognizer.
* @param {Recognizer} otherRecognizer
* @returns {Recognizer} this
*/
dropRecognizeWith: function ( otherRecognizer ) {
if ( invokeArrayArg( otherRecognizer, 'dropRecognizeWith', this ) ) {
return this;
}
otherRecognizer = getRecognizerByNameIfManager( otherRecognizer, this );
delete this.simultaneous[ otherRecognizer.id ];
return this;
},
/**
* recognizer can only run when an other is failing
* @param {Recognizer} otherRecognizer
* @returns {Recognizer} this
*/
requireFailure: function ( otherRecognizer ) {
if ( invokeArrayArg( otherRecognizer, 'requireFailure', this ) ) {
return this;
}
var requireFail = this.requireFail;
otherRecognizer = getRecognizerByNameIfManager( otherRecognizer, this );
if ( inArray( requireFail, otherRecognizer ) === -1 ) {
requireFail.push( otherRecognizer );
otherRecognizer.requireFailure( this );
}
return this;
},
/**
* drop the requireFailure link. it does not remove the link on the other recognizer.
* @param {Recognizer} otherRecognizer
* @returns {Recognizer} this
*/
dropRequireFailure: function ( otherRecognizer ) {
if ( invokeArrayArg( otherRecognizer, 'dropRequireFailure', this ) ) {
return this;
}
otherRecognizer = getRecognizerByNameIfManager( otherRecognizer, this );
var index = inArray( this.requireFail, otherRecognizer );
if ( index > -1 ) {
this.requireFail.splice( index, 1 );
}
return this;
},
/**
* has require failures boolean
* @returns {boolean}
*/
hasRequireFailures: function () {
return this.requireFail.length > 0;
},
/**
* if the recognizer can recognize simultaneous with an other recognizer
* @param {Recognizer} otherRecognizer
* @returns {Boolean}
*/
canRecognizeWith: function ( otherRecognizer ) {
return !!this.simultaneous[ otherRecognizer.id ];
},
/**
* You should use `tryEmit` instead of `emit` directly to check
* that all the needed recognizers has failed before emitting.
* @param {Object} input
*/
emit: function ( input ) {
var self = this;
var state = this.state;
function emit( event ) {
self.manager.emit( event, input );
}
// 'panstart' and 'panmove'
if ( state < STATE_ENDED ) {
emit( self.options.event + stateStr( state ) );
}
emit( self.options.event ); // simple 'eventName' events
if ( input.additionalEvent ) { // additional event(panleft, panright, pinchin, pinchout...)
emit( input.additionalEvent );
}
// panend and pancancel
if ( state >= STATE_ENDED ) {
emit( self.options.event + stateStr( state ) );
}
},
/**
* Check that all the require failure recognizers has failed,
* if true, it emits a gesture event,
* otherwise, setup the state to FAILED.
* @param {Object} input
*/
tryEmit: function ( input ) {
if ( this.canEmit() ) {
return this.emit( input );
}
// it's failing anyway
this.state = STATE_FAILED;
},
/**
* can we emit?
* @returns {boolean}
*/
canEmit: function () {
var i = 0;
while ( i < this.requireFail.length ) {
if ( !( this.requireFail[ i ].state & ( STATE_FAILED | STATE_POSSIBLE ) ) ) {