// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // require: list_selection_model.js // require: list_selection_controller.js // require: list.js /** * @fileoverview This implements a grid control. Grid contains a bunch of * similar elements placed in multiple columns. It's pretty similar to the list, * except the multiple columns layout. */ cr.define('cr.ui', function() { const ListSelectionController = cr.ui.ListSelectionController; const List = cr.ui.List; const ListItem = cr.ui.ListItem; /** * Creates a new grid item element. * @param {*} dataItem The data item. * @constructor * @extends {cr.ui.ListItem} */ function GridItem(dataItem) { var el = cr.doc.createElement('span'); el.dataItem = dataItem; el.__proto__ = GridItem.prototype; return el; } GridItem.prototype = { __proto__: ListItem.prototype, /** * Called when an element is decorated as a grid item. */ decorate: function() { ListItem.prototype.decorate.call(this, arguments); this.textContent = this.dataItem; } }; /** * Creates a new grid element. * @param {Object=} opt_propertyBag Optional properties. * @constructor * @extends {cr.ui.List} */ var Grid = cr.ui.define('grid'); Grid.prototype = { __proto__: List.prototype, /** * The number of columns in the grid. Either set by the user, or lazy * calculated as the maximum number of items fitting in the grid width. * @type {number} * @private */ columns_: 0, /** * Function used to create grid items. * @type {function(): !GridItem} * @override */ itemConstructor_: GridItem, /** * In the case of multiple columns lead item must have the same height * as a regular item. * @type {number} * @override */ get leadItemHeight() { return this.getItemHeight_(); }, set leadItemHeight(height) { // Lead item height cannot be set. }, /** * @return {number} The number of columns determined by width of the grid * and width of the items. * @private */ getColumnCount_: function() { var width = this.getItemWidth_(); return width ? Math.floor(this.clientWidth / width) : 0; }, /** * The number of columns in the grid. If not set, determined automatically * as the maximum number of items fitting in the grid width. * @type {number} */ get columns() { if (!this.columns_) { this.columns_ = this.getColumnCount_(); } return this.columns_ || 1; }, set columns(value) { if (value >= 0 && value != this.columns_) { this.columns_ = value; this.redraw(); } }, /** * @param {number} index The index of the item. * @return {number} The top position of the item inside the list, not taking * into account lead item. May vary in the case of multiple columns. * @override */ getItemTop: function(index) { return Math.floor(index / this.columns) * this.getItemHeight_(); }, /** * @param {number} index The index of the item. * @return {number} The row of the item. May vary in the case * of multiple columns. * @override */ getItemRow: function(index) { return Math.floor(index / this.columns); }, /** * @param {number} row The row. * @return {number} The index of the first item in the row. * @override */ getFirstItemInRow: function(row) { return row * this.columns; }, /** * Creates the selection controller to use internally. * @param {cr.ui.ListSelectionModel} sm The underlying selection model. * @return {!cr.ui.ListSelectionController} The newly created selection * controller. * @override */ createSelectionController: function(sm) { return new GridSelectionController(sm, this); }, /** * Calculates the number of items fitting in viewport given the index of * first item and heights. * @param {number} itemHeight The height of the item. * @param {number} firstIndex Index of the first item in viewport. * @param {number} scrollTop The scroll top position. * @return {number} The number of items in view port. * @override */ getItemsInViewPort: function(itemHeight, firstIndex, scrollTop) { var columns = this.columns; var clientHeight = this.clientHeight; var count = this.autoExpands_ ? this.dataModel.length : Math.max( columns * (Math.ceil(clientHeight / itemHeight) + 1), this.countItemsInRange_(firstIndex, scrollTop + clientHeight)); count = columns * Math.ceil(count / columns); count = Math.min(count, this.dataModel.length - firstIndex); return count; }, /** * Adds items to the list and {@code newCachedItems}. * @param {number} firstIndex The index of first item, inclusively. * @param {number} lastIndex The index of last item, exclusively. * @param {Object.<string, ListItem>} cachedItems Old items cache. * @param {Object.<string, ListItem>} newCachedItems New items cache. * @override */ addItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) { var listItem; var dataModel = this.dataModel; var spacers = this.spacers_ || {}; var spacerIndex = 0; var columns = this.columns; for (var y = firstIndex; y < lastIndex; y++) { if (y % columns == 0 && y > 0) { var spacer = spacers[spacerIndex]; if (!spacer) { spacer = this.ownerDocument.createElement('div'); spacer.className = 'spacer'; spacers[spacerIndex] = spacer; } this.appendChild(spacer); spacerIndex++; } var dataItem = dataModel.item(y); listItem = cachedItems[y] || this.createItem(dataItem); listItem.listIndex = y; this.appendChild(listItem); newCachedItems[y] = listItem; } this.spacers_ = spacers; }, /** * Returns the height of after filler in the list. * @param {number} lastIndex The index of item past the last in viewport. * @param {number} itemHeight The height of the item. * @return {number} The height of after filler. * @override */ getAfterFillerHeight: function(lastIndex, itemHeight) { var columns = this.columns; // We calculate the row of last item, and the row of last shown item. // The difference is the number of rows not shown. var afterRows = Math.floor((this.dataModel.length - 1) / columns) - Math.floor((lastIndex - 1) / columns); return afterRows * itemHeight; } }; /** * Creates a selection controller that is to be used with grids. * @param {cr.ui.ListSelectionModel} selectionModel The selection model to * interact with. * @param {cr.ui.Grid} grid The grid to interact with. * @constructor * @extends {!cr.ui.ListSelectionController} */ function GridSelectionController(selectionModel, grid) { this.selectionModel_ = selectionModel; this.grid_ = grid; } GridSelectionController.prototype = { __proto__: ListSelectionController.prototype, /** * Returns the index below (y axis) the given element. * @param {number} index The index to get the index below. * @return {number} The index below or -1 if not found. * @override */ getIndexBelow: function(index) { var last = this.getLastIndex(); if (index == last) { return -1; } index += this.grid_.columns; return Math.min(index, last); }, /** * Returns the index above (y axis) the given element. * @param {number} index The index to get the index above. * @return {number} The index below or -1 if not found. * @override */ getIndexAbove: function(index) { if (index == 0) { return -1; } index -= this.grid_.columns; return Math.max(index, 0); }, /** * Returns the index before (x axis) the given element. * @param {number} index The index to get the index before. * @return {number} The index before or -1 if not found. * @override */ getIndexBefore: function(index) { return index - 1; }, /** * Returns the index after (x axis) the given element. * @param {number} index The index to get the index after. * @return {number} The index after or -1 if not found. * @override */ getIndexAfter: function(index) { if (index == this.getLastIndex()) { return -1; } return index + 1; } }; return { Grid: Grid, GridItem: GridItem } });