// ========================================================================== // Project: Ember Touch - Touch and Gesture Library For Ember // Copyright: ©2011-2012 Pepe Cano and contributors // Portions ©2006-2011 Strobe Inc. // License: Licensed under MIT license // See https://raw.github.com/emberjs-addons/ember-touch/master/LICENSE // ========================================================================== (function() { Em.TimeoutTouchEventType = { Cancel: 'cancel', End: 'end' }; /** Based on custom gestures implementation. A `TimeoutTouchEvent` event is normally created to fire automatically after a given period of time. A view [gesture]Event which must be executed without being generated by an user touch event. @class TimeoutTouchEvent @namespace Ember */ Em.TimeoutTouchEvent = function(options){ this.type = options.type; }; })(); (function() { var get = Em.get; var set = Em.set; /** @module ember @submodule ember-touch */ /** This component manages and maintains a list of active touches related to a gesture recognizer. @class TouchList @namespace Ember @extends Ember.Object @private */ Em.TouchList = Em.Object.extend({ /** @property touches @type Array */ touches: null, /** @property timestamp */ timestamp: null, init: function() { this._super(); set(this, 'touches', []); }, /** Add a touch event to the list. This method is called only in the initialization of the touch session adding touchstart events. @method addTouch */ addTouch: function(touch) { var touches = get(this, 'touches'); touches.push(touch); this.notifyPropertyChange('touches'); }, /** Update a touch event from the list. Given a touch event, it will iterate the current list to replace with the event the item whose identifier is equal to the event identifier. @method updateTouch */ updateTouch: function(touch) { var touches = get(this, 'touches'); for (var i=0, l=touches.length; i 0 ) { //appGestureManager allow to pass touchEvents at the App Level var gesturesCanReceiveTouchEvent = agm.get('isBlocked')? agm.shouldReceiveTouch(this.view) : true; if ( gesturesCanReceiveTouchEvent ) { var gesture, gestureDelegate, isValid, i; for (i=0; i < l; i++) { gesture = gestures[i]; handler = gesture[eventName]; if (Em.typeOf(handler) === 'function') { gestureDelegate = gesture.get('delegate'); if ( !gesture.get('isEnabled') ) { isValid = false; //gestureDelegate allow to pass touchEvents depending on gesture state } else if ( !gestureDelegate ) { isValid = true; } else { isValid = this._applyDelegateRules( gestureDelegate, gesture, this.view, eventObject ); if ( isValid === undefined ) { isValid = gestureDelegate.shouldReceiveTouch( gesture, this.view, eventObject ); } } if ( isValid ) { result = handler.call(gesture, eventObject); } } } } } // browser delivers the event to the DOM element // bubble the event to the parentView var parentView = this.view.get('parentView'); if ( parentView ) { var manager = parentView.get('eventManager'); if ( manager ) { manager._invokeEvent(eventName, eventObject); } } } return result; }, /** Iterates all `GestureDelegateRule` instances of the gestureDelegate parameter executing its shouldReceiveTouch method and return the value whenever a rule respond with a defined value. @private @method _applyDelegateRules @return Boolean */ _applyDelegateRules: function(gestureDelegate, gesture, view, event) { var rules = gestureDelegate.rules, length = rules.length; if ( length > 0 ) { var i, result; for (i=0;i'; }, /** Reset the touches list. @private @method _resetState */ _resetState: function() { this.touches.removeAllTouches(); }, //.............................................. // Touch event handlers /** Given a `touchstart` event, updates the list of touches. If the `numberOfRequiredTouches` hasn't been reached yet, it sets the WAITING_FOR_TOUCHES state. Otherwise when the gesture is discrete, it moves to a BEGAN state and applies its logic. Continous gestures are setup to the POSSIBLE state and execute their `didBecomePossible` method. @method touchStart */ touchStart: function(evt) { var targetTouches = evt.originalEvent.targetTouches; var _touches = this.touches; var state = get(this, 'state'); set(_touches, 'timestamp', Date.now()); //Collect touches by their identifiers for (var i=0, l=targetTouches.length; i= this._deltaThreshold; }, didChange: function() { var scale = this._previousScale = get(this, 'scale'); var timeDifference = this.touches.timestamp - this._previousTimestamp; var currentDistanceBetweenTouches = this.distance(get(this.touches,'touches')); var distanceDifference = (currentDistanceBetweenTouches - this._previousDistance); set(this, 'velocity', distanceDifference / timeDifference); set(this, 'scale', currentDistanceBetweenTouches / this._previousDistance); this._previousTimestamp = get(this.touches,'timestamp'); this._previousDistance = currentDistanceBetweenTouches; }, eventWasRejected: function() { set(this, 'scale', this._previousScale); } }); })(); (function() { var get = Em.get, set = Em.set; /** @module ember @submodule ember-touch */ /** Recognizes a multi-touch pan gesture. Pan gestures require a specified number of fingers to move. It will record and update the center point between the touches. For panChange events, the pan gesture recognizer includes a translation property which can be applied as a CSS transform directly. Translation values are hashes which contain an x and a y value. var myview = Em.View.create({ elementId: 'gestureTest', panChange: function(rec, evt) { var val = rec.get('translation'); this.$().css({ translateX: '%@=%@'.fmt((val.x < 0)? '-' : '+',Math.abs(val.x)), translateY: '%@=%@'.fmt((val.y < 0)? '-' : '+',Math.abs(val.y)) }); } }); The number of touches required to start the gesture can be specified with the _numberOfRequiredTouches_ property. This property can be set in the panOptions hash. var myview = Em.View.create({ panOptions: { numberOfRequiredTouches: 2 } }); @class PanGestureRecognizer @namespace Ember @extends Em.Gesture */ Em.PanGestureRecognizer = Em.Gesture.extend({ /** The translation value which represents the current amount of movement that has been applied to the view. @type Location */ translation: null, /** The pixel distance that the fingers need to move before the gesture is recognized. It should be set depending on the device factor and view behaviors. Distance is calculated separately on vertical and horizontal directions depending on the direction property. @private @type Number */ initThreshold: 5, direction: Em.GestureDirection.Horizontal | Em.GestureDirection.Vertical , //.................................................. // Private Methods and Properties /** Used to measure offsets @private @type Number */ _previousLocation: null, /** Used for rejected events @private @type Hash */ _previousTranslation: null, init: function() { this._super(); set(this, 'translation', {x:0,y:0}); }, didBecomePossible: function() { this._previousLocation = this.centerPointForTouches(get(this.touches,'touches')); }, shouldBegin: function() { var previousLocation = this._previousLocation; var currentLocation = this.centerPointForTouches(get(this.touches,'touches')); var x = previousLocation.x; var y = previousLocation.y; var x0 = currentLocation.x; var y0 = currentLocation.y; var shouldBegin = false; //shouldBegin = Math.sqrt( (x - x0)*(x - x0) + (y - y0)*(y - y0) ) >= this.initThreshold; if ( this.direction & Em.GestureDirection.Vertical ) { shouldBegin = Math.abs( y - y0 ) >= this.initThreshold; } if (!shouldBegin && ( this.direction & Em.GestureDirection.Horizontal ) ) { shouldBegin = Math.abs( x - x0 ) >= this.initThreshold; } return shouldBegin; }, didChange: function() { var previousLocation = this._previousLocation; var currentLocation = this.centerPointForTouches(get(this.touches,'touches')); var translation = {x:currentLocation.x, y:currentLocation.y}; translation.x = currentLocation.x - previousLocation.x; translation.y = currentLocation.y - previousLocation.y; this._previousTranslation = get(this, 'translation'); set(this, 'translation', translation); this._previousLocation = currentLocation; }, eventWasRejected: function() { set(this, 'translation', this._previousTranslation); }, toString: function() { return Em.PanGestureRecognizer+'<'+Em.guidFor(this)+'>'; } }); })(); (function() { var get = Em.get, set = Em.set; /** @module ember @submodule ember-touch */ /** Recognizes a multi-touch tap gesture. Tap gestures allow for a certain amount of wiggle-room between a start and end of a touch. Taps are discrete gestures so only tapEnd() will get fired on a view. var myview = Em.View.create({ elementId: 'gestureTest', tapEnd: function(recognizer, evt) { $('#gestureTest').css('background','yellow'); } }); The number of touches required to start the gesture can be specified with the _numberOfRequiredTouches_ property, which can be set in the tapOptions hash. var myview = Em.View.create({ tapOptions: { numberOfRequiredTouches: 3 } }); And the number of taps required to fire the gesture can be specified using the _numberOfTaps_ property. var myview = Em.View.create({ tapOptions: { numberOfTaps: 3, delayBetweenTaps: 150 } }); @class TapGestureRecognizer @namespace Ember @extends Em.Gesture */ Em.TapGestureRecognizer = Em.Gesture.extend({ /** The translation value which represents the current amount of movement that has been applied to the view. @type Location */ numberOfTaps: 1, delayBetweenTaps: 500, tapThreshold: 10, //.................................................. // Private Methods and Properties /** @private */ gestureIsDiscrete: true, /** @private */ _initialLocation: null, /** @private */ _waitingTimeout: null, /** @private */ _waitingForMoreTouches: false, _internalTouches: null, init: function(){ this._super(); this._internalTouches = Em.TouchList.create(); Em.assert( get(this, 'numberOfRequiredTouches')===1, 'TODO: implement!!' ); }, shouldBegin: function() { return get(this.touches,'length') === get(this, 'numberOfRequiredTouches'); }, didBegin: function() { this._initialLocation = this.centerPointForTouches(get(this.touches,'touches')); this._internalTouches.addTouch( this.touches[0] ); this._waitingForMoreTouches = get(this._internalTouches,'length') < get(this, 'numberOfTaps'); if ( this._waitingForMoreTouches ) { var that = this; this._waitingTimeout = window.setTimeout( function() { that._waitingFired(that); }, this.delayBetweenTaps); } }, shouldEnd: function() { var currentLocation = this.centerPointForTouches(get(this.touches,'touches')); var x = this._initialLocation.x; var y = this._initialLocation.y; var x0 = currentLocation.x; var y0 = currentLocation.y; var distance = Math.sqrt((x -= x0) * x + (y -= y0) * y); return (Math.abs(distance) < this.tapThreshold) && !this._waitingForMoreTouches; }, didEnd: function() { window.clearTimeout( this._waitingTimeout ); // clean internalState this._initialLocation = null; this._internalTouches.removeAllTouches(); }, _waitingFired: function() { // clean internalState this._initialLocation = null; this._internalTouches.removeAllTouches(); // set state for the gesture manager set(this, 'state', Em.Gesture.CANCELLED); var eventName = this.name+'Cancel'; var evt = new Em.TimeoutTouchEvent({type: Em.TimeoutTouchEventType.Cancel}); this.attemptGestureEventDelivery(eventName, evt); this._resetState(); }, toString: function() { return Em.TapGestureRecognizer+'<'+Em.guidFor(this)+'>'; } }); })(); (function() { var get = Em.get, set = Em.set; /** @module ember @submodule ember-touch */ /** Recognizes a multi-touch press gesture. Press gestures allow for a certain amount of wiggle-room between a start and end of a touch, and requires a minimum hold period to be triggered. The press gesture also requires to stop touching the screen to be triggered. Press gestures are discrete so only _pressEnd_ will get fired. var myview = Em.View.create({ elementId: 'gestureTest', pressEnd: function(recognizer, evt) { } }); The number of touches required to start the gesture can be specified with the _numberOfRequiredTouches_ and _pressPeriodThreshold_ properties. This properties can be set in the _pressHoldOptions_ hash: var myview = Em.View.create({ pressOptions: { pressPeriodThreshold: 500 } }); @class PressGestureRecognizer @namespace Ember @extends Em.Gesture */ Em.PressGestureRecognizer = Em.Gesture.extend({ /** The minimum period (ms) that the fingers must be held to recognize the gesture end. @private @type Number */ pressPeriodThreshold: 500, //.................................................. // Private Methods and Properties /** @private */ gestureIsDiscrete: true, /** @private */ _initialLocation: null, /** @private */ _moveThreshold: 10, /** @private */ _initialTimestamp: null, shouldBegin: function() { return get(this.touches,'length') === get(this, 'numberOfRequiredTouches'); }, didBegin: function() { this._initialLocation = this.centerPointForTouches(get(this.touches,'touches')); this._initialTimestamp = get(this.touches,'timestamp'); }, shouldEnd: function() { var currentLocation = this.centerPointForTouches(get(this.touches,'touches')); var x = this._initialLocation.x; var y = this._initialLocation.y; var x0 = currentLocation.x; var y0 = currentLocation.y; var distance = Math.sqrt((x -= x0) * x + (y -= y0) * y); var isValidDistance = (Math.abs(distance) < this._moveThreshold); var nowTimestamp = get(this.touches,'timestamp'); var isValidHoldPeriod = (nowTimestamp - this._initialTimestamp ) >= this.pressPeriodThreshold; var result = isValidDistance && isValidHoldPeriod; if ( !result ) { set(this, 'state', Em.Gesture.CANCELLED); this.didCancel(); } return result; }, didEnd: function() { this._resetCounters(); }, didCancel: function() { this._resetCounters(); }, _resetCounters: function() { this._initialLocation = null; this._initialTimestamp = null; }, toString: function() { return Em.PressGestureRecognizer+'<'+Em.guidFor(this)+'>'; } }); })(); (function() { var get = Em.get, set = Em.set; /** @module ember @submodule ember-touch */ /** Recognizes a multi-touch touch and hold gesture. Touch and Hold gestures allow move the finger on the same view, and after the user leaves its finger motionless during a specific period the end view event is automatically triggered. TouchHold are discrete gestures so only _touchHoldEnd_ will get fired. var myview = Em.View.create({ elementId: 'gestureTest', touchHoldEnd: function(recognizer, evt) { } }); The number of touches required to start the gesture can be specified with the following properties: - _numberOfRequiredTouches_ - a minimum _holdPeriod_ the finger must be held to trigger the end event - _modeThreshold_ which allows to move the finger a specific number of pixels This properties can be set in the touchHoldOptions var myview = Em.View.create({ touchHoldOptions: { holdPeriod: 500, moveThreshold: 10 } }); @class TouchHoldGestureRecognizer @namespace Ember @extends Em.Gesture **/ Em.TouchHoldGestureRecognizer = Em.Gesture.extend({ /** The minimum period (ms) that the fingers must be held to trigger the event. @private @type Number */ holdPeriod: 2000, moveThreshold: 50, //.................................................. // Private Methods and Properties /** @private */ gestureIsDiscrete: true, _endTimeout: null, _targetElement: null, shouldBegin: function() { return get(this.touches,'length') === get(this, 'numberOfRequiredTouches'); }, didBegin: function() { this._initialLocation = this.centerPointForTouches(get(this.touches,'touches')); var target = get(this.touches,'touches')[0].target; set(this,'_target', target ); var that = this; this._endTimeout = window.setTimeout( function() { that._endFired(that); }, this.holdPeriod); }, didChange: function() { var currentLocation = this.centerPointForTouches(get(this.touches,'touches')); var x = this._initialLocation.x; var y = this._initialLocation.y; var x0 = currentLocation.x; var y0 = currentLocation.y; var distance = Math.sqrt((x -= x0) * x + (y -= y0) * y); var isValidMovement = (Math.abs(distance) < this.moveThreshold); // ideal situation would be using touchleave event to be notified // the touch leaves the DOM element if ( !isValidMovement ) { this._disableEndFired(); set(this, 'state', Em.Gesture.CANCELLED); //this._resetState(); // let be executed on touchEnd } }, // when a touchend event was fired ( cause of removed finger ) // disable interval action trigger and block end state // this event is responsable for gesture cancel shouldEnd: function() { this._disableEndFired(); set(this, 'state', Em.Gesture.CANCELLED); this.didCancel(); return false; }, _endFired: function() { this._disableEndFired(); if ( this.state === Em.Gesture.BEGAN || this.state === Em.Gesture.CHANGED ) { set(this, 'state', Em.Gesture.ENDED); var eventName = this.name+'End'; var evt = new Em.TimeoutTouchEvent({type: Em.TimeoutTouchEventType.End}); this.attemptGestureEventDelivery(eventName, evt); //this._resetState(); // let be executed on touchEnd } }, _disableEndFired: function() { window.clearTimeout(this._endTimeout); }, toString: function() { return Em.TouchHoldGestureRecognizer+'<'+Em.guidFor(this)+'>'; } }); })(); (function() { var get = Em.get, set = Em.set; /** @module ember @submodule ember-touch */ /** Recognizes a swipe gesture in one or more directions. Swipes are continuous gestures that will get fired on a view. var myview = Em.View.create({ swipeStart: function(recognizer, evt) { }, swipeChange: function(recognizer, evt) { }, // usually, you will only use this method swipeEnd: function(recognizer, evt) { }, swipeCancel: function(recognizer, evt) { } }); SwipeGestureRecognizer recognizes a swipe when the touch has moved to a (direction) far enough (swipeThreshold) in a period (cancelPeriod). The current implementation will only recognize a direction on swipeEnd on (recognizer.swipeDirection). var myview = Em.View.create({ swipeOptions: { direction: Em.OneGestureDirection.Left | Em.OneGestureDirection.Right, cancelPeriod: 100, swipeThreshold: 10 } }); @class SwipeGestureRecognizer @namespace Ember @extends Em.Gesture */ Em.SwipeGestureRecognizer = Em.Gesture.extend({ /** The period (ms) in which the gesture should have been recognized. @private @type Number */ cancelPeriod: 100, swipeThreshold: 50, /* It should be set up depending of the device factor and view behaviors. Distance is calculated separately on vertical and horizontal directions depending on the direction property. */ initThreshold: 5, direction: Em.OneGestureDirection.Right, //.................................................. // Private Methods and Properties numberOfRequiredTouches: 1, swipeDirection: null, _initialLocation: null, _previousLocation: null, _cancelTimeout: null, /** The pixel distance that the fingers need to move before this gesture is recognized. @private @type Number */ didBecomePossible: function() { this._previousLocation = this.centerPointForTouches(get(this.touches,'touches')); }, shouldBegin: function() { var previousLocation = this._previousLocation; var currentLocation = this.centerPointForTouches(get(this.touches,'touches')); var x = previousLocation.x; var y = previousLocation.y; var x0 = currentLocation.x; var y0 = currentLocation.y; // var distance = Math.sqrt((x -= x0) * x + (y -= y0) * y); var shouldBegin = false; if ( this.direction & Em.OneGestureDirection.Right ) { shouldBegin = ( (x0-x) > this.initThreshold); } if ( !shouldBegin && ( this.direction & Em.OneGestureDirection.Left ) ) { shouldBegin = ( (x-x0) > this.initThreshold); } if ( !shouldBegin && ( this.direction & Em.OneGestureDirection.Down ) ) { shouldBegin = ( (y0-y) > this.initThreshold); } if ( !shouldBegin && ( this.direction & Em.OneGestureDirection.Up ) ) { shouldBegin = ( (y-y0) > this.initThreshold); } return shouldBegin; }, didBegin: function() { this._initialLocation = this.centerPointForTouches(get(this.touches,'touches')); var that = this; this._cancelTimeout = window.setTimeout( function() { that._cancelFired(that); }, this.cancelPeriod); }, didChange: function(evt) { var currentLocation = this.centerPointForTouches(get(this.touches,'touches')); var x = this._initialLocation.x; var y = this._initialLocation.y; var x0 = currentLocation.x; var y0 = currentLocation.y; var isValidMovement = false; if ( this.direction & Em.OneGestureDirection.Right ) { isValidMovement = ( (x0-x) > this.swipeThreshold); this.swipeDirection = Em.OneGestureDirection.Right; } if ( !isValidMovement && ( this.direction & Em.OneGestureDirection.Left ) ) { isValidMovement = ( (x-x0) > this.swipeThreshold); this.swipeDirection = Em.OneGestureDirection.Left; } if ( !isValidMovement && ( this.direction & Em.OneGestureDirection.Down ) ) { isValidMovement = ( (y0-y) > this.swipeThreshold); this.swipeDirection = Em.OneGestureDirection.Down; } if ( !isValidMovement && ( this.direction & Em.OneGestureDirection.Up ) ) { isValidMovement = ( (y-y0) > this.swipeThreshold); this.swipeDirection = Em.OneGestureDirection.Up; } if ( isValidMovement ) { this._disableCancelFired(); set(this, 'state', Em.Gesture.ENDED); var eventName = this.name+'End'; this.attemptGestureEventDelivery(eventName, evt); this._resetState(); } }, // touch end should cancel the gesture shouldEnd: function() { this._cancelFired(); return false; }, _cancelFired: function() { this._disableCancelFired(); set(this, 'state', Em.Gesture.CANCELLED); var eventName = this.name+'Cancel'; var evt = new Em.TimeoutTouchEvent({type: Em.TimeoutTouchEventType.Cancel}); this.attemptGestureEventDelivery(eventName, evt); this._resetState(); }, _disableCancelFired: function() { window.clearTimeout( this._cancelTimeout ); }, toString: function() { return Em.SwipeGestureRecognizer+'<'+Em.guidFor(this)+'>'; } }); })(); (function() { })(); (function() { /** A lightweight library for building and using touch gestures with Ember Applications @module ember @submodule ember-touch @main ember-touch */ })();