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