@sdziadkowiec/react-datasheet-grid
Version:
An Excel-like React component to create beautiful spreadsheets.
1,068 lines • 67.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DataSheetGrid = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = __importStar(require("react"));
const useColumnWidths_1 = require("../hooks/useColumnWidths");
const react_resize_detector_1 = require("react-resize-detector");
const useColumns_1 = require("../hooks/useColumns");
const useEdges_1 = require("../hooks/useEdges");
const useDeepEqualState_1 = require("../hooks/useDeepEqualState");
const useDocumentEventListener_1 = require("../hooks/useDocumentEventListener");
const useGetBoundingClientRect_1 = require("../hooks/useGetBoundingClientRect");
const AddRows_1 = require("./AddRows");
const useDebounceState_1 = require("../hooks/useDebounceState");
const fast_deep_equal_1 = __importDefault(require("fast-deep-equal"));
const ContextMenu_1 = require("./ContextMenu");
const copyPasting_1 = require("../utils/copyPasting");
const typeCheck_1 = require("../utils/typeCheck");
const tab_1 = require("../utils/tab");
const Grid_1 = require("./Grid");
const SelectionRect_1 = require("./SelectionRect");
const useRowHeights_1 = require("../hooks/useRowHeights");
const DEFAULT_DATA = [];
const DEFAULT_COLUMNS = [];
const DEFAULT_CREATE_ROW = () => ({});
const DEFAULT_EMPTY_CALLBACK = () => null;
const DEFAULT_DUPLICATE_ROW = ({ rowData, }) => (Object.assign({}, rowData));
// eslint-disable-next-line react/display-name
exports.DataSheetGrid = react_1.default.memo(react_1.default.forwardRef(({ value: data = DEFAULT_DATA, className, style, height: maxHeight = 400, onChange = DEFAULT_EMPTY_CALLBACK, columns: rawColumns = DEFAULT_COLUMNS, rowHeight = 40, headerRowHeight = typeof rowHeight === 'number' ? rowHeight : 40, gutterColumn, stickyRightColumn, frozenColumns = 0, rowKey, addRowsComponent: AddRowsComponent = AddRows_1.AddRows, createRow = DEFAULT_CREATE_ROW, autoAddRow = false, lockRows = false, disableExpandSelection = false, disableSmartDelete = false, duplicateRow = DEFAULT_DUPLICATE_ROW, contextMenuComponent: ContextMenuComponent = ContextMenu_1.ContextMenu, disableContextMenu: disableContextMenuRaw = false, onFocus = DEFAULT_EMPTY_CALLBACK, onBlur = DEFAULT_EMPTY_CALLBACK, onActiveCellChange = DEFAULT_EMPTY_CALLBACK, onSelectionChange = DEFAULT_EMPTY_CALLBACK, rowClassName, cellClassName, onScroll, }, ref) => {
var _a, _b, _c, _d, _e, _f;
const lastEditingCellRef = (0, react_1.useRef)(null);
const disableContextMenu = disableContextMenuRaw || lockRows;
const columns = (0, useColumns_1.useColumns)(rawColumns, gutterColumn, stickyRightColumn);
const hasStickyRightColumn = Boolean(stickyRightColumn);
const innerRef = (0, react_1.useRef)(null);
const outerRef = (0, react_1.useRef)(null);
const beforeTabIndexRef = (0, react_1.useRef)(null);
const afterTabIndexRef = (0, react_1.useRef)(null);
// Default value is 1 for the border
const [heightDiff, setHeightDiff] = (0, useDebounceState_1.useDebounceState)(1, 100);
const { getRowSize, totalSize, getRowIndex } = (0, useRowHeights_1.useRowHeights)({
value: data,
rowHeight,
});
// Height of the list (including scrollbars and borders) to display
const displayHeight = Math.min(maxHeight, headerRowHeight + totalSize(maxHeight) + heightDiff);
// Width and height of the scrollable area
const { width, height } = (0, react_resize_detector_1.useResizeDetector)({
targetRef: outerRef,
refreshMode: 'throttle',
refreshRate: 100,
});
setHeightDiff(height ? displayHeight - height : 0);
const edges = (0, useEdges_1.useEdges)(outerRef, width, height);
const { fullWidth, totalWidth: contentWidth, columnWidths, columnRights, } = (0, useColumnWidths_1.useColumnWidths)(columns, width);
// x,y coordinates of the right click
const [contextMenu, setContextMenu] = (0, react_1.useState)(null);
// Items of the context menu
const [contextMenuItems, setContextMenuItems] = (0, react_1.useState)([]);
// True when the active cell is being edited
const [editing, setEditing] = (0, react_1.useState)(false);
// Number of rows the user is expanding the selection by, always a number, even when not expanding selection
const [expandSelectionRowsCount, setExpandSelectionRowsCount] = (0, react_1.useState)(0);
// When not null, represents the index of the row from which we are expanding
const [expandingSelectionFromRowIndex, setExpandingSelectionFromRowIndex,] = (0, react_1.useState)(null);
// Highlighted cell, null when not focused
const [activeCell, setActiveCell] = (0, useDeepEqualState_1.useDeepEqualState)(null);
// The selection cell and the active cell are the two corners of the selection, null when nothing is selected
const [selectionCell, setSelectionCell] = (0, useDeepEqualState_1.useDeepEqualState)(null);
// Min and max of the current selection (rectangle defined by the active cell and the selection cell), null when nothing is selected
const selection = (0, react_1.useMemo)(() => activeCell &&
selectionCell && {
min: {
col: Math.min(activeCell.col, selectionCell.col),
row: Math.min(activeCell.row, selectionCell.row),
},
max: {
col: Math.max(activeCell.col, selectionCell.col),
row: Math.max(activeCell.row, selectionCell.row),
},
}, [activeCell, selectionCell]);
// Behavior of the selection when the user drags the mouse around
const [selectionMode, setSelectionMode] = (0, useDeepEqualState_1.useDeepEqualState)({
// True when the position of the cursor should impact the columns of the selection
columns: false,
// True when the position of the cursor should impact the rows of the selection
rows: false,
// True when the user is dragging the mouse around to select
active: false,
});
// Same as expandSelectionRowsCount but is null when we should not be able to expand the selection
const expandSelection = disableExpandSelection ||
editing ||
selectionMode.active ||
(activeCell === null || activeCell === void 0 ? void 0 : activeCell.row) === (data === null || data === void 0 ? void 0 : data.length) - 1 ||
(selection === null || selection === void 0 ? void 0 : selection.max.row) === (data === null || data === void 0 ? void 0 : data.length) - 1 ||
(activeCell &&
columns
.slice(((_a = selection === null || selection === void 0 ? void 0 : selection.min.col) !== null && _a !== void 0 ? _a : activeCell.col) + 1, ((_b = selection === null || selection === void 0 ? void 0 : selection.max.col) !== null && _b !== void 0 ? _b : activeCell.col) + 2)
.every((column) => column.disabled === true))
? null
: expandSelectionRowsCount;
const getInnerBoundingClientRect = (0, useGetBoundingClientRect_1.useGetBoundingClientRect)(innerRef);
const getOuterBoundingClientRect = (0, useGetBoundingClientRect_1.useGetBoundingClientRect)(outerRef);
// Blur any element on focusing the grid
(0, react_1.useEffect)(() => {
var _a;
if (activeCell !== null) {
;
document.activeElement.blur();
(_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.removeAllRanges();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeCell !== null]);
// Extract the coordinates of the cursor from a mouse event
const getCursorIndex = (0, react_1.useCallback)((event, force = false, includeSticky = false) => {
const innerBoundingClientRect = getInnerBoundingClientRect(force);
const outerBoundingClientRect = includeSticky && getOuterBoundingClientRect(force);
if (innerBoundingClientRect && columnRights && columnWidths) {
let x = event.clientX - innerBoundingClientRect.left;
let y = event.clientY - innerBoundingClientRect.top;
if (outerBoundingClientRect) {
if (event.clientY - outerBoundingClientRect.top <=
headerRowHeight) {
y = 0;
}
if (event.clientX - outerBoundingClientRect.left <=
columnWidths[0]) {
x = 0;
}
if (hasStickyRightColumn &&
outerBoundingClientRect.right - event.clientX <=
columnWidths[columnWidths.length - 1]) {
x = columnRights[columnRights.length - 2] + 1;
}
}
return {
col: columnRights.findIndex((right) => x < right) - 1,
row: getRowIndex(y - headerRowHeight),
};
}
return null;
}, [
columnRights,
columnWidths,
data.length,
getInnerBoundingClientRect,
getOuterBoundingClientRect,
headerRowHeight,
hasStickyRightColumn,
getRowIndex,
]);
const dataRef = (0, react_1.useRef)(data);
dataRef.current = data;
const isCellDisabled = (0, react_1.useCallback)((cell) => {
const disabled = columns[cell.col + 1].disabled;
return Boolean(typeof disabled === 'function'
? disabled({
rowData: dataRef.current[cell.row],
rowIndex: cell.row,
})
: disabled);
}, [columns]);
const insertRowAfter = (0, react_1.useCallback)((row, count = 1) => {
if (lockRows) {
return;
}
setSelectionCell(null);
setEditing(false);
onChange([
...dataRef.current.slice(0, row + 1),
...new Array(count).fill(0).map(createRow),
...dataRef.current.slice(row + 1),
], [
{
type: 'CREATE',
fromRowIndex: row + 1,
toRowIndex: row + 1 + count,
},
]);
setActiveCell((a) => ({
col: (a === null || a === void 0 ? void 0 : a.col) || 0,
row: row + count,
doNotScrollX: true,
}));
}, [createRow, lockRows, onChange, setActiveCell, setSelectionCell]);
const duplicateRows = (0, react_1.useCallback)((rowMin, rowMax = rowMin) => {
if (lockRows) {
return;
}
onChange([
...dataRef.current.slice(0, rowMax + 1),
...dataRef.current
.slice(rowMin, rowMax + 1)
.map((rowData, i) => duplicateRow({ rowData, rowIndex: i + rowMin })),
...dataRef.current.slice(rowMax + 1),
], [
{
type: 'CREATE',
fromRowIndex: rowMax + 1,
toRowIndex: rowMax + 2 + rowMax - rowMin,
},
]);
setActiveCell({ col: 0, row: rowMax + 1, doNotScrollX: true });
setSelectionCell({
col: columns.length - (hasStickyRightColumn ? 3 : 2),
row: 2 * rowMax - rowMin + 1,
doNotScrollX: true,
});
setEditing(false);
}, [
columns.length,
duplicateRow,
lockRows,
onChange,
setActiveCell,
setSelectionCell,
hasStickyRightColumn,
]);
// Scroll to any given cell making sure it is in view
const scrollTo = (0, react_1.useCallback)((cell) => {
if (!height || !width) {
return;
}
if (!cell.doNotScrollY) {
// Align top
const topMax = getRowSize(cell.row).top;
// Align bottom
const topMin = getRowSize(cell.row).top +
getRowSize(cell.row).height +
headerRowHeight -
height +
1;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const scrollTop = outerRef.current.scrollTop;
if (scrollTop > topMax) {
outerRef.current.scrollTop = topMax;
}
else if (scrollTop < topMin) {
outerRef.current.scrollTop = topMin;
}
}
if (columnRights &&
columnWidths &&
outerRef.current &&
!cell.doNotScrollX) {
// Align left
const leftMax = columnRights[cell.col] - columnRights[0];
// Align right
const leftMin = columnRights[cell.col] +
columnWidths[cell.col + 1] +
(hasStickyRightColumn
? columnWidths[columnWidths.length - 1]
: 0) -
width +
1;
const scrollLeft = outerRef.current.scrollLeft;
if (scrollLeft > leftMax) {
outerRef.current.scrollLeft = leftMax;
}
else if (scrollLeft < leftMin) {
outerRef.current.scrollLeft = leftMin;
}
}
}, [
height,
width,
headerRowHeight,
columnRights,
columnWidths,
getRowSize,
hasStickyRightColumn,
]);
// Scroll to the selectionCell cell when it changes
(0, react_1.useEffect)(() => {
if (selectionCell) {
scrollTo(selectionCell);
}
}, [selectionCell, scrollTo]);
// Scroll to the active cell when it changes
(0, react_1.useEffect)(() => {
if (activeCell) {
scrollTo(activeCell);
}
}, [activeCell, scrollTo]);
const setRowData = (0, react_1.useCallback)((rowIndex, item) => {
var _a, _b;
onChange([
...(_a = dataRef.current) === null || _a === void 0 ? void 0 : _a.slice(0, rowIndex),
item,
...(_b = dataRef.current) === null || _b === void 0 ? void 0 : _b.slice(rowIndex + 1),
], [
{
type: 'UPDATE',
fromRowIndex: rowIndex,
toRowIndex: rowIndex + 1,
},
]);
}, [onChange]);
const deleteRows = (0, react_1.useCallback)((rowMin, rowMax = rowMin) => {
if (lockRows) {
return;
}
setEditing(false);
setActiveCell((a) => {
const row = Math.min(dataRef.current.length - 2 - rowMax + rowMin, rowMin);
if (row < 0) {
return null;
}
return a && { col: a.col, row };
});
setSelectionCell(null);
onChange([
...dataRef.current.slice(0, rowMin),
...dataRef.current.slice(rowMax + 1),
], [
{
type: 'DELETE',
fromRowIndex: rowMin,
toRowIndex: rowMax + 1,
},
]);
}, [lockRows, onChange, setActiveCell, setSelectionCell]);
const deleteSelection = (0, react_1.useCallback)((_smartDelete = true) => {
const smartDelete = _smartDelete && !disableSmartDelete;
if (!activeCell) {
return;
}
const min = (selection === null || selection === void 0 ? void 0 : selection.min) || activeCell;
const max = (selection === null || selection === void 0 ? void 0 : selection.max) || activeCell;
if (data
.slice(min.row, max.row + 1)
.every((rowData, i) => columns.every((column) => column.isCellEmpty({ rowData, rowIndex: i + min.row })))) {
if (smartDelete) {
deleteRows(min.row, max.row);
}
return;
}
const newData = [...data];
for (let row = min.row; row <= max.row; ++row) {
for (let col = min.col; col <= max.col; ++col) {
if (!isCellDisabled({ col, row })) {
const { deleteValue = ({ rowData }) => rowData } = columns[col + 1];
newData[row] = deleteValue({
rowData: newData[row],
rowIndex: row,
});
}
}
}
if (smartDelete && (0, fast_deep_equal_1.default)(newData, data)) {
setActiveCell({ col: 0, row: min.row, doNotScrollX: true });
setSelectionCell({
col: columns.length - (hasStickyRightColumn ? 3 : 2),
row: max.row,
doNotScrollX: true,
});
return;
}
onChange(newData, [
{
type: 'UPDATE',
fromRowIndex: min.row,
toRowIndex: max.row + 1,
},
]);
}, [
activeCell,
columns,
data,
deleteRows,
isCellDisabled,
onChange,
selection === null || selection === void 0 ? void 0 : selection.max,
selection === null || selection === void 0 ? void 0 : selection.min,
setActiveCell,
setSelectionCell,
hasStickyRightColumn,
]);
const stopEditing = (0, react_1.useCallback)(({ nextRow = true } = {}) => {
if ((activeCell === null || activeCell === void 0 ? void 0 : activeCell.row) === dataRef.current.length - 1) {
if (nextRow && autoAddRow) {
insertRowAfter(activeCell.row);
}
else {
setEditing(false);
}
}
else {
setEditing(false);
if (nextRow) {
setActiveCell((a) => a && { col: a.col, row: a.row + 1 });
}
}
}, [activeCell === null || activeCell === void 0 ? void 0 : activeCell.row, autoAddRow, insertRowAfter, setActiveCell]);
const onCopy = (0, react_1.useCallback)((event) => __awaiter(void 0, void 0, void 0, function* () {
var _g, _h;
if (!editing && activeCell) {
const copyData = [];
const min = (selection === null || selection === void 0 ? void 0 : selection.min) || activeCell;
const max = (selection === null || selection === void 0 ? void 0 : selection.max) || activeCell;
for (let row = min.row; row <= max.row; ++row) {
copyData.push([]);
for (let col = min.col; col <= max.col; ++col) {
const { copyValue = () => null } = columns[col + 1];
copyData[row - min.row].push(copyValue({ rowData: data[row], rowIndex: row }));
}
}
const textPlain = copyData.map((row) => row.join('\t')).join('\n');
const textHtml = `<table>${copyData
.map((row) => `<tr>${row
.map((cell) => `<td>${(0, copyPasting_1.encodeHtml)(String(cell !== null && cell !== void 0 ? cell : '')).replace(/\n/g, '<br/>')}</td>`)
.join('')}</tr>`)
.join('')}</table>`;
if (event !== undefined) {
(_g = event.clipboardData) === null || _g === void 0 ? void 0 : _g.setData('text/plain', textPlain);
(_h = event.clipboardData) === null || _h === void 0 ? void 0 : _h.setData('text/html', textHtml);
event.preventDefault();
return;
}
let success = false;
if (navigator.clipboard.write !== undefined) {
const textBlob = new Blob([textPlain], {
type: 'text/plain',
});
const htmlBlob = new Blob([textHtml], { type: 'text/html' });
const clipboardData = [
new ClipboardItem({
'text/plain': textBlob,
'text/html': htmlBlob,
}),
];
yield navigator.clipboard.write(clipboardData).then(() => {
success = true;
});
}
else if (navigator.clipboard.writeText !== undefined) {
yield navigator.clipboard.writeText(textPlain).then(() => {
success = true;
});
}
else if (document.execCommand !== undefined) {
const result = document.execCommand('copy');
if (result) {
success = true;
}
}
if (!success) {
alert('This action is unavailable in your browser, but you can still use Ctrl+C for copy or Ctrl+X for cut');
}
}
}), [activeCell, columns, data, editing, selection]);
(0, useDocumentEventListener_1.useDocumentEventListener)('copy', onCopy);
const onCopyWithHeaders = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
var _j;
if (!editing && activeCell) {
const copyData = [];
const headerData = [];
const min = (selection === null || selection === void 0 ? void 0 : selection.min) || activeCell;
const max = (selection === null || selection === void 0 ? void 0 : selection.max) || activeCell;
// Extract headers for selected columns
for (let col = min.col; col <= max.col; ++col) {
const columnTitle = (_j = columns[col + 1]) === null || _j === void 0 ? void 0 : _j.title;
let headerText;
if (typeof columnTitle === 'string') {
headerText = columnTitle;
}
else if (typeof columnTitle === 'number') {
headerText = String(columnTitle);
}
else if (columnTitle && typeof columnTitle === 'object') {
// For React elements, try to extract text content
try {
headerText = String(columnTitle);
}
catch (_k) {
headerText = `Column ${col + 1}`;
}
}
else {
headerText = `Column ${col + 1}`;
}
headerData.push(headerText);
}
// Add header row to copy data
copyData.push(headerData);
// Extract data for selected cells
for (let row = min.row; row <= max.row; ++row) {
copyData.push([]);
for (let col = min.col; col <= max.col; ++col) {
const { copyValue = () => null } = columns[col + 1];
copyData[row - min.row + 1].push(copyValue({ rowData: data[row], rowIndex: row }));
}
}
const textPlain = copyData.map((row) => row.join('\t')).join('\n');
const textHtml = `<table>${copyData
.map((row, index) => `<tr>${row
.map((cell) => `<t${index === 0 ? 'h' : 'd'}>${(0, copyPasting_1.encodeHtml)(String(cell !== null && cell !== void 0 ? cell : '')).replace(/\n/g, '<br/>')}</t${index === 0 ? 'h' : 'd'}>`)
.join('')}</tr>`)
.join('')}</table>`;
let success = false;
if (navigator.clipboard.write !== undefined) {
const textBlob = new Blob([textPlain], {
type: 'text/plain',
});
const htmlBlob = new Blob([textHtml], { type: 'text/html' });
const clipboardData = [
new ClipboardItem({
'text/plain': textBlob,
'text/html': htmlBlob,
}),
];
yield navigator.clipboard.write(clipboardData).then(() => {
success = true;
});
}
else if (navigator.clipboard.writeText !== undefined) {
yield navigator.clipboard.writeText(textPlain).then(() => {
success = true;
});
}
else if (document.execCommand !== undefined) {
const result = document.execCommand('copy');
if (result) {
success = true;
}
}
if (!success) {
alert('This action is unavailable in your browser, but you can still use Ctrl+C for copy or Ctrl+X for cut');
}
}
}), [activeCell, columns, data, editing, selection]);
const onCut = (0, react_1.useCallback)((event) => {
if (!editing && activeCell) {
onCopy(event);
deleteSelection(false);
}
}, [activeCell, deleteSelection, editing, onCopy]);
(0, useDocumentEventListener_1.useDocumentEventListener)('cut', onCut);
const applyPasteDataToDatasheet = (0, react_1.useCallback)((pasteData) => __awaiter(void 0, void 0, void 0, function* () {
var _l, _m;
if (!editing && activeCell) {
const min = (selection === null || selection === void 0 ? void 0 : selection.min) || activeCell;
const max = (selection === null || selection === void 0 ? void 0 : selection.max) || activeCell;
const results = yield Promise.all(pasteData[0].map((_, columnIndex) => {
var _a, _b;
const prePasteValues = (_a = columns[min.col + columnIndex + 1]) === null || _a === void 0 ? void 0 : _a.prePasteValues;
const values = pasteData.map((row) => row[columnIndex]);
return (_b = prePasteValues === null || prePasteValues === void 0 ? void 0 : prePasteValues(values)) !== null && _b !== void 0 ? _b : values;
}));
pasteData = pasteData.map((_, rowIndex) => results.map((column) => column[rowIndex]));
// Paste single row
// Paste single row
if (pasteData.length === 1) {
const newData = [...data];
for (let columnIndex = 0; columnIndex < pasteData[0].length; columnIndex++) {
const pasteValue = (_l = columns[min.col + columnIndex + 1]) === null || _l === void 0 ? void 0 : _l.pasteValue;
if (pasteValue) {
for (let rowIndex = min.row; rowIndex <= max.row; rowIndex++) {
if (!isCellDisabled({
col: columnIndex + min.col,
row: rowIndex,
})) {
newData[rowIndex] = yield pasteValue({
rowData: newData[rowIndex],
value: pasteData[0][columnIndex],
rowIndex,
});
}
}
}
}
onChange(newData, [
{
type: 'UPDATE',
fromRowIndex: min.row,
toRowIndex: max.row + 1,
},
]);
setActiveCell({ col: min.col, row: min.row });
setSelectionCell({
col: Math.min(min.col + pasteData[0].length - 1, columns.length - (hasStickyRightColumn ? 3 : 2)),
row: max.row,
});
}
else {
// Paste multiple rows
let newData = [...data];
const missingRows = min.row + pasteData.length - data.length;
if (missingRows > 0) {
if (!lockRows) {
newData = [
...newData,
...new Array(missingRows).fill(0).map(() => createRow()),
];
}
else {
pasteData.splice(pasteData.length - missingRows, missingRows);
}
}
for (let columnIndex = 0; columnIndex < pasteData[0].length &&
min.col + columnIndex <
columns.length - (hasStickyRightColumn ? 2 : 1); columnIndex++) {
const pasteValue = (_m = columns[min.col + columnIndex + 1]) === null || _m === void 0 ? void 0 : _m.pasteValue;
if (pasteValue) {
for (let rowIndex = 0; rowIndex < pasteData.length; rowIndex++) {
if (!isCellDisabled({
col: min.col + columnIndex,
row: min.row + rowIndex,
})) {
newData[min.row + rowIndex] = yield pasteValue({
rowData: newData[min.row + rowIndex],
value: pasteData[rowIndex][columnIndex],
rowIndex: min.row + rowIndex,
});
}
}
}
}
const operations = [
{
type: 'UPDATE',
fromRowIndex: min.row,
toRowIndex: min.row +
pasteData.length -
(!lockRows && missingRows > 0 ? missingRows : 0),
},
];
if (missingRows > 0 && !lockRows) {
operations.push({
type: 'CREATE',
fromRowIndex: min.row + pasteData.length - missingRows,
toRowIndex: min.row + pasteData.length,
});
}
onChange(newData, operations);
setActiveCell({ col: min.col, row: min.row });
setSelectionCell({
col: Math.min(min.col + pasteData[0].length - 1, columns.length - (hasStickyRightColumn ? 3 : 2)),
row: min.row + pasteData.length - 1,
});
}
}
}), [
activeCell,
columns,
createRow,
data,
editing,
hasStickyRightColumn,
isCellDisabled,
lockRows,
onChange,
selection === null || selection === void 0 ? void 0 : selection.max,
selection === null || selection === void 0 ? void 0 : selection.min,
setActiveCell,
setSelectionCell,
]);
const onPaste = (0, react_1.useCallback)((event) => {
var _a, _b, _c, _d, _e, _f;
if (activeCell && !editing) {
let pasteData = [['']];
if ((_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.types.includes('text/html')) {
pasteData = (0, copyPasting_1.parseTextHtmlData)((_b = event.clipboardData) === null || _b === void 0 ? void 0 : _b.getData('text/html'));
}
else if ((_c = event.clipboardData) === null || _c === void 0 ? void 0 : _c.types.includes('text/plain')) {
pasteData = (0, copyPasting_1.parseTextPlainData)((_d = event.clipboardData) === null || _d === void 0 ? void 0 : _d.getData('text/plain'));
}
else if ((_e = event.clipboardData) === null || _e === void 0 ? void 0 : _e.types.includes('text')) {
pasteData = (0, copyPasting_1.parseTextPlainData)((_f = event.clipboardData) === null || _f === void 0 ? void 0 : _f.getData('text'));
}
applyPasteDataToDatasheet(pasteData);
event.preventDefault();
}
}, [activeCell, applyPasteDataToDatasheet, editing]);
(0, useDocumentEventListener_1.useDocumentEventListener)('paste', onPaste);
const onMouseDown = (0, react_1.useCallback)((event) => {
var _a, _b, _c;
if (contextMenu && contextMenuItems.length) {
return;
}
const rightClick = event.button === 2 || (event.button === 0 && event.ctrlKey);
const clickInside = ((_a = innerRef.current) === null || _a === void 0 ? void 0 : _a.contains(event.target)) || false;
const cursorIndex = clickInside
? getCursorIndex(event, true, true)
: null;
if (!clickInside &&
editing &&
activeCell &&
columns[activeCell.col + 1].keepFocus) {
return;
}
if (event.target instanceof HTMLElement &&
event.target.className.includes('dsg-expand-rows-indicator')) {
setExpandingSelectionFromRowIndex(Math.max((_b = activeCell === null || activeCell === void 0 ? void 0 : activeCell.row) !== null && _b !== void 0 ? _b : 0, (_c = selection === null || selection === void 0 ? void 0 : selection.max.row) !== null && _c !== void 0 ? _c : 0));
return;
}
const clickOnActiveCell = cursorIndex &&
activeCell &&
activeCell.col === cursorIndex.col &&
activeCell.row === cursorIndex.row &&
!isCellDisabled(activeCell);
if (clickOnActiveCell && editing) {
return;
}
const clickOnStickyRightColumn = (cursorIndex === null || cursorIndex === void 0 ? void 0 : cursorIndex.col) === columns.length - 2 && hasStickyRightColumn;
const rightClickInSelection = rightClick &&
selection &&
cursorIndex &&
cursorIndex.row >= selection.min.row &&
cursorIndex.row <= selection.max.row &&
cursorIndex.col >= selection.min.col &&
cursorIndex.col <= selection.max.col;
const rightClickOnSelectedHeaders = rightClick &&
selection &&
cursorIndex &&
cursorIndex.row === -1 &&
cursorIndex.col >= selection.min.col &&
cursorIndex.col <= selection.max.col;
const rightClickOnSelectedGutter = rightClick &&
selection &&
cursorIndex &&
cursorIndex.row >= selection.min.row &&
cursorIndex.row <= selection.max.row &&
cursorIndex.col === -1;
const clickOnSelectedStickyRightColumn = clickOnStickyRightColumn &&
selection &&
cursorIndex &&
cursorIndex.row >= selection.min.row &&
cursorIndex.row <= selection.max.row;
if (rightClick && !disableContextMenu) {
setContextMenu({
x: event.clientX,
y: event.clientY,
cursorIndex: cursorIndex,
});
}
if ((!(event.shiftKey && activeCell) || rightClick) &&
data.length > 0) {
setActiveCell(cursorIndex && {
col: (rightClickInSelection || rightClickOnSelectedHeaders) &&
activeCell
? activeCell.col
: Math.max(0, clickOnStickyRightColumn ? 0 : cursorIndex.col),
row: (rightClickInSelection ||
rightClickOnSelectedGutter ||
clickOnSelectedStickyRightColumn) &&
activeCell
? activeCell.row
: Math.max(0, cursorIndex.row),
doNotScrollX: Boolean((rightClickInSelection && activeCell) ||
clickOnStickyRightColumn ||
cursorIndex.col === -1),
doNotScrollY: Boolean((rightClickInSelection && activeCell) ||
cursorIndex.row === -1),
});
}
if (clickOnActiveCell && !rightClick) {
lastEditingCellRef.current = activeCell;
}
setEditing(Boolean(clickOnActiveCell && !rightClick));
setSelectionMode(cursorIndex && !rightClick
? {
columns: (cursorIndex.col !== -1 && !clickOnStickyRightColumn) ||
Boolean(event.shiftKey && activeCell),
rows: cursorIndex.row !== -1 ||
Boolean(event.shiftKey && activeCell),
active: true,
}
: {
columns: false,
rows: false,
active: false,
});
if (event.shiftKey && activeCell && !rightClick) {
setSelectionCell(cursorIndex && {
col: Math.max(0, cursorIndex.col - (clickOnStickyRightColumn ? 1 : 0)),
row: Math.max(0, cursorIndex.row),
});
}
else if (!rightClickInSelection) {
if (cursorIndex &&
((cursorIndex === null || cursorIndex === void 0 ? void 0 : cursorIndex.col) === -1 ||
(cursorIndex === null || cursorIndex === void 0 ? void 0 : cursorIndex.row) === -1 ||
clickOnStickyRightColumn)) {
let col = cursorIndex.col;
let row = cursorIndex.row;
let doNotScrollX = false;
let doNotScrollY = false;
if (cursorIndex.col === -1 || clickOnStickyRightColumn) {
col = columns.length - (hasStickyRightColumn ? 3 : 2);
doNotScrollX = true;
}
if (cursorIndex.row === -1) {
row = data.length - 1;
doNotScrollY = true;
}
if (rightClickOnSelectedHeaders && selectionCell) {
col = selectionCell.col;
doNotScrollY = true;
}
if ((rightClickOnSelectedGutter ||
clickOnSelectedStickyRightColumn) &&
selectionCell) {
row = selectionCell.row;
doNotScrollX = true;
}
setSelectionCell({ col, row, doNotScrollX, doNotScrollY });
}
else {
setSelectionCell(null);
}
if (clickInside) {
event.preventDefault();
}
}
}, [
contextMenu,
contextMenuItems.length,
getCursorIndex,
editing,
activeCell,
columns,
isCellDisabled,
selection,
hasStickyRightColumn,
disableContextMenu,
setSelectionMode,
setActiveCell,
setSelectionCell,
selectionCell,
data.length,
]);
(0, useDocumentEventListener_1.useDocumentEventListener)('mousedown', onMouseDown);
const onMouseUp = (0, react_1.useCallback)(() => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
if (expandingSelectionFromRowIndex !== null) {
if (expandSelectionRowsCount > 0 && activeCell) {
let copyData = [];
const min = (selection === null || selection === void 0 ? void 0 : selection.min) || activeCell;
const max = (selection === null || selection === void 0 ? void 0 : selection.max) || activeCell;
for (let row = min.row; row <= max.row; ++row) {
copyData.push([]);
for (let col = min.col; col <= max.col; ++col) {
const { copyValue = () => null } = columns[col + 1];
copyData[row - min.row].push(String((_a = copyValue({ rowData: data[row], rowIndex: row })) !== null && _a !== void 0 ? _a : ''));
}
}
Promise.all(copyData[0].map((_, columnIndex) => {
var _a, _b;
const prePasteValues = (_a = columns[min.col + columnIndex + 1]) === null || _a === void 0 ? void 0 : _a.prePasteValues;
const values = copyData.map((row) => row[columnIndex]);
return (_b = prePasteValues === null || prePasteValues === void 0 ? void 0 : prePasteValues(values)) !== null && _b !== void 0 ? _b : values;
})).then((results) => {
var _a;
copyData = copyData.map((_, rowIndex) => results.map((column) => column[rowIndex]));
const newData = [...data];
for (let columnIndex = 0; columnIndex < copyData[0].length; columnIndex++) {
const pasteValue = (_a = columns[min.col + columnIndex + 1]) === null || _a === void 0 ? void 0 : _a.pasteValue;
if (pasteValue) {
for (let rowIndex = max.row + 1; rowIndex <= max.row + expandSelectionRowsCount; rowIndex++) {
if (!isCellDisabled({
col: columnIndex + min.col,
row: rowIndex,
})) {
newData[rowIndex] = pasteValue({
rowData: newData[rowIndex],
value: copyData[(rowIndex - max.row - 1) % copyData.length][columnIndex],
rowIndex,
});
}
}
}
}
onChange(newData, [
{
type: 'UPDATE',
fromRowIndex: max.row + 1,
toRowIndex: max.row + 1 + expandSelectionRowsCount,
},
]);
});
setExpandSelectionRowsCount(0);
setActiveCell({
col: Math.min((_b = activeCell === null || activeCell === void 0 ? void 0 : activeCell.col) !== null && _b !== void 0 ? _b : Infinity, (_c = selection === null || selection === void 0 ? void 0 : selection.min.col) !== null && _c !== void 0 ? _c : Infinity),
row: Math.min((_d = activeCell === null || activeCell === void 0 ? void 0 : activeCell.row) !== null && _d !== void 0 ? _d : Infinity, (_e = selection === null || selection === void 0 ? void 0 : selection.min.row) !== null && _e !== void 0 ? _e : Infinity),
doNotScrollX: true,
doNotScrollY: true,
});
setSelectionCell({
col: Math.max((_f = activeCell === null || activeCell === void 0 ? void 0 : activeCell.col) !== null && _f !== void 0 ? _f : 0, (_g = selection === null || selection === void 0 ? void 0 : selection.max.col) !== null && _g !== void 0 ? _g : 0),
row: Math.max((_h = activeCell === null || activeCell === void 0 ? void 0 : activeCell.row) !== null && _h !== void 0 ? _h : 0, (_j = selection === null || selection === void 0 ? void 0 : selection.max.row) !== null && _j !== void 0 ? _j : 0) +
expandSelectionRowsCount,
});
}
setExpandingSelectionFromRowIndex(null);
}
setSelectionMode({
columns: false,
rows: false,
active: false,
});
}, [
expandingSelectionFromRowIndex,
setSelectionMode,
expandSelectionRowsCount,
activeCell,
selection === null || selection === void 0 ? void 0 : selection.min,
selection === null || selection === void 0 ? void 0 : selection.max,
data,
onChange,
setActiveCell,
setSelectionCell,
columns,
isCellDisabled,
]);
(0, useDocumentEventListener_1.useDocumentEventListener)('mouseup', onMouseUp);
const onMouseMove = (0, react_1.useCallback)((event) => {
if (expandingSelectionFromRowIndex !== null) {
const cursorIndex = getCursorIndex(event);
if (cursorIndex) {
setExpandSelectionRowsCount(Math.max(0, cursorIndex.row - expandingSelectionFromRowIndex));
scrollTo({
col: cursorIndex.col,
row: Math.max(cursorIndex.row, expandingSelectionFromRowIndex),
});
}
}
if (selectionMode.active) {
const cursorIndex = getCursorIndex(event);
const lastColumnIndex = columns.length - (hasStickyRightColumn ? 3 : 2);
setSelectionCell(cursorIndex && {
col: selectionMode.columns
? Math.max(0, Math.min(lastColumnIndex, cursorIndex.col))
: lastColumnIndex,
row: selectionMode.rows
? Math.max(0, cursorIndex.row)
: data.length - 1,
doNotScrollX: !selectionMode.columns,
doNotScrollY: !selectionMode.rows,
});
setEditing(false);
}
}, [
scrollTo,
selectionMode.active,
selectionMode.columns,
selectionMode.rows,
getCursorIndex,
columns.length,
hasStickyRightColumn,
setSelectionCell,
data.length,
expandingSelectionFromRowIndex,
]);
(0, useDocumentEventListener_1.useDocumentEventListener)('mousemove', onMouseMove);
const onKeyDown = (0, react_1.useCallback)((event) => {
var _a;
if (!activeCell) {
return;
}
if (event.isComposing) {
return;
}
// Tab from last cell of a row
if (event.key === 'Tab' &&
!event.shiftKey &&
activeCell.col ===
columns.length - (hasStickyRightColumn ? 3 : 2) &&
!columns[activeCell.col + 1].disableKeys) {
// Last row
if (activeCell.row === data.length - 1) {
if (afterTabIndexRef.current) {
event.preventDefault();
setActiveCell(null);
setSelectionCell(null);
setEditing(false);
const allElements = (0, tab_1.getAllTabbableElements)();
const index = allElements.indexOf(afterTabIndexRef.current);
allElements[(index + 1) % allElements.length].focus();
return;
}
}
else {
setActiveCell((cell) => { var _a; return ({ col: 0, row: ((_a = cell === null || cell === void 0 ? void 0 : cell.row) !== null && _a !== void 0 ? _a : 0) + 1 }); });
setSelectionCell(null);
setEditing(false);
event.preventDefault();
return;
}
}
// Shift+Tab from first cell of a row
if (event.key === 'Tab' &&
event.shiftKey &&
activeCell.col === 0 &&
!columns[activeCell.col + 1].disableKeys) {
// First row
if (activeCell.row === 0) {
if (beforeTabIndexRef.current) {
event.preventDefault();
setActiveCell(null);
setSelectionCell(null);
setEditing(false);
const allElements = (0, tab_1.getAllTabbableElements)();
const index = allElements.indexOf(beforeTabIndexRef.current);
allElements[(index - 1 + allElements.length) % allElements.length].focus();
return;
}
}
else {
setActiveCell((cell) => {
var _a;
return ({
col: columns.length - (hasStickyRightColumn ? 3 : 2),
row: ((_a = cell === null || cell === void 0 ? void 0 : cell.row) !== null && _a !== void 0 ? _a : 1) - 1,
});
});
setSelectionCell(null);
setEditing(false);
event.preventDefault();
return;
}
}
if (((_a = event.key) === null || _a === void 0 ? void 0 : _a.startsWith('Arrow')) || event.key === 'Tab') {
if (editing && columns[activeCell.col + 1].disableKeys) {
return;
}
if (editing && ['ArrowLeft', 'ArrowRight'].includes(event.key)) {
return;
}
const add = ([x, y], cell) => cell && {
col: Math.max(0, Math.min(columns