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 |