UNPKG

chrome-devtools-frontend

Version:
260 lines (224 loc) • 8.79 kB
// Copyright 2020 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. import * as Platform from '../../platform/platform.js'; import * as LitHtml from '../../third_party/lit-html/lit-html.js'; import * as DataGridRenderers from './DataGridRenderers.js'; /** * A column is an object with the following properties: * * - `id`: a unique ID for that column. * - `title`: the user visible title. * - `visible`: if the column is visible when rendered * - `hideable`: if the user is able to show/hide the column via the context menu. * - `width`: a number that denotes the width of the column. This is percentage * based, out of 100. * - `sortable`: an optional property to denote if the column is sortable. * Note, if you're rendering a data-grid yourself you likely shouldn't set * this. It's set by the `data-grid-controller`, which is the component you * want if your table needs to be sortable. */ export interface Column { id: string; title: string; sortable?: boolean; widthWeighting: number; hideable: boolean; visible: boolean; } export type CellValue = string|number|boolean|null; /** * A cell contains a `columnId`, which is the ID of the column the cell * reprsents, and the `value`, which is a string value for that cell. * * Note that currently cells cannot render complex data (e.g. nested HTML) but * in future we may extend the DataGrid to support this. */ export interface Cell { columnId: string; value: CellValue; title?: string; renderer?: (value: CellValue) => LitHtml.TemplateResult; } export type Row = { cells: Cell[], hidden?: boolean, }; export const enum SortDirection { ASC = 'ASC', DESC = 'DESC', } export interface SortState { columnId: string; direction: SortDirection; } export type CellPosition = readonly [columnIndex: number, rowIndex: number]; export function getRowEntryForColumnId(row: Row, id: string): Cell { const rowEntry = row.cells.find(r => r.columnId === id); if (rowEntry === undefined) { throw new Error(`Found a row that was missing an entry for column ${id}.`); } return rowEntry; } export function renderCellValue(cell: Cell): LitHtml.TemplateResult { if (cell.renderer) { return cell.renderer(cell.value); } return DataGridRenderers.primitiveRenderer(cell.value); } /** * When the user passes in columns we want to know how wide each one should be. * We don't work in exact percentages, or pixel values, because it's then * unclear what to do in the event that one column is hidden. How do we * distribute up the extra space? * * Instead, each column has a weighting, which is its width proportionate to the * total weighting of all columns. For example: * * -> two columns both with widthWeighting: 1, will be 50% each, because the * total weight = 2, and each column is 1 * * -> if you have two columns, the first width a weight of 2, and the second * with a weight of 1, the first will take up 66% and the other 33%. * * This way, when you are calculating the %, it's easy to do because if a * particular column becomes hidden, you ignore it / give it a weighting of 0, * and the space is evenly distributed amongst the remaining visible columns. * * @param allColumns * @param columnId */ export function calculateColumnWidthPercentageFromWeighting(allColumns: readonly Column[], columnId: string): number { const totalWeights = allColumns.filter(c => c.visible).reduce((sumOfWeights, col) => sumOfWeights + col.widthWeighting, 0); const matchingColumn = allColumns.find(c => c.id === columnId); if (!matchingColumn) { throw new Error(`Could not find column with ID ${columnId}`); } if (matchingColumn.widthWeighting < 1) { throw new Error(`Error with column ${columnId}: width weightings must be >= 1.`); } if (!matchingColumn.visible) { return 0; } return Math.round((matchingColumn.widthWeighting / totalWeights) * 100); } export interface HandleArrowKeyOptions { key: Platform.KeyboardUtilities.ArrowKey; currentFocusedCell: readonly[number, number]; columns: readonly Column[]; rows: readonly Row[]; } export function handleArrowKeyNavigation(options: HandleArrowKeyOptions): CellPosition { const {key, currentFocusedCell, columns, rows} = options; const [selectedColIndex, selectedRowIndex] = currentFocusedCell; switch (key) { case Platform.KeyboardUtilities.ArrowKey.LEFT: { const firstVisibleColumnIndex = columns.findIndex(c => c.visible); if (selectedColIndex === firstVisibleColumnIndex) { // User is as far left as they can go, so don't move them. return [selectedColIndex, selectedRowIndex]; } // Set the next index to first be the column we are already on, and then // iterate back through all columns to our left, breaking the loop if we // find one that's not hidden. If we don't find one, we'll stay where we // are. let nextColIndex = selectedColIndex; for (let i = nextColIndex - 1; i >= 0; i--) { const col = columns[i]; if (col.visible) { nextColIndex = i; break; } } return [nextColIndex, selectedRowIndex]; } case Platform.KeyboardUtilities.ArrowKey.RIGHT: { // Set the next index to first be the column we are already on, and then // iterate through all columns to our right, breaking the loop if we // find one that's not hidden. If we don't find one, we'll stay where we // are. let nextColIndex = selectedColIndex; for (let i = nextColIndex + 1; i < columns.length; i++) { const col = columns[i]; if (col.visible) { nextColIndex = i; break; } } return [nextColIndex, selectedRowIndex]; } case Platform.KeyboardUtilities.ArrowKey.UP: { const columnsSortable = columns.some(col => col.sortable === true); const minRowIndex = columnsSortable ? 0 : 1; if (selectedRowIndex === minRowIndex) { // If any columns are sortable the user can navigate into the column // header row, else they cannot. So if they are on the highest row they // can be, just return the current cell as they cannot move up. return [selectedColIndex, selectedRowIndex]; } let rowIndexToMoveTo = selectedRowIndex; for (let i = selectedRowIndex - 1; i >= minRowIndex; i--) { // This means we got past all the body rows and therefore the user needs // to go into the column row. if (i === 0) { rowIndexToMoveTo = 0; break; } const matchingRow = rows[i - 1]; if (!matchingRow.hidden) { rowIndexToMoveTo = i; break; } } return [selectedColIndex, rowIndexToMoveTo]; } case Platform.KeyboardUtilities.ArrowKey.DOWN: { if (selectedRowIndex === 0) { // The user is on the column header. So find the first visible body row and take them there! const firstVisibleBodyRowIndex = rows.findIndex(row => !row.hidden); if (firstVisibleBodyRowIndex > -1) { return [selectedColIndex, firstVisibleBodyRowIndex + 1]; } // If we didn't find a single visible row, leave the user where they are. return [selectedColIndex, selectedRowIndex]; } let rowIndexToMoveTo = selectedRowIndex; // Work down from our starting position to find the next visible row to move to. for (let i = rowIndexToMoveTo + 1; i < rows.length + 1; i++) { const matchingRow = rows[i - 1]; if (!matchingRow.hidden) { rowIndexToMoveTo = i; break; } } return [selectedColIndex, rowIndexToMoveTo]; } default: return Platform.assertNever(key, `Unknown arrow key: ${key}`); } } export const calculateFirstFocusableCell = (options: {columns: readonly Column[], rows: readonly Row[]}): [colIndex: number, rowIndex: number] => { const {columns, rows} = options; const someColumnsSortable = columns.some(col => col.sortable === true); const focusableRowIndex = someColumnsSortable ? 0 : rows.findIndex(row => !row.hidden) + 1; const focusableColIndex = columns.findIndex(col => col.visible); return [focusableColIndex, focusableRowIndex]; }; export class ContextMenuColumnSortClickEvent extends Event { data: { column: Column, }; constructor(column: Column) { super('context-menu-column-sort-click'); this.data = { column, }; } } export class ContextMenuHeaderResetClickEvent extends Event { constructor() { super('context-menu-header-reset-click'); } }