summaryrefslogtreecommitdiffstats
path: root/chrome/browser/resources/file_manager/js/drag_selector.js
blob: 1a5974744317f5e7553a53ee92bb3fa509f6fb1a (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
213
214
215
216
217
218
219
220
221
222
223
// Copyright 2013 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.

'use strict';

/**
 * Drag selector used on the file list or the grid table.
 * TODO(hirono): Support drag selection for grid view. crbug.com/224832
 * @constructor
 */
function DragSelector() {
  /**
   * Target list of drag selection.
   * @type {cr.ui.List}
   * @private
   */
  this.target_ = null;

  /**
   * Border element of drag handle.
   * @type {HtmlElement}
   * @private
   */
  this.border_ = null;

  /**
   * Start point of dragging.
   * @type {number?}
   * @private
   */
  this.startX_ = null;

  /**
   * Start point of dragging.
   * @type {number?}
   * @private
   */
  this.startY_ = null;

  /**
   * Indexes of selected items by dragging at the last update.
   * @type {Array.<number>!}
   * @private
   */
  this.lastSelection_ = [];

  /**
   * Indexes of selected items at the start of dragging.
   * @type {Array.<number>!}
   * @private
   */
  this.originalSelection_ = [];

  // Bind handlers to make them removable.
  this.onMouseMoveBound_ = this.onMouseMove_.bind(this);
  this.onMouseUpBound_ = this.onMouseUp_.bind(this);
}

/**
 * Flag that shows whether the item is included in the selection or not.
 * @enum {number}
 * @private
 */
DragSelector.SelectionFlag_ = {
  IN_LAST_SELECTION: 1 << 0,
  IN_CURRENT_SELECTION: 1 << 1
};

/**
 * Starts drag selection by reacting dragstart event.
 * This function must be called from handlers of dragstart event.
 *
 * @this {DragSelector}
 * @param {cr.ui.List} list List where the drag selection starts.
 * @param {Event} event The dragstart event.
 */
DragSelector.prototype.startDragSelection = function(list, event) {
  // Precondition check
  if (!list.selectionModel_.multiple || this.target_)
    return;

  // Set the target of the drag selection
  this.target_ = list;

  // Create and add the border element
  if (!this.border_) {
    this.border_ = this.target_.ownerDocument.createElement('div');
    this.border_.className = 'drag-selection-border';
  }
  list.appendChild(this.border_);

  // Prevent default action.
  event.preventDefault();

  // If no modifier key is pressed, clear the original selection.
  if (!event.shiftKey && !event.ctrlKey) {
    this.target_.selectionModel_.unselectAll();
  }

  // Save the start state.
  var rect = list.getBoundingClientRect();
  this.startX_ = event.clientX - rect.left + list.scrollLeft;
  this.startY_ = event.clientY - rect.top + list.scrollTop;
  this.border_.style.left = this.startX_ + 'px';
  this.border_.style.top = this.startY_ + 'px';
  this.lastSelection_ = [];
  this.originalSelection_ = this.target_.selectionModel_.selectedIndexes;

  // Register event handlers.
  // The handlers are bounded at the constructor.
  this.target_.ownerDocument.addEventListener(
      'mousemove', this.onMouseMoveBound_, true);
  this.target_.ownerDocument.addEventListener(
      'mouseup', this.onMouseUpBound_, true);
};

/**
 * Handle the mousemove event.
 * @private
 * @param {MouseEvent} event The mousemove event.
 */
DragSelector.prototype.onMouseMove_ = function(event) {
  // Get the selection bounds.
  var inRect = this.target_.getBoundingClientRect();
  var x = event.clientX - inRect.left + this.target_.scrollLeft;
  var y = event.clientY - inRect.top + this.target_.scrollTop;
  var borderBounds = {
    left: Math.max(Math.min(this.startX_, x), 0),
    top: Math.max(Math.min(this.startY_, y), 0),
    right: Math.min(Math.max(this.startX_, x), this.target_.scrollWidth),
    bottom: Math.min(Math.max(this.startY_, y), this.target_.scrollHeight)
  };
  borderBounds.width = borderBounds.right - borderBounds.left;
  borderBounds.height = borderBounds.bottom - borderBounds.top;

  // Collect items within the selection rect.
  var currentSelection = [];
  var leadIndex = -1;
  for (var i = 0; i < this.target_.selectionModel_.length; i++) {
    // TODO(hirono): Stop to use the private method. crbug.com/246459
    var itemMetrics = this.target_.getHeightsForIndex_(i);
    itemMetrics.bottom = itemMetrics.top + itemMetrics.height;
    if (itemMetrics.top < borderBounds.bottom &&
        itemMetrics.bottom >= borderBounds.top) {
      currentSelection.push(i);
    }
    var pointed = itemMetrics.top <= y && y < itemMetrics.bottom;
    if (pointed)
      leadIndex = i;
  }

  // Diff the selection between currentSelection and this.lastSelection_.
  var selectionFlag = [];
  for (var i = 0; i < this.lastSelection_.length; i++) {
    var index = this.lastSelection_[i];
    // Bit operator can be used for undefined value.
    selectionFlag[index] =
        selectionFlag[index] | DragSelector.SelectionFlag_.IN_LAST_SELECTION;
  }
  for (var i = 0; i < currentSelection.length; i++) {
    var index = currentSelection[i];
    // Bit operator can be used for undefined value.
    selectionFlag[index] =
        selectionFlag[index] | DragSelector.SelectionFlag_.IN_CURRENT_SELECTION;
  }

  // Update the selection
  this.target_.selectionModel_.beginChange();
  for (var name in selectionFlag) {
    var index = parseInt(name);
    var flag = selectionFlag[name];
    // The flag may be one of followings:
    // - IN_LAST_SELECTION | IN_CURRENT_SELECTION
    // - IN_LAST_SELECTION
    // - IN_CURRENT_SELECTION
    // - undefined

    // If the flag equals to (IN_LAST_SELECTION | IN_CURRENT_SELECTION),
    // this is included in both the last selection and the current selection.
    // We have nothing to do for this item.

    if (flag == DragSelector.SelectionFlag_.IN_LAST_SELECTION) {
      // If the flag equals to IN_LAST_SELECTION,
      // then the item is included in lastSelection but not in currentSelection.
      // Revert the selection state to this.originalSelection_.
      this.target_.selectionModel_.setIndexSelected(
          index, this.originalSelection_.indexOf(index) != -1);
    } else if (flag == DragSelector.SelectionFlag_.IN_CURRENT_SELECTION) {
      // If the flag equals to IN_CURRENT_SELECTION,
      // this is included in currentSelection but not in lastSelection.
      this.target_.selectionModel_.setIndexSelected(index, true);
    }
  }
  if (leadIndex != -1) {
    this.target_.selectionModel_.leadIndex = leadIndex;
    this.target_.selectionModel_.anchorIndex = leadIndex;
  }
  this.target_.selectionModel_.endChange();
  this.lastSelection_ = currentSelection;

  // Update the size of border
  this.border_.style.left = borderBounds.left + 'px';
  this.border_.style.top = borderBounds.top + 'px';
  this.border_.style.width = borderBounds.width + 'px';
  this.border_.style.height = borderBounds.height + 'px';
};

/**
 * Handle the mouseup event.
 * @private
 * @param {MouseEvent} event The mouseup event.
 */
DragSelector.prototype.onMouseUp_ = function(event) {
  this.onMouseMove_(event);
  this.target_.removeChild(this.border_);
  this.target_.ownerDocument.removeEventListener(
      'mousemove', this.onMouseMoveBound_, true);
  this.target_.ownerDocument.removeEventListener(
      'mouseup', this.onMouseUpBound_, true);
  event.stopPropagation();
  this.target_ = null;
};