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 * @constructor | |
arv (Not doing code reviews)
2011/03/10 19:25:59
Missing description
Rick Byers
2011/03/11 02:44:33
Done.
| |
19 * @param {!Element} element The element that can be grabbed and moved. | |
20 */ | |
21 function Grabber(element) { | |
22 /** | |
23 * @type {!Element} | |
24 * @private | |
25 */ | |
26 this.element_ = element; | |
27 | |
28 /** | |
29 * @type {!TouchHandler} | |
30 * @private | |
31 */ | |
32 this.touchHandler_ = new TouchHandler(this.element_); | |
33 | |
34 /** | |
35 * @type {boolean} | |
36 * @private | |
37 */ | |
38 this.grabbed_ = false; | |
39 | |
40 /** | |
41 * @type {boolean} | |
42 * @private | |
43 */ | |
44 this.dragging_ = false; | |
45 | |
46 /** | |
47 * @type {EventTracker} | |
48 * @private | |
49 */ | |
50 this.events_ = new EventTracker(); | |
51 | |
52 this.touchHandler_.enable(/* opt_capture */ false); | |
arv (Not doing code reviews)
2011/03/10 19:25:59
this.touchHandler_.enabled = false;
seems like a
Rick Byers
2011/03/11 02:44:33
enabled=false would suggest to me that the touchHa
arv (Not doing code reviews)
2011/03/11 20:08:46
OK, then it makes sense.
| |
53 | |
54 // Prevent any built-in drag-and-drop support from activating for the element. | |
55 // Note that we don't want details of how we're implementing dragging here to | |
56 // leak out of this file (eg. we may switch to using webkit drag-and-drop). | |
57 this.events_.add(this.element_, 'dragstart', function(e) { | |
58 e.preventDefault(); | |
59 }, true); | |
60 | |
61 // Add our TouchHandler event listeners | |
62 this.events_.add(this.element_, TouchHandler.EventType.TOUCH_START, | |
63 this.onTouchStart_.bind(this), false); | |
64 this.events_.add(this.element_, TouchHandler.EventType.LONG_PRESS, | |
65 this.onLongPress_.bind(this), false); | |
66 this.events_.add(this.element_, TouchHandler.EventType.DRAG_START, | |
67 this.onDragStart_.bind(this), false); | |
68 this.events_.add(this.element_, TouchHandler.EventType.DRAG_MOVE, | |
69 this.onDragMove_.bind(this), false); | |
70 this.events_.add(this.element_, TouchHandler.EventType.DRAG_END, | |
71 this.onDragEnd_.bind(this), false); | |
72 this.events_.add(this.element_, TouchHandler.EventType.TOUCH_END, | |
73 this.onTouchEnd_.bind(this), false); | |
74 } | |
75 | |
76 /** | |
77 * Events fired by the grabber. | |
78 * Events are fired at the element affected (not the element being dragged). | |
arv (Not doing code reviews)
2011/03/10 19:25:59
@enum {string}
Rick Byers
2011/03/11 02:44:33
Done.
| |
79 */ | |
80 Grabber.EventType = { | |
81 // Fired at the grabber element when it is first grabbed | |
82 GRAB: 'grabber:grab', | |
83 // Fired at the grabber element when dragging begins (after GRAB) | |
84 DRAG_START: 'grabber:dragstart', | |
85 // Fired at an element when something is dragged over top of it. | |
86 DRAG_ENTER: 'grabber:dragenter', | |
87 // Fired at an element when something is no longer over top of it. | |
88 // Not fired at all in the case of a DROP | |
89 DRAG_LEAVE: 'grabber:drag', | |
90 // Fired at an element when something is dropped on top of it. | |
91 DROP: 'grabber:drop', | |
92 // Fired at the grabber element when dragging ends (successfully or not) - | |
93 // after any DROP or DRAG_LEAVE | |
94 DRAG_END: 'grabber:dragend', | |
95 // Fired at the grabber element when it is released (even if no drag | |
96 // occured) - after any DRAG_END event. | |
97 RELEASE: 'grabber:release' | |
98 }; | |
99 | |
100 /** | |
101 * The CSS class to apply when an element is touched but not yet | |
102 * grabbed. | |
103 * @type {string} | |
104 */ | |
105 Grabber.PRESSED_CLASS = 'grabber-pressed'; | |
106 | |
107 /** | |
108 * The class to apply when an element has been held (including when it is | |
109 * being dragged. | |
110 * @type {string} | |
111 */ | |
112 Grabber.GRAB_CLASS = 'grabber-grabbed'; | |
113 | |
114 /** | |
115 * The class to apply when a grabbed element is being dragged. | |
116 * @type {string} | |
117 */ | |
118 Grabber.DRAGGING_CLASS = 'grabber-dragging'; | |
119 | |
120 /** | |
121 * The webkitTransform applied to the element when it first started being | |
122 * dragged. | |
123 * @type {string|undefined} | |
124 */ | |
125 Grabber.prototype.baseTransform_; | |
126 | |
127 /** | |
128 * The element for which a DRAG_ENTER event was last fired | |
129 * @type {Element|undefined} | |
130 */ | |
131 Grabber.prototype.lastEnter_; | |
132 | |
133 /** | |
134 * Clean up all event handlers (eg. if the underlying element will be removed) | |
135 */ | |
136 Grabber.prototype.dispose = function() { | |
137 this.touchHandler_.disable(); | |
138 this.events_.removeAll(); | |
139 | |
140 // Clean-up any active touch/drag | |
141 if (this.dragging_) { | |
arv (Not doing code reviews)
2011/03/10 19:25:59
Chrome style is to not use curly braces for single
Rick Byers
2011/03/11 02:44:33
Done.
Does this apply to if/else blocks to? Eg. I
arv (Not doing code reviews)
2011/03/11 20:08:46
Yes.
If either the if or else part is more than o
Rick Byers
2011/03/15 21:47:54
Ok, thanks. I believe I've updated for this braci
| |
142 this.stopDragging_(); | |
143 } | |
144 this.onTouchEnd_(); | |
145 }; | |
146 | |
147 /** | |
148 * Invoked whenever this element is first touched | |
149 * @param {!CustomEvent} e The TouchHandler event. | |
arv (Not doing code reviews)
2011/03/10 19:25:59
CustomEvent is a useless DOM interface. Just use E
Rick Byers
2011/03/11 02:44:33
The only reason I used CustomEvent is that it adds
| |
150 * @private | |
151 */ | |
152 Grabber.prototype.onTouchStart_ = function(e) { | |
153 this.element_.classList.add(Grabber.PRESSED_CLASS); | |
154 | |
155 // Always permit the touch to perhaps trigger a drag | |
156 e.detail.enableDrag = true; | |
157 }; | |
158 | |
159 /** | |
160 * Invoked whenever the element stops being touched. | |
161 * Can be called explicitly to cleanup any active touch. | |
162 * @param {!CustomEvent=} opt_e The TouchHandler event. | |
163 * @private | |
164 */ | |
165 Grabber.prototype.onTouchEnd_ = function(opt_e) { | |
166 if (this.grabbed_) { | |
167 // Mark this element as no longer being grabbed | |
168 this.element_.classList.remove(Grabber.GRAB_CLASS); | |
169 this.grabbed_ = false; | |
170 | |
171 this.dispatchEvent_(Grabber.EventType.RELEASE); | |
arv (Not doing code reviews)
2011/03/10 19:25:59
cr.dispatchSimpleEvent(this, Grabber.EventType.REL
Rick Byers
2011/03/11 02:44:33
I should have looked more closely at the utilities
arv (Not doing code reviews)
2011/03/11 20:08:46
(I misunderstood this. See more in next comment)
Rick Byers
2011/03/15 21:47:54
See below.
| |
172 } | |
173 else | |
174 { | |
175 this.element_.classList.remove(Grabber.PRESSED_CLASS); | |
176 } | |
177 }; | |
178 | |
179 /** | |
180 * Handler for TouchHandler's LONG_PRESS event | |
181 * Invoked when the element is held (without being dragged) | |
182 * @param {!CustomEvent} e The TouchHandler event. | |
183 * @private | |
184 */ | |
185 Grabber.prototype.onLongPress_ = function(e) { | |
186 assert(!this.grabbed_, 'Got longPress while still being held'); | |
187 | |
188 this.element_.classList.remove(Grabber.PRESSED_CLASS); | |
189 this.element_.classList.add(Grabber.GRAB_CLASS); | |
190 this.grabbed_ = true; | |
191 | |
192 this.dispatchEvent_(Grabber.EventType.GRAB); | |
193 }; | |
194 | |
195 /** | |
196 * Invoked when the element is dragged. | |
197 * @param {!CustomEvent} e The TouchHandler event. | |
198 * @private | |
199 */ | |
200 Grabber.prototype.onDragStart_ = function(e) { | |
201 var that = this; | |
202 assert(!this.lastEnter_, 'only expect one drag to occur at a time'); | |
203 assert(!this.dragging_); | |
204 | |
205 // We only want to drag the element if its been grabbed | |
206 if (this.grabbed_) { | |
207 // Mark the item as being dragged | |
208 // Ensures our translate transform won't be animated and cancels any | |
209 // outstanding animations. | |
210 this.element_.classList.add(Grabber.DRAGGING_CLASS); | |
211 | |
212 // Determine the webkitTransform currently applied to the element. | |
213 // Note that it's important that we do this AFTER cancelling animation, | |
214 // otherwise we could see an intermediate value. | |
215 // We'll assume this value will be constant for the duration of the drag | |
216 // so that we can combine it with our translate3d transform. | |
217 this.baseTransform_ = window.getComputedStyle(this.element_, null). | |
arv (Not doing code reviews)
2011/03/10 19:25:59
is this the right window?
this.ownerDocument.defa
arv (Not doing code reviews)
2011/03/10 19:25:59
skip null here?
Rick Byers
2011/03/11 02:44:33
Interesting - thanks. I had to read up on this a
Rick Byers
2011/03/11 02:44:33
Done. I thought JSCompiler was complaining if I o
arv (Not doing code reviews)
2011/03/11 20:08:46
I assume most of the Chrome JS would fail in the p
| |
218 webkitTransform; | |
219 | |
220 this.dispatchEvent_(Grabber.EventType.DRAG_START); | |
221 e.detail.enableDrag = true; | |
222 this.dragging_ = true; | |
223 } else { | |
224 // Hasn't been grabbed - don't drag, just unpress | |
225 this.element_.classList.remove(Grabber.PRESSED_CLASS); | |
226 e.detail.enableDrag = false; | |
227 } | |
228 }; | |
229 | |
230 /** | |
231 * Invoked when a grabbed element is being dragged | |
232 * @param {!CustomEvent} e The TouchHandler event. | |
233 * @private | |
234 */ | |
235 Grabber.prototype.onDragMove_ = function(e) { | |
236 assert(this.grabbed_ && this.dragging_); | |
237 | |
238 this.translateTo_(e.detail.dragDeltaX, e.detail.dragDeltaY); | |
239 | |
240 var target = this.getCoveredElement_(e.detail); | |
241 if (target && target != this.lastEnter_) { | |
242 // Send the events | |
243 this.sendDragLeave_(e); | |
244 this.dispatchEventTo_(Grabber.EventType.DRAG_ENTER, target); | |
245 } | |
246 this.lastEnter_ = target; | |
247 }; | |
248 | |
249 /** | |
250 * Send DRAG_LEAVE to the element last sent a DRAG_ENTER if any. | |
251 * @param {!CustomEvent} e The event triggering this DRAG_LEAVE. | |
252 * @private | |
253 */ | |
254 Grabber.prototype.sendDragLeave_ = function(e) { | |
255 if (this.lastEnter_) { | |
256 this.dispatchEventTo_(Grabber.EventType.DRAG_LEAVE, this.lastEnter_); | |
257 this.lastEnter_ = undefined; | |
258 } | |
259 }; | |
260 | |
261 /** | |
262 * Moves the element to the specified position. | |
263 * @param {number} x Horizontal position to move to. | |
264 * @param {number} y Vertical position to move to. | |
265 * @private | |
266 */ | |
267 Grabber.prototype.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, ' + | |
270 y + 'px, 0) ' + this.baseTransform_; | |
arv (Not doing code reviews)
2011/03/10 19:25:59
+2 indentation
Rick Byers
2011/03/11 02:44:33
Done.
| |
271 }; | |
272 | |
273 /** | |
274 * Get the element being covered by a given touch. | |
275 * @param {TouchHandler.EventDetail} touch The details of the touch event | |
276 * indicating the position to check. | |
arv (Not doing code reviews)
2011/03/10 19:25:59
indent 4 spaces
Rick Byers
2011/03/11 02:44:33
Done.
| |
277 * @return {Element} The element under the touch or null. | |
278 * @private | |
279 */ | |
280 Grabber.prototype.getCoveredElement_ = function(touch) { | |
281 // Ensure the element being dragged doesn't get in the way | |
282 // It's unfortunate that there's not a better way to do this. | |
283 // This will probably cause a re-layout, but shouldn't cause flicker as | |
284 // repaint shouldn't happen in the middle of this function. | |
285 // We could also use zIndex, but that's more complicated and harder to | |
286 // reliably get the element out of the way. | |
287 var origDisplay = this.element_.style.display || ''; | |
288 this.element_.style.display = 'none'; | |
arv (Not doing code reviews)
2011/03/10 19:25:59
I would prefer pointerEvents = 'none' since that d
Rick Byers
2011/03/11 02:44:33
elementFromPoint does appear to be aware of pointe
| |
289 var target = document.elementFromPoint(touch.clientX, touch.clientY); | |
290 this.element_.style.display = origDisplay; | |
291 | |
292 return target; | |
293 }; | |
294 | |
295 /** | |
296 * @private | |
297 * @param {CustomEvent} e The TouchHandler event. | |
298 */ | |
299 Grabber.prototype.onDragEnd_ = function(e) { | |
300 // We should get this before the onTouchEnd. Don't change | |
301 // this.grabbed_ - it's onTouchEnd's responsibility to clear it. | |
302 assert(this.grabbed_ && this.dragging_); | |
303 var event; | |
304 | |
305 // Try to determine what element is underneath us | |
306 var target = this.getCoveredElement_(e.detail); | |
307 if (target) { | |
308 this.dispatchEventTo_(Grabber.EventType.DROP, target); | |
309 } | |
310 | |
311 // Cleanup and send DRAG_END | |
312 // Note that like HTML5 DND, we don't send DRAG_LEAVE on drop | |
313 this.stopDragging_(); | |
314 }; | |
315 | |
316 /** | |
317 * Clean-up the active drag and send DRAG_LEAVE | |
318 * @private | |
319 */ | |
320 Grabber.prototype.stopDragging_ = function() { | |
321 assert(this.dragging_); | |
322 this.lastEnter_ = undefined; | |
323 | |
324 // Mark the element as no longer being dragged | |
325 this.element_.classList.remove(Grabber.DRAGGING_CLASS); | |
326 this.element_.style.webkitTransform = ''; | |
327 | |
328 this.dragging_ = false; | |
329 this.dispatchEvent_(Grabber.EventType.DRAG_END); | |
330 }; | |
331 | |
332 /** | |
333 * @return {!Element} The element the grabber is attached to. | |
334 */ | |
335 Grabber.prototype.getElement = function() { | |
arv (Not doing code reviews)
2011/03/10 19:25:59
Use getters and setters for getters and setters
G
Rick Byers
2011/03/11 02:44:33
Thanks, but JSCompiler doesn't yet support this ES
arv (Not doing code reviews)
2011/03/11 20:08:46
It is a such a relief to not have to be hamstrung
Rick Byers
2011/03/15 21:47:54
Ok. I agree we shouldn't be sacrificing good styl
| |
336 return this.element_; | |
337 }; | |
338 | |
339 /** | |
340 * Send a Grabber event to a specific element | |
341 * @param {string} eventType The type of event to send. | |
342 * @param {!Element} target The element to send the event to. | |
343 * @private | |
344 */ | |
345 Grabber.prototype.dispatchEventTo_ = function(eventType, target) { | |
346 var event = document.createEvent('Event'); | |
arv (Not doing code reviews)
2011/03/10 19:25:59
cr.dispatchSimpleEvent
Rick Byers
2011/03/11 02:44:33
See above.
arv (Not doing code reviews)
2011/03/11 20:08:46
Ah, I was confused by the overloaded term, sender.
Rick Byers
2011/03/15 21:47:54
Darn - I forgot that I added a 'sender' property m
| |
347 event.initEvent(eventType, true, true); | |
348 event.sender = this; | |
349 target.dispatchEvent(event); | |
350 }; | |
351 | |
352 /** | |
353 * Send a Grabber event to the grabbed element | |
354 * @param {string} eventType The type of event to send. | |
355 * @private | |
356 */ | |
357 Grabber.prototype.dispatchEvent_ = function(eventType) { | |
358 this.dispatchEventTo_(eventType, this.element_); | |
359 }; | |
360 | |
OLD | NEW |