Javascript  |  299行  |  8.75 KB

// 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
  }
});