UNPKG

@blueprintjs/table

Version:

Scalable interactive table component

677 lines 25 kB
/* * Copyright 2016 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 { FocusMode } from "./common/cellTypes"; import * as Classes from "./common/classes"; import { Utils } from "./common/utils"; /** * `Region`s contain sets of cells. Additionally, a distinction is drawn, for * example, between all cells within a column and the whole column itself. * The `RegionCardinality` enum represents these distinct types of `Region`s. */ export var RegionCardinality; (function (RegionCardinality) { /** * A region that contains a finite rectangular group of table cells */ RegionCardinality["CELLS"] = "cells"; /** * A region that represents all cells within 1 or more rows. */ RegionCardinality["FULL_ROWS"] = "full-rows"; /** * A region that represents all cells within 1 or more columns. */ RegionCardinality["FULL_COLUMNS"] = "full-columns"; /** * A region that represents all cells in the table. */ RegionCardinality["FULL_TABLE"] = "full-table"; })(RegionCardinality || (RegionCardinality = {})); /** * A convenience object for subsets of `RegionCardinality` that are commonly * used as the `selectionMode` prop of the `<Table>`. */ export const SelectionModes = { ALL: [ RegionCardinality.FULL_TABLE, RegionCardinality.FULL_COLUMNS, RegionCardinality.FULL_ROWS, RegionCardinality.CELLS, ], COLUMNS_AND_CELLS: [RegionCardinality.FULL_COLUMNS, RegionCardinality.CELLS], COLUMNS_ONLY: [RegionCardinality.FULL_COLUMNS], NONE: [], ROWS_AND_CELLS: [RegionCardinality.FULL_ROWS, RegionCardinality.CELLS], ROWS_ONLY: [RegionCardinality.FULL_ROWS], }; export var ColumnLoadingOption; (function (ColumnLoadingOption) { ColumnLoadingOption["CELLS"] = "cells"; ColumnLoadingOption["HEADER"] = "column-header"; })(ColumnLoadingOption || (ColumnLoadingOption = {})); export var RowLoadingOption; (function (RowLoadingOption) { RowLoadingOption["CELLS"] = "cells"; RowLoadingOption["HEADER"] = "row-header"; })(RowLoadingOption || (RowLoadingOption = {})); export var TableLoadingOption; (function (TableLoadingOption) { TableLoadingOption["CELLS"] = "cells"; TableLoadingOption["COLUMN_HEADERS"] = "column-header"; TableLoadingOption["ROW_HEADERS"] = "row-header"; })(TableLoadingOption || (TableLoadingOption = {})); /** * Table Regions API. * * @see https://blueprintjs.com/docs/#table/api.region */ /* eslint-disable-next-line @typescript-eslint/no-extraneous-class */ export class Regions { /** * Determines the cardinality of a region. We use null values to indicate * an unbounded interval. Therefore, an example of a region containing the * second and third columns would be: * * ```js * { rows: null, cols: [1, 2] } * ``` * * In this case, this method would return `RegionCardinality.FULL_COLUMNS`. * * If both rows and columns are unbounded, then the region covers the * entire table. Therefore, a region like this: * * ```js * { rows: null, cols: null } * ``` * * will return `RegionCardinality.FULL_TABLE`. * * An example of a region containing a single cell in the table would be: * * ```js * { rows: [5, 5], cols: [2, 2] } * ``` * * In this case, this method would return `RegionCardinality.CELLS`. */ static getRegionCardinality(region) { if (region.cols != null && region.rows != null) { return RegionCardinality.CELLS; } else if (region.cols != null) { return RegionCardinality.FULL_COLUMNS; } else if (region.rows != null) { return RegionCardinality.FULL_ROWS; } else { return RegionCardinality.FULL_TABLE; } } static getFocusCellCoordinatesFromRegion(region) { const regionCardinality = Regions.getRegionCardinality(region); // HACKHACK: non-null assertions ahead, consider designing some type guards instead switch (regionCardinality) { case RegionCardinality.FULL_TABLE: return { col: 0, row: 0 }; case RegionCardinality.FULL_COLUMNS: return { col: region.cols[0], row: 0 }; case RegionCardinality.FULL_ROWS: return { col: 0, row: region.rows[0] }; case RegionCardinality.CELLS: return { col: region.cols[0], row: region.rows[0] }; } } /** * Returns a deep copy of the provided region. */ static copy(region) { const cardinality = Regions.getRegionCardinality(region); // HACKHACK: non-null assertions ahead, consider designing some type guards instead // N.B. we need to be careful not to explicitly spell out `rows: undefined` // (e.g.) if the "rows" key is completely absent, otherwise // deep-equality checks will fail. if (cardinality === RegionCardinality.CELLS) { return Regions.cell(region.rows[0], region.cols[0], region.rows[1], region.cols[1]); } else if (cardinality === RegionCardinality.FULL_COLUMNS) { return Regions.column(region.cols[0], region.cols[1]); } else if (cardinality === RegionCardinality.FULL_ROWS) { return Regions.row(region.rows[0], region.rows[1]); } else { return Regions.table(); } } /** * Returns a region containing one or more cells. */ static cell(row, col, row2, col2) { return { cols: this.normalizeInterval(col, col2), rows: this.normalizeInterval(row, row2), }; } /** * Returns a region containing one or more full rows. */ static row(row, row2) { return { rows: this.normalizeInterval(row, row2) }; } /** * Returns a region containing one or more full columns. */ static column(col, col2) { return { cols: this.normalizeInterval(col, col2) }; } /** * Returns a region containing the entire table. */ static table() { return {}; } /** * Adds the region to the end of a cloned copy of the supplied region * array. */ static add(regions, region) { const copy = regions.slice(); copy.push(region); return copy; } /** * Replaces the region at the end of a cloned copy of the supplied region * array, or at the specific index if one is provided. */ static update(regions, region, index) { const copy = regions.slice(); if (index != null) { copy.splice(index, 1, region); } else { copy.pop(); copy.push(region); } return copy; } /** * Clamps the region's start and end indices between 0 and the provided * maximum values. */ static clampRegion(region, maxRowIndex, maxColumnIndex) { const nextRegion = Regions.copy(region); if (region.rows != null) { nextRegion.rows[0] = Utils.clamp(region.rows[0], 0, maxRowIndex); nextRegion.rows[1] = Utils.clamp(region.rows[1], 0, maxRowIndex); } if (region.cols != null) { nextRegion.cols[0] = Utils.clamp(region.cols[0], 0, maxColumnIndex); nextRegion.cols[1] = Utils.clamp(region.cols[1], 0, maxColumnIndex); } return nextRegion; } /** * Returns true iff the specified region is equal to the last region in * the region list. This allows us to avoid immediate additive re-selection. */ static lastRegionIsEqual(regions, region) { if (regions == null || regions.length === 0) { return false; } const lastRegion = regions[regions.length - 1]; return Regions.regionsEqual(lastRegion, region); } /** * Returns the index of the region that is equal to the supplied * parameter. Returns -1 if no such region is found. */ static findMatchingRegion(regions, region) { if (regions == null) { return -1; } for (let i = 0; i < regions.length; i++) { if (Regions.regionsEqual(regions[i], region)) { return i; } } return -1; } /** * Returns the index of the region that wholly contains the supplied * parameter. Returns -1 if no such region is found. */ static findContainingRegion(regions, region) { if (regions == null) { return -1; } for (let i = 0; i < regions.length; i++) { if (Regions.regionContains(regions[i], region)) { return i; } } return -1; } /** * Returns true if the regions contain a region that has FULL_COLUMNS * cardinality and contains the specified column index. */ static hasFullColumn(regions, col) { if (regions == null) { return false; } for (const region of regions) { const cardinality = Regions.getRegionCardinality(region); if (cardinality === RegionCardinality.FULL_TABLE) { return true; } if (cardinality === RegionCardinality.FULL_COLUMNS && Regions.intervalContainsIndex(region.cols, col)) { return true; } } return false; } /** * Returns true if the regions contain a region that has FULL_ROWS * cardinality and contains the specified row index. */ static hasFullRow(regions, row) { if (regions == null) { return false; } for (const region of regions) { const cardinality = Regions.getRegionCardinality(region); if (cardinality === RegionCardinality.FULL_TABLE) { return true; } if (cardinality === RegionCardinality.FULL_ROWS && Regions.intervalContainsIndex(region.rows, row)) { return true; } } return false; } /** * Returns true if the regions contain a region that has FULL_TABLE cardinality */ static hasFullTable(regions) { if (regions == null) { return false; } for (const region of regions) { const cardinality = Regions.getRegionCardinality(region); if (cardinality === RegionCardinality.FULL_TABLE) { return true; } } return false; } /** * Returns true if the regions fully contain the query region. */ static containsRegion(regions, query) { return Regions.overlapsRegion(regions, query, false); } /** * Returns true if the regions at least partially overlap the query region. */ static overlapsRegion(regions, query, allowPartialOverlap = false) { const intervalCompareFn = allowPartialOverlap ? Regions.intervalOverlaps : Regions.intervalContains; if (regions == null || query == null) { return false; } for (const region of regions) { const cardinality = Regions.getRegionCardinality(region); switch (cardinality) { case RegionCardinality.FULL_TABLE: return true; case RegionCardinality.FULL_COLUMNS: if (intervalCompareFn(region.cols, query.cols)) { return true; } continue; case RegionCardinality.FULL_ROWS: if (intervalCompareFn(region.rows, query.rows)) { return true; } continue; case RegionCardinality.CELLS: if (intervalCompareFn(region.cols, query.cols) && intervalCompareFn(region.rows, query.rows)) { return true; } continue; default: break; } } return false; } static eachUniqueFullColumn(regions, iteratee) { if (regions == null || regions.length === 0 || iteratee == null) { return; } const seen = {}; regions.forEach((region) => { if (Regions.getRegionCardinality(region) === RegionCardinality.FULL_COLUMNS) { const [start, end] = region.cols; for (let col = start; col <= end; col++) { if (!seen[col]) { seen[col] = true; iteratee(col); } } } }); } static eachUniqueFullRow(regions, iteratee) { if (regions == null || regions.length === 0 || iteratee == null) { return; } const seen = {}; regions.forEach((region) => { if (Regions.getRegionCardinality(region) === RegionCardinality.FULL_ROWS) { const [start, end] = region.rows; for (let row = start; row <= end; row++) { if (!seen[row]) { seen[row] = true; iteratee(row); } } } }); } /** * Using the supplied array of non-contiguous `Region`s, this method * returns an ordered array of every unique cell that exists in those * regions. */ static enumerateUniqueCells(regions, numRows, numCols) { if (regions == null || regions.length === 0) { return []; } const seen = {}; const list = []; for (const region of regions) { Regions.eachCellInRegion(region, numRows, numCols, (row, col) => { // add to list if not seen const key = `${row}-${col}`; if (seen[key] !== true) { seen[key] = true; list.push([row, col]); } }); } // sort list by rows then columns list.sort(Regions.rowFirstComparator); return list; } /** * Using the supplied region, returns an "equivalent" region of * type CELLS that define the bounds of the given region */ static getCellRegionFromRegion(region, numRows, numCols) { const regionCardinality = Regions.getRegionCardinality(region); switch (regionCardinality) { case RegionCardinality.FULL_TABLE: return Regions.cell(0, 0, numRows - 1, numCols - 1); case RegionCardinality.FULL_COLUMNS: return Regions.cell(0, region.cols[0], numRows - 1, region.cols[1]); case RegionCardinality.FULL_ROWS: return Regions.cell(region.rows[0], 0, region.rows[1], numCols - 1); case RegionCardinality.CELLS: return Regions.cell(region.rows[0], region.cols[0], region.rows[1], region.cols[1]); } } static getRegionFromFocusedRegion(focusedRegion) { switch (focusedRegion.type) { case FocusMode.CELL: return Regions.cell(focusedRegion.row, focusedRegion.col); case FocusMode.ROW: return Regions.row(focusedRegion.row); } } /** * Maps a dense array of cell coordinates to a sparse 2-dimensional array * of cell values. * * We create a new 2-dimensional array representing the smallest single * contiguous `Region` that contains all cells in the supplied array. We * invoke the mapper callback only on the cells in the supplied coordinate * array and store the result. Returns the resulting 2-dimensional array. * * If there is no contiguous `Region` which contains all the cells, we * return `undefined`. */ static sparseMapCells(cells, mapper) { const bounds = Regions.getBoundingRegion(cells); if (bounds === undefined) { return undefined; } const numRows = bounds.rows[1] + 1 - bounds.rows[0]; const numCols = bounds.cols[1] + 1 - bounds.cols[0]; const result = Utils.times(numRows, () => new Array(numCols)); cells.forEach(([row, col]) => { result[row - bounds.rows[0]][col - bounds.cols[0]] = mapper(row, col); }); return result; } /** * Returns the smallest single contiguous `Region` that contains all cells in the * supplied array. */ static getBoundingRegion(cells) { let minRow; let maxRow; let minCol; let maxCol; for (const [row, col] of cells) { minRow = minRow === undefined || row < minRow ? row : minRow; maxRow = maxRow === undefined || row > maxRow ? row : maxRow; minCol = minCol === undefined || col < minCol ? col : minCol; maxCol = maxCol === undefined || col > maxCol ? col : maxCol; } if (minRow === undefined || maxRow === undefined || minCol === undefined || maxCol === undefined) { return undefined; } return { cols: [minCol, maxCol], rows: [minRow, maxRow], }; } static isValid(region) { if (region == null) { return false; } if (region.rows != null && (region.rows[0] < 0 || region.rows[1] < 0)) { return false; } if (region.cols != null && (region.cols[0] < 0 || region.cols[1] < 0)) { return false; } return true; } static isRegionValidForTable(region, numRows, numCols) { if (numRows === 0 || numCols === 0) { return false; } else if (region.rows != null && !intervalInRangeInclusive(region.rows, 0, numRows - 1)) { return false; } else if (region.cols != null && !intervalInRangeInclusive(region.cols, 0, numCols - 1)) { return false; } return true; } static joinStyledRegionGroups(selectedRegions, otherRegions, focusedRegionOrCell) { const focusedRegion = focusedRegionOrCell == null ? undefined : "type" in focusedRegionOrCell ? focusedRegionOrCell : { ...focusedRegionOrCell, type: FocusMode.CELL }; let regionGroups = []; if (otherRegions != null) { regionGroups = regionGroups.concat(otherRegions); } if (selectedRegions != null && selectedRegions.length > 0) { regionGroups.push({ className: Classes.TABLE_SELECTION_REGION, regions: selectedRegions, }); } if (focusedRegion !== undefined) { regionGroups.push({ className: Classes.TABLE_FOCUS_REGION, regions: [Regions.getRegionFromFocusedRegion(focusedRegion)], }); } return regionGroups; } static regionsEqual(regionA, regionB) { return Regions.intervalsEqual(regionA.rows, regionB.rows) && Regions.intervalsEqual(regionA.cols, regionB.cols); } /** * Expands an old region to the minimal bounding region that also contains * the new region. If the regions have different cardinalities, then the new * region is returned. Useful for expanding a selected region on * shift+click, for instance. */ static expandRegion(oldRegion, newRegion) { const oldRegionCardinality = Regions.getRegionCardinality(oldRegion); const newRegionCardinality = Regions.getRegionCardinality(newRegion); if (newRegionCardinality !== oldRegionCardinality) { return newRegion; } switch (newRegionCardinality) { case RegionCardinality.FULL_ROWS: { const rowStart = Math.min(oldRegion.rows[0], newRegion.rows[0]); const rowEnd = Math.max(oldRegion.rows[1], newRegion.rows[1]); return Regions.row(rowStart, rowEnd); } case RegionCardinality.FULL_COLUMNS: { const colStart = Math.min(oldRegion.cols[0], newRegion.cols[0]); const colEnd = Math.max(oldRegion.cols[1], newRegion.cols[1]); return Regions.column(colStart, colEnd); } case RegionCardinality.CELLS: { const rowStart = Math.min(oldRegion.rows[0], newRegion.rows[0]); const colStart = Math.min(oldRegion.cols[0], newRegion.cols[0]); const rowEnd = Math.max(oldRegion.rows[1], newRegion.rows[1]); const colEnd = Math.max(oldRegion.cols[1], newRegion.cols[1]); return Regions.cell(rowStart, colStart, rowEnd, colEnd); } default: return Regions.table(); } } /** * Iterates over the cells within an `Region`, invoking the callback with * each cell's coordinates. */ static eachCellInRegion(region, numRows, numCols, iteratee) { const cardinality = Regions.getRegionCardinality(region); switch (cardinality) { case RegionCardinality.FULL_TABLE: for (let row = 0; row < numRows; row++) { for (let col = 0; col < numCols; col++) { iteratee(row, col); } } break; case RegionCardinality.FULL_COLUMNS: for (let row = 0; row < numRows; row++) { for (let col = region.cols[0]; col <= region.cols[1]; col++) { iteratee(row, col); } } break; case RegionCardinality.FULL_ROWS: for (let row = region.rows[0]; row <= region.rows[1]; row++) { for (let col = 0; col < numCols; col++) { iteratee(row, col); } } break; case RegionCardinality.CELLS: for (let row = region.rows[0]; row <= region.rows[1]; row++) { for (let col = region.cols[0]; col <= region.cols[1]; col++) { iteratee(row, col); } } break; default: break; } } static regionContains(regionA, regionB) { // containsRegion expects an array of regions as the first param return Regions.overlapsRegion([regionA], regionB, false); } static intervalsEqual(ivalA, ivalB) { if (ivalA == null) { return ivalB == null; } else if (ivalB == null) { return false; } else { return ivalA[0] === ivalB[0] && ivalA[1] === ivalB[1]; } } static intervalContainsIndex(interval, index) { if (interval == null) { return false; } return interval[0] <= index && interval[1] >= index; } static intervalContains(ivalA, ivalB) { if (ivalA == null || ivalB == null) { return false; } return ivalA[0] <= ivalB[0] && ivalB[1] <= ivalA[1]; } static intervalOverlaps(ivalA, ivalB) { if (ivalA == null || ivalB == null) { return false; } if (ivalA[1] < ivalB[0] || ivalA[0] > ivalB[1]) { return false; } return true; } static rowFirstComparator(a, b) { const rowDiff = a[0] - b[0]; return rowDiff === 0 ? a[1] - b[1] : rowDiff; } static numericalComparator(a, b) { return a - b; } static normalizeInterval(coord, coord2) { if (coord2 == null) { coord2 = coord; } const interval = [coord, coord2]; interval.sort(Regions.numericalComparator); return interval; } } function intervalInRangeInclusive(interval, minInclusive, maxInclusive) { return (inRangeInclusive(interval[0], minInclusive, maxInclusive) && inRangeInclusive(interval[1], minInclusive, maxInclusive)); } function inRangeInclusive(value, minInclusive, maxInclusive) { return value >= minInclusive && value <= maxInclusive; } //# sourceMappingURL=regions.js.map