1 // Copyright (c) 2011 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 ListSelectionController = cr.ui.ListSelectionController; 17 const List = cr.ui.List; 18 const 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('span'); 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 * In the case of multiple columns lead item must have the same height 73 * as a regular item. 74 * @type {number} 75 * @override 76 */ 77 get leadItemHeight() { 78 return this.getItemHeight_(); 79 }, 80 set leadItemHeight(height) { 81 // Lead item height cannot be set. 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 var width = this.getItemWidth_(); 91 return width ? Math.floor(this.clientWidth / width) : 0; 92 }, 93 94 /** 95 * The number of columns in the grid. If not set, determined automatically 96 * as the maximum number of items fitting in the grid width. 97 * @type {number} 98 */ 99 get columns() { 100 if (!this.columns_) { 101 this.columns_ = this.getColumnCount_(); 102 } 103 return this.columns_ || 1; 104 }, 105 set columns(value) { 106 if (value >= 0 && value != this.columns_) { 107 this.columns_ = value; 108 this.redraw(); 109 } 110 }, 111 112 /** 113 * @param {number} index The index of the item. 114 * @return {number} The top position of the item inside the list, not taking 115 * into account lead item. May vary in the case of multiple columns. 116 * @override 117 */ 118 getItemTop: function(index) { 119 return Math.floor(index / this.columns) * this.getItemHeight_(); 120 }, 121 122 /** 123 * @param {number} index The index of the item. 124 * @return {number} The row of the item. May vary in the case 125 * of multiple columns. 126 * @override 127 */ 128 getItemRow: function(index) { 129 return Math.floor(index / this.columns); 130 }, 131 132 /** 133 * @param {number} row The row. 134 * @return {number} The index of the first item in the row. 135 * @override 136 */ 137 getFirstItemInRow: function(row) { 138 return row * this.columns; 139 }, 140 141 /** 142 * Creates the selection controller to use internally. 143 * @param {cr.ui.ListSelectionModel} sm The underlying selection model. 144 * @return {!cr.ui.ListSelectionController} The newly created selection 145 * controller. 146 * @override 147 */ 148 createSelectionController: function(sm) { 149 return new GridSelectionController(sm, this); 150 }, 151 152 /** 153 * Calculates the number of items fitting in viewport given the index of 154 * first item and heights. 155 * @param {number} itemHeight The height of the item. 156 * @param {number} firstIndex Index of the first item in viewport. 157 * @param {number} scrollTop The scroll top position. 158 * @return {number} The number of items in view port. 159 * @override 160 */ 161 getItemsInViewPort: function(itemHeight, firstIndex, scrollTop) { 162 var columns = this.columns; 163 var clientHeight = this.clientHeight; 164 var count = this.autoExpands_ ? this.dataModel.length : Math.max( 165 columns * (Math.ceil(clientHeight / itemHeight) + 1), 166 this.countItemsInRange_(firstIndex, scrollTop + clientHeight)); 167 count = columns * Math.ceil(count / columns); 168 count = Math.min(count, this.dataModel.length - firstIndex); 169 return count; 170 }, 171 172 /** 173 * Adds items to the list and {@code newCachedItems}. 174 * @param {number} firstIndex The index of first item, inclusively. 175 * @param {number} lastIndex The index of last item, exclusively. 176 * @param {Object.<string, ListItem>} cachedItems Old items cache. 177 * @param {Object.<string, ListItem>} newCachedItems New items cache. 178 * @override 179 */ 180 addItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) { 181 var listItem; 182 var dataModel = this.dataModel; 183 var spacers = this.spacers_ || {}; 184 var spacerIndex = 0; 185 var columns = this.columns; 186 187 for (var y = firstIndex; y < lastIndex; y++) { 188 if (y % columns == 0 && y > 0) { 189 var spacer = spacers[spacerIndex]; 190 if (!spacer) { 191 spacer = this.ownerDocument.createElement('div'); 192 spacer.className = 'spacer'; 193 spacers[spacerIndex] = spacer; 194 } 195 this.appendChild(spacer); 196 spacerIndex++; 197 } 198 var dataItem = dataModel.item(y); 199 listItem = cachedItems[y] || this.createItem(dataItem); 200 listItem.listIndex = y; 201 this.appendChild(listItem); 202 newCachedItems[y] = listItem; 203 } 204 205 this.spacers_ = spacers; 206 }, 207 208 /** 209 * Returns the height of after filler in the list. 210 * @param {number} lastIndex The index of item past the last in viewport. 211 * @param {number} itemHeight The height of the item. 212 * @return {number} The height of after filler. 213 * @override 214 */ 215 getAfterFillerHeight: function(lastIndex, itemHeight) { 216 var columns = this.columns; 217 // We calculate the row of last item, and the row of last shown item. 218 // The difference is the number of rows not shown. 219 var afterRows = Math.floor((this.dataModel.length - 1) / columns) - 220 Math.floor((lastIndex - 1) / columns); 221 return afterRows * itemHeight; 222 } 223 }; 224 225 /** 226 * Creates a selection controller that is to be used with grids. 227 * @param {cr.ui.ListSelectionModel} selectionModel The selection model to 228 * interact with. 229 * @param {cr.ui.Grid} grid The grid to interact with. 230 * @constructor 231 * @extends {!cr.ui.ListSelectionController} 232 */ 233 function GridSelectionController(selectionModel, grid) { 234 this.selectionModel_ = selectionModel; 235 this.grid_ = grid; 236 } 237 238 GridSelectionController.prototype = { 239 __proto__: ListSelectionController.prototype, 240 241 /** 242 * Returns the index below (y axis) the given element. 243 * @param {number} index The index to get the index below. 244 * @return {number} The index below or -1 if not found. 245 * @override 246 */ 247 getIndexBelow: function(index) { 248 var last = this.getLastIndex(); 249 if (index == last) { 250 return -1; 251 } 252 index += this.grid_.columns; 253 return Math.min(index, last); 254 }, 255 256 /** 257 * Returns the index above (y axis) the given element. 258 * @param {number} index The index to get the index above. 259 * @return {number} The index below or -1 if not found. 260 * @override 261 */ 262 getIndexAbove: function(index) { 263 if (index == 0) { 264 return -1; 265 } 266 index -= this.grid_.columns; 267 return Math.max(index, 0); 268 }, 269 270 /** 271 * Returns the index before (x axis) the given element. 272 * @param {number} index The index to get the index before. 273 * @return {number} The index before or -1 if not found. 274 * @override 275 */ 276 getIndexBefore: function(index) { 277 return index - 1; 278 }, 279 280 /** 281 * Returns the index after (x axis) the given element. 282 * @param {number} index The index to get the index after. 283 * @return {number} The index after or -1 if not found. 284 * @override 285 */ 286 getIndexAfter: function(index) { 287 if (index == this.getLastIndex()) { 288 return -1; 289 } 290 return index + 1; 291 } 292 }; 293 294 return { 295 Grid: Grid, 296 GridItem: GridItem 297 } 298 }); 299