Home | History | Annotate | Download | only in ntp4
      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