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