• 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     * 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