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