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.
5 // require: list_selection_model.js
6 // require: list_selection_controller.js
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.
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;
21 * Creates a new grid item element.
22 * @param {*} dataItem The data item.
24 * @extends {cr.ui.ListItem}
26 function GridItem(dataItem) {
27 var el = cr.doc.createElement('li');
28 el.dataItem = dataItem;
29 el.__proto__ = GridItem.prototype;
33 GridItem.prototype = {
34 __proto__: ListItem.prototype,
37 * Called when an element is decorated as a grid item.
39 decorate: function() {
40 ListItem.prototype.decorate.call(this, arguments);
41 this.textContent = this.dataItem;
46 * Creates a new grid element.
47 * @param {Object=} opt_propertyBag Optional properties.
49 * @extends {cr.ui.List}
51 var Grid = cr.ui.define('grid');
54 __proto__: List.prototype,
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.
65 * Function used to create grid items.
66 * @type {function(): !GridItem}
69 itemConstructor_: GridItem,
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.
79 set fixedHeight(fixedHeight) {
81 console.warn('cr.ui.Grid does not support fixedHeight = false');
85 * @return {number} The number of columns determined by width of the grid
86 * and width of the items.
89 getColumnCount_: function() {
90 // Size comes here with margin already collapsed.
91 var size = this.getDefaultItemSize_();
93 // We should uncollapse margin, since margin isn't collapsed for
94 // inline-block elements according to css spec which are thumbnail items.
96 var width = size.width + Math.min(size.marginLeft, size.marginRight);
97 var height = size.height + Math.min(size.marginTop, size.marginBottom);
99 if (!width || !height)
102 var itemCount = this.dataModel ? this.dataModel.length : 0;
106 var columns = Math.floor(this.clientWidthWithoutScrollbar_ / width);
110 var rows = Math.ceil(itemCount / columns);
111 if (rows * height <= this.clientHeight_)
114 return Math.floor(this.clientWidthWithScrollbar_ / width);
118 * Measure and cache client width and height with and without scrollbar.
119 * Must be updated when offsetWidth and/or offsetHeight changed.
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;
127 if (this.lastOffsetWidth_ == offsetWidth &&
128 this.lastOverflowY == overflowY) {
129 this.lastOffsetHeight_ = offsetHeight;
133 this.lastOffsetWidth_ = offsetWidth;
134 this.lastOffsetHeight_ = offsetHeight;
135 this.lastOverflowY = overflowY;
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;
150 // Show scrollbar and recalculate clientWidth (triggers reflow).
151 this.style.overflowY = 'scroll';
152 this.clientWidthWithScrollbar_ = this.clientWidth;
154 this.style.overflowY = '';
156 this.clientWidthWithoutScrollbar_ = this.clientWidthWithScrollbar_ =
158 this.clientHeight_ = this.clientHeight;
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.
168 if (!this.columns_) {
169 this.columns_ = this.getColumnCount_();
171 return this.columns_ || 1;
174 if (value >= 0 && value != this.columns_) {
175 this.columns_ = value;
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.
186 getItemTop: function(index) {
187 return Math.floor(index / this.columns) * this.getDefaultItemHeight_();
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.
196 getItemRow: function(index) {
197 return Math.floor(index / this.columns);
201 * @param {number} row The row.
202 * @return {number} The index of the first item in the row.
205 getFirstItemInRow: function(row) {
206 return row * this.columns;
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
216 createSelectionController: function(sm) {
217 return new GridSelectionController(sm, this);
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.
228 getItemsInViewPort: function(scrollTop, clientHeight) {
229 var itemHeight = this.getDefaultItemHeight_();
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);
241 last: firstIndex + count - 1
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.
254 mergeItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) {
255 List.prototype.mergeItems.call(this,
256 firstIndex, lastIndex, cachedItems, newCachedItems);
258 var afterFiller = this.afterFiller_;
259 var columns = this.columns;
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);
269 var index = item.listIndex;
270 var nextIndex = index + 1;
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;
281 var spacer = this.ownerDocument.createElement('div');
282 spacer.className = 'spacer';
283 this.insertBefore(spacer, next);
290 function isSpacer(child) {
291 return child.classList.contains('spacer') &&
292 child != afterFiller; // Must not be removed.
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.
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;
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.
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');
324 this.updateMetrics_();
325 var itemCount = this.dataModel ? this.dataModel.length : 0;
326 if (this.lastItemCount_ != itemCount) {
327 this.lastItemCount_ = itemCount;
328 // Force recalculation.
332 List.prototype.redraw.call(this);
337 * Creates a selection controller that is to be used with grids.
338 * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
340 * @param {cr.ui.Grid} grid The grid to interact with.
342 * @extends {!cr.ui.ListSelectionController}
344 function GridSelectionController(selectionModel, grid) {
345 this.selectionModel_ = selectionModel;
349 GridSelectionController.prototype = {
350 __proto__: ListSelectionController.prototype,
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.
360 isAccessibilityEnabled: function() {
361 return window.cvox && window.cvox.Api &&
362 window.cvox.Api.isChromeVoxActive &&
363 window.cvox.Api.isChromeVoxActive();
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.
372 getIndexBelow: function(index) {
373 if (this.isAccessibilityEnabled())
374 return this.getIndexAfter(index);
375 var last = this.getLastIndex();
378 index += this.grid_.columns;
379 return Math.min(index, last);
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.
388 getIndexAbove: function(index) {
389 if (this.isAccessibilityEnabled())
390 return this.getIndexBefore(index);
393 index -= this.grid_.columns;
394 return Math.max(index, 0);
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.
403 getIndexBefore: function(index) {
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.
413 getIndexAfter: function(index) {
414 if (index == this.getLastIndex()) {
424 GridSelectionController: GridSelectionController