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 table data model
7 */
8cr.define('cr.ui.table', function() {
9  const EventTarget = cr.EventTarget;
10  const Event = cr.Event;
11  const ArrayDataModel = cr.ui.ArrayDataModel;
12
13  /**
14   * A table data model that supports sorting by storing initial indexes of
15   * elements for each position in sorted array.
16   * @param {!Array} items The underlying array.
17   * @constructor
18   * @extends {ArrayDataModel}
19   */
20  function TableDataModel(items) {
21    ArrayDataModel.apply(this, arguments);
22    this.indexes_ = [];
23    for (var i = 0; i < items.length; i++) {
24      this.indexes_.push(i);
25    }
26  }
27
28  TableDataModel.prototype = {
29    __proto__: ArrayDataModel.prototype,
30
31    /**
32     * Returns the item at the given index.
33     * This implementation returns the item at the given index in the source
34     * array before sort.
35     * @param {number} index The index of the element to get.
36     * @return {*} The element at the given index.
37     */
38    getItemByUnsortedIndex_: function(unsortedIndex) {
39      return ArrayDataModel.prototype.item.call(this, unsortedIndex);
40    },
41
42    /**
43     * Returns the item at the given index.
44     * This implementation returns the item at the given index in the sorted
45     * array.
46     * @param {number} index The index of the element to get.
47     * @return {*} The element at the given index.
48     */
49    item: function(index) {
50      if (index >= 0 && index < this.length)
51        return this.getItemByUnsortedIndex_(this.indexes_[index]);
52      return undefined;
53    },
54
55    /**
56     * Returns compare function set for given field.
57     * @param {string} field The field to get compare function for.
58     * @return {Function(*, *): number} Compare function set for given field.
59     */
60    compareFunction: function(field) {
61      return this.compareFunctions_[field];
62    },
63
64    /**
65     * Sets compare function for given field.
66     * @param {string} field The field to set compare function.
67     * @param {Function(*, *): number} Compare function to set for given field.
68     */
69    setCompareFunction: function(field, compareFunction) {
70      this.compareFunctions_[field] = compareFunction;
71    },
72
73    /**
74     * Returns current sort status.
75     * @return {!Object} Current sort status.
76     */
77    get sortStatus() {
78      if (this.sortStatus_) {
79        return this.createSortStatus(
80            this.sortStatus_.field, this.sortStatus_.direction);
81      } else {
82        return this.createSortStatus(null, null);
83      }
84    },
85
86    /**
87     * This removes and adds items to the model.
88     * This dispatches a splice event.
89     * This implementation runs sort after splice and creates permutation for
90     * the whole change.
91     * @param {number} index The index of the item to update.
92     * @param {number} deleteCount The number of items to remove.
93     * @param {...*} The items to add.
94     * @return {!Array} An array with the removed items.
95     */
96    splice: function(index, deleteCount, var_args) {
97      var addCount = arguments.length - 2;
98      var newIndexes = [];
99      var deletePermutation = [];
100      var deleted = 0;
101      for (var i = 0; i < this.indexes_.length; i++) {
102        var oldIndex = this.indexes_[i];
103        if (oldIndex < index) {
104          newIndexes.push(oldIndex);
105          deletePermutation.push(i - deleted);
106        } else if (oldIndex >= index + deleteCount) {
107          newIndexes.push(oldIndex - deleteCount + addCount);
108          deletePermutation.push(i - deleted);
109        } else {
110          deletePermutation.push(-1);
111          deleted++;
112        }
113      }
114      for (var i = 0; i < addCount; i++) {
115        newIndexes.push(index + i);
116      }
117      this.indexes_ = newIndexes;
118
119      var rv = ArrayDataModel.prototype.splice.apply(this, arguments);
120
121      var splicePermutation;
122      if (this.sortStatus.field) {
123        var sortPermutation = this.doSort_(this.sortStatus.field,
124                                           this.sortStatus.direction);
125        splicePermutation = deletePermutation.map(function(element) {
126          return element != -1 ? sortPermutation[element] : -1;
127        });
128      } else {
129        splicePermutation = deletePermutation;
130      }
131      this.dispatchSortEvent_(splicePermutation);
132
133      return rv;
134    },
135
136    /**
137     * Use this to update a given item in the array. This does not remove and
138     * reinsert a new item.
139     * This dispatches a change event.
140     * This implementation runs sort after updating.
141     * @param {number} index The index of the item to update.
142     */
143    updateIndex: function(index) {
144      ArrayDataModel.prototype.updateIndex.apply(this, arguments);
145
146      if (this.sortStatus.field)
147        this.sort(this.sortStatus.field, this.sortStatus.direction);
148    },
149
150    /**
151     * Creates sort status with given field and direction.
152     * @param {string} field Sort field.
153     * @param {string} direction Sort direction.
154     * @return {!Object} Created sort status.
155     */
156    createSortStatus: function(field, direction) {
157      return {
158        field: field,
159        direction: direction
160      };
161    },
162
163    /**
164     * Called before a sort happens so that you may fetch additional data
165     * required for the sort.
166     *
167     * @param {string} field Sort field.
168     * @param {function()} callback The function to invoke when preparation
169     *     is complete.
170     */
171    prepareSort: function(field, callback) {
172      callback();
173    },
174
175    /**
176     * Sorts data model according to given field and direction and dispathes
177     * sorted event.
178     * @param {string} field Sort field.
179     * @param {string} direction Sort direction.
180     */
181    sort: function(field, direction) {
182      var self = this;
183
184      this.prepareSort(field, function() {
185        var sortPermutation = self.doSort_(field, direction);
186        self.dispatchSortEvent_(sortPermutation);
187      });
188    },
189
190    /**
191     * Sorts data model according to given field and direction.
192     * @param {string} field Sort field.
193     * @param {string} direction Sort direction.
194     */
195    doSort_: function(field, direction) {
196      var compareFunction = this.sortFunction_(field, direction);
197      var positions = [];
198      for (var i = 0; i < this.length; i++) {
199        positions[this.indexes_[i]] = i;
200      }
201      this.indexes_.sort(compareFunction);
202      this.sortStatus_ = this.createSortStatus(field, direction);
203      var sortPermutation = [];
204      for (var i = 0; i < this.length; i++) {
205        sortPermutation[positions[this.indexes_[i]]] = i;
206      }
207      return sortPermutation;
208    },
209
210    dispatchSortEvent_: function(sortPermutation) {
211      var e = new Event('sorted');
212      e.sortPermutation = sortPermutation;
213      this.dispatchEvent(e);
214    },
215
216    /**
217     * Creates compare function for the field.
218     * Returns the function set as sortFunction for given field
219     * or default compare function
220     * @param {string} field Sort field.
221     * @param {Function(*, *): number} Compare function.
222     */
223    createCompareFunction_: function(field) {
224      var compareFunction =
225          this.compareFunctions_ ? this.compareFunctions_[field] : null;
226      var defaultValuesCompareFunction = this.defaultValuesCompareFunction;
227      if (compareFunction) {
228        return compareFunction;
229      } else {
230        return function(a, b) {
231          return defaultValuesCompareFunction.call(null, a[field], b[field]);
232        }
233      }
234      return compareFunction;
235    },
236
237    /**
238     * Creates compare function for given field and direction.
239     * @param {string} field Sort field.
240     * @param {string} direction Sort direction.
241     * @param {Function(*, *): number} Compare function.
242     */
243    sortFunction_: function(field, direction) {
244      var compareFunction = this.createCompareFunction_(field);
245      var dirMultiplier = direction == 'desc' ? -1 : 1;
246
247      return function(index1, index2) {
248        var item1 = this.getItemByUnsortedIndex_(index1);
249        var item2 = this.getItemByUnsortedIndex_(index2);
250
251        var compareResult = compareFunction.call(null, item1, item2);
252        if (compareResult != 0)
253          return dirMultiplier * compareResult;
254        return dirMultiplier * this.defaultValuesCompareFunction(index1,
255                                                                 index2);
256      }.bind(this);
257    },
258
259    /**
260     * Default compare function.
261     */
262    defaultValuesCompareFunction: function(a, b) {
263      // We could insert i18n comparisons here.
264      if (a < b)
265        return -1;
266      if (a > b)
267        return 1;
268      return 0;
269    }
270  };
271
272  return {
273    TableDataModel: TableDataModel
274  };
275});
276