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