Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / array_data_model.js
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.
4
5 /**
6  * @fileoverview This is a data model representin
7  */
8
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.
14
15 //<include src="../../assert.js">
16
17 cr.define('cr.ui', function() {
18   /** @const */ var EventTarget = cr.EventTarget;
19
20   /**
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.
24    * @constructor
25    * @extends {cr.EventTarget}
26    */
27   function ArrayDataModel(array) {
28     this.array_ = array;
29     this.indexes_ = [];
30     this.compareFunctions_ = {};
31
32     for (var i = 0; i < array.length; i++) {
33       this.indexes_.push(i);
34     }
35   }
36
37   ArrayDataModel.prototype = {
38     __proto__: EventTarget.prototype,
39
40     /**
41      * The length of the data model.
42      * @type {number}
43      */
44     get length() {
45       return this.array_.length;
46     },
47
48     /**
49      * Returns the item at the given index.
50      * This implementation returns the item at the given index in the sorted
51      * array.
52      * @param {number} index The index of the element to get.
53      * @return {*} The element at the given index.
54      */
55     item: function(index) {
56       if (index >= 0 && index < this.length)
57         return this.array_[this.indexes_[index]];
58       return undefined;
59     },
60
61     /**
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.
65      */
66     compareFunction: function(field) {
67       return this.compareFunctions_[field];
68     },
69
70     /**
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
74      *     for given field.
75      */
76     setCompareFunction: function(field, compareFunction) {
77       if (!this.compareFunctions_) {
78         this.compareFunctions_ = {};
79       }
80       this.compareFunctions_[field] = compareFunction;
81     },
82
83     /**
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.
87      */
88     isSortable: function(field) {
89       return this.compareFunctions_ && field in this.compareFunctions_;
90     },
91
92     /**
93      * Returns current sort status.
94      * @return {!Object} Current sort status.
95      */
96     get sortStatus() {
97       if (this.sortStatus_) {
98         return this.createSortStatus(
99             this.sortStatus_.field, this.sortStatus_.direction);
100       } else {
101         return this.createSortStatus(null, null);
102       }
103     },
104
105     /**
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.
111      */
112     indexOf: function(item, opt_fromIndex) {
113       for (var i = opt_fromIndex || 0; i < this.indexes_.length; i++) {
114         if (item === this.item(i))
115           return i;
116       }
117       return -1;
118     },
119
120     /**
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.
125      */
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] });
130     },
131
132     /**
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
136      * the whole change.
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.
141      */
142     splice: function(index, deleteCount, var_args) {
143       var addCount = arguments.length - 2;
144       var newIndexes = [];
145       var deletePermutation = [];
146       var deletedItems = [];
147       var newArray = [];
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]]);
155       }
156       // Delete items.
157       for (; i < index + deleteCount; i++) {
158         deletePermutation.push(-1);
159         deletedItems.push(this.array_[this.indexes_[i]]);
160       }
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]);
165       }
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]]);
171       }
172
173       this.indexes_ = newIndexes;
174
175       this.array_ = newArray;
176
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);
181
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;
189         });
190         this.dispatchPermutedEvent_(splicePermutation);
191         spliceEvent.index = sortPermutation[index];
192       } else {
193         this.dispatchPermutedEvent_(deletePermutation);
194         spliceEvent.index = index;
195       }
196
197       this.dispatchEvent(spliceEvent);
198
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.
203       if (status.field)
204         this.delayedSort_(status.field, status.direction);
205
206       return deletedItems;
207     },
208
209     /**
210      * Appends items to the end of the model.
211      *
212      * This dispatches a splice event.
213      *
214      * @param {...*} var_args The items to append.
215      * @return {number} The new length of the model.
216      */
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);
221       return this.length;
222     },
223
224     /**
225      * Updates the existing item with the new item.
226      *
227      * The existing item and the new item are regarded as the same item and the
228      * permutation tracks these indexes.
229      *
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.
233      */
234     replaceItem: function(oldItem, newItem) {
235       var index = this.indexOf(oldItem);
236       if (index < 0)
237         return;
238       this.array_[this.indexes_[index]] = newItem;
239       this.updateIndex(index);
240     },
241
242     /**
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.
248      */
249     updateIndex: function(index) {
250       this.updateIndexes([index]);
251     },
252
253     /**
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.
259      */
260     updateIndexes: function(indexes) {
261       indexes.forEach(function(index) {
262         assert(index >= 0 && index < this.length, 'Invalid index');
263       }, this);
264
265       for (var i = 0; i < indexes.length; i++) {
266         var e = new Event('change');
267         e.index = indexes[i];
268         this.dispatchEvent(e);
269       }
270
271       if (this.sortStatus.field) {
272         var status = this.sortStatus;
273         var sortPermutation = this.doSort_(this.sortStatus.field,
274                                            this.sortStatus.direction);
275         if (sortPermutation)
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);
281       }
282     },
283
284     /**
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.
289      */
290     createSortStatus: function(field, direction) {
291       return {
292         field: field,
293         direction: direction
294       };
295     },
296
297     /**
298      * Called before a sort happens so that you may fetch additional data
299      * required for the sort.
300      *
301      * @param {string} field Sort field.
302      * @param {function()} callback The function to invoke when preparation
303      *     is complete.
304      */
305     prepareSort: function(field, callback) {
306       callback();
307     },
308
309     /**
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.
314      * @private
315      */
316     delayedSort_: function(field, direction) {
317       var self = this;
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);
324         }
325       }, 0);
326     },
327
328     /**
329      * Sorts data model according to given field and direction and dispathes
330      * sorted event.
331      * @param {string} field Sort field.
332      * @param {string} direction Sort direction.
333      */
334     sort: function(field, direction) {
335       var self = this;
336
337       this.prepareSort(field, function() {
338         var sortPermutation = self.doSort_(field, direction);
339         if (sortPermutation)
340           self.dispatchPermutedEvent_(sortPermutation);
341         self.dispatchSortEvent_();
342       });
343     },
344
345     /**
346      * Sorts data model according to given field and direction.
347      * @param {string} field Sort field.
348      * @param {string} direction Sort direction.
349      * @private
350      */
351     doSort_: function(field, direction) {
352       var compareFunction = this.sortFunction_(field, direction);
353       var positions = [];
354       for (var i = 0; i < this.length; i++) {
355         positions[this.indexes_[i]] = i;
356       }
357       var sorted = this.indexes_.every(function(element, index, array) {
358         return index == 0 || compareFunction(element, array[index - 1]) >= 0;
359       });
360       if (!sorted)
361         this.indexes_.sort(compareFunction);
362       this.sortStatus_ = this.createSortStatus(field, direction);
363       var sortPermutation = [];
364       var changed = false;
365       for (var i = 0; i < this.length; i++) {
366         if (positions[this.indexes_[i]] != i)
367           changed = true;
368         sortPermutation[positions[this.indexes_[i]]] = i;
369       }
370       if (changed)
371         return sortPermutation;
372       return null;
373     },
374
375     dispatchSortEvent_: function() {
376       var e = new Event('sorted');
377       this.dispatchEvent(e);
378     },
379
380     dispatchPermutedEvent_: function(permutation) {
381       var e = new Event('permuted');
382       e.permutation = permutation;
383       e.newLength = this.length;
384       this.dispatchEvent(e);
385     },
386
387     /**
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.
393      * @private
394      */
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;
401       } else {
402         return function(a, b) {
403           return defaultValuesCompareFunction.call(null, a[field], b[field]);
404         }
405       }
406     },
407
408     /**
409      * Creates compare function for given field and direction.
410      * @param {string} field Sort field.
411      * @param {string} direction Sort direction.
412      * @private
413      */
414     sortFunction_: function(field, direction) {
415       var compareFunction = null;
416       if (field !== null)
417         compareFunction = this.createCompareFunction_(field);
418       var dirMultiplier = direction == 'desc' ? -1 : 1;
419
420       return function(index1, index2) {
421         var item1 = this.array_[index1];
422         var item2 = this.array_[index2];
423
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,
430                                                                  index2);
431       }.bind(this);
432     },
433
434     /**
435      * Default compare function.
436      */
437     defaultValuesCompareFunction: function(a, b) {
438       // We could insert i18n comparisons here.
439       if (a < b)
440         return -1;
441       if (a > b)
442         return 1;
443       return 0;
444     }
445   };
446
447   return {
448     ArrayDataModel: ArrayDataModel
449   };
450 });