Home | History | Annotate | Download | only in ui
      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