- add sources.
[platform/framework/web/crosswalk.git] / src / ui / webui / resources / js / cr / ui / grid.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 // require: list_selection_model.js
6 // require: list_selection_controller.js
7 // require: list.js
8
9 /**
10  * @fileoverview This implements a grid control. Grid contains a bunch of
11  * similar elements placed in multiple columns. It's pretty similar to the list,
12  * except the multiple columns layout.
13  */
14
15 cr.define('cr.ui', function() {
16   /** @const */ var ListSelectionController = cr.ui.ListSelectionController;
17   /** @const */ var List = cr.ui.List;
18   /** @const */ var ListItem = cr.ui.ListItem;
19
20   /**
21    * Creates a new grid item element.
22    * @param {*} dataItem The data item.
23    * @constructor
24    * @extends {cr.ui.ListItem}
25    */
26   function GridItem(dataItem) {
27     var el = cr.doc.createElement('li');
28     el.dataItem = dataItem;
29     el.__proto__ = GridItem.prototype;
30     return el;
31   }
32
33   GridItem.prototype = {
34     __proto__: ListItem.prototype,
35
36     /**
37      * Called when an element is decorated as a grid item.
38      */
39     decorate: function() {
40       ListItem.prototype.decorate.call(this, arguments);
41       this.textContent = this.dataItem;
42     }
43   };
44
45   /**
46    * Creates a new grid element.
47    * @param {Object=} opt_propertyBag Optional properties.
48    * @constructor
49    * @extends {cr.ui.List}
50    */
51   var Grid = cr.ui.define('grid');
52
53   Grid.prototype = {
54     __proto__: List.prototype,
55
56     /**
57      * The number of columns in the grid. Either set by the user, or lazy
58      * calculated as the maximum number of items fitting in the grid width.
59      * @type {number}
60      * @private
61      */
62     columns_: 0,
63
64     /**
65      * Function used to create grid items.
66      * @type {function(): !GridItem}
67      * @override
68      */
69     itemConstructor_: GridItem,
70
71     /**
72      * Whether or not the rows on list have various heights.
73      * Shows a warning at the setter because cr.ui.Grid does not support this.
74      * @type {boolean}
75      */
76     get fixedHeight() {
77       return true;
78     },
79     set fixedHeight(fixedHeight) {
80       if (!fixedHeight)
81         console.warn('cr.ui.Grid does not support fixedHeight = false');
82     },
83
84     /**
85      * @return {number} The number of columns determined by width of the grid
86      *     and width of the items.
87      * @private
88      */
89     getColumnCount_: function() {
90       // Size comes here with margin already collapsed.
91       var size = this.getDefaultItemSize_();
92
93       // We should uncollapse margin, since margin isn't collapsed for
94       // inline-block elements according to css spec which are thumbnail items.
95
96       var width = size.width + Math.min(size.marginLeft, size.marginRight);
97       var height = size.height + Math.min(size.marginTop, size.marginBottom);
98
99       if (!width || !height)
100         return 0;
101
102       var itemCount = this.dataModel ? this.dataModel.length : 0;
103       if (!itemCount)
104         return 0;
105
106       var columns = Math.floor(this.clientWidthWithoutScrollbar_ / width);
107       if (!columns)
108         return 0;
109
110       var rows = Math.ceil(itemCount / columns);
111       if (rows * height <= this.clientHeight_)
112         return columns;
113
114       return Math.floor(this.clientWidthWithScrollbar_ / width);
115     },
116
117     /**
118      * Measure and cache client width and height with and without scrollbar.
119      * Must be updated when offsetWidth and/or offsetHeight changed.
120      */
121     updateMetrics_: function() {
122       // Check changings that may affect number of columns.
123       var offsetWidth = this.offsetWidth;
124       var offsetHeight = this.offsetHeight;
125       var overflowY = window.getComputedStyle(this).overflowY;
126
127       if (this.lastOffsetWidth_ == offsetWidth &&
128           this.lastOverflowY == overflowY) {
129         this.lastOffsetHeight_ = offsetHeight;
130         return;
131       }
132
133       this.lastOffsetWidth_ = offsetWidth;
134       this.lastOffsetHeight_ = offsetHeight;
135       this.lastOverflowY = overflowY;
136       this.columns_ = 0;
137
138       if (overflowY == 'auto' && offsetWidth > 0) {
139         // Column number may depend on whether scrollbar is present or not.
140         var originalClientWidth = this.clientWidth;
141         // At first make sure there is no scrollbar and calculate clientWidth
142         // (triggers reflow).
143         this.style.overflowY = 'hidden';
144         this.clientWidthWithoutScrollbar_ = this.clientWidth;
145         this.clientHeight_ = this.clientHeight;
146         if (this.clientWidth != originalClientWidth) {
147           // If clientWidth changed then previously scrollbar was shown.
148           this.clientWidthWithScrollbar_ = originalClientWidth;
149         } else {
150           // Show scrollbar and recalculate clientWidth (triggers reflow).
151           this.style.overflowY = 'scroll';
152           this.clientWidthWithScrollbar_ = this.clientWidth;
153         }
154         this.style.overflowY = '';
155       } else {
156         this.clientWidthWithoutScrollbar_ = this.clientWidthWithScrollbar_ =
157             this.clientWidth;
158         this.clientHeight_ = this.clientHeight;
159       }
160     },
161
162     /**
163      * The number of columns in the grid. If not set, determined automatically
164      * as the maximum number of items fitting in the grid width.
165      * @type {number}
166      */
167     get columns() {
168       if (!this.columns_) {
169         this.columns_ = this.getColumnCount_();
170       }
171       return this.columns_ || 1;
172     },
173     set columns(value) {
174       if (value >= 0 && value != this.columns_) {
175         this.columns_ = value;
176         this.redraw();
177       }
178     },
179
180     /**
181      * @param {number} index The index of the item.
182      * @return {number} The top position of the item inside the list, not taking
183      *     into account lead item. May vary in the case of multiple columns.
184      * @override
185      */
186     getItemTop: function(index) {
187       return Math.floor(index / this.columns) * this.getDefaultItemHeight_();
188     },
189
190     /**
191      * @param {number} index The index of the item.
192      * @return {number} The row of the item. May vary in the case
193      *     of multiple columns.
194      * @override
195      */
196     getItemRow: function(index) {
197       return Math.floor(index / this.columns);
198     },
199
200     /**
201      * @param {number} row The row.
202      * @return {number} The index of the first item in the row.
203      * @override
204      */
205     getFirstItemInRow: function(row) {
206       return row * this.columns;
207     },
208
209     /**
210      * Creates the selection controller to use internally.
211      * @param {cr.ui.ListSelectionModel} sm The underlying selection model.
212      * @return {!cr.ui.ListSelectionController} The newly created selection
213      *     controller.
214      * @override
215      */
216     createSelectionController: function(sm) {
217       return new GridSelectionController(sm, this);
218     },
219
220     /**
221      * Calculates the number of items fitting in the given viewport.
222      * @param {number} scrollTop The scroll top position.
223      * @param {number} clientHeight The height of viewport.
224      * @return {{first: number, length: number, last: number}} The index of
225      *     first item in view port, The number of items, The item past the last.
226      * @override
227      */
228     getItemsInViewPort: function(scrollTop, clientHeight) {
229       var itemHeight = this.getDefaultItemHeight_();
230       var firstIndex =
231           this.autoExpands ? 0 : this.getIndexForListOffset_(scrollTop);
232       var columns = this.columns;
233       var count = this.autoExpands_ ? this.dataModel.length : Math.max(
234           columns * (Math.ceil(clientHeight / itemHeight) + 1),
235           this.countItemsInRange_(firstIndex, scrollTop + clientHeight));
236       count = columns * Math.ceil(count / columns);
237       count = Math.min(count, this.dataModel.length - firstIndex);
238       return {
239         first: firstIndex,
240         length: count,
241         last: firstIndex + count - 1
242       };
243     },
244
245     /**
246      * Merges list items. Calls the base class implementation and then
247      * puts spacers on the right places.
248      * @param {number} firstIndex The index of first item, inclusively.
249      * @param {number} lastIndex The index of last item, exclusively.
250      * @param {Object.<string, ListItem>} cachedItems Old items cache.
251      * @param {Object.<string, ListItem>} newCachedItems New items cache.
252      * @override
253      */
254     mergeItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) {
255       List.prototype.mergeItems.call(this,
256           firstIndex, lastIndex, cachedItems, newCachedItems);
257
258       var afterFiller = this.afterFiller_;
259       var columns = this.columns;
260
261       for (var item = this.beforeFiller_.nextSibling; item != afterFiller;) {
262         var next = item.nextSibling;
263         if (isSpacer(item)) {
264           // Spacer found on a place it mustn't be.
265           this.removeChild(item);
266           item = next;
267           continue;
268         }
269         var index = item.listIndex;
270         var nextIndex = index + 1;
271
272         // Invisible pinned item could be outside of the
273         // [firstIndex, lastIndex). Ignore it.
274         if (index >= firstIndex && nextIndex < lastIndex &&
275             nextIndex % columns == 0) {
276           if (isSpacer(next)) {
277             // Leave the spacer on its place.
278             item = next.nextSibling;
279           } else {
280             // Insert spacer.
281             var spacer = this.ownerDocument.createElement('div');
282             spacer.className = 'spacer';
283             this.insertBefore(spacer, next);
284             item = next;
285           }
286         } else
287           item = next;
288       }
289
290       function isSpacer(child) {
291         return child.classList.contains('spacer') &&
292                child != afterFiller;  // Must not be removed.
293       }
294     },
295
296     /**
297      * Returns the height of after filler in the list.
298      * @param {number} lastIndex The index of item past the last in viewport.
299      * @return {number} The height of after filler.
300      * @override
301      */
302     getAfterFillerHeight: function(lastIndex) {
303       var columns = this.columns;
304       var itemHeight = this.getDefaultItemHeight_();
305       // We calculate the row of last item, and the row of last shown item.
306       // The difference is the number of rows not shown.
307       var afterRows = Math.floor((this.dataModel.length - 1) / columns) -
308           Math.floor((lastIndex - 1) / columns);
309       return afterRows * itemHeight;
310     },
311
312     /**
313      * Returns true if the child is a list item.
314      * @param {Node} child Child of the list.
315      * @return {boolean} True if a list item.
316      */
317     isItem: function(child) {
318       // Non-items are before-, afterFiller and spacers added in mergeItems.
319       return child.nodeType == Node.ELEMENT_NODE &&
320              !child.classList.contains('spacer');
321     },
322
323     redraw: function() {
324       this.updateMetrics_();
325       var itemCount = this.dataModel ? this.dataModel.length : 0;
326       if (this.lastItemCount_ != itemCount) {
327         this.lastItemCount_ = itemCount;
328         // Force recalculation.
329         this.columns_ = 0;
330       }
331
332       List.prototype.redraw.call(this);
333     }
334   };
335
336   /**
337    * Creates a selection controller that is to be used with grids.
338    * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
339    *     interact with.
340    * @param {cr.ui.Grid} grid The grid to interact with.
341    * @constructor
342    * @extends {!cr.ui.ListSelectionController}
343    */
344   function GridSelectionController(selectionModel, grid) {
345     this.selectionModel_ = selectionModel;
346     this.grid_ = grid;
347   }
348
349   GridSelectionController.prototype = {
350     __proto__: ListSelectionController.prototype,
351
352     /**
353      * Check if accessibility is enabled: if ChromeVox is running
354      * (which provides spoken feedback for accessibility), make up/down
355      * behave the same as left/right. That's because the 2-dimensional
356      * structure of the grid isn't exposed, so it makes more sense to a
357      * user who is relying on spoken feedback to flatten it.
358      * @return {boolean} True if accessibility is enabled.
359      */
360     isAccessibilityEnabled: function() {
361       return window.cvox && window.cvox.Api &&
362              window.cvox.Api.isChromeVoxActive &&
363              window.cvox.Api.isChromeVoxActive();
364     },
365
366     /**
367      * Returns the index below (y axis) the given element.
368      * @param {number} index The index to get the index below.
369      * @return {number} The index below or -1 if not found.
370      * @override
371      */
372     getIndexBelow: function(index) {
373       if (this.isAccessibilityEnabled())
374         return this.getIndexAfter(index);
375       var last = this.getLastIndex();
376       if (index == last)
377         return -1;
378       index += this.grid_.columns;
379       return Math.min(index, last);
380     },
381
382     /**
383      * Returns the index above (y axis) the given element.
384      * @param {number} index The index to get the index above.
385      * @return {number} The index below or -1 if not found.
386      * @override
387      */
388     getIndexAbove: function(index) {
389       if (this.isAccessibilityEnabled())
390         return this.getIndexBefore(index);
391       if (index == 0)
392         return -1;
393       index -= this.grid_.columns;
394       return Math.max(index, 0);
395     },
396
397     /**
398      * Returns the index before (x axis) the given element.
399      * @param {number} index The index to get the index before.
400      * @return {number} The index before or -1 if not found.
401      * @override
402      */
403     getIndexBefore: function(index) {
404       return index - 1;
405     },
406
407     /**
408      * Returns the index after (x axis) the given element.
409      * @param {number} index The index to get the index after.
410      * @return {number} The index after or -1 if not found.
411      * @override
412      */
413     getIndexAfter: function(index) {
414       if (index == this.getLastIndex()) {
415         return -1;
416       }
417       return index + 1;
418     }
419   };
420
421   return {
422     Grid: Grid,
423     GridItem: GridItem,
424     GridSelectionController: GridSelectionController
425   };
426 });