@blueprintjs/table
Version:
Scalable interactive table component
278 lines (252 loc) • 10.2 kB
text/typescript
/*
* Copyright 2017 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type Region, RegionCardinality, Regions } from "../../regions";
import type { TableProps } from "../../tableProps";
import {
type CellCoordinates,
type FocusedCell,
type FocusedCellCoordinates,
type FocusedRegion,
type FocusedRow,
FocusMode,
} from "../cellTypes";
import * as Errors from "../errors";
/**
* Returns the inferred focus mode from the table props. This prefers the new focus mode API, falling back to the
* deprecated enableFocusedCell API if that is not provided.
*/
export function getFocusModeFromProps(props: TableProps): FocusMode | undefined {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const { enableFocusedCell, focusMode } = props;
return focusMode ?? getFocusModeFromEnabled(enableFocusedCell);
}
function getFocusModeFromEnabled(enableFocusedCell = false): FocusMode | undefined {
return enableFocusedCell ? FocusMode.CELL : undefined;
}
/**
* Returns the inferred focused region from the table props. This prefers the new focus mode API, falling back to the
* deprecated API if a focused region is not provided.
*/
export function getFocusedRegionFromProps(props: TableProps): FocusedRegion | undefined {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const { focusedRegion, focusedCell } = props;
return focusedRegion ?? getFocusedCellFromCoordinates(focusedCell);
}
export function getFocusedCellFromCoordinates(
focusedCell: FocusedCellCoordinates | undefined,
): FocusedCell | undefined {
return focusedCell != null ? { type: FocusMode.CELL, ...focusedCell } : undefined;
}
/**
* Returns the `focusedSelectionIndex` if both the focused region and that
* property are defined, or the last index of `selectedRegions` otherwise. If
* `selectedRegions` is empty, the function always returns `undefined`.
*/
export function getFocusedOrLastSelectedIndex(selectedRegions: Region[], focusedRegion?: FocusedRegion) {
if (selectedRegions.length === 0) {
return undefined;
} else if (focusedRegion != null) {
return focusedRegion.focusSelectionIndex;
} else {
return selectedRegions.length - 1;
}
}
/**
* Returns the proper focused region for the given set of initial conditions.
*/
export function getInitialFocusedRegion(
focusMode: FocusMode | undefined,
focusedRegionFromProps: FocusedRegion | undefined,
focusedRegionFromState: FocusedRegion | undefined,
selectedRegions: Region[],
): FocusedRegion | undefined {
return validateFocusedRegion(
focusMode,
getInitialFocusedCell(focusedRegionFromProps, focusedRegionFromState, selectedRegions),
);
}
function getInitialFocusedCell(
focusedRegionFromProps: FocusedRegion | undefined,
focusedRegionFromState: FocusedRegion | undefined,
selectedRegions: Region[],
): FocusedRegion {
return focusedRegionFromProps ?? focusedRegionFromState ?? getInitialFocusedCellFromSelection(selectedRegions);
}
function getInitialFocusedCellFromSelection(selectedRegions: Region[]): FocusedRegion {
if (selectedRegions.length === 0) {
return { col: 0, focusSelectionIndex: 0, row: 0, type: FocusMode.CELL };
}
const lastIndex = selectedRegions.length - 1;
// focus the top-left cell of the last selection
return {
...Regions.getFocusCellCoordinatesFromRegion(selectedRegions[lastIndex]),
focusSelectionIndex: lastIndex,
type: FocusMode.CELL,
};
}
/**
* Returns a focused region that matches the given focus mode if possible. If such a conversion is not possible,
* returns undefined instead.
*/
export function validateFocusedRegion(
focusMode: FocusMode | undefined,
focusedRegion: FocusedRegion,
): FocusedRegion | undefined {
if (focusMode == null) {
return undefined;
}
if (focusedRegion.type === focusMode) {
return focusedRegion;
}
if (focusedRegion.type === FocusMode.CELL && focusMode === FocusMode.ROW) {
return {
focusSelectionIndex: focusedRegion.focusSelectionIndex,
row: focusedRegion.row,
type: focusMode,
};
}
if (focusedRegion.type === FocusMode.ROW && focusMode === FocusMode.CELL) {
return {
col: 0,
focusSelectionIndex: focusedRegion.focusSelectionIndex,
row: focusedRegion.row,
type: focusMode,
};
}
return undefined;
}
/**
* Returns `true` if the focused region is located along the top boundary of the
* provided region, or `false` otherwise.
*/
export function isFocusAtRegionTop(region: Region, focusedRegion: FocusedRegion) {
return region.rows != null && focusedRegion.row === region.rows[0];
}
/**
* Returns `true` if the focused region is located along the bottom boundary of
* the provided region, or `false` otherwise.
*/
export function isFocusAtRegionBottom(region: Region, focusedRegion: FocusedRegion) {
return region.rows != null && focusedRegion.row === region.rows[1];
}
/**
* Returns `true` if the focused region is located along the left boundary of the
* provided region, or `false` otherwise.
*/
export function isFocusAtRegionLeft(region: Region, focusedRegion: FocusedRegion) {
return region.cols != null && getFocusedColumn(focusedRegion) === region.cols[0];
}
/**
* Returns `true` if the focused region is located along the right boundary of the
* provided region, or `false` otherwise.
*/
export function isFocusAtRegionRight(region: Region, focusedRegion: FocusedRegion) {
return region.cols != null && getFocusedColumn(focusedRegion) === region.cols[1];
}
/**
* Returns the column associated with this region, if there is one.
*/
export function getFocusedColumn(focusedRegion: FocusedRegion): number | undefined {
switch (focusedRegion.type) {
case FocusMode.CELL:
return focusedRegion.col;
case FocusMode.ROW:
return undefined;
}
}
/**
* Returns a new focused region object in the given focus mode that includes a focusSelectionIndex property.
*/
export function toFocusedRegion(
focusMode: FocusMode.CELL,
cellCoords: CellCoordinates,
focusSelectionIndex?: number,
): FocusedCell;
export function toFocusedRegion(
focusMode: FocusMode.ROW,
cellCoords: CellCoordinates,
focusSelectionIndex?: number,
): FocusedRow;
export function toFocusedRegion(
focusMode: FocusMode | undefined,
cellCoords: CellCoordinates,
focusSelectionIndex?: number,
): FocusedRegion | undefined;
export function toFocusedRegion(
focusMode: FocusMode | undefined,
cellCoords: CellCoordinates,
focusSelectionIndex: number = 0,
): FocusedRegion | undefined {
switch (focusMode) {
case FocusMode.CELL:
return { type: FocusMode.CELL, ...cellCoords, focusSelectionIndex };
case FocusMode.ROW:
return { focusSelectionIndex, row: cellCoords.row, type: FocusMode.ROW };
case undefined:
return undefined;
}
}
/**
* Expands an existing region to new region based on the current focused region.
* The focused region is an invariant and should not move as a result of this
* operation. This function is used, for instance, to expand a selected region
* on shift+click.
*/
export function expandFocusedRegion(focusedRegion: FocusedRegion, newRegion: Region) {
switch (Regions.getRegionCardinality(newRegion)) {
case RegionCardinality.FULL_COLUMNS: {
const [indexStart, indexEnd] = getExpandedRegionIndices(focusedRegion, newRegion, "col", "cols");
return Regions.column(indexStart, indexEnd);
}
case RegionCardinality.FULL_ROWS: {
const [indexStart, indexEnd] = getExpandedRegionIndices(focusedRegion, newRegion, "row", "rows");
return Regions.row(indexStart, indexEnd);
}
case RegionCardinality.CELLS:
const [rowIndexStart, rowIndexEnd] = getExpandedRegionIndices(focusedRegion, newRegion, "row", "rows");
const [colIndexStart, colIndexEnd] = getExpandedRegionIndices(focusedRegion, newRegion, "col", "cols");
return Regions.cell(rowIndexStart, colIndexStart, rowIndexEnd, colIndexEnd);
default:
// i.e. `case RegionCardinality.FULL_TABLE:`
return Regions.table();
}
}
function getExpandedRegionIndices(
focusedRegion: FocusedRegion,
newRegion: Region,
focusedCellDimension: "row" | "col",
regionDimension: "rows" | "cols",
) {
const sourceIndex = focusedCellDimension === "row" ? focusedRegion.row : getFocusedColumn(focusedRegion) ?? 0; // THIS IS QUESTIONABLE AT BEST
const [destinationIndex, destinationIndexEnd] = newRegion[regionDimension]!;
if (destinationIndex !== destinationIndexEnd) {
if (regionDimension === "rows") {
throw new Error(Errors.TABLE_EXPAND_FOCUSED_REGION_MULTI_ROW_REGION);
} else if (regionDimension === "cols") {
throw new Error(Errors.TABLE_EXPAND_FOCUSED_REGION_MULTI_COLUMN_REGION);
}
}
return sourceIndex <= destinationIndex ? [sourceIndex, destinationIndex] : [destinationIndex, sourceIndex];
}
export function areFocusedRegionsEqual(left: FocusedRegion, right: FocusedRegion) {
if (left.type === FocusMode.CELL && right.type === FocusMode.CELL) {
return left.row === right.row && left.col === right.col;
} else if (left.type === FocusMode.ROW && right.type === FocusMode.ROW) {
return left.row === right.row;
} else {
return false;
}
}