text: // Copyright (c) 2012 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.
  
  /**
   * @fileoverview This implements a table control.
   */
  
  cr.define('cr.ui', function() {
    /** @const */ var ListSelectionModel = cr.ui.ListSelectionModel;
    /** @const */ var ListSelectionController = cr.ui.ListSelectionController;
    /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel;
    /** @const */ var TableColumnModel = cr.ui.table.TableColumnModel;
    /** @const */ var TableList = cr.ui.table.TableList;
    /** @const */ var TableHeader = cr.ui.table.TableHeader;
  
    /**
     * Creates a new table element.
     * @param {Object=} opt_propertyBag Optional properties.
     * @constructor
     * @extends {HTMLDivElement}
     */
    var Table = cr.ui.define('div');
  
    Table.prototype = {
      __proto__: HTMLDivElement.prototype,
  
      columnModel_: new TableColumnModel([]),
  
      /**
       * The table data model.
       *
       * @type {cr.ui.ArrayDataModel}
       */
      get dataModel() {
        return this.list_.dataModel;
      },
      set dataModel(dataModel) {
        if (this.list_.dataModel != dataModel) {
          if (this.list_.dataModel) {
            this.list_.dataModel.removeEventListener('sorted',
                                                     this.boundHandleSorted_);
            this.list_.dataModel.removeEventListener('change',
                                                     this.boundHandleChangeList_);
            this.list_.dataModel.removeEventListener('splice',
                                                     this.boundHandleChangeList_);
          }
          this.list_.dataModel = dataModel;
          if (this.list_.dataModel) {
            this.list_.dataModel.addEventListener('sorted',
                                                  this.boundHandleSorted_);
            this.list_.dataModel.addEventListener('change',
                                                  this.boundHandleChangeList_);
            this.list_.dataModel.addEventListener('splice',
                                                  this.boundHandleChangeList_);
          }
          this.header_.redraw();
        }
      },
  
      /**
       * The list of table.
       *
       * @type {cr.ui.list}
       */
      get list() {
        return this.list_;
      },
  
      /**
       * The table column model.
       *
       * @type {cr.ui.table.TableColumnModel}
       */
      get columnModel() {
        return this.columnModel_;
      },
      set columnModel(columnModel) {
        if (this.columnModel_ != columnModel) {
          if (this.columnModel_)
            this.columnModel_.removeEventListener('resize', this.boundResize_);
          this.columnModel_ = columnModel;
  
          if (this.columnModel_)
            this.columnModel_.addEventListener('resize', this.boundResize_);
          this.list_.invalidate();
          this.redraw();
        }
      },
  
      /**
       * The table selection model.
       *
       * @type
       * {cr.ui.ListSelectionModel|cr.ui.table.ListSingleSelectionModel}
       */
      get selectionModel() {
        return this.list_.selectionModel;
      },
      set selectionModel(selectionModel) {
        if (this.list_.selectionModel != selectionModel) {
          if (this.dataModel)
            selectionModel.adjustLength(this.dataModel.length);
          this.list_.selectionModel = selectionModel;
        }
      },
  
      /**
       * The accessor to "autoExpands" property of the list.
       *
       * @type {boolean}
       */
      get autoExpands() {
        return this.list_.autoExpands;
      },
      set autoExpands(autoExpands) {
        this.list_.autoExpands = autoExpands;
      },
  
      get fixedHeight() {
        return this.list_.fixedHeight;
      },
      set fixedHeight(fixedHeight) {
        this.list_.fixedHeight = fixedHeight;
      },
  
      /**
       * Returns render function for row.
       * @return {Function(*, cr.ui.Table): HTMLElement} Render function.
       */
      getRenderFunction: function() {
        return this.list_.renderFunction_;
      },
  
      /**
       * Sets render function for row.
       * @param {Function(*, cr.ui.Table): HTMLElement} Render function.
       */
      setRenderFunction: function(renderFunction) {
        if (renderFunction === this.list_.renderFunction_)
          return;
  
        this.list_.renderFunction_ = renderFunction;
        cr.dispatchSimpleEvent(this, 'change');
      },
  
      /**
       * The header of the table.
       *
       * @type {cr.ui.table.TableColumnModel}
       */
      get header() {
        return this.header_;
      },
  
      /**
       * Sets width of the column at the given index.
       *
       * @param {number} index The index of the column.
       * @param {number} Column width.
       */
      setColumnWidth: function(index, width) {
        this.columnWidths_[index] = width;
      },
  
      /**
       * Initializes the element.
       */
      decorate: function() {
        this.list_ = this.ownerDocument.createElement('list');
        TableList.decorate(this.list_);
        this.list_.selectionModel = new ListSelectionModel(this);
        this.list_.table = this;
  
        this.header_ = this.ownerDocument.createElement('div');
        TableHeader.decorate(this.header_);
        this.header_.table = this;
  
        this.classList.add('table');
        this.appendChild(this.header_);
        this.appendChild(this.list_);
        this.ownerDocument.defaultView.addEventListener(
            'resize', this.header_.updateWidth.bind(this.header_));
  
        this.boundResize_ = this.resize.bind(this);
        this.boundHandleSorted_ = this.handleSorted_.bind(this);
        this.boundHandleChangeList_ = this.handleChangeList_.bind(this);
  
        // The contained list should be focusable, not the table itself.
        if (this.hasAttribute('tabindex')) {
          this.list_.setAttribute('tabindex', this.getAttribute('tabindex'));
          this.removeAttribute('tabindex');
        }
  
        this.addEventListener('focus', this.handleElementFocus_, true);
        this.addEventListener('blur', this.handleElementBlur_, true);
      },
  
      /**
       * Redraws the table.
       */
      redraw: function(index) {
        this.list_.redraw();
        this.header_.redraw();
      },
  
      startBatchUpdates: function() {
        this.list_.startBatchUpdates();
        this.header_.startBatchUpdates();
      },
  
      endBatchUpdates: function() {
        this.list_.endBatchUpdates();
        this.header_.endBatchUpdates();
      },
  
      /**
       * Resize the table columns.
       */
      resize: function() {
        // We resize columns only instead of full redraw.
        this.list_.resize();
        this.header_.resize();
      },
  
      /**
       * Ensures that a given index is inside the viewport.
       * @param {number} index The index of the item to scroll into view.
       * @return {boolean} Whether any scrolling was needed.
       */
      scrollIndexIntoView: function(i) {
        this.list_.scrollIndexIntoView(i);
      },
  
      /**
       * Find the list item element at the given index.
       * @param {number} index The index of the list item to get.
       * @return {ListItem} The found list item or null if not found.
       */
      getListItemByIndex: function(index) {
        return this.list_.getListItemByIndex(index);
      },
  
      /**
       * This handles data model 'sorted' event.
       * After sorting we need to redraw header
       * @param {Event} e The 'sorted' event.
       */
      handleSorted_: function(e) {
        this.header_.redraw();
      },
  
      /**
       * This handles data model 'change' and 'splice' events.
       * Since they may change the visibility of scrollbar, table may need to
       * re-calculation the width of column headers.
       * @param {Event} e The 'change' or 'splice' event.
       */
      handleChangeList_: function(e) {
        webkitRequestAnimationFrame(this.header_.updateWidth.bind(this.header_));
      },
  
      /**
       * Sort data by the given column.
       * @param {number} index The index of the column to sort by.
       */
      sort: function(i) {
        var cm = this.columnModel_;
        var sortStatus = this.list_.dataModel.sortStatus;
        if (sortStatus.field == cm.getId(i)) {
          var sortDirection = sortStatus.direction == 'desc' ? 'asc' : 'desc';
          this.list_.dataModel.sort(sortStatus.field, sortDirection);
        } else {
          this.list_.dataModel.sort(cm.getId(i), 'asc');
        }
      },
  
      /**
       * Called when an element in the table is focused. Marks the table as having
       * a focused element, and dispatches an event if it didn't have focus.
       * @param {Event} e The focus event.
       * @private
       */
      handleElementFocus_: function(e) {
        if (!this.hasElementFocus) {
          this.hasElementFocus = true;
          // Force styles based on hasElementFocus to take effect.
          this.list_.redraw();
        }
      },
  
      /**
       * Called when an element in the table is blurred. If focus moves outside
       * the table, marks the table as no longer having focus and dispatches an
       * event.
       * @param {Event} e The blur event.
       * @private
       */
      handleElementBlur_: function(e) {
        // When the blur event happens we do not know who is getting focus so we
        // delay this a bit until we know if the new focus node is outside the
        // table.
        var table = this;
        var list = this.list_;
        var doc = e.target.ownerDocument;
        window.setTimeout(function() {
          var activeElement = doc.activeElement;
          if (!table.contains(activeElement)) {
            table.hasElementFocus = false;
            // Force styles based on hasElementFocus to take effect.
            list.redraw();
          }
        });
      },
    };
  
    /**
     * Whether the table or one of its descendents has focus. This is necessary
     * because table contents can contain controls that can be focused, and for
     * some purposes (e.g., styling), the table can still be conceptually focused
     * at that point even though it doesn't actually have the page focus.
     */
    cr.defineProperty(Table, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR);
  
    return {
      Table: Table
    };
  });
  