@blueprintjs/table
Version:
Scalable interactive table component
364 lines • 21.2 kB
JavaScript
"use strict";
/*
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TableHotkeys = void 0;
var tslib_1 = require("tslib");
var React = tslib_1.__importStar(require("react"));
var cellTypes_1 = require("./common/cellTypes");
var clipboard_1 = require("./common/clipboard");
var direction_1 = require("./common/direction");
var errors_1 = require("./common/errors");
var FocusedCellUtils = tslib_1.__importStar(require("./common/internal/focusedCellUtils"));
var SelectionUtils = tslib_1.__importStar(require("./common/internal/selectionUtils"));
var regions_1 = require("./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(regions_1.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_1.Regions.table()]);
if (shouldUpdateFocusedCell) {
var focusMode = FocusedCellUtils.getFocusModeFromProps(_this.props);
var newFocusedCellCoordinates = regions_1.Regions.getFocusCellCoordinatesFromRegion(regions_1.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_1.Direction.UP); };
this.handleSelectionResizeDown = function (e) { return _this.handleSelectionResize(e, direction_1.Direction.DOWN); };
this.handleSelectionResizeLeft = function (e) { return _this.handleSelectionResize(e, direction_1.Direction.LEFT); };
this.handleSelectionResizeRight = function (e) { return _this.handleSelectionResize(e, direction_1.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_1.Direction.LEFT); };
this.handleFocusMoveLeftInternal = function (e) { return _this.handleFocusMoveInternal(e, direction_1.Direction.LEFT); };
this.handleFocusMoveRight = function (e) { return _this.handleFocusMove(e, direction_1.Direction.RIGHT); };
this.handleFocusMoveRightInternal = function (e) { return _this.handleFocusMoveInternal(e, direction_1.Direction.RIGHT); };
this.handleFocusMoveUp = function (e) { return _this.handleFocusMove(e, direction_1.Direction.UP); };
this.handleFocusMoveUpInternal = function (e) { return _this.handleFocusMoveInternal(e, direction_1.Direction.UP); };
this.handleFocusMoveDown = function (e) { return _this.handleFocusMove(e, direction_1.Direction.DOWN); };
this.handleFocusMoveDownInternal = function (e) { return _this.handleFocusMoveInternal(e, direction_1.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_1.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) !== cellTypes_1.FocusMode.CELL) {
// Move focus with in a selection is only supported for cell focus
return;
}
var newFocusedCell = tslib_1.__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_1.Regions.getCellRegionFromRegion(selectedRegions[0], _this.grid.numRows, _this.grid.numCols);
newFocusedCell = {
col: focusCellRegion.cols[0],
focusSelectionIndex: 0,
row: focusCellRegion.rows[0],
type: cellTypes_1.FocusMode.CELL,
};
}
else {
if (selectedRegions.length === 0) {
_this.handleFocusMove(e, direction);
return;
}
var focusCellRegion = regions_1.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_1.Direction.UP:
newFocusedCell = _this.moveFocusCell("row", "col", true, newFocusedCell, focusCellRegion);
break;
case direction_1.Direction.LEFT:
newFocusedCell = _this.moveFocusCell("col", "row", true, newFocusedCell, focusCellRegion);
break;
case direction_1.Direction.DOWN:
newFocusedCell = _this.moveFocusCell("row", "col", false, newFocusedCell, focusCellRegion);
break;
case direction_1.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_1.Regions.enumerateUniqueCells(selectedRegions, _this.grid.numRows, _this.grid.numCols);
// non-null assertion because Column.defaultProps.cellRenderer is defined
var sparse = regions_1.Regions.sparseMapCells(cells, function (row, col) {
return getCellClipboardData(row, col, _this.state.childrenArray[col].props.cellRenderer);
});
if (sparse != null) {
clipboard_1.Clipboard.copyCells(sparse)
.then(function () { return onCopy === null || onCopy === void 0 ? void 0 : onCopy(true); })
.catch(function (reason) {
console.error(errors_1.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_1.Regions.clampRegion(region, maxRowIndex, maxColumnIndex);
var nextSelectedRegions = regions_1.Regions.update(selectedRegions, clampedNextRegion, index);
this.tableHandlers.handleSelection(nextSelectedRegions);
};
TableHotkeys.moveFocusedRegionInDirection = function (focusedRegion, direction) {
switch (focusedRegion.type) {
case cellTypes_1.FocusMode.CELL:
return TableHotkeys.moveFocusedCellInDirection(focusedRegion, direction);
case cellTypes_1.FocusMode.ROW:
return TableHotkeys.moveFocusedRowInDirection(focusedRegion, direction);
}
};
TableHotkeys.moveFocusedRowInDirection = function (focusedRow, direction) {
switch (direction) {
case direction_1.Direction.UP:
return tslib_1.__assign(tslib_1.__assign({}, focusedRow), { focusSelectionIndex: 0, row: focusedRow.row - 1 });
case direction_1.Direction.DOWN:
return tslib_1.__assign(tslib_1.__assign({}, focusedRow), { focusSelectionIndex: 0, row: focusedRow.row + 1 });
case direction_1.Direction.LEFT:
case direction_1.Direction.RIGHT:
return tslib_1.__assign({}, focusedRow);
}
};
TableHotkeys.moveFocusedCellInDirection = function (focusedCell, direction) {
switch (direction) {
case direction_1.Direction.UP:
return tslib_1.__assign(tslib_1.__assign({}, focusedCell), { focusSelectionIndex: 0, row: focusedCell.row - 1 });
case direction_1.Direction.DOWN:
return tslib_1.__assign(tslib_1.__assign({}, focusedCell), { focusSelectionIndex: 0, row: focusedCell.row + 1 });
case direction_1.Direction.LEFT:
return tslib_1.__assign(tslib_1.__assign({}, focusedCell), { col: focusedCell.col - 1, focusSelectionIndex: 0 });
case direction_1.Direction.RIGHT:
return tslib_1.__assign(tslib_1.__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_1.Regions.getCellRegionFromRegion(selectedRegions[newFocusCellSelectionIndex], this.grid.numRows, this.grid.numCols);
newFocusedCell = {
col: newFocusCellRegion.cols[regionIntervalIndex],
focusSelectionIndex: newFocusCellSelectionIndex,
row: newFocusCellRegion.rows[regionIntervalIndex],
type: cellTypes_1.FocusMode.CELL,
};
}
}
return newFocusedCell;
};
return TableHotkeys;
}());
exports.TableHotkeys = TableHotkeys;
//# sourceMappingURL=tableHotkeys.js.map