// 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. /** * @fileoverview This implements a table control. */ cr.define('cr.ui', function() { const TableSelectionModel = cr.ui.table.TableSelectionModel; const ListSelectionController = cr.ui.ListSelectionController; const ArrayDataModel = cr.ui.ArrayDataModel; const TableColumnModel = cr.ui.table.TableColumnModel; const TableList = cr.ui.table.TableList; const 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.table.TableDataModel} */ get dataModel() { return this.list_.dataModel; }, set dataModel(dataModel) { if (this.list_.dataModel != dataModel) { this.list_.dataModel = dataModel; if (this.list_.dataModel) { this.list_.dataModel.removeEventListener('splice', this.boundRedraw_); this.list_.dataModel.removeEventListener('sorted', this.boundHandleSorted_); } this.list_.dataModel = dataModel; this.list_.dataModel.table = this; if (this.list_.dataModel) { this.list_.dataModel.addEventListener('splice', this.boundRedraw_); this.list_.dataModel.addEventListener('sorted', this.boundHandleSorted_); } this.header_.redraw(); } }, /** * 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('change', this.boundRedraw_); this.columnModel_.removeEventListener('resize', this.boundResize_); } this.columnModel_ = columnModel; if (this.columnModel_) { this.columnModel_.addEventListener('change', this.boundRedraw_); this.columnModel_.addEventListener('resize', this.boundResize_); } this.redraw(); } }, /** * The table selection model. * * @type * {cr.ui.table.TableSelectionModel|cr.ui.table.TableSingleSelectionModel} */ get selectionModel() { return this.list_.selectionModel; }, set selectionModel(selectionModel) { if (this.list_.selectionModel != selectionModel) { if (this.dataModel) selectionModel.adjust(0, 0, this.dataModel.length); this.list_.selectionModel = selectionModel; this.redraw(); } }, /** * 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 TableSelectionModel(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.boundRedraw_ = this.redraw.bind(this); this.boundResize_ = this.resize.bind(this); this.boundHandleSorted_ = this.handleSorted_.bind(this); // Make table focusable if (!this.hasAttribute('tabindex')) this.tabIndex = 0; this.addEventListener('focus', this.handleElementFocus_, true); this.addEventListener('blur', this.handleElementBlur_, true); }, /** * 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); }, /** * Redraws the table. * This forces the list to remove all cached items. */ redraw: function() { this.list_.startBatchUpdates(); if (this.list_.dataModel) { for (var i = 0; i < this.list_.dataModel.length; i++) { this.list_.redrawItem(i); } } this.list_.endBatchUpdates(); this.list_.redraw(); this.header_.redraw(); }, /** * This handles data model 'sorted' event. * After sorting we need to * - adjust selection * - redraw all the items * - scroll the list to show selection. * @param {Event} e The 'sorted' event. */ handleSorted_: function(e) { var sm = this.list_.selectionModel; sm.adjustToReordering(e.sortPermutation); this.redraw(); if (sm.leadIndex != -1) this.list_.scrollIndexIntoView(sm.leadIndex) }, /** * 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 }; });