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
15cr.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