- add sources.
[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 cr.define('cr.ui', function() {
10   /** @const */ var EventTarget = cr.EventTarget;
11
12   /**
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.
16    * @constructor
17    * @extends {EventTarget}
18    */
19   function ArrayDataModel(array) {
20     this.array_ = array;
21     this.indexes_ = [];
22     this.compareFunctions_ = {};
23
24     for (var i = 0; i < array.length; i++) {
25       this.indexes_.push(i);
26     }
27   }
28
29   ArrayDataModel.prototype = {
30     __proto__: EventTarget.prototype,
31
32     /**
33      * The length of the data model.
34      * @type {number}
35      */
36     get length() {
37       return this.array_.length;
38     },
39
40     /**
41      * Returns the item at the given index.
42      * This implementation returns the item at the given index in the sorted
43      * array.
44      * @param {number} index The index of the element to get.
45      * @return {*} The element at the given index.
46      */
47     item: function(index) {
48       if (index >= 0 && index < this.length)
49         return this.array_[this.indexes_[index]];
50       return undefined;
51     },
52
53     /**
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.
57      */
58     compareFunction: function(field) {
59       return this.compareFunctions_[field];
60     },
61
62     /**
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.
66      */
67     setCompareFunction: function(field, compareFunction) {
68       if (!this.compareFunctions_) {
69         this.compareFunctions_ = {};
70       }
71       this.compareFunctions_[field] = compareFunction;
72     },
73
74     /**
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.
78      */
79     isSortable: function(field) {
80       return this.compareFunctions_ && field in this.compareFunctions_;
81     },
82
83     /**
84      * Returns current sort status.
85      * @return {!Object} Current sort status.
86      */
87     get sortStatus() {
88       if (this.sortStatus_) {
89         return this.createSortStatus(
90             this.sortStatus_.field, this.sortStatus_.direction);
91       } else {
92         return this.createSortStatus(null, null);
93       }
94     },
95
96     /**
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.
102      */
103     indexOf: function(item, opt_fromIndex) {
104       for (var i = opt_fromIndex || 0; i < this.indexes_.length; i++) {
105         if (item === this.item(i))
106           return i;
107       }
108       return -1;
109     },
110
111     /**
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.
116      */
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] });
121     },
122
123     /**
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
127      * the whole change.
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.
132      */
133     splice: function(index, deleteCount, var_args) {
134       var addCount = arguments.length - 2;
135       var newIndexes = [];
136       var deletePermutation = [];
137       var deletedItems = [];
138       var newArray = [];
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]]);
146       }
147       // Delete items.
148       for (; i < index + deleteCount; i++) {
149         deletePermutation.push(-1);
150         deletedItems.push(this.array_[this.indexes_[i]]);
151       }
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]);
156       }
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]]);
162       }
163
164       this.indexes_ = newIndexes;
165
166       this.array_ = newArray;
167
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);
172
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;
180         });
181         this.dispatchPermutedEvent_(splicePermutation);
182         spliceEvent.index = sortPermutation[index];
183       } else {
184         this.dispatchPermutedEvent_(deletePermutation);
185         spliceEvent.index = index;
186       }
187
188       this.dispatchEvent(spliceEvent);
189
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.
194       if (status.field)
195         this.delayedSort_(status.field, status.direction);
196
197       return deletedItems;
198     },
199
200     /**
201      * Appends items to the end of the model.
202      *
203      * This dispatches a splice event.
204      *
205      * @param {...*} The items to append.
206      * @return {number} The new length of the model.
207      */
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);
212       return this.length;
213     },
214
215     /**
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.
221      */
222     updateIndex: function(index) {
223       if (index < 0 || index >= this.length)
224         throw Error('Invalid index, ' + index);
225
226       // TODO(arv): Maybe unify splice and change events?
227       var e = new Event('change');
228       e.index = index;
229       this.dispatchEvent(e);
230
231       if (this.sortStatus.field) {
232         var status = this.sortStatus;
233         var sortPermutation = this.doSort_(this.sortStatus.field,
234                                            this.sortStatus.direction);
235         if (sortPermutation)
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);
241       }
242     },
243
244     /**
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.
249      */
250     createSortStatus: function(field, direction) {
251       return {
252         field: field,
253         direction: direction
254       };
255     },
256
257     /**
258      * Called before a sort happens so that you may fetch additional data
259      * required for the sort.
260      *
261      * @param {string} field Sort field.
262      * @param {function()} callback The function to invoke when preparation
263      *     is complete.
264      */
265     prepareSort: function(field, callback) {
266       callback();
267     },
268
269     /**
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.
274      * @private
275      */
276     delayedSort_: function(field, direction) {
277       var self = this;
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);
284         }
285       }, 0);
286     },
287
288     /**
289      * Sorts data model according to given field and direction and dispathes
290      * sorted event.
291      * @param {string} field Sort field.
292      * @param {string} direction Sort direction.
293      */
294     sort: function(field, direction) {
295       var self = this;
296
297       this.prepareSort(field, function() {
298         var sortPermutation = self.doSort_(field, direction);
299         if (sortPermutation)
300           self.dispatchPermutedEvent_(sortPermutation);
301         self.dispatchSortEvent_();
302       });
303     },
304
305     /**
306      * Sorts data model according to given field and direction.
307      * @param {string} field Sort field.
308      * @param {string} direction Sort direction.
309      * @private
310      */
311     doSort_: function(field, direction) {
312       var compareFunction = this.sortFunction_(field, direction);
313       var positions = [];
314       for (var i = 0; i < this.length; i++) {
315         positions[this.indexes_[i]] = i;
316       }
317       var sorted = this.indexes_.every(function(element, index, array) {
318         return index == 0 || compareFunction(element, array[index - 1]) >= 0;
319       });
320       if (!sorted)
321         this.indexes_.sort(compareFunction);
322       this.sortStatus_ = this.createSortStatus(field, direction);
323       var sortPermutation = [];
324       var changed = false;
325       for (var i = 0; i < this.length; i++) {
326         if (positions[this.indexes_[i]] != i)
327           changed = true;
328         sortPermutation[positions[this.indexes_[i]]] = i;
329       }
330       if (changed)
331         return sortPermutation;
332       return null;
333     },
334
335     dispatchSortEvent_: function() {
336       var e = new Event('sorted');
337       this.dispatchEvent(e);
338     },
339
340     dispatchPermutedEvent_: function(permutation) {
341       var e = new Event('permuted');
342       e.permutation = permutation;
343       e.newLength = this.length;
344       this.dispatchEvent(e);
345     },
346
347     /**
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.
353      * @private
354      */
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;
361       } else {
362         return function(a, b) {
363           return defaultValuesCompareFunction.call(null, a[field], b[field]);
364         }
365       }
366       return compareFunction;
367     },
368
369     /**
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.
374      * @private
375      */
376     sortFunction_: function(field, direction) {
377       var compareFunction = null;
378       if (field !== null)
379         compareFunction = this.createCompareFunction_(field);
380       var dirMultiplier = direction == 'desc' ? -1 : 1;
381
382       return function(index1, index2) {
383         var item1 = this.array_[index1];
384         var item2 = this.array_[index2];
385
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,
392                                                                  index2);
393       }.bind(this);
394     },
395
396     /**
397      * Default compare function.
398      */
399     defaultValuesCompareFunction: function(a, b) {
400       // We could insert i18n comparisons here.
401       if (a < b)
402         return -1;
403       if (a > b)
404         return 1;
405       return 0;
406     }
407   };
408
409   return {
410     ArrayDataModel: ArrayDataModel
411   };
412 });