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