Home | History | Annotate | Download | only in ui
      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