[go: nahoru, domu]

blob: fc775a5671ab79ca063eda4e3e63777828da6db3 [file] [log] [blame]
// 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 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;
// The renderer function actually returns LitHtml.TemplateResult but it's a
// lot of work to teach the bridges generator about that.
// TODO (crbug.com/1011811): Fix types once TypeScriptification is complete.
renderer?: (value: CellValue) => unknown
}
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 const enum ArrowKey {
UP = 'ArrowUp',
DOWN = 'ArrowDown',
LEFT = 'ArrowLeft',
RIGHT = 'ArrowRight',
}
export const ARROW_KEYS = new Set<ArrowKey>([
ArrowKey.UP,
ArrowKey.DOWN,
ArrowKey.LEFT,
ArrowKey.RIGHT,
]);
export function keyIsArrowKey(key: string): key is ArrowKey {
return ARROW_KEYS.has(key as ArrowKey);
}
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 {
const output = cell.renderer ? cell.renderer(cell.value) as LitHtml.TemplateResult :
DataGridRenderers.primitiveRenderer(cell.value);
return output;
}
/**
* 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: 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 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 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 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 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];
}
}
}
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];
};