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