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