UNPKG

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
'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 ) ) ) {