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 5/** 6 * @fileoverview This is a data model representin 7 */ 8 9cr.define('cr.ui', function() { 10 const EventTarget = cr.EventTarget; 11 const Event = cr.Event; 12 13 /** 14 * A data model that wraps a simple array and supports sorting by storing 15 * initial indexes of elements for each position in sorted array. 16 * @param {!Array} array The underlying array. 17 * @constructor 18 * @extends {EventTarget} 19 */ 20 function ArrayDataModel(array) { 21 this.array_ = array; 22 this.indexes_ = []; 23 this.compareFunctions_ = {}; 24 25 for (var i = 0; i < array.length; i++) { 26 this.indexes_.push(i); 27 } 28 } 29 30 ArrayDataModel.prototype = { 31 __proto__: EventTarget.prototype, 32 33 /** 34 * The length of the data model. 35 * @type {number} 36 */ 37 get length() { 38 return this.array_.length; 39 }, 40 41 /** 42 * Returns the item at the given index. 43 * This implementation returns the item at the given index in the sorted 44 * array. 45 * @param {number} index The index of the element to get. 46 * @return {*} The element at the given index. 47 */ 48 item: function(index) { 49 if (index >= 0 && index < this.length) 50 return this.array_[this.indexes_[index]]; 51 return undefined; 52 }, 53 54 /** 55 * Returns compare function set for given field. 56 * @param {string} field The field to get compare function for. 57 * @return {function(*, *): number} Compare function set for given field. 58 */ 59 compareFunction: function(field) { 60 return this.compareFunctions_[field]; 61 }, 62 63 /** 64 * Sets compare function for given field. 65 * @param {string} field The field to set compare function. 66 * @param {function(*, *): number} Compare function to set for given field. 67 */ 68 setCompareFunction: function(field, compareFunction) { 69 if (!this.compareFunctions_) { 70 this.compareFunctions_ = {}; 71 } 72 this.compareFunctions_[field] = compareFunction; 73 }, 74 75 /** 76 * Returns current sort status. 77 * @return {!Object} Current sort status. 78 */ 79 get sortStatus() { 80 if (this.sortStatus_) { 81 return this.createSortStatus( 82 this.sortStatus_.field, this.sortStatus_.direction); 83 } else { 84 return this.createSortStatus(null, null); 85 } 86 }, 87 88 /** 89 * Returns the first matching item. 90 * @param {*} item The item to find. 91 * @param {number=} opt_fromIndex If provided, then the searching start at 92 * the {@code opt_fromIndex}. 93 * @return {number} The index of the first found element or -1 if not found. 94 */ 95 indexOf: function(item, opt_fromIndex) { 96 return this.array_.indexOf(item, opt_fromIndex); 97 }, 98 99 /** 100 * Returns an array of elements in a selected range. 101 * @param {number=} opt_from The starting index of the selected range. 102 * @param {number=} opt_to The ending index of selected range. 103 * @return {Array} An array of elements in the selected range. 104 */ 105 slice: function(opt_from, opt_to) { 106 return this.array_.slice.apply(this.array_, arguments); 107 }, 108 109 /** 110 * This removes and adds items to the model. 111 * This dispatches a splice event. 112 * This implementation runs sort after splice and creates permutation for 113 * the whole change. 114 * @param {number} index The index of the item to update. 115 * @param {number} deleteCount The number of items to remove. 116 * @param {...*} The items to add. 117 * @return {!Array} An array with the removed items. 118 */ 119 splice: function(index, deleteCount, var_args) { 120 var addCount = arguments.length - 2; 121 var newIndexes = []; 122 var deletePermutation = []; 123 var deleted = 0; 124 for (var i = 0; i < this.indexes_.length; i++) { 125 var oldIndex = this.indexes_[i]; 126 if (oldIndex < index) { 127 newIndexes.push(oldIndex); 128 deletePermutation.push(i - deleted); 129 } else if (oldIndex >= index + deleteCount) { 130 newIndexes.push(oldIndex - deleteCount + addCount); 131 deletePermutation.push(i - deleted); 132 } else { 133 deletePermutation.push(-1); 134 deleted++; 135 } 136 } 137 for (var i = 0; i < addCount; i++) { 138 newIndexes.push(index + i); 139 } 140 this.indexes_ = newIndexes; 141 142 var arr = this.array_; 143 144 // TODO(arv): Maybe unify splice and change events? 145 var spliceEvent = new Event('splice'); 146 spliceEvent.index = index; 147 spliceEvent.removed = arr.slice(index, index + deleteCount); 148 spliceEvent.added = Array.prototype.slice.call(arguments, 2); 149 150 var rv = arr.splice.apply(arr, arguments); 151 152 var status = this.sortStatus; 153 // if sortStatus.field is null, this restores original order. 154 var sortPermutation = this.doSort_(this.sortStatus.field, 155 this.sortStatus.direction); 156 if (sortPermutation) { 157 var splicePermutation = deletePermutation.map(function(element) { 158 return element != -1 ? sortPermutation[element] : -1; 159 }); 160 this.dispatchPermutedEvent_(splicePermutation); 161 } else { 162 this.dispatchPermutedEvent_(deletePermutation); 163 } 164 165 this.dispatchEvent(spliceEvent); 166 167 // If real sorting is needed, we should first call prepareSort (data may 168 // change), and then sort again. 169 // Still need to finish the sorting above (including events), so 170 // list will not go to inconsistent state. 171 if (status.field) { 172 setTimeout(this.sort.bind(this, status.field, status.direction), 0); 173 } 174 return rv; 175 }, 176 177 /** 178 * Appends items to the end of the model. 179 * 180 * This dispatches a splice event. 181 * 182 * @param {...*} The items to append. 183 * @return {number} The new length of the model. 184 */ 185 push: function(var_args) { 186 var args = Array.prototype.slice.call(arguments); 187 args.unshift(this.length, 0); 188 this.splice.apply(this, args); 189 return this.length; 190 }, 191 192 /** 193 * Use this to update a given item in the array. This does not remove and 194 * reinsert a new item. 195 * This dispatches a change event. 196 * This runs sort after updating. 197 * @param {number} index The index of the item to update. 198 */ 199 updateIndex: function(index) { 200 if (index < 0 || index >= this.length) 201 throw Error('Invalid index, ' + index); 202 203 // TODO(arv): Maybe unify splice and change events? 204 var e = new Event('change'); 205 e.index = index; 206 this.dispatchEvent(e); 207 208 if (this.sortStatus.field) { 209 var status = this.sortStatus; 210 var sortPermutation = this.doSort_(this.sortStatus.field, 211 this.sortStatus.direction); 212 if (sortPermutation) 213 this.dispatchPermutedEvent_(sortPermutation); 214 // We should first call prepareSort (data may change), and then sort. 215 // Still need to finish the sorting above (including events), so 216 // list will not go to inconsistent state. 217 setTimeout(this.sort.bind(this, status.field, status.direction), 0); 218 } 219 }, 220 221 /** 222 * Creates sort status with given field and direction. 223 * @param {string} field Sort field. 224 * @param {string} direction Sort direction. 225 * @return {!Object} Created sort status. 226 */ 227 createSortStatus: function(field, direction) { 228 return { 229 field: field, 230 direction: direction 231 }; 232 }, 233 234 /** 235 * Called before a sort happens so that you may fetch additional data 236 * required for the sort. 237 * 238 * @param {string} field Sort field. 239 * @param {function()} callback The function to invoke when preparation 240 * is complete. 241 */ 242 prepareSort: function(field, callback) { 243 callback(); 244 }, 245 246 /** 247 * Sorts data model according to given field and direction and dispathes 248 * sorted event. 249 * @param {string} field Sort field. 250 * @param {string} direction Sort direction. 251 */ 252 sort: function(field, direction) { 253 var self = this; 254 255 this.prepareSort(field, function() { 256 var sortPermutation = self.doSort_(field, direction); 257 if (sortPermutation) 258 self.dispatchPermutedEvent_(sortPermutation); 259 self.dispatchSortEvent_(); 260 }); 261 }, 262 263 /** 264 * Sorts data model according to given field and direction. 265 * @param {string} field Sort field. 266 * @param {string} direction Sort direction. 267 */ 268 doSort_: function(field, direction) { 269 var compareFunction = this.sortFunction_(field, direction); 270 var positions = []; 271 for (var i = 0; i < this.length; i++) { 272 positions[this.indexes_[i]] = i; 273 } 274 this.indexes_.sort(compareFunction); 275 this.sortStatus_ = this.createSortStatus(field, direction); 276 var sortPermutation = []; 277 var changed = false; 278 for (var i = 0; i < this.length; i++) { 279 if (positions[this.indexes_[i]] != i) 280 changed = true; 281 sortPermutation[positions[this.indexes_[i]]] = i; 282 } 283 if (changed) 284 return sortPermutation; 285 return null; 286 }, 287 288 dispatchSortEvent_: function() { 289 var e = new Event('sorted'); 290 this.dispatchEvent(e); 291 }, 292 293 dispatchPermutedEvent_: function(permutation) { 294 var e = new Event('permuted'); 295 e.permutation = permutation; 296 e.newLength = this.length; 297 this.dispatchEvent(e); 298 }, 299 300 /** 301 * Creates compare function for the field. 302 * Returns the function set as sortFunction for given field 303 * or default compare function 304 * @param {string} field Sort field. 305 * @param {function(*, *): number} Compare function. 306 */ 307 createCompareFunction_: function(field) { 308 var compareFunction = 309 this.compareFunctions_ ? this.compareFunctions_[field] : null; 310 var defaultValuesCompareFunction = this.defaultValuesCompareFunction; 311 if (compareFunction) { 312 return compareFunction; 313 } else { 314 return function(a, b) { 315 return defaultValuesCompareFunction.call(null, a[field], b[field]); 316 } 317 } 318 return compareFunction; 319 }, 320 321 /** 322 * Creates compare function for given field and direction. 323 * @param {string} field Sort field. 324 * @param {string} direction Sort direction. 325 * @param {function(*, *): number} Compare function. 326 */ 327 sortFunction_: function(field, direction) { 328 var compareFunction = null; 329 if (field !== null) 330 compareFunction = this.createCompareFunction_(field); 331 var dirMultiplier = direction == 'desc' ? -1 : 1; 332 333 return function(index1, index2) { 334 var item1 = this.array_[index1]; 335 var item2 = this.array_[index2]; 336 337 var compareResult = 0; 338 if (typeof(compareFunction) === 'function') 339 compareResult = compareFunction.call(null, item1, item2); 340 if (compareResult != 0) 341 return dirMultiplier * compareResult; 342 return dirMultiplier * this.defaultValuesCompareFunction(index1, 343 index2); 344 }.bind(this); 345 }, 346 347 /** 348 * Default compare function. 349 */ 350 defaultValuesCompareFunction: function(a, b) { 351 // We could insert i18n comparisons here. 352 if (a < b) 353 return -1; 354 if (a > b) 355 return 1; 356 return 0; 357 } 358 }; 359 360 return { 361 ArrayDataModel: ArrayDataModel 362 }; 363}); 364