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 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 true if the field has a compare function. 76 * @param {string} field The field to check. 77 * @return {boolean} True if the field is sortable. 78 */ 79 isSortable: function(field) { 80 return this.compareFunctions_ && field in this.compareFunctions_; 81 }, 82 83 /** 84 * Returns current sort status. 85 * @return {!Object} Current sort status. 86 */ 87 get sortStatus() { 88 if (this.sortStatus_) { 89 return this.createSortStatus( 90 this.sortStatus_.field, this.sortStatus_.direction); 91 } else { 92 return this.createSortStatus(null, null); 93 } 94 }, 95 96 /** 97 * Returns the first matching item. 98 * @param {*} item The item to find. 99 * @param {number=} opt_fromIndex If provided, then the searching start at 100 * the {@code opt_fromIndex}. 101 * @return {number} The index of the first found element or -1 if not found. 102 */ 103 indexOf: function(item, opt_fromIndex) { 104 for (var i = opt_fromIndex || 0; i < this.indexes_.length; i++) { 105 if (item === this.item(i)) 106 return i; 107 } 108 return -1; 109 }, 110 111 /** 112 * Returns an array of elements in a selected range. 113 * @param {number=} opt_from The starting index of the selected range. 114 * @param {number=} opt_to The ending index of selected range. 115 * @return {Array} An array of elements in the selected range. 116 */ 117 slice: function(opt_from, opt_to) { 118 var arr = this.array_; 119 return this.indexes_.slice(opt_from, opt_to).map( 120 function(index) { return arr[index] }); 121 }, 122 123 /** 124 * This removes and adds items to the model. 125 * This dispatches a splice event. 126 * This implementation runs sort after splice and creates permutation for 127 * the whole change. 128 * @param {number} index The index of the item to update. 129 * @param {number} deleteCount The number of items to remove. 130 * @param {...*} The items to add. 131 * @return {!Array} An array with the removed items. 132 */ 133 splice: function(index, deleteCount, var_args) { 134 var addCount = arguments.length - 2; 135 var newIndexes = []; 136 var deletePermutation = []; 137 var deletedItems = []; 138 var newArray = []; 139 index = Math.min(index, this.indexes_.length); 140 deleteCount = Math.min(deleteCount, this.indexes_.length - index); 141 // Copy items before the insertion point. 142 for (var i = 0; i < index; i++) { 143 newIndexes.push(newArray.length); 144 deletePermutation.push(i); 145 newArray.push(this.array_[this.indexes_[i]]); 146 } 147 // Delete items. 148 for (; i < index + deleteCount; i++) { 149 deletePermutation.push(-1); 150 deletedItems.push(this.array_[this.indexes_[i]]); 151 } 152 // Insert new items instead deleted ones. 153 for (var j = 0; j < addCount; j++) { 154 newIndexes.push(newArray.length); 155 newArray.push(arguments[j + 2]); 156 } 157 // Copy items after the insertion point. 158 for (; i < this.indexes_.length; i++) { 159 newIndexes.push(newArray.length); 160 deletePermutation.push(i - deleteCount + addCount); 161 newArray.push(this.array_[this.indexes_[i]]); 162 } 163 164 this.indexes_ = newIndexes; 165 166 this.array_ = newArray; 167 168 // TODO(arv): Maybe unify splice and change events? 169 var spliceEvent = new Event('splice'); 170 spliceEvent.removed = deletedItems; 171 spliceEvent.added = Array.prototype.slice.call(arguments, 2); 172 173 var status = this.sortStatus; 174 // if sortStatus.field is null, this restores original order. 175 var sortPermutation = this.doSort_(this.sortStatus.field, 176 this.sortStatus.direction); 177 if (sortPermutation) { 178 var splicePermutation = deletePermutation.map(function(element) { 179 return element != -1 ? sortPermutation[element] : -1; 180 }); 181 this.dispatchPermutedEvent_(splicePermutation); 182 spliceEvent.index = sortPermutation[index]; 183 } else { 184 this.dispatchPermutedEvent_(deletePermutation); 185 spliceEvent.index = index; 186 } 187 188 this.dispatchEvent(spliceEvent); 189 190 // If real sorting is needed, we should first call prepareSort (data may 191 // change), and then sort again. 192 // Still need to finish the sorting above (including events), so 193 // list will not go to inconsistent state. 194 if (status.field) 195 this.delayedSort_(status.field, status.direction); 196 197 return deletedItems; 198 }, 199 200 /** 201 * Appends items to the end of the model. 202 * 203 * This dispatches a splice event. 204 * 205 * @param {...*} The items to append. 206 * @return {number} The new length of the model. 207 */ 208 push: function(var_args) { 209 var args = Array.prototype.slice.call(arguments); 210 args.unshift(this.length, 0); 211 this.splice.apply(this, args); 212 return this.length; 213 }, 214 215 /** 216 * Updates the existing item with the new item. 217 * 218 * The existing item and the new item are regarded as the same item and the 219 * permutation tracks these indexes. 220 * 221 * @param {*} oldItem Old item that is contained in the model. If the item 222 * is not found in the model, the method call is just ignored. 223 * @param {*} newItem New item. 224 */ 225 replaceItem: function(oldItem, newItem) { 226 var index = this.indexOf(oldItem); 227 if (index < 0) 228 return; 229 this.array_[this.indexes_[index]] = newItem; 230 this.updateIndex(index); 231 }, 232 233 /** 234 * Use this to update a given item in the array. This does not remove and 235 * reinsert a new item. 236 * This dispatches a change event. 237 * This runs sort after updating. 238 * @param {number} index The index of the item to update. 239 */ 240 updateIndex: function(index) { 241 this.updateIndexes([index]); 242 }, 243 244 /** 245 * Notifies of update of the items in the array. This does not remove and 246 * reinsert new items. 247 * This dispatches one or more change events. 248 * This runs sort after updating. 249 * @param {Array.<number>} indexes The index list of items to update. 250 */ 251 updateIndexes: function(indexes) { 252 var isIndexesValid = indexes.every(function(index) { 253 return 0 <= index && index < this.length; 254 }, this); 255 if (!isIndexesValid) 256 throw Error('Invalid index, ' + indexes[i]); 257 258 for (var i = 0; i < indexes.length; i++) { 259 var e = new Event('change'); 260 e.index = indexes[i]; 261 this.dispatchEvent(e); 262 } 263 264 if (this.sortStatus.field) { 265 var status = this.sortStatus; 266 var sortPermutation = this.doSort_(this.sortStatus.field, 267 this.sortStatus.direction); 268 if (sortPermutation) 269 this.dispatchPermutedEvent_(sortPermutation); 270 // We should first call prepareSort (data may change), and then sort. 271 // Still need to finish the sorting above (including events), so 272 // list will not go to inconsistent state. 273 this.delayedSort_(status.field, status.direction); 274 } 275 }, 276 277 /** 278 * Creates sort status with given field and direction. 279 * @param {string} field Sort field. 280 * @param {string} direction Sort direction. 281 * @return {!Object} Created sort status. 282 */ 283 createSortStatus: function(field, direction) { 284 return { 285 field: field, 286 direction: direction 287 }; 288 }, 289 290 /** 291 * Called before a sort happens so that you may fetch additional data 292 * required for the sort. 293 * 294 * @param {string} field Sort field. 295 * @param {function()} callback The function to invoke when preparation 296 * is complete. 297 */ 298 prepareSort: function(field, callback) { 299 callback(); 300 }, 301 302 /** 303 * Sorts data model according to given field and direction and dispathes 304 * sorted event with delay. If no need to delay, use sort() instead. 305 * @param {string} field Sort field. 306 * @param {string} direction Sort direction. 307 * @private 308 */ 309 delayedSort_: function(field, direction) { 310 var self = this; 311 setTimeout(function() { 312 // If the sort status has been changed, sorting has already done 313 // on the change event. 314 if (field == self.sortStatus.field && 315 direction == self.sortStatus.direction) { 316 self.sort(field, direction); 317 } 318 }, 0); 319 }, 320 321 /** 322 * Sorts data model according to given field and direction and dispathes 323 * sorted event. 324 * @param {string} field Sort field. 325 * @param {string} direction Sort direction. 326 */ 327 sort: function(field, direction) { 328 var self = this; 329 330 this.prepareSort(field, function() { 331 var sortPermutation = self.doSort_(field, direction); 332 if (sortPermutation) 333 self.dispatchPermutedEvent_(sortPermutation); 334 self.dispatchSortEvent_(); 335 }); 336 }, 337 338 /** 339 * Sorts data model according to given field and direction. 340 * @param {string} field Sort field. 341 * @param {string} direction Sort direction. 342 * @private 343 */ 344 doSort_: function(field, direction) { 345 var compareFunction = this.sortFunction_(field, direction); 346 var positions = []; 347 for (var i = 0; i < this.length; i++) { 348 positions[this.indexes_[i]] = i; 349 } 350 var sorted = this.indexes_.every(function(element, index, array) { 351 return index == 0 || compareFunction(element, array[index - 1]) >= 0; 352 }); 353 if (!sorted) 354 this.indexes_.sort(compareFunction); 355 this.sortStatus_ = this.createSortStatus(field, direction); 356 var sortPermutation = []; 357 var changed = false; 358 for (var i = 0; i < this.length; i++) { 359 if (positions[this.indexes_[i]] != i) 360 changed = true; 361 sortPermutation[positions[this.indexes_[i]]] = i; 362 } 363 if (changed) 364 return sortPermutation; 365 return null; 366 }, 367 368 dispatchSortEvent_: function() { 369 var e = new Event('sorted'); 370 this.dispatchEvent(e); 371 }, 372 373 dispatchPermutedEvent_: function(permutation) { 374 var e = new Event('permuted'); 375 e.permutation = permutation; 376 e.newLength = this.length; 377 this.dispatchEvent(e); 378 }, 379 380 /** 381 * Creates compare function for the field. 382 * Returns the function set as sortFunction for given field 383 * or default compare function 384 * @param {string} field Sort field. 385 * @param {function(*, *): number} Compare function. 386 * @private 387 */ 388 createCompareFunction_: function(field) { 389 var compareFunction = 390 this.compareFunctions_ ? this.compareFunctions_[field] : null; 391 var defaultValuesCompareFunction = this.defaultValuesCompareFunction; 392 if (compareFunction) { 393 return compareFunction; 394 } else { 395 return function(a, b) { 396 return defaultValuesCompareFunction.call(null, a[field], b[field]); 397 } 398 } 399 return compareFunction; 400 }, 401 402 /** 403 * Creates compare function for given field and direction. 404 * @param {string} field Sort field. 405 * @param {string} direction Sort direction. 406 * @param {function(*, *): number} Compare function. 407 * @private 408 */ 409 sortFunction_: function(field, direction) { 410 var compareFunction = null; 411 if (field !== null) 412 compareFunction = this.createCompareFunction_(field); 413 var dirMultiplier = direction == 'desc' ? -1 : 1; 414 415 return function(index1, index2) { 416 var item1 = this.array_[index1]; 417 var item2 = this.array_[index2]; 418 419 var compareResult = 0; 420 if (typeof(compareFunction) === 'function') 421 compareResult = compareFunction.call(null, item1, item2); 422 if (compareResult != 0) 423 return dirMultiplier * compareResult; 424 return dirMultiplier * this.defaultValuesCompareFunction(index1, 425 index2); 426 }.bind(this); 427 }, 428 429 /** 430 * Default compare function. 431 */ 432 defaultValuesCompareFunction: function(a, b) { 433 // We could insert i18n comparisons here. 434 if (a < b) 435 return -1; 436 if (a > b) 437 return 1; 438 return 0; 439 } 440 }; 441 442 return { 443 ArrayDataModel: ArrayDataModel 444 }; 445}); 446