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 |