• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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