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.
6 * @fileoverview This is a data model representin
9 cr.define('cr.ui', function() {
10 /** @const */ var EventTarget = cr.EventTarget;
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.
17 * @extends {EventTarget}
19 function ArrayDataModel(array) {
22 this.compareFunctions_ = {};
24 for (var i = 0; i < array.length; i++) {
25 this.indexes_.push(i);
29 ArrayDataModel.prototype = {
30 __proto__: EventTarget.prototype,
33 * The length of the data model.
37 return this.array_.length;
41 * Returns the item at the given index.
42 * This implementation returns the item at the given index in the sorted
44 * @param {number} index The index of the element to get.
45 * @return {*} The element at the given index.
47 item: function(index) {
48 if (index >= 0 && index < this.length)
49 return this.array_[this.indexes_[index]];
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.
58 compareFunction: function(field) {
59 return this.compareFunctions_[field];
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.
67 setCompareFunction: function(field, compareFunction) {
68 if (!this.compareFunctions_) {
69 this.compareFunctions_ = {};
71 this.compareFunctions_[field] = compareFunction;
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.
79 isSortable: function(field) {
80 return this.compareFunctions_ && field in this.compareFunctions_;
84 * Returns current sort status.
85 * @return {!Object} Current sort status.
88 if (this.sortStatus_) {
89 return this.createSortStatus(
90 this.sortStatus_.field, this.sortStatus_.direction);
92 return this.createSortStatus(null, null);
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.
103 indexOf: function(item, opt_fromIndex) {
104 for (var i = opt_fromIndex || 0; i < this.indexes_.length; i++) {
105 if (item === this.item(i))
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.
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] });
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
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.
133 splice: function(index, deleteCount, var_args) {
134 var addCount = arguments.length - 2;
136 var deletePermutation = [];
137 var deletedItems = [];
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]]);
148 for (; i < index + deleteCount; i++) {
149 deletePermutation.push(-1);
150 deletedItems.push(this.array_[this.indexes_[i]]);
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]);
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]]);
164 this.indexes_ = newIndexes;
166 this.array_ = newArray;
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);
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;
181 this.dispatchPermutedEvent_(splicePermutation);
182 spliceEvent.index = sortPermutation[index];
184 this.dispatchPermutedEvent_(deletePermutation);
185 spliceEvent.index = index;
188 this.dispatchEvent(spliceEvent);
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.
195 this.delayedSort_(status.field, status.direction);
201 * Appends items to the end of the model.
203 * This dispatches a splice event.
205 * @param {...*} The items to append.
206 * @return {number} The new length of the model.
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);
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.
222 updateIndex: function(index) {
223 if (index < 0 || index >= this.length)
224 throw Error('Invalid index, ' + index);
226 // TODO(arv): Maybe unify splice and change events?
227 var e = new Event('change');
229 this.dispatchEvent(e);
231 if (this.sortStatus.field) {
232 var status = this.sortStatus;
233 var sortPermutation = this.doSort_(this.sortStatus.field,
234 this.sortStatus.direction);
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);
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.
250 createSortStatus: function(field, direction) {
258 * Called before a sort happens so that you may fetch additional data
259 * required for the sort.
261 * @param {string} field Sort field.
262 * @param {function()} callback The function to invoke when preparation
265 prepareSort: function(field, callback) {
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.
276 delayedSort_: function(field, direction) {
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);
289 * Sorts data model according to given field and direction and dispathes
291 * @param {string} field Sort field.
292 * @param {string} direction Sort direction.
294 sort: function(field, direction) {
297 this.prepareSort(field, function() {
298 var sortPermutation = self.doSort_(field, direction);
300 self.dispatchPermutedEvent_(sortPermutation);
301 self.dispatchSortEvent_();
306 * Sorts data model according to given field and direction.
307 * @param {string} field Sort field.
308 * @param {string} direction Sort direction.
311 doSort_: function(field, direction) {
312 var compareFunction = this.sortFunction_(field, direction);
314 for (var i = 0; i < this.length; i++) {
315 positions[this.indexes_[i]] = i;
317 var sorted = this.indexes_.every(function(element, index, array) {
318 return index == 0 || compareFunction(element, array[index - 1]) >= 0;
321 this.indexes_.sort(compareFunction);
322 this.sortStatus_ = this.createSortStatus(field, direction);
323 var sortPermutation = [];
325 for (var i = 0; i < this.length; i++) {
326 if (positions[this.indexes_[i]] != i)
328 sortPermutation[positions[this.indexes_[i]]] = i;
331 return sortPermutation;
335 dispatchSortEvent_: function() {
336 var e = new Event('sorted');
337 this.dispatchEvent(e);
340 dispatchPermutedEvent_: function(permutation) {
341 var e = new Event('permuted');
342 e.permutation = permutation;
343 e.newLength = this.length;
344 this.dispatchEvent(e);
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.
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;
362 return function(a, b) {
363 return defaultValuesCompareFunction.call(null, a[field], b[field]);
366 return compareFunction;
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.
376 sortFunction_: function(field, direction) {
377 var compareFunction = null;
379 compareFunction = this.createCompareFunction_(field);
380 var dirMultiplier = direction == 'desc' ? -1 : 1;
382 return function(index1, index2) {
383 var item1 = this.array_[index1];
384 var item2 = this.array_[index2];
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,
397 * Default compare function.
399 defaultValuesCompareFunction: function(a, b) {
400 // We could insert i18n comparisons here.
410 ArrayDataModel: ArrayDataModel