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) 34var 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