1// Copyright (c) 2012 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 5cr.define('cr.ui', function() { 6 /** @const */ var EventTarget = cr.EventTarget; 7 8 /** 9 * Creates a new selection model that is to be used with lists. 10 * 11 * @param {number=} opt_length The number items in the selection. 12 * 13 * @constructor 14 * @extends {cr.EventTarget} 15 */ 16 function ListSelectionModel(opt_length) { 17 this.length_ = opt_length || 0; 18 // Even though selectedIndexes_ is really a map we use an array here to get 19 // iteration in the order of the indexes. 20 this.selectedIndexes_ = []; 21 22 // True if any item could be lead or anchor. False if only selected ones. 23 this.independentLeadItem_ = !cr.isMac && !cr.isChromeOS; 24 } 25 26 ListSelectionModel.prototype = { 27 __proto__: EventTarget.prototype, 28 29 /** 30 * The number of items in the model. 31 * @type {number} 32 */ 33 get length() { 34 return this.length_; 35 }, 36 37 /** 38 * The selected indexes. 39 * Setter also changes lead and anchor indexes if value list is nonempty. 40 * @type {!Array} 41 */ 42 get selectedIndexes() { 43 return Object.keys(this.selectedIndexes_).map(Number); 44 }, 45 set selectedIndexes(selectedIndexes) { 46 this.beginChange(); 47 var unselected = {}; 48 for (var index in this.selectedIndexes_) { 49 unselected[index] = true; 50 } 51 52 for (var i = 0; i < selectedIndexes.length; i++) { 53 var index = selectedIndexes[i]; 54 if (index in this.selectedIndexes_) { 55 delete unselected[index]; 56 } else { 57 this.selectedIndexes_[index] = true; 58 // Mark the index as changed. If previously marked, then unmark, 59 // since it just got reverted to the original state. 60 if (index in this.changedIndexes_) 61 delete this.changedIndexes_[index]; 62 else 63 this.changedIndexes_[index] = true; 64 } 65 } 66 67 for (var index in unselected) { 68 delete this.selectedIndexes_[index]; 69 // Mark the index as changed. If previously marked, then unmark, 70 // since it just got reverted to the original state. 71 if (index in this.changedIndexes_) 72 delete this.changedIndexes_[index]; 73 else 74 this.changedIndexes_[index] = false; 75 } 76 77 if (selectedIndexes.length) { 78 this.leadIndex = this.anchorIndex = selectedIndexes[0]; 79 } else { 80 this.leadIndex = this.anchorIndex = -1; 81 } 82 this.endChange(); 83 }, 84 85 /** 86 * Convenience getter which returns the first selected index. 87 * Setter also changes lead and anchor indexes if value is nonnegative. 88 * @type {number} 89 */ 90 get selectedIndex() { 91 for (var i in this.selectedIndexes_) { 92 return Number(i); 93 } 94 return -1; 95 }, 96 set selectedIndex(selectedIndex) { 97 this.selectedIndexes = selectedIndex != -1 ? [selectedIndex] : []; 98 }, 99 100 /** 101 * Returns the nearest selected index or -1 if no item selected. 102 * @param {number} index The origin index. 103 * @return {number} 104 * @private 105 */ 106 getNearestSelectedIndex_: function(index) { 107 if (index == -1) 108 return -1; 109 110 var result = Infinity; 111 for (var i in this.selectedIndexes_) { 112 if (Math.abs(i - index) < Math.abs(result - index)) 113 result = i; 114 } 115 return result < this.length ? Number(result) : -1; 116 }, 117 118 /** 119 * Selects a range of indexes, starting with {@code start} and ends with 120 * {@code end}. 121 * @param {number} start The first index to select. 122 * @param {number} end The last index to select. 123 */ 124 selectRange: function(start, end) { 125 // Swap if starts comes after end. 126 if (start > end) { 127 var tmp = start; 128 start = end; 129 end = tmp; 130 } 131 132 this.beginChange(); 133 134 for (var index = start; index != end; index++) { 135 this.setIndexSelected(index, true); 136 } 137 this.setIndexSelected(end, true); 138 139 this.endChange(); 140 }, 141 142 /** 143 * Selects all indexes. 144 */ 145 selectAll: function() { 146 this.selectRange(0, this.length - 1); 147 }, 148 149 /** 150 * Clears the selection 151 */ 152 clear: function() { 153 this.beginChange(); 154 this.length_ = 0; 155 this.anchorIndex = this.leadIndex = -1; 156 this.unselectAll(); 157 this.endChange(); 158 }, 159 160 /** 161 * Unselects all selected items. 162 */ 163 unselectAll: function() { 164 this.beginChange(); 165 for (var i in this.selectedIndexes_) { 166 this.setIndexSelected(+i, false); 167 } 168 this.endChange(); 169 }, 170 171 /** 172 * Sets the selected state for an index. 173 * @param {number} index The index to set the selected state for. 174 * @param {boolean} b Whether to select the index or not. 175 */ 176 setIndexSelected: function(index, b) { 177 var oldSelected = index in this.selectedIndexes_; 178 if (oldSelected == b) 179 return; 180 181 if (b) 182 this.selectedIndexes_[index] = true; 183 else 184 delete this.selectedIndexes_[index]; 185 186 this.beginChange(); 187 188 this.changedIndexes_[index] = b; 189 190 // End change dispatches an event which in turn may update the view. 191 this.endChange(); 192 }, 193 194 /** 195 * Whether a given index is selected or not. 196 * @param {number} index The index to check. 197 * @return {boolean} Whether an index is selected. 198 */ 199 getIndexSelected: function(index) { 200 return index in this.selectedIndexes_; 201 }, 202 203 /** 204 * This is used to begin batching changes. Call {@code endChange} when you 205 * are done making changes. 206 */ 207 beginChange: function() { 208 if (!this.changeCount_) { 209 this.changeCount_ = 0; 210 this.changedIndexes_ = {}; 211 this.oldLeadIndex_ = this.leadIndex_; 212 this.oldAnchorIndex_ = this.anchorIndex_; 213 } 214 this.changeCount_++; 215 }, 216 217 /** 218 * Call this after changes are done and it will dispatch a change event if 219 * any changes were actually done. 220 */ 221 endChange: function() { 222 this.changeCount_--; 223 if (!this.changeCount_) { 224 // Calls delayed |dispatchPropertyChange|s, only when |leadIndex| or 225 // |anchorIndex| has been actually changed in the batch. 226 this.leadIndex_ = this.adjustIndex_(this.leadIndex_); 227 if (this.leadIndex_ != this.oldLeadIndex_) { 228 cr.dispatchPropertyChange(this, 'leadIndex', 229 this.leadIndex_, this.oldLeadIndex_); 230 } 231 this.oldLeadIndex_ = null; 232 233 this.anchorIndex_ = this.adjustIndex_(this.anchorIndex_); 234 if (this.anchorIndex_ != this.oldAnchorIndex_) { 235 cr.dispatchPropertyChange(this, 'anchorIndex', 236 this.anchorIndex_, this.oldAnchorIndex_); 237 } 238 this.oldAnchorIndex_ = null; 239 240 var indexes = Object.keys(this.changedIndexes_); 241 if (indexes.length) { 242 var e = new Event('change'); 243 e.changes = indexes.map(function(index) { 244 return { 245 index: Number(index), 246 selected: this.changedIndexes_[index] 247 }; 248 }, this); 249 this.dispatchEvent(e); 250 } 251 this.changedIndexes_ = {}; 252 } 253 }, 254 255 leadIndex_: -1, 256 oldLeadIndex_: null, 257 258 /** 259 * The leadIndex is used with multiple selection and it is the index that 260 * the user is moving using the arrow keys. 261 * @type {number} 262 */ 263 get leadIndex() { 264 return this.leadIndex_; 265 }, 266 set leadIndex(leadIndex) { 267 var oldValue = this.leadIndex_; 268 var newValue = this.adjustIndex_(leadIndex); 269 this.leadIndex_ = newValue; 270 // Delays the call of dispatchPropertyChange if batch is running. 271 if (!this.changeCount_ && newValue != oldValue) 272 cr.dispatchPropertyChange(this, 'leadIndex', newValue, oldValue); 273 }, 274 275 anchorIndex_: -1, 276 oldAnchorIndex_: null, 277 278 /** 279 * The anchorIndex is used with multiple selection. 280 * @type {number} 281 */ 282 get anchorIndex() { 283 return this.anchorIndex_; 284 }, 285 set anchorIndex(anchorIndex) { 286 var oldValue = this.anchorIndex_; 287 var newValue = this.adjustIndex_(anchorIndex); 288 this.anchorIndex_ = newValue; 289 // Delays the call of dispatchPropertyChange if batch is running. 290 if (!this.changeCount_ && newValue != oldValue) 291 cr.dispatchPropertyChange(this, 'anchorIndex', newValue, oldValue); 292 }, 293 294 /** 295 * Helper method that adjustes a value before assiging it to leadIndex or 296 * anchorIndex. 297 * @param {number} index New value for leadIndex or anchorIndex. 298 * @return {number} Corrected value. 299 */ 300 adjustIndex_: function(index) { 301 index = Math.max(-1, Math.min(this.length_ - 1, index)); 302 // On Mac and ChromeOS lead and anchor items are forced to be among 303 // selected items. This rule is not enforces until end of batch update. 304 if (!this.changeCount_ && !this.independentLeadItem_ && 305 !this.getIndexSelected(index)) { 306 var index2 = this.getNearestSelectedIndex_(index); 307 index = index2; 308 } 309 return index; 310 }, 311 312 /** 313 * Whether the selection model supports multiple selected items. 314 * @type {boolean} 315 */ 316 get multiple() { 317 return true; 318 }, 319 320 /** 321 * Adjusts the selection after reordering of items in the table. 322 * @param {!Array.<number>} permutation The reordering permutation. 323 */ 324 adjustToReordering: function(permutation) { 325 this.beginChange(); 326 var oldLeadIndex = this.leadIndex; 327 var oldAnchorIndex = this.anchorIndex; 328 var oldSelectedItemsCount = this.selectedIndexes.length; 329 330 this.selectedIndexes = this.selectedIndexes.map(function(oldIndex) { 331 return permutation[oldIndex]; 332 }).filter(function(index) { 333 return index != -1; 334 }); 335 336 // Will be adjusted in endChange. 337 if (oldLeadIndex != -1) 338 this.leadIndex = permutation[oldLeadIndex]; 339 if (oldAnchorIndex != -1) 340 this.anchorIndex = permutation[oldAnchorIndex]; 341 342 if (oldSelectedItemsCount && !this.selectedIndexes.length && 343 this.length_ && oldLeadIndex != -1) { 344 // All selected items are deleted. We move selection to next item of 345 // last selected item. 346 this.selectedIndexes = [Math.min(oldLeadIndex, this.length_ - 1)]; 347 } 348 349 this.endChange(); 350 }, 351 352 /** 353 * Adjusts selection model length. 354 * @param {number} length New selection model length. 355 */ 356 adjustLength: function(length) { 357 this.length_ = length; 358 } 359 }; 360 361 return { 362 ListSelectionModel: ListSelectionModel 363 }; 364}); 365