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