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