UNPKG

chrome-devtools-frontend

Version:
1,530 lines (1,382 loc) • 78.5 kB
/* * Copyright (C) 2008 Apple Inc. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as Common from '../common/common.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import * as UI from '../ui/ui.js'; export const UIStrings = { /** *@description Accessible text label for expandible nodes in datagrids */ expanded: 'expanded', /** *@description accessible name for expandible nodes in datagrids */ collapsed: 'collapsed', /** *@description Accessible text for datagrid *@example {Coverage grid} PH1 *@example {expanded} PH2 */ sRowS: '{PH1} Row {PH2}', /** *@description Number of rows in a grid *@example {1} PH1 */ rowsS: 'Rows: {PH1}', /** * @description Default Accessible Text for a Datagrid. This text is read to the user by a * screenreader when they navigate to a table structure. The placeholders tell the user something * brief about the table contents i.e. the topic and how much data is in it. * @example {Network} PH1 * @example {Rows: 27} PH2 */ sSUseTheUpAndDownArrowKeysTo: '{PH1} {PH2}, use the up and down arrow keys to navigate and interact with the rows of the table; Use browse mode to read cell by cell.', /** *@description A context menu item in the Data Grid of a data grid */ sortByString: 'Sort By', /** *@description A context menu item in data grids to reset the columns to their default weight */ resetColumns: 'Reset Columns', /** *@description A context menu item in data grids to list header options. */ headerOptions: 'Header Options', /** *@description Text to refresh the page */ refresh: 'Refresh', /** *@description A context menu item in the Data Grid of a data grid */ addNew: 'Add new', /** *@description A context menu item in the Data Grid of a data grid *@example {pattern} PH1 */ editS: 'Edit "{PH1}"', /** *@description Text to delete something */ delete: 'Delete', /** *@description Depth of a node in the datagrid *@example {1} PH1 */ levelS: 'level {PH1}', /** *@description Text exposed to screen readers on checked items. */ checked: 'checked', }; const str_ = i18n.i18n.registerUIStrings('data_grid/DataGrid.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); /** @type {!WeakMap<!Element, string>} */ const elementToLongTextMap = new WeakMap(); /** @type {!WeakMap<!Node, string>} */ const nodeToColumnIdMap = new WeakMap(); /** @type {!WeakMap<!Element, !UI.Icon.Icon>} */ const elementToSortIconMap = new WeakMap(); /** @type {!WeakMap<!Element, number>} */ const elementToPreferedWidthMap = new WeakMap(); /** @type {!WeakMap<!Element, number>} */ const elementToPositionMap = new WeakMap(); /** @type {!WeakMap<!Element, number>} */ const elementToIndexMap = new WeakMap(); /** * @template NODE_TYPE */ export class DataGridImpl extends Common.ObjectWrapper.ObjectWrapper { /** * @param {!Parameters} dataGridParameters */ constructor(dataGridParameters) { super(); const {displayName, columns: columnsArray, editCallback, deleteCallback, refreshCallback} = dataGridParameters; this.element = document.createElement('div'); this.element.classList.add('data-grid'); UI.Utils.appendStyle(this.element, 'data_grid/dataGrid.css', {enableLegacyPatching: true}); this.element.tabIndex = 0; this.element.addEventListener('keydown', this._keyDown.bind(this), false); this.element.addEventListener('contextmenu', this._contextMenu.bind(this), true); this.element.addEventListener('focusin', event => { this.updateGridAccessibleNameOnFocus(); event.consume(true); }); this.element.addEventListener('focusout', event => { this.updateGridAccessibleName(/* text */ ''); event.consume(true); }); UI.ARIAUtils.markAsApplication(this.element); this._displayName = displayName; this._editCallback = editCallback; this._deleteCallback = deleteCallback; this._refreshCallback = refreshCallback; const headerContainer = this.element.createChild('div', 'header-container'); /** @type {!Element} */ this._headerTable = headerContainer.createChild('table', 'header'); // Hide the header table from screen readers since titles are also added to data table. UI.ARIAUtils.markAsHidden(this._headerTable); /** @type {!Object.<string, !Element>} */ this._headerTableHeaders = {}; /** @type {!Element} */ this._scrollContainer = this.element.createChild('div', 'data-container'); /** @type {!Element} */ this._dataTable = this._scrollContainer.createChild('table', 'data'); // FIXME: Add a createCallback which is different from editCallback and has different // behavior when creating a new node. if (editCallback) { this._dataTable.addEventListener('dblclick', this._ondblclick.bind(this), false); } this._dataTable.addEventListener('mousedown', this._mouseDownInDataTable.bind(this)); this._dataTable.addEventListener('click', this._clickInDataTable.bind(this), true); /** @type {boolean} */ this._inline = false; /** @type {!Array.<!ColumnDescriptor>} */ this._columnsArray = []; /** @type {!Object.<string, !ColumnDescriptor>} */ this._columns = {}; /** @type {!Array.<!ColumnDescriptor>} */ this.visibleColumnsArray = columnsArray; columnsArray.forEach(column => this._innerAddColumn(column)); /** @type {?string} */ this._cellClass = null; /** @type {!Element} */ this._headerTableColumnGroup = this._headerTable.createChild('colgroup'); /** @type {!HTMLTableSectionElement} */ this._headerTableBody = /** @type {!HTMLTableSectionElement} */ (this._headerTable.createChild('tbody')); /** @type {!Element} */ this._headerRow = this._headerTableBody.createChild('tr'); /** @type {!Element} */ this._dataTableColumnGroup = this._dataTable.createChild('colgroup'); /** * @type {!Element} */ this.dataTableBody = this._dataTable.createChild('tbody'); /** @type {!HTMLElement} */ this._topFillerRow = /** @type {!HTMLElement} */ (this.dataTableBody.createChild('tr', 'data-grid-filler-row revealed')); /** @type {!HTMLElement} */ this._bottomFillerRow = /** @type {!HTMLElement} */ (this.dataTableBody.createChild('tr', 'data-grid-filler-row revealed')); this.setVerticalPadding(0, 0); this._refreshHeader(); /** @type {boolean} */ this._editing = false; /** @type {?DataGridNode<!NODE_TYPE>} */ this.selectedNode = null; /** @type {boolean} */ this.expandNodesWhenArrowing = false; this.setRootNode(/** @type {!DataGridNode<!NODE_TYPE>} */ (new DataGridNode())); this.setHasSelection(false); /** @type {number} */ this.indentWidth = 15; /** @type {!Array.<!HTMLElement>} */ this._resizers = []; /** @type {boolean} */ this._columnWidthsInitialized = false; /** @type {number} */ this._cornerWidth = CornerWidth; /** @type {!ResizeMethod} */ this._resizeMethod = ResizeMethod.Nearest; /** @type {?function(!UI.ContextMenu.SubMenu):void} */ this._headerContextMenuCallback = null; /** @type {?function(!UI.ContextMenu.ContextMenu, !DataGridNode<!NODE_TYPE>):void} */ this._rowContextMenuCallback = null; /** * @type {!WeakMap<!Node, !DataGridNode<!NODE_TYPE>>} */ this.elementToDataGridNode = new WeakMap(); } /** * @return {?DataGridNode<!NODE_TYPE>|undefined} */ _firstSelectableNode() { let firstSelectableNode = this._rootNode; while (firstSelectableNode && !firstSelectableNode.selectable) { firstSelectableNode = firstSelectableNode.traverseNextNode(true) || undefined; } return firstSelectableNode; } /** * @return {!DataGridNode<!NODE_TYPE>|undefined} */ _lastSelectableNode() { let lastSelectableNode = this._rootNode; let iterator = this._rootNode; while (iterator) { if (iterator.selectable) { lastSelectableNode = iterator; } iterator = iterator.traverseNextNode(true) || undefined; } return lastSelectableNode; } /** * @param {!Element} element * @param {*} value */ setElementContent(element, value) { const columnId = this.columnIdFromNode(element); if (!columnId) { return; } const column = this._columns[columnId]; if (column.dataType === DataType.Boolean) { DataGridImpl.setElementBoolean(element, /** @type {boolean} */ (Boolean(value))); } else if (value !== null) { DataGridImpl.setElementText(element, /** @type {string} */ (value), Boolean(column.longText)); } } /** * @param {!Element} element * @param {string} newText * @param {boolean} longText */ static setElementText(element, newText, longText) { if (longText && newText.length > 1000) { element.textContent = Platform.StringUtilities.trimEndWithMaxLength(newText, 1000); UI.Tooltip.Tooltip.install(element, newText); elementToLongTextMap.set(element, newText); } else { element.textContent = newText; UI.Tooltip.Tooltip.install(element, ''); elementToLongTextMap.delete(element); } } /** * @param {!Element} element * @param {boolean} value */ static setElementBoolean(element, value) { element.textContent = value ? '\u2713' : ''; UI.Tooltip.Tooltip.install(element, ''); } /** * @param {boolean} isStriped */ setStriped(isStriped) { this.element.classList.toggle('striped-data-grid', isStriped); } /** * @param {boolean} focusable */ setFocusable(focusable) { this.element.tabIndex = focusable ? 0 : -1; if (focusable === false) { UI.ARIAUtils.removeRole(this.element); } } /** * @param {boolean} hasSelected */ setHasSelection(hasSelected) { // 'no-selection' class causes datagrid to have a focus-indicator border this.element.classList.toggle('no-selection', !hasSelected); } /** * @param {string=} text */ updateGridAccessibleName(text) { // Update the label with the provided text or the current selected node const accessibleText = (this.selectedNode && this.selectedNode.existingElement()) ? this.selectedNode.nodeAccessibleText : ''; if (this.element === this.element.ownerDocument.deepActiveElement()) { // Only alert if the datagrid has focus UI.ARIAUtils.alert(text ? text : accessibleText, this.element); } } updateGridAccessibleNameOnFocus() { // When a grid gets focus // 1) If an item is selected - Read the content of the row let accessibleText; if (this.selectedNode && this.selectedNode.existingElement()) { let expandText = ''; if (this.selectedNode.hasChildren()) { expandText = this.selectedNode.expanded ? i18nString(UIStrings.expanded) : i18nString(UIStrings.collapsed); } const rowHeader = i18nString(UIStrings.sRowS, {PH1: this._displayName, PH2: expandText}); accessibleText = `${rowHeader} ${this.selectedNode.nodeAccessibleText}`; } else { // 2) If there is no selected item - Read the name of the grid and give instructions if (!this._rootNode) { return; } const children = this._enumerateChildren(this._rootNode, [], 1); const items = i18nString(UIStrings.rowsS, {PH1: children.length}); accessibleText = i18nString(UIStrings.sSUseTheUpAndDownArrowKeysTo, {PH1: this._displayName, PH2: items}); } UI.ARIAUtils.alert(accessibleText, this.element); } /** * @return {!Element} */ headerTableBody() { return this._headerTableBody; } /** * @param {!ColumnDescriptor} column * @param {number=} position */ _innerAddColumn(column, position) { column.defaultWeight = column.weight; const columnId = column.id; if (columnId in this._columns) { this._innerRemoveColumn(columnId); } if (position === undefined) { position = this._columnsArray.length; } this._columnsArray.splice(position, 0, column); this._columns[columnId] = column; if (column.disclosure) { this.disclosureColumnId = columnId; } const cell = document.createElement('th'); cell.className = columnId + '-column'; nodeToColumnIdMap.set(cell, columnId); this._headerTableHeaders[columnId] = cell; const div = document.createElement('div'); if (column.titleDOMFragment) { div.appendChild(column.titleDOMFragment); } else { div.textContent = column.title || null; } cell.appendChild(div); if (column.sort) { cell.classList.add(column.sort); /** @type {!Element} */ this._sortColumnCell = cell; } if (column.sortable) { cell.addEventListener('click', this._clickInHeaderCell.bind(this), false); cell.classList.add('sortable'); const icon = UI.Icon.Icon.create('', 'sort-order-icon'); cell.createChild('div', 'sort-order-icon-container').appendChild(icon); elementToSortIconMap.set(cell, icon); } } /** * @param {!ColumnDescriptor} column * @param {number=} position */ addColumn(column, position) { this._innerAddColumn(column, position); } /** * @param {string} columnId */ _innerRemoveColumn(columnId) { const column = this._columns[columnId]; if (!column) { return; } delete this._columns[columnId]; const index = this._columnsArray.findIndex(columnConfig => columnConfig.id === columnId); this._columnsArray.splice(index, 1); const cell = this._headerTableHeaders[columnId]; if (cell.parentElement) { cell.parentElement.removeChild(cell); } delete this._headerTableHeaders[columnId]; } /** * @param {string} columnId */ removeColumn(columnId) { this._innerRemoveColumn(columnId); } /** * @param {string} cellClass */ setCellClass(cellClass) { this._cellClass = cellClass; } _refreshHeader() { this._headerTableColumnGroup.removeChildren(); this._dataTableColumnGroup.removeChildren(); this._headerRow.removeChildren(); this._topFillerRow.removeChildren(); this._bottomFillerRow.removeChildren(); for (let i = 0; i < this.visibleColumnsArray.length; ++i) { const column = this.visibleColumnsArray[i]; const columnId = column.id; const headerColumn = /** @type {!HTMLElement} */ (this._headerTableColumnGroup.createChild('col')); const dataColumn = /** @type {!HTMLElement} */ (this._dataTableColumnGroup.createChild('col')); if (column.width) { headerColumn.style.width = column.width; dataColumn.style.width = column.width; } this._headerRow.appendChild(this._headerTableHeaders[columnId]); const topFillerRowCell = /** @type {!HTMLTableCellElement} */ (this._topFillerRow.createChild('th', 'top-filler-td')); topFillerRowCell.textContent = column.title || null; topFillerRowCell.scope = 'col'; const bottomFillerRowChild = this._bottomFillerRow.createChild('td', 'bottom-filler-td'); nodeToColumnIdMap.set(bottomFillerRowChild, columnId); } this._headerRow.createChild('th', 'corner'); const topFillerRowCornerCell = /** @type {!HTMLTableCellElement} */ (this._topFillerRow.createChild('th', 'corner')); topFillerRowCornerCell.classList.add('top-filler-td'); topFillerRowCornerCell.scope = 'col'; this._bottomFillerRow.createChild('td', 'corner').classList.add('bottom-filler-td'); this._headerTableColumnGroup.createChild('col', 'corner'); this._dataTableColumnGroup.createChild('col', 'corner'); } /** * @param {number} top * @param {number} bottom * @protected */ setVerticalPadding(top, bottom) { const topPx = top + 'px'; const bottomPx = (top || bottom) ? bottom + 'px' : 'auto'; if (this._topFillerRow.style.height === topPx && this._bottomFillerRow.style.height === bottomPx) { return; } this._topFillerRow.style.height = topPx; this._bottomFillerRow.style.height = bottomPx; this.dispatchEventToListeners(Events.PaddingChanged); } /** * @param {!DataGridNode<!NODE_TYPE>} rootNode * @protected */ setRootNode(rootNode) { if (this._rootNode) { this._rootNode.removeChildren(); this._rootNode.dataGrid = null; this._rootNode._isRoot = false; } /** @type {!DataGridNode<!NODE_TYPE>} */ this._rootNode = rootNode; rootNode._isRoot = true; rootNode.setHasChildren(false); rootNode._expanded = true; rootNode._revealed = true; rootNode.selectable = false; rootNode.dataGrid = this; } /** * @return {!DataGridNode<!NODE_TYPE>} */ rootNode() { let rootNode = this._rootNode; if (!rootNode) { rootNode = new DataGridNode(); this.setRootNode(rootNode); } return rootNode; } /** * @param {!Event} event */ _ondblclick(event) { if (this._editing || this._editingNode) { return; } const columnId = this.columnIdFromNode(/** @type {!Node} */ (event.target)); if (!columnId || !this._columns[columnId].editable) { return; } this._startEditing(/** @type {!Node} */ (event.target)); } /** * @param {!DataGridNode<!NODE_TYPE>} node * @param {number} cellIndex */ _startEditingColumnOfDataGridNode(node, cellIndex) { this._editing = true; /** @type {?DataGridNode<!NODE_TYPE>} */ this._editingNode = node; this._editingNode.select(); const editingNodeElement = this._editingNode._element; if (!editingNodeElement) { return; } const element = editingNodeElement.children[cellIndex]; const elementLongText = elementToLongTextMap.get(element); if (elementLongText) { element.textContent = elementLongText; } const column = this.visibleColumnsArray[cellIndex]; if (column.dataType === DataType.Boolean) { const checkboxLabel = UI.UIUtils.CheckboxLabel.create(undefined, /** @type {boolean} */ (node.data[column.id])); UI.ARIAUtils.setAccessibleName(checkboxLabel, column.title || ''); let hasChanged = false; checkboxLabel.style.height = '100%'; const checkboxElement = checkboxLabel.checkboxElement; checkboxElement.classList.add('inside-datagrid'); const initialValue = checkboxElement.checked; checkboxElement.addEventListener('change', () => { hasChanged = true; this._editingCommitted(element, checkboxElement.checked, initialValue, undefined, 'forward'); }, false); checkboxElement.addEventListener('keydown', event => { if (event.key === 'Tab') { event.consume(true); hasChanged = true; return this._editingCommitted( element, checkboxElement.checked, initialValue, undefined, event.shiftKey ? 'backward' : 'forward'); } if (event.key === ' ') { event.consume(true); checkboxElement.checked = !checkboxElement.checked; } else if (event.key === 'Enter') { event.consume(true); hasChanged = true; this._editingCommitted(element, checkboxElement.checked, initialValue, undefined, 'forward'); } }, false); checkboxElement.addEventListener('blur', () => { if (hasChanged) { return; } this._editingCommitted(element, checkboxElement.checked, checkboxElement.checked, undefined, 'next'); }, false); element.innerHTML = ''; element.appendChild(checkboxLabel); checkboxElement.focus(); } else { UI.InplaceEditor.InplaceEditor.startEditing(element, this._startEditingConfig(element)); const componentSelection = element.getComponentSelection(); if (componentSelection) { componentSelection.selectAllChildren(element); } } } /** * @param {!DataGridNode<!NODE_TYPE>} node * @param {string} columnIdentifier */ startEditingNextEditableColumnOfDataGridNode(node, columnIdentifier) { const column = this._columns[columnIdentifier]; const cellIndex = this.visibleColumnsArray.indexOf(column); const nextEditableColumn = this._nextEditableColumn(cellIndex); if (nextEditableColumn !== -1) { this._startEditingColumnOfDataGridNode(node, nextEditableColumn); } } /** * @param {!Node} target */ _startEditing(target) { const element = /** @type {?Element} */ (UI.UIUtils.enclosingNodeOrSelfWithNodeName(target, 'td')); if (!element) { return; } this._editingNode = this.dataGridNodeFromNode(target); if (!this._editingNode) { if (!this.creationNode) { return; } this._editingNode = this.creationNode; } // Force editing the 1st column when editing the creation node if (this._editingNode instanceof CreationDataGridNode && this._editingNode.isCreationNode) { this._startEditingColumnOfDataGridNode(this._editingNode, this._nextEditableColumn(-1)); return; } const columnId = this.columnIdFromNode(target); if (!columnId) { return; } const column = this._columns[columnId]; const cellIndex = this.visibleColumnsArray.indexOf(column); if (this._editingNode) { this._startEditingColumnOfDataGridNode(this._editingNode, cellIndex); } } renderInline() { this.element.classList.add('inline'); this._cornerWidth = 0; this._inline = true; this.updateWidths(); } /** * @param {!Element} element * @return {!UI.InplaceEditor.Config<?>} */ _startEditingConfig(element) { return new UI.InplaceEditor.Config(this._editingCommitted.bind(this), this._editingCancelled.bind(this)); } /** * @param {!Element} element * @param {*} newText * @param {*} oldText * @param {string|undefined} context * @param {string} moveDirection */ _editingCommitted(element, newText, oldText, context, moveDirection) { const columnId = this.columnIdFromNode(element); if (!columnId) { this._editingCancelled(element); return; } const column = this._columns[columnId]; const cellIndex = this.visibleColumnsArray.indexOf(column); if (!this._editingNode) { return; } const valueBeforeEditing = /** @type {string|boolean} */ ( this._editingNode.data[columnId] === null ? '' : this._editingNode.data[columnId]); const currentEditingNode = this._editingNode; /** * @param {boolean} wasChange * @this {DataGridImpl<!NODE_TYPE>} */ function moveToNextIfNeeded(wasChange) { if (!moveDirection) { return; } if (moveDirection === 'forward') { const firstEditableColumn = this._nextEditableColumn(-1); const isCreationNode = currentEditingNode instanceof CreationDataGridNode && currentEditingNode.isCreationNode; if (isCreationNode && cellIndex === firstEditableColumn && !wasChange) { return; } const nextEditableColumn = this._nextEditableColumn(cellIndex); if (nextEditableColumn !== -1) { this._startEditingColumnOfDataGridNode(currentEditingNode, nextEditableColumn); return; } const nextDataGridNode = currentEditingNode.traverseNextNode(true, null, true); if (nextDataGridNode) { this._startEditingColumnOfDataGridNode(nextDataGridNode, firstEditableColumn); return; } if (isCreationNode && wasChange && this.creationNode) { this.addCreationNode(false); this._startEditingColumnOfDataGridNode(this.creationNode, firstEditableColumn); return; } return; } if (moveDirection === 'backward') { const prevEditableColumn = this._nextEditableColumn(cellIndex, true); if (prevEditableColumn !== -1) { this._startEditingColumnOfDataGridNode(currentEditingNode, prevEditableColumn); return; } const lastEditableColumn = this._nextEditableColumn(this.visibleColumnsArray.length, true); const nextDataGridNode = currentEditingNode.traversePreviousNode(true, true); if (nextDataGridNode) { this._startEditingColumnOfDataGridNode(nextDataGridNode, lastEditableColumn); } return; } } // Show trimmed text after editing. this.setElementContent(element, newText); if (valueBeforeEditing === newText) { this._editingCancelled(element); moveToNextIfNeeded.call(this, false); return; } // Update the text in the datagrid that we typed this._editingNode.data[columnId] = newText; if (!this._editCallback) { return; } // Make the callback - expects an editing node (table row), the column number that is being edited, // the text that used to be there, and the new text. this._editCallback(this._editingNode, columnId, valueBeforeEditing, newText); if (this._editingNode instanceof CreationDataGridNode && this._editingNode.isCreationNode) { this.addCreationNode(false); } this._editingCancelled(element); moveToNextIfNeeded.call(this, true); } /** * @param {!Element} element */ _editingCancelled(element) { this._editing = false; this._editingNode = null; } /** * @param {number} cellIndex * @param {boolean=} moveBackward * @return {number} */ _nextEditableColumn(cellIndex, moveBackward) { const increment = moveBackward ? -1 : 1; const columns = this.visibleColumnsArray; for (let i = cellIndex + increment; (i >= 0) && (i < columns.length); i += increment) { if (columns[i].editable) { return i; } } return -1; } /** * @return {?string} */ sortColumnId() { if (!this._sortColumnCell) { return null; } return nodeToColumnIdMap.get(this._sortColumnCell) || null; } /** * @return {?string} */ sortOrder() { if (!this._sortColumnCell || this._sortColumnCell.classList.contains(Order.Ascending)) { return Order.Ascending; } if (this._sortColumnCell.classList.contains(Order.Descending)) { return Order.Descending; } return null; } /** * @return {boolean} */ isSortOrderAscending() { return !this._sortColumnCell || this._sortColumnCell.classList.contains(Order.Ascending); } /** * @param {!Array.<number>} widths * @param {number} minPercent * @param {number=} maxPercent * @return {!Array.<number>} */ _autoSizeWidths(widths, minPercent, maxPercent) { if (minPercent) { minPercent = Math.min(minPercent, Math.floor(100 / widths.length)); } let totalWidth = 0; for (let i = 0; i < widths.length; ++i) { totalWidth += widths[i]; } let totalPercentWidth = 0; for (let i = 0; i < widths.length; ++i) { let width = Math.round(100 * widths[i] / totalWidth); if (minPercent && width < minPercent) { width = minPercent; } else if (maxPercent && width > maxPercent) { width = maxPercent; } totalPercentWidth += width; widths[i] = width; } let recoupPercent = totalPercentWidth - 100; while (minPercent && recoupPercent > 0) { for (let i = 0; i < widths.length; ++i) { if (widths[i] > minPercent) { --widths[i]; --recoupPercent; if (!recoupPercent) { break; } } } } while (maxPercent && recoupPercent < 0) { for (let i = 0; i < widths.length; ++i) { if (widths[i] < maxPercent) { ++widths[i]; ++recoupPercent; if (!recoupPercent) { break; } } } } return widths; } /** * The range of |minPercent| and |maxPercent| is [0, 100]. * @param {number} minPercent * @param {number=} maxPercent * @param {number=} maxDescentLevel */ autoSizeColumns(minPercent, maxPercent, maxDescentLevel) { let widths = []; for (let i = 0; i < this._columnsArray.length; ++i) { widths.push((this._columnsArray[i].title || '').length); } maxDescentLevel = maxDescentLevel || 0; if (!this._rootNode) { return; } const children = this._enumerateChildren(this._rootNode, [], maxDescentLevel + 1); for (let i = 0; i < children.length; ++i) { const node = children[i]; for (let j = 0; j < this._columnsArray.length; ++j) { const text = String(node.data[this._columnsArray[j].id]); if (text.length > widths[j]) { widths[j] = text.length; } } } widths = this._autoSizeWidths(widths, minPercent, maxPercent); for (let i = 0; i < this._columnsArray.length; ++i) { this._columnsArray[i].weight = widths[i]; } this._columnWidthsInitialized = false; this.updateWidths(); } /** * @param {!DataGridNode<!NODE_TYPE>} rootNode * @param {!Array<!DataGridNode<!NODE_TYPE>>} result * @param {number} maxLevel * @return {!Array<!DataGridNode<!NODE_TYPE>>} */ _enumerateChildren(rootNode, result, maxLevel) { if (!rootNode._isRoot) { result.push(rootNode); } if (!maxLevel) { return []; } for (let i = 0; i < rootNode.children.length; ++i) { this._enumerateChildren(rootNode.children[i], result, maxLevel - 1); } return result; } onResize() { this.updateWidths(); } // Updates the widths of the table, including the positions of the column // resizers. // // IMPORTANT: This function MUST be called once after the element of the // DataGrid is attached to its parent element and every subsequent time the // width of the parent element is changed in order to make it possible to // resize the columns. // // If this function is not called after the DataGrid is attached to its // parent element, then the DataGrid's columns will not be resizable. updateWidths() { // Do not attempt to use offsetes if we're not attached to the document tree yet. if (!this._columnWidthsInitialized && this.element.offsetWidth) { // Give all the columns initial widths now so that during a resize, // when the two columns that get resized get a percent value for // their widths, all the other columns already have percent values // for their widths. // Use container size to avoid changes of table width caused by change of column widths. const tableWidth = this.element.offsetWidth - this._cornerWidth; const cells = this._headerTableBody.rows[0].cells; const numColumns = cells.length - 1; // Do not process corner column. for (let i = 0; i < numColumns; i++) { const column = this.visibleColumnsArray[i]; if (!column.weight) { column.weight = 100 * cells[i].offsetWidth / tableWidth || 10; } } this._columnWidthsInitialized = true; } this._applyColumnWeights(); } /** * @param {string} columnId * @returns {number} */ indexOfVisibleColumn(columnId) { return this.visibleColumnsArray.findIndex(column => column.id === columnId); } /** * @param {string} name */ setName(name) { this._columnWeightsSetting = Common.Settings.Settings.instance().createSetting('dataGrid-' + name + '-columnWeights', {}); this._loadColumnWeights(); } _resetColumnWeights() { for (const column of this._columnsArray) { if (!column.defaultWeight) { continue; } column.weight = column.defaultWeight; } this._applyColumnWeights(); this._saveColumnWeights(); } _loadColumnWeights() { if (!this._columnWeightsSetting) { return; } const weights = this._columnWeightsSetting.get(); for (let i = 0; i < this._columnsArray.length; ++i) { const column = this._columnsArray[i]; const weight = weights[column.id]; if (weight) { column.weight = weight; } } this._applyColumnWeights(); } _saveColumnWeights() { if (!this._columnWeightsSetting) { return; } /** @type {!Object<string,*>} */ const weights = {}; for (let i = 0; i < this._columnsArray.length; ++i) { const column = this._columnsArray[i]; weights[column.id] = column.weight; } this._columnWeightsSetting.set(weights); } wasShown() { this._loadColumnWeights(); } willHide() { } _applyColumnWeights() { let tableWidth = this.element.offsetWidth - this._cornerWidth; if (tableWidth <= 0) { return; } let sumOfWeights = 0.0; const fixedColumnWidths = []; for (let i = 0; i < this.visibleColumnsArray.length; ++i) { const column = this.visibleColumnsArray[i]; if (column.fixedWidth) { const currentChild = this._headerTableColumnGroup.children[i]; const width = elementToPreferedWidthMap.get(currentChild) || this._headerTableBody.rows[0].cells[i].offsetWidth; fixedColumnWidths[i] = width; tableWidth -= width; } else { sumOfWeights += (this.visibleColumnsArray[i].weight || 0); } } let sum = 0; let lastOffset = 0; const minColumnWidth = 14; // px for (let i = 0; i < this.visibleColumnsArray.length; ++i) { const column = this.visibleColumnsArray[i]; let width; if (column.fixedWidth) { width = fixedColumnWidths[i]; } else { sum += (column.weight || 0); const offset = (sum * tableWidth / sumOfWeights) | 0; width = Math.max(offset - lastOffset, minColumnWidth); lastOffset = offset; } this._setPreferredWidth(i, width); } this._positionResizers(); } /** * @param {!Set<string>} columnsVisibility */ setColumnsVisiblity(columnsVisibility) { this.visibleColumnsArray = []; for (const column of this._columnsArray) { if (columnsVisibility.has(column.id)) { this.visibleColumnsArray.push(column); } } this._refreshHeader(); this._applyColumnWeights(); const nodes = this._enumerateChildren(this.rootNode(), [], -1); for (const node of nodes) { node.refresh(); } } get scrollContainer() { return /** @type {!HTMLElement} */ (this._scrollContainer); } _positionResizers() { const headerTableColumns = this._headerTableColumnGroup.children; const numColumns = headerTableColumns.length - 1; // Do not process corner column. /** @type {!Array<number>} */ const left = []; const resizers = this._resizers; while (resizers.length > numColumns - 1) { const resizer = resizers.pop(); if (resizer) { resizer.remove(); } } for (let i = 0; i < numColumns - 1; i++) { // Get the width of the cell in the first (and only) row of the // header table in order to determine the width of the column, since // it is not possible to query a column for its width. left[i] = (left[i - 1] || 0) + this._headerTableBody.rows[0].cells[i].offsetWidth; } // Make n - 1 resizers for n columns. for (let i = 0; i < numColumns - 1; i++) { let resizer = resizers[i]; if (!resizer) { // This is the first call to updateWidth, so the resizers need // to be created. resizer = document.createElement('div'); elementToIndexMap.set(resizer, i); resizer.classList.add('data-grid-resizer'); // This resizer is associated with the column to its right. UI.UIUtils.installDragHandle( resizer, this._startResizerDragging.bind(this), this._resizerDragging.bind(this), this._endResizerDragging.bind(this), 'col-resize'); this.element.appendChild(resizer); resizers.push(/** @type {!HTMLElement} */ (resizer)); } if (elementToPositionMap.get(resizer) !== left[i]) { elementToPositionMap.set(resizer, left[i]); resizer.style.left = left[i] + 'px'; } } } /** * @param {boolean=} hasChildren */ addCreationNode(hasChildren) { if (this.creationNode) { this.creationNode.makeNormal(); } /** @type {!Object<string,*>} */ const emptyData = {}; for (const column in this._columns) { emptyData[column] = null; } this.creationNode = new CreationDataGridNode(emptyData, hasChildren); this.rootNode().appendChild(this.creationNode); } /** * @param {!Event} event */ _keyDown(event) { if (!(event instanceof KeyboardEvent)) { return; } if (event.shiftKey || event.metaKey || event.ctrlKey || this._editing || UI.UIUtils.isEditing()) { return; } let handled = false; let nextSelectedNode; if (!this.selectedNode) { // Select the first or last node based on the arrow key direction if (event.key === 'ArrowUp' && !event.altKey) { nextSelectedNode = this._lastSelectableNode(); } else if (event.key === 'ArrowDown' && !event.altKey) { nextSelectedNode = this._firstSelectableNode(); } handled = nextSelectedNode ? true : false; } else if (event.key === 'ArrowUp' && !event.altKey) { nextSelectedNode = this.selectedNode.traversePreviousNode(true); while (nextSelectedNode && !nextSelectedNode.selectable) { nextSelectedNode = nextSelectedNode.traversePreviousNode(true); } handled = nextSelectedNode ? true : false; } else if (event.key === 'ArrowDown' && !event.altKey) { nextSelectedNode = this.selectedNode.traverseNextNode(true); while (nextSelectedNode && !nextSelectedNode.selectable) { nextSelectedNode = nextSelectedNode.traverseNextNode(true); } handled = nextSelectedNode ? true : false; } else if (event.key === 'ArrowLeft') { if (this.selectedNode.expanded) { if (event.altKey) { this.selectedNode.collapseRecursively(); } else { this.selectedNode.collapse(); } handled = true; } else if (this.selectedNode.parent && !this.selectedNode.parent._isRoot) { handled = true; if (this.selectedNode.parent.selectable) { nextSelectedNode = this.selectedNode.parent; handled = nextSelectedNode ? true : false; } else if (this.selectedNode.parent) { this.selectedNode.parent.collapse(); } } } else if (event.key === 'ArrowRight') { if (!this.selectedNode.revealed) { this.selectedNode.reveal(); handled = true; } else if (this.selectedNode.hasChildren()) { handled = true; if (this.selectedNode.expanded) { nextSelectedNode = this.selectedNode.children[0]; handled = nextSelectedNode ? true : false; } else { if (event.altKey) { this.selectedNode.expandRecursively(); } else { this.selectedNode.expand(); } } } } else if (event.keyCode === 8 || event.keyCode === 46) { if (this._deleteCallback) { handled = true; this._deleteCallback(this.selectedNode); } } else if (event.key === 'Enter') { if (this._editCallback) { handled = true; const selectedNodeElement = this.selectedNode._element; if (!selectedNodeElement) { return; } this._startEditing(selectedNodeElement.children[this._nextEditableColumn(-1)]); } else { this.dispatchEventToListeners(Events.OpenedNode, this.selectedNode); } } if (nextSelectedNode) { nextSelectedNode.reveal(); nextSelectedNode.select(); } if ((event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'ArrowLeft' || event.key === 'ArrowRight') && document.activeElement !== this.element) { // crbug.com/1005449 // navigational keys pressed but current DataGrid panel has lost focus; // re-focus to ensure subsequent keydowns can be registered within this DataGrid this.element.focus(); } if (handled) { event.consume(true); } } /** * @param {?DataGridNode<!NODE_TYPE>} root * @param {boolean} onlyAffectsSubtree */ updateSelectionBeforeRemoval(root, onlyAffectsSubtree) { let ancestor = this.selectedNode; while (ancestor && ancestor !== root) { ancestor = ancestor.parent; } // Selection is not in the subtree being deleted. if (!ancestor) { return; } let nextSelectedNode; // Skip subtree being deleted when looking for the next selectable node. for (ancestor = root; ancestor && !ancestor.nextSibling; ancestor = ancestor.parent) { } if (ancestor) { nextSelectedNode = ancestor.nextSibling; } while (nextSelectedNode && !nextSelectedNode.selectable) { nextSelectedNode = nextSelectedNode.traverseNextNode(true); } const isCreationNode = nextSelectedNode instanceof CreationDataGridNode && nextSelectedNode.isCreationNode; if (!nextSelectedNode || isCreationNode) { if (!root) { return; } nextSelectedNode = root.traversePreviousNode(true); while (nextSelectedNode && !nextSelectedNode.selectable) { nextSelectedNode = nextSelectedNode.traversePreviousNode(true); } } if (nextSelectedNode) { nextSelectedNode.reveal(); nextSelectedNode.select(); } else if (this.selectedNode) { this.selectedNode.deselect(); } } /** * @param {!Node} target * @return {?DataGridNode<!NODE_TYPE>} */ dataGridNodeFromNode(target) { const rowElement = UI.UIUtils.enclosingNodeOrSelfWithNodeName(target, 'tr'); return (rowElement && this.elementToDataGridNode.get(rowElement)) || null; } /** * @param {!Node} target * @return {?string} */ columnIdFromNode(target) { const cellElement = UI.UIUtils.enclosingNodeOrSelfWithNodeName(target, 'td'); return (cellElement && nodeToColumnIdMap.get(cellElement)) || null; } /** * @param {!Event} event */ _clickInHeaderCell(event) { const cell = UI.UIUtils.enclosingNodeOrSelfWithNodeName(/** @type {!Node} */ (event.target), 'th'); if (!cell) { return; } this._sortByColumnHeaderCell(/** @type {!HTMLElement} */ (cell)); } /** * @param {!Element} cell */ _sortByColumnHeaderCell(cell) { if (!nodeToColumnIdMap.has(cell) || !cell.classList.contains('sortable')) { return; } let sortOrder = Order.Ascending; if ((cell === this._sortColumnCell) && this.isSortOrderAscending()) { sortOrder = Order.Descending; } if (this._sortColumnCell) { this._sortColumnCell.classList.remove(Order.Ascending, Order.Descending); } this._sortColumnCell = cell; cell.classList.add(sortOrder); const icon = elementToSortIconMap.get(cell); if (!icon) { return; } icon.setIconType(sortOrder === Order.Ascending ? 'smallicon-triangle-up' : 'smallicon-triangle-down'); this.dispatchEventToListeners(Events.SortingChanged); } /** * @param {string} columnId * @param {!Order} sortOrder */ markColumnAsSortedBy(columnId, sortOrder) { if (this._sortColumnCell) { this._sortColumnCell.classList.remove(Order.Ascending, Order.Descending); } this._sortColumnCell = this._headerTableHeaders[columnId]; this._sortColumnCell.classList.add(sortOrder); } /** * @param {string} columnId * @return {!Element} */ headerTableHeader(columnId) { return this._headerTableHeaders[columnId]; } /** * @param {!Event} event */ _mouseDownInDataTable(event) { const target = /** @type {!Node} */ (event.target); const gridNode = this.dataGridNodeFromNode(target); if (!gridNode || !gridNode.selectable || gridNode.isEventWithinDisclosureTriangle(/** @type {!MouseEvent} */ (event))) { return; } const columnId = this.columnIdFromNode(target); if (columnId && this._columns[columnId].nonSelectable) { return; } if (/** @type {!MouseEvent} */ (event).metaKey) { if (gridNode.selected) { gridNode.deselect(); } else { gridNode.select(); } } else { gridNode.select(); this.dispatchEventToListeners(Events.OpenedNode, gridNode); } } /** * @param {?function(!UI.ContextMenu.SubMenu):void} callback */ setHeaderContextMenuCallback(callback) { this._headerContextMenuCallback = callback; } /** * @param {?function(!UI.ContextMenu.ContextMenu, !DataGridNode<!NODE_TYPE>):void} callback */ setRowContextMenuCallback(callback) { this._rowContextMenuCallback = callback; } /** * @param {!Event} event */ _contextMenu(event) { if (!(event instanceof MouseEvent)) { return; } const contextMenu = new UI.ContextMenu.ContextMenu(event); const target = /** @type {!Node} */ (event.target); const sortableVisibleColumns = this.visibleColumnsArray.filter(column => { return (column.sortable && column.title); }); const sortableHiddenColumns = this._columnsArray.filter( column => sortableVisibleColumns.indexOf(column) === -1 && column.allowInSortByEvenWhenHidden); const sortableColumns = [...sortableVisibleColumns, ...sortableHiddenColumns]; if (sortableColumns.length > 0) { const sortMenu = contextMenu.defaultSection().appendSubMenuItem(i18nString(UIStrings.sortByString)); for (const column of sortableColumns) { const headerCell = this._headerTableHeaders[column.id]; sortMenu.defaultSection().appendItem( /** @type {string} */ (column.title), this._sortByColumnHeaderCell.bind(this, headerCell)); } } if (target.isSelfOrDescendant(this._headerTableBody)) { if (this._headerContextMenuCallback) { this._headerContextMenuCallback(contextMenu); } contextMenu.defaultSection().appendItem(i18nString(UIStrings.resetColumns), this._resetColumnWeights.bind(this)); contextMenu.show(); return; } // Add header context menu to a subsection available from the body const headerSubMenu = contextMenu.defaultSection().appendSubMenuItem(i18nString(UIStrings.headerOptions)); if (this._headerContextMenuCallback) { this._headerContextMenuCallback(headerSubMenu); } headerSubMenu.defaultSection().appendItem(i18nString(UIStrings.resetColumns), this._resetColumnWeights.bind(this)); const isContextMenuKey = (event.button === 0); const gridNode = isContextMenuKey ? this.selectedNode : this.dataGridNodeFromNode(target); const selectedNodeElement = this.selectedNode && this.selectedNode.existingElement(); if (isContextMenuKey && selectedNodeElement) { const boundingRowRect = selectedNodeElement.getBoundingClientRect(); if (boundingRowRect) { const x = (boundingRowRect.right + boundingRowRect.left) / 2; const y = (boundingRowRect.bottom + boundingRowRect.top) / 2; contextMenu.setX(x); contextMenu.setY(y); } } if (this._refreshCallback && (!gridNode || gridNode !== this.creationNode)) { contextMenu.defaultSection().appendItem(i18nString(UIStrings.refresh), this._refreshCallback.bind(this)); } if (gridNode && gridNode.selectable && !gridNode.isEventWithinDisclosureTriangle(event)) { if (this._editCallback) { if (gridNode === this.creationNode) { const firstEditColumnIndex = this._nextEditableColumn(-1); const tableCellElement = gridNode.element().children[firstEditColumnIndex]; contextMenu.defaultSection().appendItem( i18nString(UIStrings.addNew), this._startEditing.bind(this, tableCellElement)); } else if (isContextMenuKey) { const firstEditColumnIndex = this._nextEditableColumn(-1); if (firstEditColumnIndex > -1) { const firstColumn = this.visibleColumnsArray[firstEditColumnIndex]; if (firstColumn && firstColumn.editable) { contextMenu.defaultSection().appendItem( i18nString(UIStrings.editS, {PH1: firstColumn.title}), this._startEditingColumnOfDataGridNode.bind(this, gridNode, firstEditColumnIndex)); } } } else { const columnId = this.columnIdFromNode(target); if (columnId && this._columns[columnId].editable) { contextMenu.defaultSection().appendItem( i18nString(UIStrings.editS, {PH1: this._columns[columnId].title}), this._startEditing.bind(this,