UNPKG

@blueprintjs/table

Version:

Scalable interactive table component

362 lines 20.5 kB
/* * Copyright 2021 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 { __assign } from "tslib"; import * as React from "react"; import { FocusMode } from "./common/cellTypes"; import { Clipboard } from "./common/clipboard"; import { Direction } from "./common/direction"; import { TABLE_COPY_FAILED } from "./common/errors"; import {} from "./common/grid"; import * as FocusedCellUtils from "./common/internal/focusedCellUtils"; import * as SelectionUtils from "./common/internal/selectionUtils"; import { RegionCardinality, Regions } from "./regions"; var TableHotkeys = /** @class */ (function () { function TableHotkeys(props, state, tableHandlers) { var _this = this; this.props = props; this.state = state; this.tableHandlers = tableHandlers; // Selection // ========= this.selectAll = function (shouldUpdateFocusedCell) { var selectionHandler = _this.tableHandlers.getEnabledSelectionHandler(RegionCardinality.FULL_TABLE); // clicking on upper left hand corner sets selection to "all" // regardless of current selection state (clicking twice does not deselect table) selectionHandler([Regions.table()]); if (shouldUpdateFocusedCell) { var focusMode = FocusedCellUtils.getFocusModeFromProps(_this.props); var newFocusedCellCoordinates = Regions.getFocusCellCoordinatesFromRegion(Regions.table()); var newFocusedRegion = FocusedCellUtils.toFocusedRegion(focusMode, newFocusedCellCoordinates); if (newFocusedRegion != null) { _this.tableHandlers.handleFocus(newFocusedRegion); } } }; this.handleSelectAllHotkey = function (e) { // prevent "real" select all from happening as well e.preventDefault(); e.stopPropagation(); // selecting-all via the keyboard should not move the focused cell. _this.selectAll(false); }; this.handleSelectionResizeUp = function (e) { return _this.handleSelectionResize(e, Direction.UP); }; this.handleSelectionResizeDown = function (e) { return _this.handleSelectionResize(e, Direction.DOWN); }; this.handleSelectionResizeLeft = function (e) { return _this.handleSelectionResize(e, Direction.LEFT); }; this.handleSelectionResizeRight = function (e) { return _this.handleSelectionResize(e, Direction.RIGHT); }; this.handleSelectionResize = function (e, direction) { e.preventDefault(); e.stopPropagation(); var _a = _this.state, focusedRegion = _a.focusedRegion, selectedRegions = _a.selectedRegions; var index = FocusedCellUtils.getFocusedOrLastSelectedIndex(selectedRegions, focusedRegion); if (index === undefined) { return; } var region = selectedRegions[index]; var nextRegion = SelectionUtils.resizeRegion(region, direction, focusedRegion); _this.updateSelectedRegionAtIndex(nextRegion, index); }; // Focus // ===== this.handleFocusMoveLeft = function (e) { return _this.handleFocusMove(e, Direction.LEFT); }; this.handleFocusMoveLeftInternal = function (e) { return _this.handleFocusMoveInternal(e, Direction.LEFT); }; this.handleFocusMoveRight = function (e) { return _this.handleFocusMove(e, Direction.RIGHT); }; this.handleFocusMoveRightInternal = function (e) { return _this.handleFocusMoveInternal(e, Direction.RIGHT); }; this.handleFocusMoveUp = function (e) { return _this.handleFocusMove(e, Direction.UP); }; this.handleFocusMoveUpInternal = function (e) { return _this.handleFocusMoveInternal(e, Direction.UP); }; this.handleFocusMoveDown = function (e) { return _this.handleFocusMove(e, Direction.DOWN); }; this.handleFocusMoveDownInternal = function (e) { return _this.handleFocusMoveInternal(e, Direction.DOWN); }; // no good way to call arrow-key keyboard events from tests /* istanbul ignore next */ this.handleFocusMove = function (e, direction) { var focusedRegion = _this.state.focusedRegion; if (focusedRegion == null) { // halt early if we have a selectedRegionTransform or something else in play that nixes // the focused cell. return; } var newFocusedRegion = TableHotkeys.moveFocusedRegionInDirection(focusedRegion, direction); if (_this.isOutOfBounds(newFocusedRegion)) { return; } e.preventDefault(); e.stopPropagation(); // change selection to match new focus region location var newSelectionRegions = [Regions.getRegionFromFocusedRegion(newFocusedRegion)]; var selectedRegionTransform = _this.props.selectedRegionTransform; var transformedSelectionRegions = selectedRegionTransform != null ? newSelectionRegions.map(function (region) { return selectedRegionTransform(region, e); }) : newSelectionRegions; _this.tableHandlers.handleSelection(transformedSelectionRegions); _this.tableHandlers.handleFocus(newFocusedRegion); // keep the focused region in view _this.scrollBodyToFocusedRegion(newFocusedRegion); }; // no good way to call arrow-key keyboard events from tests /* istanbul ignore next */ this.handleFocusMoveInternal = function (e, direction) { var _a = _this.state, focusedRegion = _a.focusedRegion, selectedRegions = _a.selectedRegions; if ((focusedRegion === null || focusedRegion === void 0 ? void 0 : focusedRegion.type) !== FocusMode.CELL) { // Move focus with in a selection is only supported for cell focus return; } var newFocusedCell = __assign({}, focusedRegion); // if we're not in any particular focus cell region, and one exists, go to the first cell of the first one if (focusedRegion.focusSelectionIndex == null && selectedRegions.length > 0) { var focusCellRegion = Regions.getCellRegionFromRegion(selectedRegions[0], _this.grid.numRows, _this.grid.numCols); newFocusedCell = { col: focusCellRegion.cols[0], focusSelectionIndex: 0, row: focusCellRegion.rows[0], type: FocusMode.CELL, }; } else { if (selectedRegions.length === 0) { _this.handleFocusMove(e, direction); return; } var focusCellRegion = Regions.getCellRegionFromRegion(selectedRegions[focusedRegion.focusSelectionIndex], _this.grid.numRows, _this.grid.numCols); if (focusCellRegion.cols[0] === focusCellRegion.cols[1] && focusCellRegion.rows[0] === focusCellRegion.rows[1] && selectedRegions.length === 1) { _this.handleFocusMove(e, direction); return; } switch (direction) { case Direction.UP: newFocusedCell = _this.moveFocusCell("row", "col", true, newFocusedCell, focusCellRegion); break; case Direction.LEFT: newFocusedCell = _this.moveFocusCell("col", "row", true, newFocusedCell, focusCellRegion); break; case Direction.DOWN: newFocusedCell = _this.moveFocusCell("row", "col", false, newFocusedCell, focusCellRegion); break; case Direction.RIGHT: newFocusedCell = _this.moveFocusCell("col", "row", false, newFocusedCell, focusCellRegion); break; default: break; } } if (_this.isOutOfBounds(newFocusedCell)) { return; } e.preventDefault(); e.stopPropagation(); _this.tableHandlers.handleFocus(newFocusedCell); // keep the focused cell in view _this.scrollBodyToFocusedRegion(newFocusedCell); }; this.scrollBodyToFocusedRegion = function (focusedRegion) { var row = focusedRegion.row; var col = FocusedCellUtils.getFocusedColumn(focusedRegion); var viewportRect = _this.state.viewportRect; if (viewportRect === undefined || _this.grid === undefined) { return; } var frozenRowsHeight = _this.grid.getCumulativeHeightBefore(_this.state.numFrozenRowsClamped); var frozenColumnsWidth = _this.grid.getCumulativeWidthBefore(_this.state.numFrozenColumnsClamped); // sort keys in normal CSS position order (per the trusty TRBL/"tr ouble" acronym) /* eslint-disable sort-keys */ var viewportBounds = { top: viewportRect.top, right: viewportRect.left + viewportRect.width, bottom: viewportRect.top + viewportRect.height, left: viewportRect.left, }; var _a = _this.tableHandlers.getHeaderDimensions(), columnHeaderHeight = _a.columnHeaderHeight, rowHeaderWidth = _a.rowHeaderWidth; // Bounds of the part of the viewport that contains visible, scrollable cells. var scrollableSectionBounds = { top: viewportBounds.top + columnHeaderHeight + frozenRowsHeight, right: viewportBounds.right, bottom: viewportBounds.bottom, left: viewportBounds.left + rowHeaderWidth + frozenColumnsWidth, }; // Cumulative col widths and row heights coordinates start do _not_ include header size. ViewportRect does. Add // header size so that we use the same origin. var focusedCellBounds = { top: _this.grid.getCumulativeHeightBefore(row) + columnHeaderHeight, right: _this.grid.getCumulativeWidthAt(col !== null && col !== void 0 ? col : 0) + rowHeaderWidth, bottom: _this.grid.getCumulativeHeightAt(row) + columnHeaderHeight, left: _this.grid.getCumulativeWidthBefore(col !== null && col !== void 0 ? col : 0) + rowHeaderWidth, }; /* eslint-enable sort-keys */ var ss = {}; // Vertical scroll var focusedCellHeight = focusedCellBounds.bottom - focusedCellBounds.top; var scrollableSectionHeight = scrollableSectionBounds.bottom - scrollableSectionBounds.top; var prevScrollTop = viewportBounds.top; if (focusedCellHeight > scrollableSectionHeight || focusedCellBounds.top < scrollableSectionBounds.top) { // scroll up (minus one pixel to avoid clipping the focused-cell border) ss.nextScrollTop = prevScrollTop - (scrollableSectionBounds.top - focusedCellBounds.top) - 1; } else if (scrollableSectionBounds.bottom < focusedCellBounds.bottom) { // scroll down ss.nextScrollTop = prevScrollTop + (focusedCellBounds.bottom - viewportBounds.bottom); } // Horizontal scroll if (col != null) { var focusedCellWidth = focusedCellBounds.right - focusedCellBounds.left; var scrollableSectionWidth = scrollableSectionBounds.right - scrollableSectionBounds.left; var prevScrollLeft = viewportBounds.left; if (focusedCellWidth > scrollableSectionWidth || focusedCellBounds.left < scrollableSectionBounds.left) { // scroll left (again minus one additional pixel) ss.nextScrollLeft = prevScrollLeft - (scrollableSectionBounds.left - focusedCellBounds.left) - 1; } else if (scrollableSectionBounds.right < focusedCellBounds.right) { // scroll right ss.nextScrollLeft = prevScrollLeft + (focusedCellBounds.right - viewportBounds.right); } } _this.tableHandlers.syncViewportPosition(ss); }; this.handleCopy = function (e) { var _a = _this.props, getCellClipboardData = _a.getCellClipboardData, onCopy = _a.onCopy; var selectedRegions = _this.state.selectedRegions; if (getCellClipboardData == null || _this.grid === undefined) { return; } // prevent "real" copy from being called e.preventDefault(); e.stopPropagation(); var cells = Regions.enumerateUniqueCells(selectedRegions, _this.grid.numRows, _this.grid.numCols); // non-null assertion because Column.defaultProps.cellRenderer is defined var sparse = Regions.sparseMapCells(cells, function (row, col) { return getCellClipboardData(row, col, _this.state.childrenArray[col].props.cellRenderer); }); if (sparse != null) { Clipboard.copyCells(sparse) .then(function () { return onCopy === null || onCopy === void 0 ? void 0 : onCopy(true); }) .catch(function (reason) { console.error(TABLE_COPY_FAILED, reason); onCopy === null || onCopy === void 0 ? void 0 : onCopy(false); }); } }; // no-op } TableHotkeys.prototype.setGrid = function (grid) { this.grid = grid; }; TableHotkeys.prototype.setProps = function (props) { this.props = props; }; TableHotkeys.prototype.setState = function (newState) { if (newState.focusedRegion != null && (this.state.focusedRegion == null || !FocusedCellUtils.areFocusedRegionsEqual(this.state.focusedRegion, newState.focusedRegion))) { this.scrollBodyToFocusedRegion(newState.focusedRegion); } this.state = newState; }; /** * Replaces the selected region at the specified array index, with the * region provided. */ TableHotkeys.prototype.updateSelectedRegionAtIndex = function (region, index) { var _a = this.props, children = _a.children, numRows = _a.numRows; var selectedRegions = this.state.selectedRegions; var numColumns = React.Children.count(children); var maxRowIndex = Math.max(0, numRows - 1); var maxColumnIndex = Math.max(0, numColumns - 1); var clampedNextRegion = Regions.clampRegion(region, maxRowIndex, maxColumnIndex); var nextSelectedRegions = Regions.update(selectedRegions, clampedNextRegion, index); this.tableHandlers.handleSelection(nextSelectedRegions); }; TableHotkeys.moveFocusedRegionInDirection = function (focusedRegion, direction) { switch (focusedRegion.type) { case FocusMode.CELL: return TableHotkeys.moveFocusedCellInDirection(focusedRegion, direction); case FocusMode.ROW: return TableHotkeys.moveFocusedRowInDirection(focusedRegion, direction); } }; TableHotkeys.moveFocusedRowInDirection = function (focusedRow, direction) { switch (direction) { case Direction.UP: return __assign(__assign({}, focusedRow), { focusSelectionIndex: 0, row: focusedRow.row - 1 }); case Direction.DOWN: return __assign(__assign({}, focusedRow), { focusSelectionIndex: 0, row: focusedRow.row + 1 }); case Direction.LEFT: case Direction.RIGHT: return __assign({}, focusedRow); } }; TableHotkeys.moveFocusedCellInDirection = function (focusedCell, direction) { switch (direction) { case Direction.UP: return __assign(__assign({}, focusedCell), { focusSelectionIndex: 0, row: focusedCell.row - 1 }); case Direction.DOWN: return __assign(__assign({}, focusedCell), { focusSelectionIndex: 0, row: focusedCell.row + 1 }); case Direction.LEFT: return __assign(__assign({}, focusedCell), { col: focusedCell.col - 1, focusSelectionIndex: 0 }); case Direction.RIGHT: return __assign(__assign({}, focusedCell), { col: focusedCell.col + 1, focusSelectionIndex: 0 }); } }; TableHotkeys.prototype.isOutOfBounds = function (focusedRegion) { var _a; var column = (_a = FocusedCellUtils.getFocusedColumn(focusedRegion)) !== null && _a !== void 0 ? _a : 0; return (focusedRegion.row < 0 || focusedRegion.row >= this.grid.numRows || column < 0 || column >= this.grid.numCols); }; // Quadrant refs // ============= TableHotkeys.prototype.moveFocusCell = function (primaryAxis, secondaryAxis, isUpOrLeft, newFocusedCell, focusCellRegion) { var selectedRegions = this.state.selectedRegions; var primaryAxisPlural = primaryAxis === "row" ? "rows" : "cols"; var secondaryAxisPlural = secondaryAxis === "row" ? "rows" : "cols"; var movementDirection = isUpOrLeft ? -1 : +1; var regionIntervalIndex = isUpOrLeft ? 1 : 0; // try moving the cell in the direction along the primary axis newFocusedCell[primaryAxis] += movementDirection; var isPrimaryIndexOutOfBounds = isUpOrLeft ? newFocusedCell[primaryAxis] < focusCellRegion[primaryAxisPlural][0] : newFocusedCell[primaryAxis] > focusCellRegion[primaryAxisPlural][1]; if (isPrimaryIndexOutOfBounds) { // if we moved outside the bounds of selection region, // move to the start (or end) of the primary axis, and move one along the secondary newFocusedCell[primaryAxis] = focusCellRegion[primaryAxisPlural][regionIntervalIndex]; newFocusedCell[secondaryAxis] += movementDirection; var isSecondaryIndexOutOfBounds = isUpOrLeft ? newFocusedCell[secondaryAxis] < focusCellRegion[secondaryAxisPlural][0] : newFocusedCell[secondaryAxis] > focusCellRegion[secondaryAxisPlural][1]; if (isSecondaryIndexOutOfBounds) { // if moving along the secondary also moves us outside // go to the start (or end) of the next (or previous region) // (note that if there's only one region you'll be moving to the opposite corner, which is fine) var newFocusCellSelectionIndex = newFocusedCell.focusSelectionIndex + movementDirection; // newFocusCellSelectionIndex should be one more (or less), unless we need to wrap around if (isUpOrLeft ? newFocusCellSelectionIndex < 0 : newFocusCellSelectionIndex >= selectedRegions.length) { newFocusCellSelectionIndex = isUpOrLeft ? selectedRegions.length - 1 : 0; } var newFocusCellRegion = Regions.getCellRegionFromRegion(selectedRegions[newFocusCellSelectionIndex], this.grid.numRows, this.grid.numCols); newFocusedCell = { col: newFocusCellRegion.cols[regionIntervalIndex], focusSelectionIndex: newFocusCellSelectionIndex, row: newFocusCellRegion.rows[regionIntervalIndex], type: FocusMode.CELL, }; } } return newFocusedCell; }; return TableHotkeys; }()); export { TableHotkeys }; //# sourceMappingURL=tableHotkeys.js.map