1// Copyright 2013 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'use strict';
6
7/**
8 * Drag selector used on the file list or the grid table.
9 * TODO(hirono): Support drag selection for grid view. crbug.com/224832
10 * @constructor
11 */
12function DragSelector() {
13  /**
14   * Target list of drag selection.
15   * @type {cr.ui.List}
16   * @private
17   */
18  this.target_ = null;
19
20  /**
21   * Border element of drag handle.
22   * @type {HtmlElement}
23   * @private
24   */
25  this.border_ = null;
26
27  /**
28   * Start point of dragging.
29   * @type {number?}
30   * @private
31   */
32  this.startX_ = null;
33
34  /**
35   * Start point of dragging.
36   * @type {number?}
37   * @private
38   */
39  this.startY_ = null;
40
41  /**
42   * Indexes of selected items by dragging at the last update.
43   * @type {Array.<number>!}
44   * @private
45   */
46  this.lastSelection_ = [];
47
48  /**
49   * Indexes of selected items at the start of dragging.
50   * @type {Array.<number>!}
51   * @private
52   */
53  this.originalSelection_ = [];
54
55  // Bind handlers to make them removable.
56  this.onMouseMoveBound_ = this.onMouseMove_.bind(this);
57  this.onMouseUpBound_ = this.onMouseUp_.bind(this);
58
59  Object.seal(this);
60}
61
62/**
63 * Flag that shows whether the item is included in the selection or not.
64 * @enum {number}
65 * @private
66 */
67DragSelector.SelectionFlag_ = {
68  IN_LAST_SELECTION: 1 << 0,
69  IN_CURRENT_SELECTION: 1 << 1
70};
71
72/**
73 * Obtains the scrolled position in the element of mouse pointer from the mouse
74 * event.
75 *
76 * @param {HTMLElement} element Element that has the scroll bars.
77 * @param {Event} event The mouse event.
78 * @return {object} Scrolled position.
79 */
80DragSelector.getScrolledPosition = function(element, event) {
81  if (!element.cachedBounds) {
82    element.cachedBounds = element.getBoundingClientRect();
83    if (!element.cachedBounds)
84      return null;
85  }
86  var rect = element.cachedBounds;
87  return {
88    x: event.clientX - rect.left + element.scrollLeft,
89    y: event.clientY - rect.top + element.scrollTop
90  };
91};
92
93/**
94 * Starts drag selection by reacting dragstart event.
95 * This function must be called from handlers of dragstart event.
96 *
97 * @this {DragSelector}
98 * @param {cr.ui.List} list List where the drag selection starts.
99 * @param {Event} event The dragstart event.
100 */
101DragSelector.prototype.startDragSelection = function(list, event) {
102  // Precondition check
103  if (!list.selectionModel_.multiple || this.target_)
104    return;
105
106  // Set the target of the drag selection
107  this.target_ = list;
108
109  // Save the start state.
110  var startPos = DragSelector.getScrolledPosition(list, event);
111  if (!startPos)
112    return;
113  this.startX_ = startPos.x;
114  this.startY_ = startPos.y;
115  this.lastSelection_ = [];
116  this.originalSelection_ = this.target_.selectionModel_.selectedIndexes;
117
118  // Create and add the border element
119  if (!this.border_) {
120    this.border_ = this.target_.ownerDocument.createElement('div');
121    this.border_.className = 'drag-selection-border';
122  }
123  this.border_.style.left = this.startX_ + 'px';
124  this.border_.style.top = this.startY_ + 'px';
125  this.border_.style.width = '0';
126  this.border_.style.height = '0';
127  list.appendChild(this.border_);
128
129  // If no modifier key is pressed, clear the original selection.
130  if (!event.shiftKey && !event.ctrlKey)
131    this.target_.selectionModel_.unselectAll();
132
133  // Register event handlers.
134  // The handlers are bounded at the constructor.
135  this.target_.ownerDocument.addEventListener(
136      'mousemove', this.onMouseMoveBound_, true);
137  this.target_.ownerDocument.addEventListener(
138      'mouseup', this.onMouseUpBound_, true);
139  cr.dispatchSimpleEvent(this.target_, 'dragselectionstart');
140};
141
142/**
143 * Handles the mousemove event.
144 * @private
145 * @param {MouseEvent} event The mousemove event.
146 */
147DragSelector.prototype.onMouseMove_ = function(event) {
148  // Get the selection bounds.
149  var pos = DragSelector.getScrolledPosition(this.target_, event);
150  var borderBounds = {
151    left: Math.max(Math.min(this.startX_, pos.x), 0),
152    top: Math.max(Math.min(this.startY_, pos.y), 0),
153    right: Math.min(Math.max(this.startX_, pos.x), this.target_.scrollWidth),
154    bottom: Math.min(Math.max(this.startY_, pos.y), this.target_.scrollHeight)
155  };
156  borderBounds.width = borderBounds.right - borderBounds.left;
157  borderBounds.height = borderBounds.bottom - borderBounds.top;
158
159  // Collect items within the selection rect.
160  var currentSelection = this.target_.getHitElements(
161      borderBounds.left,
162      borderBounds.top,
163      borderBounds.width,
164      borderBounds.height);
165  var pointedElements = this.target_.getHitElements(pos.x, pos.y);
166  var leadIndex = pointedElements.length ? pointedElements[0] : -1;
167
168  // Diff the selection between currentSelection and this.lastSelection_.
169  var selectionFlag = [];
170  for (var i = 0; i < this.lastSelection_.length; i++) {
171    var index = this.lastSelection_[i];
172    // Bit operator can be used for undefined value.
173    selectionFlag[index] =
174        selectionFlag[index] | DragSelector.SelectionFlag_.IN_LAST_SELECTION;
175  }
176  for (var i = 0; i < currentSelection.length; i++) {
177    var index = currentSelection[i];
178    // Bit operator can be used for undefined value.
179    selectionFlag[index] =
180        selectionFlag[index] | DragSelector.SelectionFlag_.IN_CURRENT_SELECTION;
181  }
182
183  // Update the selection
184  this.target_.selectionModel_.beginChange();
185  for (var name in selectionFlag) {
186    var index = parseInt(name);
187    var flag = selectionFlag[name];
188    // The flag may be one of followings:
189    // - IN_LAST_SELECTION | IN_CURRENT_SELECTION
190    // - IN_LAST_SELECTION
191    // - IN_CURRENT_SELECTION
192    // - undefined
193
194    // If the flag equals to (IN_LAST_SELECTION | IN_CURRENT_SELECTION),
195    // this is included in both the last selection and the current selection.
196    // We have nothing to do for this item.
197
198    if (flag == DragSelector.SelectionFlag_.IN_LAST_SELECTION) {
199      // If the flag equals to IN_LAST_SELECTION,
200      // then the item is included in lastSelection but not in currentSelection.
201      // Revert the selection state to this.originalSelection_.
202      this.target_.selectionModel_.setIndexSelected(
203          index, this.originalSelection_.indexOf(index) != -1);
204    } else if (flag == DragSelector.SelectionFlag_.IN_CURRENT_SELECTION) {
205      // If the flag equals to IN_CURRENT_SELECTION,
206      // this is included in currentSelection but not in lastSelection.
207      this.target_.selectionModel_.setIndexSelected(index, true);
208    }
209  }
210  if (leadIndex != -1) {
211    this.target_.selectionModel_.leadIndex = leadIndex;
212    this.target_.selectionModel_.anchorIndex = leadIndex;
213  }
214  this.target_.selectionModel_.endChange();
215  this.lastSelection_ = currentSelection;
216
217  // Update the size of border
218  this.border_.style.left = borderBounds.left + 'px';
219  this.border_.style.top = borderBounds.top + 'px';
220  this.border_.style.width = borderBounds.width + 'px';
221  this.border_.style.height = borderBounds.height + 'px';
222};
223
224/**
225 * Handle the mouseup event.
226 * @private
227 * @param {MouseEvent} event The mouseup event.
228 */
229DragSelector.prototype.onMouseUp_ = function(event) {
230  this.onMouseMove_(event);
231  this.target_.removeChild(this.border_);
232  this.target_.ownerDocument.removeEventListener(
233      'mousemove', this.onMouseMoveBound_, true);
234  this.target_.ownerDocument.removeEventListener(
235      'mouseup', this.onMouseUpBound_, true);
236  cr.dispatchSimpleEvent(this.target_, 'dragselectionend');
237  this.target_.cachedBounds = null;
238  this.target_ = null;
239  // The target may select an item by reacting to the mouseup event.
240  // This suppress to the selecting behavior.
241  event.stopPropagation();
242};
243