Home | History | Annotate | Download | only in ntp
      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 // The delegate interface:
      6 //   dragContainer -->
      7 //         element containing the draggable items
      8 //
      9 //   transitionsDuration -->
     10 //         length of time of transitions in ms
     11 //
     12 //   dragItem -->
     13 //         get / set property containing the item being dragged
     14 //
     15 //   getItem(e) -->
     16 //         get's the item that is under the mouse event |e|
     17 //
     18 //   canDropOn(coordinates) -->
     19 //         returns true if the coordinates (relative to the drag container)
     20 //         point to a valid place to drop an item
     21 //
     22 //   setDragPlaceholder(coordinates) -->
     23 //         tells the delegate that the dragged item is currently above
     24 //         the specified coordinates.
     25 //
     26 //   saveDrag(draggedItem) -->
     27 //         tells the delegate that the drag is done. move the item to the
     28 //         position last specified by setDragPlaceholder (e.g., commit changes).
     29 //         draggedItem was the item being dragged.
     30 //
     31 
     32 // The distance, in px, that the mouse must move before initiating a drag.
     33 var DRAG_THRESHOLD = 35;
     34 
     35 function DragAndDropController(delegate) {
     36   this.delegate_ = delegate;
     37 
     38   // Install the 'mousedown' handler, the entry point to drag and drop.
     39   var el = this.delegate_.dragContainer;
     40   el.addEventListener('mousedown', this.handleMouseDown_.bind(this));
     41 }
     42 
     43 DragAndDropController.prototype = {
     44   isDragging_: false,
     45   startItem_: null,
     46   startItemXY_: null,
     47   startMouseXY_: null,
     48   mouseXY_: null,
     49 
     50   // Enables the handlers that are only active during a drag.
     51   enableHandlers_: function() {
     52     // Record references to the generated functions so we can
     53     // remove the listeners later.
     54     this.mouseMoveListener_ = this.handleMouseMove_.bind(this);
     55     this.mouseUpListener_ = this.handleMouseUp_.bind(this);
     56     this.scrollListener_ = this.handleScroll_.bind(this);
     57 
     58     document.addEventListener('mousemove', this.mouseMoveListener_, true);
     59     document.addEventListener('mouseup', this.mouseUpListener_, true);
     60     document.addEventListener('scroll', this.scrollListener_, true);
     61   },
     62 
     63   disableHandlers_: function() {
     64     document.removeEventListener('mousemove', this.mouseMoveListener_, true);
     65     document.removeEventListener('mouseup', this.mouseUpListener_, true);
     66     document.removeEventListener('scroll', this.scrollListener_, true);
     67   },
     68 
     69   isDragging: function() {
     70     return this.isDragging_;
     71   },
     72 
     73   distance_: function(p1, p2) {
     74     var x2 = Math.pow(p1.x - p2.x, 2);
     75     var y2 = Math.pow(p1.y - p2.y, 2);
     76     return Math.sqrt(x2 + y2);
     77   },
     78 
     79   // Shifts the client coordinates, |xy|, so they are relative to the top left
     80   // of the drag container.
     81   getCoordinates_: function(xy) {
     82     var rect = this.delegate_.dragContainer.getBoundingClientRect();
     83     var coordinates = {
     84       x: xy.x - rect.left,
     85       y: xy.y - rect.top
     86     };
     87 
     88     // If we're in an RTL language, reflect the coordinates so the delegate
     89     // doesn't need to worry about it.
     90     if (isRtl())
     91       coordinates.x = this.delegate_.dragContainer.offsetWidth - coordinates.x;
     92 
     93     return coordinates;
     94   },
     95 
     96   // Listen to mousedown to get the relative position of the cursor when
     97   // starting drag and drop.
     98   handleMouseDown_: function(e) {
     99     var item = this.delegate_.getItem(e);
    100 
    101     // This can't be a drag & drop event if it's not the left mouse button
    102     // or if the mouse is not above an item. We also bail out if the dragging
    103     // flag is still set (the flag remains around for a bit so that 'click'
    104     // event handlers can distinguish between a click and drag).
    105     if (!item || e.button != 0 || this.isDragging())
    106       return;
    107 
    108     this.startItem_ = item;
    109     this.startItemXY_ = {x: item.offsetLeft, y: item.offsetTop};
    110     this.startMouseXY_ = {x: e.clientX, y: e.clientY};
    111     this.startScrollXY_ = {x: window.scrollX, y: window.scrollY};
    112 
    113     this.enableHandlers_();
    114   },
    115 
    116   handleMouseMove_: function(e) {
    117     this.mouseXY_ = {x: e.clientX, y: e.clientY};
    118 
    119     if (this.isDragging()) {
    120       this.handleDrag_();
    121       return;
    122     }
    123 
    124     // Initiate the drag if the mouse has moved far enough.
    125     if (this.distance_(this.startMouseXY_, this.mouseXY_) >= DRAG_THRESHOLD)
    126       this.handleDragStart_();
    127   },
    128 
    129   handleMouseUp_: function() {
    130     this.handleDrop_();
    131   },
    132 
    133   handleScroll_: function(e) {
    134     if (this.isDragging())
    135       this.handleDrag_();
    136   },
    137 
    138   handleDragStart_: function() {
    139     // Use the item that the mouse was above when 'mousedown' fired.
    140     var item = this.startItem_;
    141     if (!item)
    142       return;
    143 
    144     this.isDragging_ = true;
    145     this.delegate_.dragItem = item;
    146     item.classList.add('dragging');
    147     item.style.zIndex = 2;
    148   },
    149 
    150   handleDragOver_: function() {
    151     var coordinates = this.getCoordinates_(this.mouseXY_);
    152     if (!this.delegate_.canDropOn(coordinates))
    153       return;
    154 
    155     this.delegate_.setDragPlaceholder(coordinates);
    156   },
    157 
    158   handleDrop_: function() {
    159     this.disableHandlers_();
    160 
    161     var dragItem = this.delegate_.dragItem;
    162     if (!dragItem)
    163       return;
    164 
    165     this.delegate_.dragItem = this.startItem_ = null;
    166     this.delegate_.saveDrag(dragItem);
    167     dragItem.classList.remove('dragging');
    168 
    169     setTimeout(function() {
    170       // Keep the flag around a little so other 'mouseup' and 'click'
    171       // listeners know the event is from a drag operation.
    172       this.isDragging_ = false;
    173       dragItem.style.zIndex = 0;
    174     }.bind(this), this.delegate_.transitionsDuration);
    175   },
    176 
    177   handleDrag_: function() {
    178     // Moves the drag item making sure that it is not displayed outside the
    179     // drag container.
    180     var dragItem = this.delegate_.dragItem;
    181     var dragContainer = this.delegate_.dragContainer;
    182     var rect = dragContainer.getBoundingClientRect();
    183 
    184     // First, move the item the same distance the mouse has moved.
    185     var x = this.startItemXY_.x + this.mouseXY_.x - this.startMouseXY_.x +
    186               window.scrollX - this.startScrollXY_.x;
    187     var y = this.startItemXY_.y + this.mouseXY_.y - this.startMouseXY_.y +
    188               window.scrollY - this.startScrollXY_.y;
    189 
    190     var w = this.delegate_.dimensions.width;
    191     var h = this.delegate_.dimensions.height;
    192 
    193     var offset = parseInt(getComputedStyle(dragContainer).marginLeft);
    194 
    195     // The position of the item is relative to the drag container. We
    196     // want to make sure that half of the item's width or height is within
    197     // the container.
    198     x = Math.max(x, - w / 2 - offset);
    199     x = Math.min(x, rect.width  + w / 2 - offset);
    200 
    201     y = Math.max(- h / 2, y);
    202     y = Math.min(y, rect.height - h / 2);
    203 
    204     dragItem.style.left = x + 'px';
    205     dragItem.style.top = y + 'px';
    206 
    207     // Update the layouts and positions based on the new drag location.
    208     this.handleDragOver_();
    209 
    210     this.delegate_.scrollPage(this.mouseXY_);
    211   }
    212 };
    213