| OLD | NEW |
| (Empty) |
| 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 Grabber implementation. | |
| 7 * Allows you to pick up objects (with a long-press) and drag them around the | |
| 8 * screen. | |
| 9 * | |
| 10 * Note: This should perhaps really use standard drag-and-drop events, but there | |
| 11 * is no standard for them on touch devices. We could define a model for | |
| 12 * activating touch-based dragging of elements (programatically and/or with | |
| 13 * CSS attributes) and use it here (even have a JS library to generate such | |
| 14 * events when the browser doesn't support them). | |
| 15 */ | |
| 16 | |
| 17 // Use an anonymous function to enable strict mode just for this file (which | |
| 18 // will be concatenated with other files when embedded in Chrome) | |
| 19 var Grabber = (function() { | |
| 20 'use strict'; | |
| 21 | |
| 22 /** | |
| 23 * Create a Grabber object to enable grabbing and dragging a given element. | |
| 24 * @constructor | |
| 25 * @param {!Element} element The element that can be grabbed and moved. | |
| 26 */ | |
| 27 function Grabber(element) { | |
| 28 /** | |
| 29 * The element the grabber is attached to. | |
| 30 * @type {!Element} | |
| 31 * @private | |
| 32 */ | |
| 33 this.element_ = element; | |
| 34 | |
| 35 /** | |
| 36 * The TouchHandler responsible for firing lower-level touch events when the | |
| 37 * element is manipulated. | |
| 38 * @type {!TouchHandler} | |
| 39 * @private | |
| 40 */ | |
| 41 this.touchHandler_ = new TouchHandler(this.element); | |
| 42 | |
| 43 /** | |
| 44 * Tracks all event listeners we have created. | |
| 45 * @type {EventTracker} | |
| 46 * @private | |
| 47 */ | |
| 48 this.events_ = new EventTracker(); | |
| 49 | |
| 50 // Enable the generation of events when the element is touched (but no need | |
| 51 // to use the early capture phase of event processing). | |
| 52 this.touchHandler_.enable(/* opt_capture */ false); | |
| 53 | |
| 54 // Prevent any built-in drag-and-drop support from activating for the | |
| 55 // element. Note that we don't want details of how we're implementing | |
| 56 // dragging here to leak out of this file (eg. we may switch to using webkit | |
| 57 // drag-and-drop). | |
| 58 this.events_.add(this.element, 'dragstart', function(e) { | |
| 59 e.preventDefault(); | |
| 60 }, true); | |
| 61 | |
| 62 // Add our TouchHandler event listeners | |
| 63 this.events_.add(this.element, TouchHandler.EventType.TOUCH_START, | |
| 64 this.onTouchStart_.bind(this), false); | |
| 65 this.events_.add(this.element, TouchHandler.EventType.LONG_PRESS, | |
| 66 this.onLongPress_.bind(this), false); | |
| 67 this.events_.add(this.element, TouchHandler.EventType.DRAG_START, | |
| 68 this.onDragStart_.bind(this), false); | |
| 69 this.events_.add(this.element, TouchHandler.EventType.DRAG_MOVE, | |
| 70 this.onDragMove_.bind(this), false); | |
| 71 this.events_.add(this.element, TouchHandler.EventType.DRAG_END, | |
| 72 this.onDragEnd_.bind(this), false); | |
| 73 this.events_.add(this.element, TouchHandler.EventType.TOUCH_END, | |
| 74 this.onTouchEnd_.bind(this), false); | |
| 75 } | |
| 76 | |
| 77 /** | |
| 78 * Events fired by the grabber. | |
| 79 * Events are fired at the element affected (not the element being dragged). | |
| 80 * @enum {string} | |
| 81 */ | |
| 82 Grabber.EventType = { | |
| 83 // Fired at the grabber element when it is first grabbed | |
| 84 GRAB: 'grabber:grab', | |
| 85 // Fired at the grabber element when dragging begins (after GRAB) | |
| 86 DRAG_START: 'grabber:dragstart', | |
| 87 // Fired at an element when something is dragged over top of it. | |
| 88 DRAG_ENTER: 'grabber:dragenter', | |
| 89 // Fired at an element when something is no longer over top of it. | |
| 90 // Not fired at all in the case of a DROP | |
| 91 DRAG_LEAVE: 'grabber:drag', | |
| 92 // Fired at an element when something is dropped on top of it. | |
| 93 DROP: 'grabber:drop', | |
| 94 // Fired at the grabber element when dragging ends (successfully or not) - | |
| 95 // after any DROP or DRAG_LEAVE | |
| 96 DRAG_END: 'grabber:dragend', | |
| 97 // Fired at the grabber element when it is released (even if no drag | |
| 98 // occured) - after any DRAG_END event. | |
| 99 RELEASE: 'grabber:release' | |
| 100 }; | |
| 101 | |
| 102 /** | |
| 103 * The type of Event sent by Grabber | |
| 104 * @constructor | |
| 105 * @param {string} type The type of event (one of Grabber.EventType). | |
| 106 * @param {Element!} grabbedElement The element being dragged. | |
| 107 */ | |
| 108 Grabber.Event = function(type, grabbedElement) { | |
| 109 var event = document.createEvent('Event'); | |
| 110 event.initEvent(type, true, true); | |
| 111 event.__proto__ = Grabber.Event.prototype; | |
| 112 | |
| 113 /** | |
| 114 * The element which is being dragged. For some events this will be the | |
| 115 * same as 'target', but for events like DROP that are fired at another | |
| 116 * element it will be different. | |
| 117 * @type {!Element} | |
| 118 */ | |
| 119 event.grabbedElement = grabbedElement; | |
| 120 | |
| 121 return event; | |
| 122 }; | |
| 123 | |
| 124 Grabber.Event.prototype = { | |
| 125 __proto__: Event.prototype | |
| 126 }; | |
| 127 | |
| 128 | |
| 129 /** | |
| 130 * The CSS class to apply when an element is touched but not yet | |
| 131 * grabbed. | |
| 132 * @type {string} | |
| 133 */ | |
| 134 Grabber.PRESSED_CLASS = 'grabber-pressed'; | |
| 135 | |
| 136 /** | |
| 137 * The class to apply when an element has been held (including when it is | |
| 138 * being dragged. | |
| 139 * @type {string} | |
| 140 */ | |
| 141 Grabber.GRAB_CLASS = 'grabber-grabbed'; | |
| 142 | |
| 143 /** | |
| 144 * The class to apply when a grabbed element is being dragged. | |
| 145 * @type {string} | |
| 146 */ | |
| 147 Grabber.DRAGGING_CLASS = 'grabber-dragging'; | |
| 148 | |
| 149 Grabber.prototype = { | |
| 150 /** | |
| 151 * @return {!Element} The element that can be grabbed. | |
| 152 */ | |
| 153 get element() { | |
| 154 return this.element_; | |
| 155 }, | |
| 156 | |
| 157 /** | |
| 158 * Clean up all event handlers (eg. if the underlying element will be | |
| 159 * removed) | |
| 160 */ | |
| 161 dispose: function() { | |
| 162 this.touchHandler_.disable(); | |
| 163 this.events_.removeAll(); | |
| 164 | |
| 165 // Clean-up any active touch/drag | |
| 166 if (this.dragging_) | |
| 167 this.stopDragging_(); | |
| 168 this.onTouchEnd_(); | |
| 169 }, | |
| 170 | |
| 171 /** | |
| 172 * Invoked whenever this element is first touched | |
| 173 * @param {!TouchHandler.Event} e The TouchHandler event. | |
| 174 * @private | |
| 175 */ | |
| 176 onTouchStart_: function(e) { | |
| 177 this.element.classList.add(Grabber.PRESSED_CLASS); | |
| 178 | |
| 179 // Always permit the touch to perhaps trigger a drag | |
| 180 e.enableDrag = true; | |
| 181 }, | |
| 182 | |
| 183 /** | |
| 184 * Invoked whenever the element stops being touched. | |
| 185 * Can be called explicitly to cleanup any active touch. | |
| 186 * @param {!TouchHandler.Event=} opt_e The TouchHandler event. | |
| 187 * @private | |
| 188 */ | |
| 189 onTouchEnd_: function(opt_e) { | |
| 190 if (this.grabbed_) { | |
| 191 // Mark this element as no longer being grabbed | |
| 192 this.element.classList.remove(Grabber.GRAB_CLASS); | |
| 193 this.element.style.pointerEvents = ''; | |
| 194 this.grabbed_ = false; | |
| 195 | |
| 196 this.sendEvent_(Grabber.EventType.RELEASE, this.element); | |
| 197 } else { | |
| 198 this.element.classList.remove(Grabber.PRESSED_CLASS); | |
| 199 } | |
| 200 }, | |
| 201 | |
| 202 /** | |
| 203 * Handler for TouchHandler's LONG_PRESS event | |
| 204 * Invoked when the element is held (without being dragged) | |
| 205 * @param {!TouchHandler.Event} e The TouchHandler event. | |
| 206 * @private | |
| 207 */ | |
| 208 onLongPress_: function(e) { | |
| 209 assert(!this.grabbed_, 'Got longPress while still being held'); | |
| 210 | |
| 211 this.element.classList.remove(Grabber.PRESSED_CLASS); | |
| 212 this.element.classList.add(Grabber.GRAB_CLASS); | |
| 213 | |
| 214 // Disable mouse events from the element - we care only about what's | |
| 215 // under the element after it's grabbed (since we're getting move events | |
| 216 // from the body - not the element itself). Note that we can't wait until | |
| 217 // onDragStart to do this because it won't have taken effect by the first | |
| 218 // onDragMove. | |
| 219 this.element.style.pointerEvents = 'none'; | |
| 220 | |
| 221 this.grabbed_ = true; | |
| 222 | |
| 223 this.sendEvent_(Grabber.EventType.GRAB, this.element); | |
| 224 }, | |
| 225 | |
| 226 /** | |
| 227 * Invoked when the element is dragged. | |
| 228 * @param {!TouchHandler.Event} e The TouchHandler event. | |
| 229 * @private | |
| 230 */ | |
| 231 onDragStart_: function(e) { | |
| 232 assert(!this.lastEnter_, 'only expect one drag to occur at a time'); | |
| 233 assert(!this.dragging_); | |
| 234 | |
| 235 // We only want to drag the element if its been grabbed | |
| 236 if (this.grabbed_) { | |
| 237 // Mark the item as being dragged | |
| 238 // Ensures our translate transform won't be animated and cancels any | |
| 239 // outstanding animations. | |
| 240 this.element.classList.add(Grabber.DRAGGING_CLASS); | |
| 241 | |
| 242 // Determine the webkitTransform currently applied to the element. | |
| 243 // Note that it's important that we do this AFTER cancelling animation, | |
| 244 // otherwise we could see an intermediate value. | |
| 245 // We'll assume this value will be constant for the duration of the drag | |
| 246 // so that we can combine it with our translate3d transform. | |
| 247 this.baseTransform_ = this.element.ownerDocument.defaultView. | |
| 248 getComputedStyle(this.element).webkitTransform; | |
| 249 | |
| 250 this.sendEvent_(Grabber.EventType.DRAG_START, this.element); | |
| 251 e.enableDrag = true; | |
| 252 this.dragging_ = true; | |
| 253 | |
| 254 } else { | |
| 255 // Hasn't been grabbed - don't drag, just unpress | |
| 256 this.element.classList.remove(Grabber.PRESSED_CLASS); | |
| 257 e.enableDrag = false; | |
| 258 } | |
| 259 }, | |
| 260 | |
| 261 /** | |
| 262 * Invoked when a grabbed element is being dragged | |
| 263 * @param {!TouchHandler.Event} e The TouchHandler event. | |
| 264 * @private | |
| 265 */ | |
| 266 onDragMove_: function(e) { | |
| 267 assert(this.grabbed_ && this.dragging_); | |
| 268 | |
| 269 this.translateTo_(e.dragDeltaX, e.dragDeltaY); | |
| 270 | |
| 271 var target = e.touchedElement; | |
| 272 if (target && target != this.lastEnter_) { | |
| 273 // Send the events | |
| 274 this.sendDragLeave_(e); | |
| 275 this.sendEvent_(Grabber.EventType.DRAG_ENTER, target); | |
| 276 } | |
| 277 this.lastEnter_ = target; | |
| 278 }, | |
| 279 | |
| 280 /** | |
| 281 * Send DRAG_LEAVE to the element last sent a DRAG_ENTER if any. | |
| 282 * @param {!TouchHandler.Event} e The event triggering this DRAG_LEAVE. | |
| 283 * @private | |
| 284 */ | |
| 285 sendDragLeave_: function(e) { | |
| 286 if (this.lastEnter_) { | |
| 287 this.sendEvent_(Grabber.EventType.DRAG_LEAVE, this.lastEnter_); | |
| 288 this.lastEnter_ = undefined; | |
| 289 } | |
| 290 }, | |
| 291 | |
| 292 /** | |
| 293 * Moves the element to the specified position. | |
| 294 * @param {number} x Horizontal position to move to. | |
| 295 * @param {number} y Vertical position to move to. | |
| 296 * @private | |
| 297 */ | |
| 298 translateTo_: function(x, y) { | |
| 299 // Order is important here - we want to translate before doing the zoom | |
| 300 this.element.style.WebkitTransform = 'translate3d(' + x + 'px, ' + | |
| 301 y + 'px, 0) ' + this.baseTransform_; | |
| 302 }, | |
| 303 | |
| 304 /** | |
| 305 * Invoked when the element is no longer being dragged. | |
| 306 * @param {TouchHandler.Event} e The TouchHandler event. | |
| 307 * @private | |
| 308 */ | |
| 309 onDragEnd_: function(e) { | |
| 310 // We should get this before the onTouchEnd. Don't change | |
| 311 // this.grabbed_ - it's onTouchEnd's responsibility to clear it. | |
| 312 assert(this.grabbed_ && this.dragging_); | |
| 313 var event; | |
| 314 | |
| 315 // Send the drop event to the element underneath the one we're dragging. | |
| 316 var target = e.touchedElement; | |
| 317 if (target) | |
| 318 this.sendEvent_(Grabber.EventType.DROP, target); | |
| 319 | |
| 320 // Cleanup and send DRAG_END | |
| 321 // Note that like HTML5 DND, we don't send DRAG_LEAVE on drop | |
| 322 this.stopDragging_(); | |
| 323 }, | |
| 324 | |
| 325 /** | |
| 326 * Clean-up the active drag and send DRAG_LEAVE | |
| 327 * @private | |
| 328 */ | |
| 329 stopDragging_: function() { | |
| 330 assert(this.dragging_); | |
| 331 this.lastEnter_ = undefined; | |
| 332 | |
| 333 // Mark the element as no longer being dragged | |
| 334 this.element.classList.remove(Grabber.DRAGGING_CLASS); | |
| 335 this.element.style.webkitTransform = ''; | |
| 336 | |
| 337 this.dragging_ = false; | |
| 338 this.sendEvent_(Grabber.EventType.DRAG_END, this.element); | |
| 339 }, | |
| 340 | |
| 341 /** | |
| 342 * Send a Grabber event to a specific element | |
| 343 * @param {string} eventType The type of event to send. | |
| 344 * @param {!Element} target The element to send the event to. | |
| 345 * @private | |
| 346 */ | |
| 347 sendEvent_: function(eventType, target) { | |
| 348 var event = new Grabber.Event(eventType, this.element); | |
| 349 target.dispatchEvent(event); | |
| 350 }, | |
| 351 | |
| 352 /** | |
| 353 * Whether or not the element is currently grabbed. | |
| 354 * @type {boolean} | |
| 355 * @private | |
| 356 */ | |
| 357 grabbed_: false, | |
| 358 | |
| 359 /** | |
| 360 * Whether or not the element is currently being dragged. | |
| 361 * @type {boolean} | |
| 362 * @private | |
| 363 */ | |
| 364 dragging_: false, | |
| 365 | |
| 366 /** | |
| 367 * The webkitTransform applied to the element when it first started being | |
| 368 * dragged. | |
| 369 * @type {string|undefined} | |
| 370 * @private | |
| 371 */ | |
| 372 baseTransform_: undefined, | |
| 373 | |
| 374 /** | |
| 375 * The element for which a DRAG_ENTER event was last fired | |
| 376 * @type {Element|undefined} | |
| 377 * @private | |
| 378 */ | |
| 379 lastEnter_: undefined | |
| 380 }; | |
| 381 | |
| 382 return Grabber; | |
| 383 })(); | |
| OLD | NEW |