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