1// Copyright (c) 2011 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 Event = cr.Event; 7 const EventTarget = cr.EventTarget; 8 9 /** 10 * Creates a new selection model that is to be used with lists. 11 * 12 * @param {number=} opt_length The number items in the selection. 13 * 14 * @constructor 15 * @extends {!cr.EventTarget} 16 */ 17 function ListSelectionModel(opt_length) { 18 this.length_ = opt_length || 0; 19 // Even though selectedIndexes_ is really a map we use an array here to get 20 // iteration in the order of the indexes. 21 this.selectedIndexes_ = []; 22 } 23 24 ListSelectionModel.prototype = { 25 __proto__: EventTarget.prototype, 26 27 /** 28 * The number of items in the model. 29 * @type {number} 30 */ 31 get length() { 32 return this.length_; 33 }, 34 35 /** 36 * @type {!Array} The selected indexes. 37 */ 38 get selectedIndexes() { 39 return Object.keys(this.selectedIndexes_).map(Number); 40 }, 41 set selectedIndexes(selectedIndexes) { 42 this.beginChange(); 43 this.unselectAll(); 44 for (var i = 0; i < selectedIndexes.length; i++) { 45 this.setIndexSelected(selectedIndexes[i], true); 46 } 47 if (selectedIndexes.length) { 48 this.leadIndex = this.anchorIndex = selectedIndexes[0]; 49 } else { 50 this.leadIndex = this.anchorIndex = -1; 51 } 52 this.endChange(); 53 }, 54 55 /** 56 * Convenience getter which returns the first selected index. 57 * @type {number} 58 */ 59 get selectedIndex() { 60 for (var i in this.selectedIndexes_) { 61 return Number(i); 62 } 63 return -1; 64 }, 65 set selectedIndex(selectedIndex) { 66 this.beginChange(); 67 this.unselectAll(); 68 if (selectedIndex != -1) { 69 this.selectedIndexes = [selectedIndex]; 70 } else { 71 this.leadIndex = this.anchorIndex = -1; 72 } 73 this.endChange(); 74 }, 75 76 /** 77 * Selects a range of indexes, starting with {@code start} and ends with 78 * {@code end}. 79 * @param {number} start The first index to select. 80 * @param {number} end The last index to select. 81 */ 82 selectRange: function(start, end) { 83 // Swap if starts comes after end. 84 if (start > end) { 85 var tmp = start; 86 start = end; 87 end = tmp; 88 } 89 90 this.beginChange(); 91 92 for (var index = start; index != end; index++) { 93 this.setIndexSelected(index, true); 94 } 95 this.setIndexSelected(end, true); 96 97 this.endChange(); 98 }, 99 100 /** 101 * Selects all indexes. 102 */ 103 selectAll: function() { 104 this.selectRange(0, this.length - 1); 105 }, 106 107 /** 108 * Clears the selection 109 */ 110 clear: function() { 111 this.beginChange(); 112 this.length_ = 0; 113 this.anchorIndex = this.leadIndex = -1; 114 this.unselectAll(); 115 this.endChange(); 116 }, 117 118 /** 119 * Unselects all selected items. 120 */ 121 unselectAll: function() { 122 this.beginChange(); 123 for (var i in this.selectedIndexes_) { 124 this.setIndexSelected(i, false); 125 } 126 this.endChange(); 127 }, 128 129 /** 130 * Sets the selected state for an index. 131 * @param {number} index The index to set the selected state for. 132 * @param {boolean} b Whether to select the index or not. 133 */ 134 setIndexSelected: function(index, b) { 135 var oldSelected = index in this.selectedIndexes_; 136 if (oldSelected == b) 137 return; 138 139 if (b) 140 this.selectedIndexes_[index] = true; 141 else 142 delete this.selectedIndexes_[index]; 143 144 this.beginChange(); 145 146 // Changing back? 147 if (index in this.changedIndexes_ && this.changedIndexes_[index] == !b) { 148 delete this.changedIndexes_[index]; 149 } else { 150 this.changedIndexes_[index] = b; 151 } 152 153 // End change dispatches an event which in turn may update the view. 154 this.endChange(); 155 }, 156 157 /** 158 * Whether a given index is selected or not. 159 * @param {number} index The index to check. 160 * @return {boolean} Whether an index is selected. 161 */ 162 getIndexSelected: function(index) { 163 return index in this.selectedIndexes_; 164 }, 165 166 /** 167 * This is used to begin batching changes. Call {@code endChange} when you 168 * are done making changes. 169 */ 170 beginChange: function() { 171 if (!this.changeCount_) { 172 this.changeCount_ = 0; 173 this.changedIndexes_ = {}; 174 } 175 this.changeCount_++; 176 }, 177 178 /** 179 * Call this after changes are done and it will dispatch a change event if 180 * any changes were actually done. 181 */ 182 endChange: function() { 183 this.changeCount_--; 184 if (!this.changeCount_) { 185 var indexes = Object.keys(this.changedIndexes_); 186 if (indexes.length) { 187 var e = new Event('change'); 188 e.changes = indexes.map(function(index) { 189 return { 190 index: index, 191 selected: this.changedIndexes_[index] 192 }; 193 }, this); 194 this.dispatchEvent(e); 195 } 196 this.changedIndexes_ = {}; 197 } 198 }, 199 200 leadIndex_: -1, 201 202 /** 203 * The leadIndex is used with multiple selection and it is the index that 204 * the user is moving using the arrow keys. 205 * @type {number} 206 */ 207 get leadIndex() { 208 return this.leadIndex_; 209 }, 210 set leadIndex(leadIndex) { 211 var li = Math.max(-1, Math.min(this.length_ - 1, leadIndex)); 212 if (li != this.leadIndex_) { 213 var oldLeadIndex = this.leadIndex_; 214 this.leadIndex_ = li; 215 cr.dispatchPropertyChange(this, 'leadIndex', li, oldLeadIndex); 216 } 217 }, 218 219 anchorIndex_: -1, 220 221 /** 222 * The anchorIndex is used with multiple selection. 223 * @type {number} 224 */ 225 get anchorIndex() { 226 return this.anchorIndex_; 227 }, 228 set anchorIndex(anchorIndex) { 229 var ai = Math.max(-1, Math.min(this.length_ - 1, anchorIndex)); 230 if (ai != this.anchorIndex_) { 231 var oldAnchorIndex = this.anchorIndex_; 232 this.anchorIndex_ = ai; 233 cr.dispatchPropertyChange(this, 'anchorIndex', ai, oldAnchorIndex); 234 } 235 }, 236 237 /** 238 * Whether the selection model supports multiple selected items. 239 * @type {boolean} 240 */ 241 get multiple() { 242 return true; 243 }, 244 245 /** 246 * Adjusts the selection after reordering of items in the table. 247 * @param {!Array.<number>} permutation The reordering permutation. 248 */ 249 adjustToReordering: function(permutation) { 250 }, 251 252 /** 253 * Adjust the selection by adding or removing a certain numbers of items. 254 * This should be called by the owner of the selection model as items are 255 * added and removed from the underlying data model. 256 * @param {number} index The index of the first change. 257 * @param {number} itemsRemoved Number of items removed. 258 * @param {number} itemsAdded Number of items added. 259 */ 260 adjust: function(index, itemsRemoved, itemsAdded) { 261 function getNewAdjustedIndex(i) { 262 if (i >= index && i < index + itemsRemoved) { 263 return index 264 } else if (i >= index) { 265 return i - itemsRemoved + itemsAdded; 266 } 267 return i; 268 } 269 270 this.length_ += itemsAdded - itemsRemoved; 271 272 var newMap = []; 273 for (var i in this.selectedIndexes_) { 274 i = Number(i); 275 if (i < index) { 276 newMap[i] = true; 277 } else if (i < index + itemsRemoved) { 278 // noop 279 } else { 280 newMap[i + itemsAdded - itemsRemoved] = true; 281 } 282 } 283 this.selectedIndexes_ = newMap; 284 285 this.leadIndex = getNewAdjustedIndex(this.leadIndex); 286 this.anchorIndex = getNewAdjustedIndex(this.anchorIndex); 287 } 288 }; 289 290 return { 291 ListSelectionModel: ListSelectionModel 292 }; 293}); 294