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 })(); 384