• 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// 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.
33var DRAG_THRESHOLD = 35;
34
35function 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
43DragAndDropController.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