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