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 // The include directives are put into Javascript-style comments to prevent
10 // parsing errors in non-flattened mode. The flattener still sees them.
11 // Note that this makes the flattener to comment out the first line of the
12 // included file but that's all right since any javascript file should start
13 // with a copyright comment anyway.
15 //<include src="../../assert.js">
17 cr.define('cr.ui', function() {
18 /** @const */ var EventTarget = cr.EventTarget;
21 * A data model that wraps a simple array and supports sorting by storing
22 * initial indexes of elements for each position in sorted array.
23 * @param {!Array} array The underlying array.
25 * @extends {cr.EventTarget}
27 function ArrayDataModel(array) {
30 this.compareFunctions_ = {};
32 for (var i = 0; i < array.length; i++) {
33 this.indexes_.push(i);
37 ArrayDataModel.prototype = {
38 __proto__: EventTarget.prototype,
41 * The length of the data model.
45 return this.array_.length;
49 * Returns the item at the given index.
50 * This implementation returns the item at the given index in the sorted
52 * @param {number} index The index of the element to get.
53 * @return {*} The element at the given index.
55 item: function(index) {
56 if (index >= 0 && index < this.length)
57 return this.array_[this.indexes_[index]];
62 * Returns compare function set for given field.
63 * @param {string} field The field to get compare function for.
64 * @return {function(*, *): number} Compare function set for given field.
66 compareFunction: function(field) {
67 return this.compareFunctions_[field];
71 * Sets compare function for given field.
72 * @param {string} field The field to set compare function.
73 * @param {function(*, *): number} compareFunction Compare function to set
76 setCompareFunction: function(field, compareFunction) {
77 if (!this.compareFunctions_) {
78 this.compareFunctions_ = {};
80 this.compareFunctions_[field] = compareFunction;
84 * Returns true if the field has a compare function.
85 * @param {string} field The field to check.
86 * @return {boolean} True if the field is sortable.
88 isSortable: function(field) {
89 return this.compareFunctions_ && field in this.compareFunctions_;
93 * Returns current sort status.
94 * @return {!Object} Current sort status.
97 if (this.sortStatus_) {
98 return this.createSortStatus(
99 this.sortStatus_.field, this.sortStatus_.direction);
101 return this.createSortStatus(null, null);
106 * Returns the first matching item.
107 * @param {*} item The item to find.
108 * @param {number=} opt_fromIndex If provided, then the searching start at
109 * the {@code opt_fromIndex}.
110 * @return {number} The index of the first found element or -1 if not found.
112 indexOf: function(item, opt_fromIndex) {
113 for (var i = opt_fromIndex || 0; i < this.indexes_.length; i++) {
114 if (item === this.item(i))
121 * Returns an array of elements in a selected range.
122 * @param {number=} opt_from The starting index of the selected range.
123 * @param {number=} opt_to The ending index of selected range.
124 * @return {Array} An array of elements in the selected range.
126 slice: function(opt_from, opt_to) {
127 var arr = this.array_;
128 return this.indexes_.slice(opt_from, opt_to).map(
129 function(index) { return arr[index] });
133 * This removes and adds items to the model.
134 * This dispatches a splice event.
135 * This implementation runs sort after splice and creates permutation for
137 * @param {number} index The index of the item to update.
138 * @param {number} deleteCount The number of items to remove.
139 * @param {...*} var_args The items to add.
140 * @return {!Array} An array with the removed items.
142 splice: function(index, deleteCount, var_args) {
143 var addCount = arguments.length - 2;
145 var deletePermutation = [];
146 var deletedItems = [];
148 index = Math.min(index, this.indexes_.length);
149 deleteCount = Math.min(deleteCount, this.indexes_.length - index);
150 // Copy items before the insertion point.
151 for (var i = 0; i < index; i++) {
152 newIndexes.push(newArray.length);
153 deletePermutation.push(i);
154 newArray.push(this.array_[this.indexes_[i]]);
157 for (; i < index + deleteCount; i++) {
158 deletePermutation.push(-1);
159 deletedItems.push(this.array_[this.indexes_[i]]);
161 // Insert new items instead deleted ones.
162 for (var j = 0; j < addCount; j++) {
163 newIndexes.push(newArray.length);
164 newArray.push(arguments[j + 2]);
166 // Copy items after the insertion point.
167 for (; i < this.indexes_.length; i++) {
168 newIndexes.push(newArray.length);
169 deletePermutation.push(i - deleteCount + addCount);
170 newArray.push(this.array_[this.indexes_[i]]);
173 this.indexes_ = newIndexes;
175 this.array_ = newArray;
177 // TODO(arv): Maybe unify splice and change events?
178 var spliceEvent = new Event('splice');
179 spliceEvent.removed = deletedItems;
180 spliceEvent.added = Array.prototype.slice.call(arguments, 2);
182 var status = this.sortStatus;
183 // if sortStatus.field is null, this restores original order.
184 var sortPermutation = this.doSort_(this.sortStatus.field,
185 this.sortStatus.direction);
186 if (sortPermutation) {
187 var splicePermutation = deletePermutation.map(function(element) {
188 return element != -1 ? sortPermutation[element] : -1;
190 this.dispatchPermutedEvent_(splicePermutation);
191 spliceEvent.index = sortPermutation[index];
193 this.dispatchPermutedEvent_(deletePermutation);
194 spliceEvent.index = index;
197 this.dispatchEvent(spliceEvent);
199 // If real sorting is needed, we should first call prepareSort (data may
200 // change), and then sort again.
201 // Still need to finish the sorting above (including events), so
202 // list will not go to inconsistent state.
204 this.delayedSort_(status.field, status.direction);
210 * Appends items to the end of the model.
212 * This dispatches a splice event.
214 * @param {...*} var_args The items to append.
215 * @return {number} The new length of the model.
217 push: function(var_args) {
218 var args = Array.prototype.slice.call(arguments);
219 args.unshift(this.length, 0);
220 this.splice.apply(this, args);
225 * Updates the existing item with the new item.
227 * The existing item and the new item are regarded as the same item and the
228 * permutation tracks these indexes.
230 * @param {*} oldItem Old item that is contained in the model. If the item
231 * is not found in the model, the method call is just ignored.
232 * @param {*} newItem New item.
234 replaceItem: function(oldItem, newItem) {
235 var index = this.indexOf(oldItem);
238 this.array_[this.indexes_[index]] = newItem;
239 this.updateIndex(index);
243 * Use this to update a given item in the array. This does not remove and
244 * reinsert a new item.
245 * This dispatches a change event.
246 * This runs sort after updating.
247 * @param {number} index The index of the item to update.
249 updateIndex: function(index) {
250 this.updateIndexes([index]);
254 * Notifies of update of the items in the array. This does not remove and
255 * reinsert new items.
256 * This dispatches one or more change events.
257 * This runs sort after updating.
258 * @param {Array.<number>} indexes The index list of items to update.
260 updateIndexes: function(indexes) {
261 indexes.forEach(function(index) {
262 assert(index >= 0 && index < this.length, 'Invalid index');
265 for (var i = 0; i < indexes.length; i++) {
266 var e = new Event('change');
267 e.index = indexes[i];
268 this.dispatchEvent(e);
271 if (this.sortStatus.field) {
272 var status = this.sortStatus;
273 var sortPermutation = this.doSort_(this.sortStatus.field,
274 this.sortStatus.direction);
276 this.dispatchPermutedEvent_(sortPermutation);
277 // We should first call prepareSort (data may change), and then sort.
278 // Still need to finish the sorting above (including events), so
279 // list will not go to inconsistent state.
280 this.delayedSort_(status.field, status.direction);
285 * Creates sort status with given field and direction.
286 * @param {?string} field Sort field.
287 * @param {?string} direction Sort direction.
288 * @return {!Object} Created sort status.
290 createSortStatus: function(field, direction) {
298 * Called before a sort happens so that you may fetch additional data
299 * required for the sort.
301 * @param {string} field Sort field.
302 * @param {function()} callback The function to invoke when preparation
305 prepareSort: function(field, callback) {
310 * Sorts data model according to given field and direction and dispathes
311 * sorted event with delay. If no need to delay, use sort() instead.
312 * @param {string} field Sort field.
313 * @param {string} direction Sort direction.
316 delayedSort_: function(field, direction) {
318 setTimeout(function() {
319 // If the sort status has been changed, sorting has already done
320 // on the change event.
321 if (field == self.sortStatus.field &&
322 direction == self.sortStatus.direction) {
323 self.sort(field, direction);
329 * Sorts data model according to given field and direction and dispathes
331 * @param {string} field Sort field.
332 * @param {string} direction Sort direction.
334 sort: function(field, direction) {
337 this.prepareSort(field, function() {
338 var sortPermutation = self.doSort_(field, direction);
340 self.dispatchPermutedEvent_(sortPermutation);
341 self.dispatchSortEvent_();
346 * Sorts data model according to given field and direction.
347 * @param {string} field Sort field.
348 * @param {string} direction Sort direction.
351 doSort_: function(field, direction) {
352 var compareFunction = this.sortFunction_(field, direction);
354 for (var i = 0; i < this.length; i++) {
355 positions[this.indexes_[i]] = i;
357 var sorted = this.indexes_.every(function(element, index, array) {
358 return index == 0 || compareFunction(element, array[index - 1]) >= 0;
361 this.indexes_.sort(compareFunction);
362 this.sortStatus_ = this.createSortStatus(field, direction);
363 var sortPermutation = [];
365 for (var i = 0; i < this.length; i++) {
366 if (positions[this.indexes_[i]] != i)
368 sortPermutation[positions[this.indexes_[i]]] = i;
371 return sortPermutation;
375 dispatchSortEvent_: function() {
376 var e = new Event('sorted');
377 this.dispatchEvent(e);
380 dispatchPermutedEvent_: function(permutation) {
381 var e = new Event('permuted');
382 e.permutation = permutation;
383 e.newLength = this.length;
384 this.dispatchEvent(e);
388 * Creates compare function for the field.
389 * Returns the function set as sortFunction for given field
390 * or default compare function
391 * @param {string} field Sort field.
392 * @return {function(*, *): number} Compare function.
395 createCompareFunction_: function(field) {
396 var compareFunction =
397 this.compareFunctions_ ? this.compareFunctions_[field] : null;
398 var defaultValuesCompareFunction = this.defaultValuesCompareFunction;
399 if (compareFunction) {
400 return compareFunction;
402 return function(a, b) {
403 return defaultValuesCompareFunction.call(null, a[field], b[field]);
409 * Creates compare function for given field and direction.
410 * @param {string} field Sort field.
411 * @param {string} direction Sort direction.
414 sortFunction_: function(field, direction) {
415 var compareFunction = null;
417 compareFunction = this.createCompareFunction_(field);
418 var dirMultiplier = direction == 'desc' ? -1 : 1;
420 return function(index1, index2) {
421 var item1 = this.array_[index1];
422 var item2 = this.array_[index2];
424 var compareResult = 0;
425 if (typeof(compareFunction) === 'function')
426 compareResult = compareFunction.call(null, item1, item2);
427 if (compareResult != 0)
428 return dirMultiplier * compareResult;
429 return dirMultiplier * this.defaultValuesCompareFunction(index1,
435 * Default compare function.
437 defaultValuesCompareFunction: function(a, b) {
438 // We could insert i18n comparisons here.
448 ArrayDataModel: ArrayDataModel