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 * Use this to update a given item in the array. This does not remove and 217 * reinsert a new item. 218 * This dispatches a change event. 219 * This runs sort after updating. 220 * @param {number} index The index of the item to update. 221 */ 222 updateIndex: function(index) { 223 if (index < 0 || index >= this.length) 224 throw Error('Invalid index, ' + index); 225 226 // TODO(arv): Maybe unify splice and change events? 227 var e = new Event('change'); 228 e.index = index; 229 this.dispatchEvent(e); 230 231 if (this.sortStatus.field) { 232 var status = this.sortStatus; 233 var sortPermutation = this.doSort_(this.sortStatus.field, 234 this.sortStatus.direction); 235 if (sortPermutation) 236 this.dispatchPermutedEvent_(sortPermutation); 237 // We should first call prepareSort (data may change), and then sort. 238 // Still need to finish the sorting above (including events), so 239 // list will not go to inconsistent state. 240 this.delayedSort_(status.field, status.direction); 241 } 242 }, 243 244 /** 245 * Creates sort status with given field and direction. 246 * @param {string} field Sort field. 247 * @param {string} direction Sort direction. 248 * @return {!Object} Created sort status. 249 */ 250 createSortStatus: function(field, direction) { 251 return { 252 field: field, 253 direction: direction 254 }; 255 }, 256 257 /** 258 * Called before a sort happens so that you may fetch additional data 259 * required for the sort. 260 * 261 * @param {string} field Sort field. 262 * @param {function()} callback The function to invoke when preparation 263 * is complete. 264 */ 265 prepareSort: function(field, callback) { 266 callback(); 267 }, 268 269 /** 270 * Sorts data model according to given field and direction and dispathes 271 * sorted event with delay. If no need to delay, use sort() instead. 272 * @param {string} field Sort field. 273 * @param {string} direction Sort direction. 274 * @private 275 */ 276 delayedSort_: function(field, direction) { 277 var self = this; 278 setTimeout(function() { 279 // If the sort status has been changed, sorting has already done 280 // on the change event. 281 if (field == self.sortStatus.field && 282 direction == self.sortStatus.direction) { 283 self.sort(field, direction); 284 } 285 }, 0); 286 }, 287 288 /** 289 * Sorts data model according to given field and direction and dispathes 290 * sorted event. 291 * @param {string} field Sort field. 292 * @param {string} direction Sort direction. 293 */ 294 sort: function(field, direction) { 295 var self = this; 296 297 this.prepareSort(field, function() { 298 var sortPermutation = self.doSort_(field, direction); 299 if (sortPermutation) 300 self.dispatchPermutedEvent_(sortPermutation); 301 self.dispatchSortEvent_(); 302 }); 303 }, 304 305 /** 306 * Sorts data model according to given field and direction. 307 * @param {string} field Sort field. 308 * @param {string} direction Sort direction. 309 * @private 310 */ 311 doSort_: function(field, direction) { 312 var compareFunction = this.sortFunction_(field, direction); 313 var positions = []; 314 for (var i = 0; i < this.length; i++) { 315 positions[this.indexes_[i]] = i; 316 } 317 var sorted = this.indexes_.every(function(element, index, array) { 318 return index == 0 || compareFunction(element, array[index - 1]) >= 0; 319 }); 320 if (!sorted) 321 this.indexes_.sort(compareFunction); 322 this.sortStatus_ = this.createSortStatus(field, direction); 323 var sortPermutation = []; 324 var changed = false; 325 for (var i = 0; i < this.length; i++) { 326 if (positions[this.indexes_[i]] != i) 327 changed = true; 328 sortPermutation[positions[this.indexes_[i]]] = i; 329 } 330 if (changed) 331 return sortPermutation; 332 return null; 333 }, 334 335 dispatchSortEvent_: function() { 336 var e = new Event('sorted'); 337 this.dispatchEvent(e); 338 }, 339 340 dispatchPermutedEvent_: function(permutation) { 341 var e = new Event('permuted'); 342 e.permutation = permutation; 343 e.newLength = this.length; 344 this.dispatchEvent(e); 345 }, 346 347 /** 348 * Creates compare function for the field. 349 * Returns the function set as sortFunction for given field 350 * or default compare function 351 * @param {string} field Sort field. 352 * @param {function(*, *): number} Compare function. 353 * @private 354 */ 355 createCompareFunction_: function(field) { 356 var compareFunction = 357 this.compareFunctions_ ? this.compareFunctions_[field] : null; 358 var defaultValuesCompareFunction = this.defaultValuesCompareFunction; 359 if (compareFunction) { 360 return compareFunction; 361 } else { 362 return function(a, b) { 363 return defaultValuesCompareFunction.call(null, a[field], b[field]); 364 } 365 } 366 return compareFunction; 367 }, 368 369 /** 370 * Creates compare function for given field and direction. 371 * @param {string} field Sort field. 372 * @param {string} direction Sort direction. 373 * @param {function(*, *): number} Compare function. 374 * @private 375 */ 376 sortFunction_: function(field, direction) { 377 var compareFunction = null; 378 if (field !== null) 379 compareFunction = this.createCompareFunction_(field); 380 var dirMultiplier = direction == 'desc' ? -1 : 1; 381 382 return function(index1, index2) { 383 var item1 = this.array_[index1]; 384 var item2 = this.array_[index2]; 385 386 var compareResult = 0; 387 if (typeof(compareFunction) === 'function') 388 compareResult = compareFunction.call(null, item1, item2); 389 if (compareResult != 0) 390 return dirMultiplier * compareResult; 391 return dirMultiplier * this.defaultValuesCompareFunction(index1, 392 index2); 393 }.bind(this); 394 }, 395 396 /** 397 * Default compare function. 398 */ 399 defaultValuesCompareFunction: function(a, b) { 400 // We could insert i18n comparisons here. 401 if (a < b) 402 return -1; 403 if (a > b) 404 return 1; 405 return 0; 406 } 407 }; 408 409 return { 410 ArrayDataModel: ArrayDataModel 411 }; 412}); 413