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