Home | History | Annotate | Download | only in touch_ntp
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview Touch Handler. Class that handles all touch events and
      7  * uses them to interpret higher level gestures and behaviors. TouchEvent is a
      8  * built in mobile safari type:
      9  * http://developer.apple.com/safari/library/documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html.
     10  * This class is intended to work with all webkit browsers, tested on Chrome and
     11  * iOS.
     12  *
     13  * The following types of gestures are currently supported.  See the definition
     14  * of TouchHandler.EventType for details.
     15  *
     16  * Single Touch:
     17  *      This provides simple single-touch events.  Any secondary touch is
     18  *      ignored.
     19  *
     20  * Drag:
     21  *      A single touch followed by some movement. This behavior will handle all
     22  *      of the required events and report the properties of the drag to you
     23  *      while the touch is happening and at the end of the drag sequence. This
     24  *      behavior will NOT perform the actual dragging (redrawing the element)
     25  *      for you, this responsibility is left to the client code.
     26  *
     27  * Long press:
     28  *     When your element is touched and held without any drag occuring, the
     29  *     LONG_PRESS event will fire.
     30  */
     31 
     32 // Use an anonymous function to enable strict mode just for this file (which
     33 // will be concatenated with other files when embedded in Chrome)
     34 var TouchHandler = (function() {
     35   'use strict';
     36 
     37   /**
     38    * A TouchHandler attaches to an Element, listents for low-level touch (or
     39    * mouse) events and dispatching higher-level events on the element.
     40    * @param {!Element} element The element to listen on and fire events
     41    * for.
     42    * @constructor
     43    */
     44   function TouchHandler(element) {
     45     /**
     46      * @type {!Element}
     47      * @private
     48      */
     49     this.element_ = element;
     50 
     51     /**
     52      * The absolute sum of all touch y deltas.
     53      * @type {number}
     54      * @private
     55      */
     56     this.totalMoveY_ = 0;
     57 
     58     /**
     59      * The absolute sum of all touch x deltas.
     60      * @type {number}
     61      * @private
     62      */
     63     this.totalMoveX_ = 0;
     64 
     65     /**
     66      * An array of tuples where the first item is the horizontal component of a
     67      * recent relevant touch and the second item is the touch's time stamp. Old
     68      * touches are removed based on the max tracking time and when direction
     69      * changes.
     70       * @type {!Array.<number>}
     71       * @private
     72       */
     73     this.recentTouchesX_ = [];
     74 
     75     /**
     76      * An array of tuples where the first item is the vertical component of a
     77      * recent relevant touch and the second item is the touch's time stamp. Old
     78      * touches are removed based on the max tracking time and when direction
     79      * changes.
     80      * @type {!Array.<number>}
     81      * @private
     82      */
     83     this.recentTouchesY_ = [];
     84 
     85     /**
     86      * Used to keep track of all events we subscribe to so we can easily clean
     87      * up
     88      * @type {EventTracker}
     89      * @private
     90      */
     91     this.events_ = new EventTracker();
     92   }
     93 
     94 
     95   /**
     96    * DOM Events that may be fired by the TouchHandler at the element
     97    */
     98   TouchHandler.EventType = {
     99     // Fired whenever the element is touched as the only touch to the device.
    100     // enableDrag defaults to false, set to true to permit dragging.
    101     TOUCH_START: 'touchhandler:touch_start',
    102 
    103     // Fired when an element is held for a period of time.  Prevents dragging
    104     // from occuring (even if enableDrag was set to true).
    105     LONG_PRESS: 'touchhandler:long_press',
    106 
    107     // If enableDrag was set to true at TOUCH_START, DRAG_START will fire when
    108     // the touch first moves sufficient distance.  enableDrag is set to true but
    109     // can be reset to false to cancel the drag.
    110     DRAG_START: 'touchhandler:drag_start',
    111 
    112     // If enableDrag was true after DRAG_START, DRAG_MOVE will fire whenever the
    113     // touch is moved.
    114     DRAG_MOVE: 'touchhandler:drag_move',
    115 
    116     // Fired just before TOUCH_END when a drag is released.  Correlates 1:1 with
    117     // a DRAG_START.
    118     DRAG_END: 'touchhandler:drag_end',
    119 
    120     // Fired whenever a touch that is being tracked has been released.
    121     // Correlates 1:1 with a TOUCH_START.
    122     TOUCH_END: 'touchhandler:touch_end'
    123   };
    124 
    125 
    126   /**
    127    * The type of event sent by TouchHandler
    128    * @constructor
    129    * @param {string} type The type of event (one of Grabber.EventType).
    130    * @param {boolean} bubbles Whether or not the event should bubble.
    131    * @param {number} clientX The X location of the touch.
    132    * @param {number} clientY The Y location of the touch.
    133    * @param {!Element} touchedElement The element at the current location of the
    134    *        touch.
    135    */
    136   TouchHandler.Event = function(type, bubbles, clientX, clientY,
    137       touchedElement) {
    138     var event = document.createEvent('Event');
    139     event.initEvent(type, bubbles, true);
    140     event.__proto__ = TouchHandler.Event.prototype;
    141 
    142     /**
    143      * The X location of the touch affected
    144      * @type {number}
    145      */
    146     event.clientX = clientX;
    147 
    148     /**
    149      * The Y location of the touch affected
    150      * @type {number}
    151      */
    152     event.clientY = clientY;
    153 
    154     /**
    155      * The element at the current location of the touch.
    156      * @type {!Element}
    157      */
    158     event.touchedElement = touchedElement;
    159 
    160     return event;
    161   };
    162 
    163   TouchHandler.Event.prototype = {
    164     __proto__: Event.prototype,
    165 
    166     /**
    167      * For TOUCH_START and DRAG START events, set to true to enable dragging or
    168      * false to disable dragging.
    169      * @type {boolean|undefined}
    170      */
    171     enableDrag: undefined,
    172 
    173     /**
    174      * For DRAG events, provides the horizontal component of the
    175      * drag delta. Drag delta is defined as the delta of the start touch
    176      * position and the current drag position.
    177      * @type {number|undefined}
    178      */
    179     dragDeltaX: undefined,
    180 
    181     /**
    182      * For DRAG events, provides the vertical component of the
    183      * drag delta.
    184      * @type {number|undefined}
    185      */
    186     dragDeltaY: undefined
    187   };
    188 
    189   /**
    190    * Minimum movement of touch required to be considered a drag.
    191    * @type {number}
    192    * @private
    193    */
    194   TouchHandler.MIN_TRACKING_FOR_DRAG_ = 8;
    195 
    196 
    197   /**
    198    * The maximum number of ms to track a touch event. After an event is older
    199    * than this value, it will be ignored in velocity calculations.
    200    * @type {number}
    201    * @private
    202    */
    203   TouchHandler.MAX_TRACKING_TIME_ = 250;
    204 
    205 
    206   /**
    207    * The maximum number of touches to track.
    208    * @type {number}
    209    * @private
    210    */
    211   TouchHandler.MAX_TRACKING_TOUCHES_ = 5;
    212 
    213 
    214   /**
    215    * The maximum velocity to return, in pixels per millisecond, that is used
    216    * to guard against errors in calculating end velocity of a drag. This is a
    217    * very fast drag velocity.
    218    * @type {number}
    219    * @private
    220    */
    221   TouchHandler.MAXIMUM_VELOCITY_ = 5;
    222 
    223 
    224   /**
    225    * The velocity to return, in pixel per millisecond, when the time stamps on
    226    * the events are erroneous. The browser can return bad time stamps if the
    227    * thread is blocked for the duration of the drag. This is a low velocity to
    228    * prevent the content from moving quickly after a slow drag. It is less
    229    * jarring if the content moves slowly after a fast drag.
    230    * @type {number}
    231    * @private
    232    */
    233   TouchHandler.VELOCITY_FOR_INCORRECT_EVENTS_ = 1;
    234 
    235   /**
    236    * The time, in milliseconds, that a touch must be held to be considered
    237    * 'long'.
    238    * @type {number}
    239    * @private
    240    */
    241   TouchHandler.TIME_FOR_LONG_PRESS_ = 500;
    242 
    243   TouchHandler.prototype = {
    244     /**
    245      * If defined, the identifer of the single touch that is active.  Note that
    246      * 0 is a valid touch identifier - it should not be treated equivalently to
    247      * undefined.
    248      * @type {number|undefined}
    249      * @private
    250      */
    251     activeTouch_: undefined,
    252 
    253     /**
    254      * @type {boolean|undefined}
    255      * @private
    256      */
    257     tracking_: undefined,
    258 
    259     /**
    260      * @type {number|undefined}
    261      * @private
    262      */
    263     startTouchX_: undefined,
    264 
    265     /**
    266      * @type {number|undefined}
    267      * @private
    268      */
    269     startTouchY_: undefined,
    270 
    271     /**
    272      * @type {number|undefined}
    273      * @private
    274      */
    275     endTouchX_: undefined,
    276 
    277     /**
    278      * @type {number|undefined}
    279      * @private
    280      */
    281     endTouchY_: undefined,
    282 
    283     /**
    284      * Time of the touchstart event.
    285      * @type {number|undefined}
    286      * @private
    287      */
    288     startTime_: undefined,
    289 
    290     /**
    291      * The time of the touchend event.
    292      * @type {number|undefined}
    293      * @private
    294      */
    295     endTime_: undefined,
    296 
    297     /**
    298      * @type {number|undefined}
    299      * @private
    300      */
    301     lastTouchX_: undefined,
    302 
    303     /**
    304      * @type {number|undefined}
    305      * @private
    306      */
    307     lastTouchY_: undefined,
    308 
    309     /**
    310      * @type {number|undefined}
    311      * @private
    312      */
    313     lastMoveX_: undefined,
    314 
    315     /**
    316      * @type {number|undefined}
    317      * @private
    318      */
    319     lastMoveY_: undefined,
    320 
    321     /**
    322      * @type {number|undefined}
    323      * @private
    324      */
    325     longPressTimeout_: undefined,
    326 
    327     /**
    328      * If defined and true, the next click event should be swallowed
    329      * @type {boolean|undefined}
    330      * @private
    331      */
    332     swallowNextClick_: undefined,
    333 
    334     /**
    335      * Start listenting for events.
    336      * @param {boolean=} opt_capture True if the TouchHandler should listen to
    337      *      during the capture phase.
    338      */
    339     enable: function(opt_capture) {
    340       var capture = !!opt_capture;
    341 
    342       // Just listen to start events for now. When a touch is occuring we'll
    343       // want to be subscribed to move and end events on the document, but we
    344       // don't want to incur the cost of lots of no-op handlers on the document.
    345       this.events_.add(this.element_, 'touchstart', this.onStart_.bind(this),
    346                        capture);
    347       this.events_.add(this.element_, 'mousedown',
    348                        this.mouseToTouchCallback_(this.onStart_.bind(this)),
    349                        capture);
    350 
    351       // If the element is long-pressed, we may need to swallow a click
    352       this.events_.add(this.element_, 'click', this.onClick_.bind(this), true);
    353     },
    354 
    355     /**
    356      * Stop listening to all events.
    357      */
    358     disable: function() {
    359       this.stopTouching_();
    360       this.events_.removeAll();
    361     },
    362 
    363     /**
    364      * Wraps a callback with translations of mouse events to touch events.
    365      * NOTE: These types really should be function(Event) but then we couldn't
    366      * use this with bind (which operates on any type of function).  Doesn't
    367      * JSDoc support some sort of polymorphic types?
    368      * @param {Function} callback The event callback.
    369      * @return {Function} The wrapping callback.
    370      * @private
    371      */
    372     mouseToTouchCallback_: function(callback) {
    373       return function(e) {
    374         // Note that there may be synthesizes mouse events caused by touch
    375         // events (a mouseDown after a touch-click).  We leave it up to the
    376         // client to worry about this if it matters to them (typically a short
    377         // mouseDown/mouseUp without a click is no big problem and it's not
    378         // obvious how we identify such synthesized events in a general way).
    379         var touch = {
    380           // any fixed value will do for the identifier - there will only
    381           // ever be a single active 'touch' when using the mouse.
    382           identifier: 0,
    383           clientX: e.clientX,
    384           clientY: e.clientY,
    385           target: e.target
    386         };
    387         e.touches = [];
    388         e.targetTouches = [];
    389         e.changedTouches = [touch];
    390         if (e.type != 'mouseup') {
    391           e.touches[0] = touch;
    392           e.targetTouches[0] = touch;
    393         }
    394         callback(e);
    395       };
    396     },
    397 
    398     /**
    399      * Begin tracking the touchable element, it is eligible for dragging.
    400      * @private
    401      */
    402     beginTracking_: function() {
    403       this.tracking_ = true;
    404     },
    405 
    406     /**
    407      * Stop tracking the touchable element, it is no longer dragging.
    408      * @private
    409      */
    410     endTracking_: function() {
    411       this.tracking_ = false;
    412       this.dragging_ = false;
    413       this.totalMoveY_ = 0;
    414       this.totalMoveX_ = 0;
    415     },
    416 
    417     /**
    418      * Reset the touchable element as if we never saw the touchStart
    419      * Doesn't dispatch any end events - be careful of existing listeners.
    420      */
    421     cancelTouch: function() {
    422       this.stopTouching_();
    423       this.endTracking_();
    424       // If clients needed to be aware of this, we could fire a cancel event
    425       // here.
    426     },
    427 
    428     /**
    429      * Record that touching has stopped
    430      * @private
    431      */
    432     stopTouching_: function() {
    433       // Mark as no longer being touched
    434       this.activeTouch_ = undefined;
    435 
    436       // If we're waiting for a long press, stop
    437       window.clearTimeout(this.longPressTimeout_);
    438 
    439       // Stop listening for move/end events until there's another touch.
    440       // We don't want to leave handlers piled up on the document.
    441       // Note that there's no harm in removing handlers that weren't added, so
    442       // rather than track whether we're using mouse or touch we do both.
    443       this.events_.remove(document, 'touchmove');
    444       this.events_.remove(document, 'touchend');
    445       this.events_.remove(document, 'touchcancel');
    446       this.events_.remove(document, 'mousemove');
    447       this.events_.remove(document, 'mouseup');
    448     },
    449 
    450     /**
    451      * Touch start handler.
    452      * @param {!TouchEvent} e The touchstart event.
    453      * @private
    454      */
    455     onStart_: function(e) {
    456       // Only process single touches.  If there is already a touch happening, or
    457       // two simultaneous touches then just ignore them.
    458       if (e.touches.length > 1)
    459         // Note that we could cancel an active touch here.  That would make
    460         // simultaneous touch behave similar to near-simultaneous. However, if
    461         // the user is dragging something, an accidental second touch could be
    462         // quite disruptive if it cancelled their drag.  Better to just ignore
    463         // it.
    464         return;
    465 
    466       // It's still possible there could be an active "touch" if the user is
    467       // simultaneously using a mouse and a touch input.
    468       if (this.activeTouch_ !== undefined)
    469         return;
    470 
    471       var touch = e.targetTouches[0];
    472       this.activeTouch_ = touch.identifier;
    473 
    474       // We've just started touching so shouldn't swallow any upcoming click
    475       if (this.swallowNextClick_)
    476         this.swallowNextClick_ = false;
    477 
    478       // Sign up for end/cancel notifications for this touch.
    479       // Note that we do this on the document so that even if the user drags
    480       // their finger off the element, we'll still know what they're doing.
    481       if (e.type == 'mousedown') {
    482         this.events_.add(document, 'mouseup',
    483             this.mouseToTouchCallback_(this.onEnd_.bind(this)), false);
    484       } else {
    485         this.events_.add(document, 'touchend', this.onEnd_.bind(this), false);
    486         this.events_.add(document, 'touchcancel', this.onEnd_.bind(this),
    487             false);
    488       }
    489 
    490       // This timeout is cleared on touchEnd and onDrag
    491       // If we invoke the function then we have a real long press
    492       window.clearTimeout(this.longPressTimeout_);
    493       this.longPressTimeout_ = window.setTimeout(
    494           this.onLongPress_.bind(this),
    495           TouchHandler.TIME_FOR_LONG_PRESS_);
    496 
    497       // Dispatch the TOUCH_START event
    498       if (!this.dispatchEvent_(TouchHandler.EventType.TOUCH_START, touch))
    499         // Dragging was not enabled, nothing more to do
    500         return;
    501 
    502       // We want dragging notifications
    503       if (e.type == 'mousedown') {
    504         this.events_.add(document, 'mousemove',
    505             this.mouseToTouchCallback_(this.onMove_.bind(this)), false);
    506       } else {
    507         this.events_.add(document, 'touchmove', this.onMove_.bind(this), false);
    508       }
    509 
    510       this.startTouchX_ = this.lastTouchX_ = touch.clientX;
    511       this.startTouchY_ = this.lastTouchY_ = touch.clientY;
    512       this.startTime_ = e.timeStamp;
    513 
    514       this.recentTouchesX_ = [];
    515       this.recentTouchesY_ = [];
    516       this.recentTouchesX_.push(touch.clientX, e.timeStamp);
    517       this.recentTouchesY_.push(touch.clientY, e.timeStamp);
    518 
    519       this.beginTracking_();
    520     },
    521 
    522     /**
    523      * Given a list of Touches, find the one matching our activeTouch
    524      * identifier. Note that Chrome currently always uses 0 as the identifier.
    525      * In that case we'll end up always choosing the first element in the list.
    526      * @param {TouchList} touches The list of Touch objects to search.
    527      * @return {!Touch|undefined} The touch matching our active ID if any.
    528      * @private
    529      */
    530     findActiveTouch_: function(touches) {
    531       assert(this.activeTouch_ !== undefined, 'Expecting an active touch');
    532       // A TouchList isn't actually an array, so we shouldn't use
    533       // Array.prototype.filter/some, etc.
    534       for (var i = 0; i < touches.length; i++) {
    535         if (touches[i].identifier == this.activeTouch_)
    536           return touches[i];
    537       }
    538       return undefined;
    539     },
    540 
    541     /**
    542      * Touch move handler.
    543      * @param {!TouchEvent} e The touchmove event.
    544      * @private
    545      */
    546     onMove_: function(e) {
    547       if (!this.tracking_)
    548         return;
    549 
    550       // Our active touch should always be in the list of touches still active
    551       assert(this.findActiveTouch_(e.touches), 'Missing touchEnd');
    552 
    553       var that = this;
    554       var touch = this.findActiveTouch_(e.changedTouches);
    555       if (!touch)
    556         return;
    557 
    558       var clientX = touch.clientX;
    559       var clientY = touch.clientY;
    560 
    561       var moveX = this.lastTouchX_ - clientX;
    562       var moveY = this.lastTouchY_ - clientY;
    563       this.totalMoveX_ += Math.abs(moveX);
    564       this.totalMoveY_ += Math.abs(moveY);
    565       this.lastTouchX_ = clientX;
    566       this.lastTouchY_ = clientY;
    567 
    568       if (!this.dragging_ && (this.totalMoveY_ >
    569           TouchHandler.MIN_TRACKING_FOR_DRAG_ ||
    570           this.totalMoveX_ >
    571           TouchHandler.MIN_TRACKING_FOR_DRAG_)) {
    572         // If we're waiting for a long press, stop
    573         window.clearTimeout(this.longPressTimeout_);
    574 
    575         // Dispatch the DRAG_START event and record whether dragging should be
    576         // allowed or not.  Note that this relies on the current value of
    577         // startTouchX/Y - handlers may use the initial drag delta to determine
    578         // if dragging should be permitted.
    579         this.dragging_ = this.dispatchEvent_(
    580             TouchHandler.EventType.DRAG_START, touch);
    581 
    582         if (this.dragging_) {
    583           // Update the start position here so that drag deltas have better
    584           // values but don't touch the recent positions so that velocity
    585           // calculations can still use touchstart position in the time and
    586           // distance delta.
    587           this.startTouchX_ = clientX;
    588           this.startTouchY_ = clientY;
    589           this.startTime_ = e.timeStamp;
    590         } else {
    591           this.endTracking_();
    592         }
    593       }
    594 
    595       if (this.dragging_) {
    596         this.dispatchEvent_(TouchHandler.EventType.DRAG_MOVE, touch);
    597 
    598         this.removeTouchesInWrongDirection_(this.recentTouchesX_,
    599             this.lastMoveX_, moveX);
    600         this.removeTouchesInWrongDirection_(this.recentTouchesY_,
    601             this.lastMoveY_, moveY);
    602         this.removeOldTouches_(this.recentTouchesX_, e.timeStamp);
    603         this.removeOldTouches_(this.recentTouchesY_, e.timeStamp);
    604         this.recentTouchesX_.push(clientX, e.timeStamp);
    605         this.recentTouchesY_.push(clientY, e.timeStamp);
    606       }
    607 
    608       this.lastMoveX_ = moveX;
    609       this.lastMoveY_ = moveY;
    610     },
    611 
    612     /**
    613      * Filters the provided recent touches array to remove all touches except
    614      * the last if the move direction has changed.
    615      * @param {!Array.<number>} recentTouches An array of tuples where the first
    616      *     item is the x or y component of the recent touch and the second item
    617      *     is the touch time stamp.
    618      * @param {number|undefined} lastMove The x or y component of the previous
    619      *     move.
    620      * @param {number} recentMove The x or y component of the most recent move.
    621      * @private
    622      */
    623     removeTouchesInWrongDirection_: function(recentTouches, lastMove,
    624         recentMove) {
    625       if (lastMove && recentMove && recentTouches.length > 2 &&
    626           (lastMove > 0 ^ recentMove > 0)) {
    627         recentTouches.splice(0, recentTouches.length - 2);
    628       }
    629     },
    630 
    631     /**
    632      * Filters the provided recent touches array to remove all touches older
    633      * than the max tracking time or the 5th most recent touch.
    634      * @param {!Array.<number>} recentTouches An array of tuples where the first
    635      *     item is the x or y component of the recent touch and the second item
    636      *     is the touch time stamp.
    637      * @param {number} recentTime The time of the most recent event.
    638      * @private
    639      */
    640     removeOldTouches_: function(recentTouches, recentTime) {
    641       while (recentTouches.length && recentTime - recentTouches[1] >
    642           TouchHandler.MAX_TRACKING_TIME_ ||
    643           recentTouches.length >
    644               TouchHandler.MAX_TRACKING_TOUCHES_ * 2) {
    645         recentTouches.splice(0, 2);
    646       }
    647     },
    648 
    649     /**
    650      * Touch end handler.
    651      * @param {!TouchEvent} e The touchend event.
    652      * @private
    653      */
    654     onEnd_: function(e) {
    655       var that = this;
    656       assert(this.activeTouch_ !== undefined, 'Expect to already be touching');
    657 
    658       // If the touch we're tracking isn't changing here, ignore this touch end.
    659       var touch = this.findActiveTouch_(e.changedTouches);
    660       if (!touch) {
    661         // In most cases, our active touch will be in the 'touches' collection,
    662         // but we can't assert that because occasionally two touchend events can
    663         // occur at almost the same time with both having empty 'touches' lists.
    664         // I.e., 'touches' seems like it can be a bit more up-to-date than the
    665         // current event.
    666         return;
    667       }
    668 
    669       // This is touchEnd for the touch we're monitoring
    670       assert(!this.findActiveTouch_(e.touches),
    671              'Touch ended also still active');
    672 
    673       // Indicate that touching has finished
    674       this.stopTouching_();
    675 
    676       if (this.tracking_) {
    677         var clientX = touch.clientX;
    678         var clientY = touch.clientY;
    679 
    680         if (this.dragging_) {
    681           this.endTime_ = e.timeStamp;
    682           this.endTouchX_ = clientX;
    683           this.endTouchY_ = clientY;
    684 
    685           this.removeOldTouches_(this.recentTouchesX_, e.timeStamp);
    686           this.removeOldTouches_(this.recentTouchesY_, e.timeStamp);
    687 
    688           this.dispatchEvent_(TouchHandler.EventType.DRAG_END, touch);
    689 
    690           // Note that in some situations we can get a click event here as well.
    691           // For now this isn't a problem, but we may want to consider having
    692           // some logic that hides clicks that appear to be caused by a touchEnd
    693           // used for dragging.
    694         }
    695 
    696         this.endTracking_();
    697       }
    698 
    699       // Note that we dispatch the touchEnd event last so that events at
    700       // different levels of semantics nest nicely (similar to how DOM
    701       // drag-and-drop events are nested inside of the mouse events that trigger
    702       // them).
    703       this.dispatchEvent_(TouchHandler.EventType.TOUCH_END, touch);
    704     },
    705 
    706     /**
    707      * Get end velocity of the drag. This method is specific to drag behavior,
    708      * so if touch behavior and drag behavior is split then this should go with
    709      * drag behavior. End velocity is defined as deltaXY / deltaTime where
    710      * deltaXY is the difference between endPosition and the oldest recent
    711      * position, and deltaTime is the difference between endTime and the oldest
    712      * recent time stamp.
    713      * @return {Object} The x and y velocity.
    714      */
    715     getEndVelocity: function() {
    716       // Note that we could move velocity to just be an end-event parameter.
    717       var velocityX = this.recentTouchesX_.length ?
    718           (this.endTouchX_ - this.recentTouchesX_[0]) /
    719           (this.endTime_ - this.recentTouchesX_[1]) : 0;
    720       var velocityY = this.recentTouchesY_.length ?
    721           (this.endTouchY_ - this.recentTouchesY_[0]) /
    722           (this.endTime_ - this.recentTouchesY_[1]) : 0;
    723 
    724       velocityX = this.correctVelocity_(velocityX);
    725       velocityY = this.correctVelocity_(velocityY);
    726 
    727       return {
    728         x: velocityX,
    729         y: velocityY
    730       };
    731     },
    732 
    733     /**
    734      * Correct erroneous velocities by capping the velocity if we think it's too
    735      * high, or setting it to a default velocity if know that the event data is
    736      * bad.
    737      * @param {number} velocity The x or y velocity component.
    738      * @return {number} The corrected velocity.
    739      * @private
    740      */
    741     correctVelocity_: function(velocity) {
    742       var absVelocity = Math.abs(velocity);
    743 
    744       // We add to recent touches for each touchstart and touchmove. If we have
    745       // fewer than 3 touches (6 entries), we assume that the thread was blocked
    746       // for the duration of the drag and we received events in quick succession
    747       // with the wrong time stamps.
    748       if (absVelocity > TouchHandler.MAXIMUM_VELOCITY_) {
    749         absVelocity = this.recentTouchesY_.length < 3 ?
    750             TouchHandler.VELOCITY_FOR_INCORRECT_EVENTS_ :
    751                 TouchHandler.MAXIMUM_VELOCITY_;
    752       }
    753       return absVelocity * (velocity < 0 ? -1 : 1);
    754     },
    755 
    756     /**
    757      * Handler when an element has been pressed for a long time
    758      * @private
    759      */
    760     onLongPress_: function() {
    761       // Swallow any click that occurs on this element without an intervening
    762       // touch start event.  This simple click-busting technique should be
    763       // sufficient here since a real click should have a touchstart first.
    764       this.swallowNextClick_ = true;
    765 
    766       // Dispatch to the LONG_PRESS
    767       this.dispatchEventXY_(TouchHandler.EventType.LONG_PRESS, this.element_,
    768           this.startTouchX_, this.startTouchY_);
    769     },
    770 
    771     /**
    772      * Click handler - used to swallow clicks after a long-press
    773      * @param {!Event} e The click event.
    774      * @private
    775      */
    776     onClick_: function(e) {
    777       if (this.swallowNextClick_) {
    778         e.preventDefault();
    779         e.stopPropagation();
    780         this.swallowNextClick_ = false;
    781       }
    782     },
    783 
    784     /**
    785      * Dispatch a TouchHandler event to the element
    786      * @param {string} eventType The event to dispatch.
    787      * @param {Touch} touch The touch triggering this event.
    788      * @return {boolean|undefined} The value of enableDrag after dispatching
    789      *         the event.
    790      * @private
    791      */
    792     dispatchEvent_: function(eventType, touch) {
    793 
    794       // Determine which element was touched.  For mouse events, this is always
    795       // the event/touch target.  But for touch events, the target is always the
    796       // target of the touchstart (and it's unlikely we can change this
    797       // since the common implementation of touch dragging relies on it). Since
    798       // touch is our primary scenario (which we want to emulate with mouse),
    799       // we'll treat both cases the same and not depend on the target.
    800       var touchedElement;
    801       if (eventType == TouchHandler.EventType.TOUCH_START) {
    802         touchedElement = touch.target;
    803       } else {
    804         touchedElement = this.element_.ownerDocument.
    805             elementFromPoint(touch.clientX, touch.clientY);
    806       }
    807 
    808       return this.dispatchEventXY_(eventType, touchedElement, touch.clientX,
    809           touch.clientY);
    810     },
    811 
    812     /**
    813      * Dispatch a TouchHandler event to the element
    814      * @param {string} eventType The event to dispatch.
    815        @param {number} clientX The X location for the event.
    816        @param {number} clientY The Y location for the event.
    817      * @return {boolean|undefined} The value of enableDrag after dispatching
    818      *         the event.
    819      * @private
    820      */
    821     dispatchEventXY_: function(eventType, touchedElement, clientX, clientY) {
    822       var isDrag = (eventType == TouchHandler.EventType.DRAG_START ||
    823           eventType == TouchHandler.EventType.DRAG_MOVE ||
    824           eventType == TouchHandler.EventType.DRAG_END);
    825 
    826       // Drag events don't bubble - we're really just dragging the element,
    827       // not affecting its parent at all.
    828       var bubbles = !isDrag;
    829 
    830       var event = new TouchHandler.Event(eventType, bubbles, clientX, clientY,
    831           touchedElement);
    832 
    833       // Set enableDrag when it can be overridden
    834       if (eventType == TouchHandler.EventType.TOUCH_START)
    835         event.enableDrag = false;
    836       else if (eventType == TouchHandler.EventType.DRAG_START)
    837         event.enableDrag = true;
    838 
    839       if (isDrag) {
    840         event.dragDeltaX = clientX - this.startTouchX_;
    841         event.dragDeltaY = clientY - this.startTouchY_;
    842       }
    843 
    844       this.element_.dispatchEvent(event);
    845       return event.enableDrag;
    846     }
    847   };
    848 
    849   return TouchHandler;
    850 })();
    851