summaryrefslogtreecommitdiffstats
path: root/chrome/browser/resources/ntp/drag_drop_controller.js
blob: 92aaebd5069c2cd459234a3754ca485091f7e297 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// The delegate interface:
//   dragContainer -->
//         element containing the draggable items
//
//   transitionsDuration -->
//         length of time of transitions in ms
//
//   dragItem -->
//         get / set property containing the item being dragged
//
//   getItem(e) -->
//         get's the item that is under the mouse event |e|
//
//   canDropOn(coordinates) -->
//         returns true if the coordinates (relative to the drag container)
//         point to a valid place to drop an item
//
//   setDragPlaceholder(coordinates) -->
//         tells the delegate that the dragged item is currently above
//         the specified coordinates.
//
//   saveDrag(draggedItem) -->
//         tells the delegate that the drag is done. move the item to the
//         position last specified by setDragPlaceholder (e.g., commit changes).
//         draggedItem was the item being dragged.
//

// The distance, in px, that the mouse must move before initiating a drag.
var DRAG_THRESHOLD = 35;

function DragAndDropController(delegate) {
  this.delegate_ = delegate;

  // Install the 'mousedown' handler, the entry point to drag and drop.
  var el = this.delegate_.dragContainer;
  el.addEventListener('mousedown', this.handleMouseDown_.bind(this));
}

DragAndDropController.prototype = {
  isDragging_: false,
  startItem_: null,
  startItemXY_: null,
  startMouseXY_: null,
  mouseXY_: null,

  // Enables the handlers that are only active during a drag.
  enableHandlers_: function() {
    // Record references to the generated functions so we can
    // remove the listeners later.
    this.mouseMoveListener_ = this.handleMouseMove_.bind(this);
    this.mouseUpListener_ = this.handleMouseUp_.bind(this);
    this.scrollListener_ = this.handleScroll_.bind(this);

    document.addEventListener('mousemove', this.mouseMoveListener_, true);
    document.addEventListener('mouseup', this.mouseUpListener_, true);
    document.addEventListener('scroll', this.scrollListener_, true);
  },

  disableHandlers_: function() {
    document.removeEventListener('mousemove', this.mouseMoveListener_, true);
    document.removeEventListener('mouseup', this.mouseUpListener_, true);
    document.removeEventListener('scroll', this.scrollListener_, true);
  },

  isDragging: function() {
    return this.isDragging_;
  },

  distance_: function(p1, p2) {
    var x2 = Math.pow(p1.x - p2.x, 2);
    var y2 = Math.pow(p1.y - p2.y, 2);
    return Math.sqrt(x2 + y2);
  },

  // Shifts the client coordinates, |xy|, so they are relative to the top left
  // of the drag container.
  getCoordinates_: function(xy) {
    var rect = this.delegate_.dragContainer.getBoundingClientRect();
    var coordinates = {
      x: xy.x - rect.left,
      y: xy.y - rect.top
    };

    // If we're in an RTL language, reflect the coordinates so the delegate
    // doesn't need to worry about it.
    if (isRtl())
      coordinates.x = this.delegate_.dragContainer.offsetWidth - coordinates.x;

    return coordinates;
  },

  // Listen to mousedown to get the relative position of the cursor when
  // starting drag and drop.
  handleMouseDown_: function(e) {
    var item = this.delegate_.getItem(e);

    // This can't be a drag & drop event if it's not the left mouse button
    // or if the mouse is not above an item. We also bail out if the dragging
    // flag is still set (the flag remains around for a bit so that 'click'
    // event handlers can distinguish between a click and drag).
    if (!item || e.button != 0 || this.isDragging())
      return;

    this.startItem_ = item;
    this.startItemXY_ = {x: item.offsetLeft, y: item.offsetTop};
    this.startMouseXY_ = {x: e.clientX, y: e.clientY};
    this.startScrollXY_ = {x: window.scrollX, y: window.scrollY};

    this.enableHandlers_();
  },

  handleMouseMove_: function(e) {
    this.mouseXY_ = {x: e.clientX, y: e.clientY};

    if (this.isDragging()) {
      this.handleDrag_();
      return;
    }

    // Initiate the drag if the mouse has moved far enough.
    if (this.distance_(this.startMouseXY_, this.mouseXY_) >= DRAG_THRESHOLD)
      this.handleDragStart_();
  },

  handleMouseUp_: function() {
    this.handleDrop_();
  },

  handleScroll_: function(e) {
    if (this.isDragging())
      this.handleDrag_();
  },

  handleDragStart_: function() {
    // Use the item that the mouse was above when 'mousedown' fired.
    var item = this.startItem_;
    if (!item)
      return;

    this.isDragging_ = true;
    this.delegate_.dragItem = item;
    item.classList.add('dragging');
    item.style.zIndex = 2;
  },

  handleDragOver_: function() {
    var coordinates = this.getCoordinates_(this.mouseXY_);
    if (!this.delegate_.canDropOn(coordinates))
      return;

    this.delegate_.setDragPlaceholder(coordinates);
  },

  handleDrop_: function() {
    this.disableHandlers_();

    var dragItem = this.delegate_.dragItem;
    if (!dragItem)
      return;

    this.delegate_.dragItem = this.startItem_ = null;
    this.delegate_.saveDrag(dragItem);
    dragItem.classList.remove('dragging');

    setTimeout(function() {
      // Keep the flag around a little so other 'mouseup' and 'click'
      // listeners know the event is from a drag operation.
      this.isDragging_ = false;
      dragItem.style.zIndex = 0;
    }.bind(this), this.delegate_.transitionsDuration);
  },

  handleDrag_: function() {
    // Moves the drag item making sure that it is not displayed outside the
    // drag container.
    var dragItem = this.delegate_.dragItem;
    var dragContainer = this.delegate_.dragContainer;
    var rect = dragContainer.getBoundingClientRect();

    // First, move the item the same distance the mouse has moved.
    var x = this.startItemXY_.x + this.mouseXY_.x - this.startMouseXY_.x +
              window.scrollX - this.startScrollXY_.x;
    var y = this.startItemXY_.y + this.mouseXY_.y - this.startMouseXY_.y +
              window.scrollY - this.startScrollXY_.y;

    var w = this.delegate_.dimensions.width;
    var h = this.delegate_.dimensions.height;

    var offset = parseInt(getComputedStyle(dragContainer).marginLeft);

    // The position of the item is relative to the drag container. We
    // want to make sure that half of the item's width or height is within
    // the container.
    x = Math.max(x, - w / 2 - offset);
    x = Math.min(x, rect.width  + w / 2 - offset);

    y = Math.max(- h / 2, y);
    y = Math.min(y, rect.height - h / 2);

    dragItem.style.left = x + 'px';
    dragItem.style.top = y + 'px';

    // Update the layouts and positions based on the new drag location.
    this.handleDragOver_();

    this.delegate_.scrollPage(this.mouseXY_);
  }
};