react-konva-grid
Version:
Declarative React Canvas Grid primitive for Data table, Pivot table, Excel Worksheets
1,209 lines • 53.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (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.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = __importStar(require("react"));
const ReactKonvaCore_1 = require("react-konva/lib/ReactKonvaCore");
const helpers_1 = require("./helpers");
const Cell_1 = require("./Cell");
const Selection_1 = __importDefault(require("./Selection"));
const FillHandle_1 = __importDefault(require("./FillHandle"));
const utils_1 = require("./utils");
const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
const types_1 = require("./types");
const DEFAULT_ESTIMATED_ITEM_SIZE = 50;
const defaultShadowSettings = {
stroke: "#000",
shadowColor: "black",
shadowBlur: 5,
shadowOpacity: 0.4,
shadowOffsetX: 2,
};
const defaultRowHeight = () => 20;
const defaultColumnWidth = () => 60;
const defaultSelectionRenderer = (props) => {
return react_1.default.createElement(Selection_1.default, Object.assign({}, props));
};
const RESET_SCROLL_EVENTS_DEBOUNCE_INTERVAL = 150;
/**
* Grid component using React Konva
* @param props
*/
const Grid = react_1.memo(react_1.forwardRef((props, forwardedRef) => {
const { width: containerWidth = 800, height: containerHeight = 600, estimatedColumnWidth, estimatedRowHeight, rowHeight = defaultRowHeight, columnWidth = defaultColumnWidth, rowCount = 0, columnCount = 0, scrollbarSize = 13, onScroll, showScrollbar = true, selectionBackgroundColor = "rgb(14, 101, 235, 0.1)", selectionBorderColor = "#1a73e8", selectionStrokeWidth = 1, activeCellStrokeWidth = 2, activeCell, selections = [], frozenRows = 0, frozenColumns = 0, itemRenderer = Cell_1.CellRenderer, mergedCells = [], snap = false, scrollThrottleTimeout = 100, onViewChange, selectionRenderer = defaultSelectionRenderer, onBeforeRenderRow, showFrozenShadow = false, shadowSettings = defaultShadowSettings, borderStyles = [], children, stageProps, wrapper = (children) => children, cellAreas = [], showFillHandle = true, fillSelection, overscanCount = 1, fillHandleProps } = props, rest = __rest(props, ["width", "height", "estimatedColumnWidth", "estimatedRowHeight", "rowHeight", "columnWidth", "rowCount", "columnCount", "scrollbarSize", "onScroll", "showScrollbar", "selectionBackgroundColor", "selectionBorderColor", "selectionStrokeWidth", "activeCellStrokeWidth", "activeCell", "selections", "frozenRows", "frozenColumns", "itemRenderer", "mergedCells", "snap", "scrollThrottleTimeout", "onViewChange", "selectionRenderer", "onBeforeRenderRow", "showFrozenShadow", "shadowSettings", "borderStyles", "children", "stageProps", "wrapper", "cellAreas", "showFillHandle", "fillSelection", "overscanCount", "fillHandleProps"]);
tiny_invariant_1.default(!(children && typeof children !== "function"), "Children should be a function");
/* Expose some methods in ref */
react_1.useImperativeHandle(forwardedRef, () => {
return {
scrollTo,
scrollBy,
scrollToItem,
stage: stageRef.current,
container: containerRef.current,
resetAfterIndices,
getScrollPosition,
isMergedCell,
getCellBounds,
getCellCoordsFromOffset,
getCellOffsetFromCoords,
focus: focusContainer,
resizeColumns,
resizeRows,
getViewPort,
};
});
const instanceProps = react_1.useRef({
columnMetadataMap: {},
rowMetadataMap: {},
lastMeasuredColumnIndex: -1,
lastMeasuredRowIndex: -1,
estimatedColumnWidth: estimatedColumnWidth || DEFAULT_ESTIMATED_ITEM_SIZE,
estimatedRowHeight: estimatedRowHeight || DEFAULT_ESTIMATED_ITEM_SIZE,
recalcColumnIndices: [],
recalcRowIndices: [],
});
const stageRef = react_1.useRef(null);
const containerRef = react_1.useRef(null);
const verticalScrollRef = react_1.useRef(null);
const wheelingRef = react_1.useRef(null);
const horizontalScrollRef = react_1.useRef(null);
const [_, forceRender] = react_1.useReducer((s) => s + 1, 0);
const [scrollState, setScrollState] = react_1.useState({
scrollTop: 0,
scrollLeft: 0,
isScrolling: false,
verticalScrollDirection: types_1.Direction.Down,
horizontalScrollDirection: types_1.Direction.Right,
});
const { scrollTop, scrollLeft, isScrolling, verticalScrollDirection, horizontalScrollDirection, } = scrollState;
/* Focus container */
const focusContainer = react_1.useCallback(() => {
var _a;
return (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
}, []);
/**
* Handle mouse wheeel
*/
react_1.useEffect(() => {
var _a;
(_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.addEventListener("wheel", handleWheel, {
passive: true,
});
}, []);
/**
* Snaps vertical scrollbar to the next/prev visible row
*/
const snapToRowFn = react_1.useCallback(({ rowStartIndex, rowCount, deltaY }) => {
if (!verticalScrollRef.current)
return;
if (deltaY !== 0) {
const nextRowIndex = deltaY < 0
? // User is scrolling up
Math.max(0, rowStartIndex)
: Math.min(rowStartIndex + frozenRows, rowCount - 1);
/* TODO: Fix bug when frozenRowHeight > minRow height, which causes rowStartIndex to be 1 even after a scroll */
const rowHeight = helpers_1.getRowHeight(nextRowIndex, instanceProps.current);
verticalScrollRef.current.scrollTop +=
(deltaY < 0 ? -1 : 1) * rowHeight;
}
}, []);
/**
* Snaps horizontal scrollbar to the next/prev visible column
*/
const snapToColumnFn = react_1.useCallback(({ columnStartIndex, columnCount, deltaX, frozenColumns, }) => {
if (!horizontalScrollRef.current)
return;
if (deltaX !== 0) {
const nextColumnIndex = deltaX < 0
? Math.max(0, columnStartIndex)
: Math.min(columnStartIndex + frozenColumns, columnCount - 1);
const columnWidth = helpers_1.getColumnWidth(nextColumnIndex, instanceProps.current);
horizontalScrollRef.current.scrollLeft +=
(deltaX < 0 ? -1 : 1) * columnWidth;
}
}, []);
const snapToRowThrottler = react_1.useRef(helpers_1.throttle(snapToRowFn, scrollThrottleTimeout));
const snapToColumnThrottler = react_1.useRef(helpers_1.throttle(snapToColumnFn, scrollThrottleTimeout));
/**
* Imperatively get the current scroll position
*/
const getScrollPosition = react_1.useCallback(() => {
return {
scrollTop,
scrollLeft,
};
}, [scrollTop, scrollLeft]);
/* Redraw grid imperatively */
const resetAfterIndices = react_1.useCallback(({ columnIndex, rowIndex }, shouldForceUpdate = true) => {
if (typeof columnIndex === "number") {
instanceProps.current.lastMeasuredColumnIndex = Math.min(instanceProps.current.lastMeasuredColumnIndex, columnIndex - 1);
}
if (typeof rowIndex === "number") {
instanceProps.current.lastMeasuredRowIndex = Math.min(instanceProps.current.lastMeasuredRowIndex, rowIndex - 1);
}
if (shouldForceUpdate)
forceRender();
}, []);
/**
* Create a map of merged cells
* [rowIndex, columnindex] => [parentRowIndex, parentColumnIndex]
*/
const mergedCellMap = react_1.useMemo(() => {
const mergedCellMap = new Map();
for (let i = 0; i < mergedCells.length; i++) {
const bounds = mergedCells[i];
const { top, left } = bounds;
for (const cell of helpers_1.getBoundedCells(bounds)) {
mergedCellMap.set(cell, bounds);
}
}
return mergedCellMap;
}, [mergedCells]);
/* Check if a cell is part of a merged cell */
const isMergedCell = react_1.useCallback(({ rowIndex, columnIndex }) => {
return mergedCellMap.has(helpers_1.cellIndentifier(rowIndex, columnIndex));
}, [mergedCellMap]);
/* Get top, left bounds of a cell */
const getCellBounds = react_1.useCallback(({ rowIndex, columnIndex }) => {
const isMerged = isMergedCell({ rowIndex, columnIndex });
if (isMerged)
return mergedCellMap.get(helpers_1.cellIndentifier(rowIndex, columnIndex));
return {
top: rowIndex,
left: columnIndex,
right: columnIndex,
bottom: rowIndex,
};
}, [mergedCellMap]);
const getVerticalRangeToRender = () => {
const startIndex = helpers_1.getRowStartIndexForOffset({
rowHeight,
columnWidth,
rowCount,
columnCount,
instanceProps: instanceProps.current,
offset: scrollTop,
});
const stopIndex = helpers_1.getRowStopIndexForStartIndex({
startIndex,
rowCount,
rowHeight,
columnWidth,
scrollTop,
containerHeight,
instanceProps: instanceProps.current,
});
// Overscan by one item in each direction so that tab/focus works.
// If there isn't at least one extra item, tab loops back around.
const overscanBackward = !isScrolling || verticalScrollDirection === types_1.Direction.Up
? Math.max(1, overscanCount)
: 1;
const overscanForward = !isScrolling || verticalScrollDirection === types_1.Direction.Down
? Math.max(1, overscanCount)
: 1;
return [
Math.max(0, startIndex - overscanBackward),
Math.max(0, Math.min(rowCount - 1, stopIndex + overscanForward)),
startIndex,
stopIndex,
];
};
const getHorizontalRangeToRender = () => {
const startIndex = helpers_1.getColumnStartIndexForOffset({
rowHeight,
columnWidth,
rowCount,
columnCount,
instanceProps: instanceProps.current,
offset: scrollLeft,
});
const stopIndex = helpers_1.getColumnStopIndexForStartIndex({
startIndex,
columnCount,
rowHeight,
columnWidth,
scrollLeft,
containerWidth,
instanceProps: instanceProps.current,
});
// Overscan by one item in each direction so that tab/focus works.
// If there isn't at least one extra item, tab loops back around.
const overscanBackward = !isScrolling || horizontalScrollDirection === types_1.Direction.Left
? Math.max(1, overscanCount)
: 1;
const overscanForward = !isScrolling || horizontalScrollDirection === types_1.Direction.Right
? Math.max(1, overscanCount)
: 1;
return [
Math.max(0, startIndex - overscanBackward),
Math.max(0, Math.min(columnCount - 1, stopIndex + overscanForward)),
startIndex,
stopIndex,
];
};
const [rowStartIndex, rowStopIndex] = getVerticalRangeToRender();
const [columnStartIndex, columnStopIndex] = getHorizontalRangeToRender();
const estimatedTotalHeight = helpers_1.getEstimatedTotalHeight(rowCount, instanceProps.current);
const estimatedTotalWidth = helpers_1.getEstimatedTotalWidth(columnCount, instanceProps.current);
/* Find frozen column boundary */
const frozenColumnWidth = react_1.useMemo(() => {
return helpers_1.getColumnOffset({
index: frozenColumns,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
}, [frozenColumns]);
const frozenRowHeight = react_1.useMemo(() => {
return helpers_1.getRowOffset({
index: frozenRows,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
}, [frozenRows]);
const isWithinFrozenColumnBoundary = react_1.useCallback((x) => {
return frozenColumns > 0 && x < frozenColumnWidth;
}, [frozenColumns, frozenColumnWidth]);
/* Find frozen row boundary */
const isWithinFrozenRowBoundary = react_1.useCallback((y) => {
return frozenRows > 0 && y < frozenRowHeight;
}, [frozenRows, frozenRowHeight]);
/**
* Get cell cordinates from current mouse x/y positions
*/
const getCellCoordsFromOffset = react_1.useCallback((left, top) => {
var _a;
if (!stageRef.current)
return null;
const stage = stageRef.current.getStage();
const rect = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
if (rect) {
left = left - rect.x;
top = top - rect.y;
}
const { x, y } = stage
.getAbsoluteTransform()
.copy()
.invert()
.point({ x: left, y: top });
const rowIndex = helpers_1.getRowStartIndexForOffset({
rowHeight,
columnWidth,
rowCount,
columnCount,
instanceProps: instanceProps.current,
offset: isWithinFrozenRowBoundary(y) ? y : y + scrollTop,
});
const columnIndex = helpers_1.getColumnStartIndexForOffset({
rowHeight,
columnWidth,
rowCount,
columnCount,
instanceProps: instanceProps.current,
offset: isWithinFrozenColumnBoundary(x) ? x : x + scrollLeft,
});
/* To be compatible with merged cells */
const bounds = getCellBounds({ rowIndex, columnIndex });
return { rowIndex: bounds.top, columnIndex: bounds.left };
}, [scrollLeft, scrollTop, rowCount, columnCount]);
/**
* Get cell offset position from rowIndex, columnIndex
*/
const getCellOffsetFromCoords = react_1.useCallback(({ rowIndex, columnIndex }) => {
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
return {
x,
y,
width,
height,
};
}, []);
/**
* Resize one or more columns
*/
const resizeColumns = react_1.useCallback((indices) => {
const leftMost = Math.min(...indices);
resetAfterIndices({ columnIndex: leftMost }, false);
instanceProps.current.recalcColumnIndices = indices;
forceRender();
}, []);
/**
* Resize one or more rows
*/
const resizeRows = react_1.useCallback((indices) => {
const topMost = Math.min(...indices);
resetAfterIndices({ rowIndex: topMost }, false);
instanceProps.current.recalcRowIndices = indices;
forceRender();
}, []);
/* Always if the viewport changes */
react_1.useEffect(() => {
if (instanceProps.current.recalcColumnIndices.length) {
instanceProps.current.recalcColumnIndices.length = 0;
}
if (instanceProps.current.recalcRowIndices.length) {
instanceProps.current.recalcRowIndices.length = 0;
}
}, [rowStopIndex, columnStopIndex]);
/* Get current view port of the grid */
const getViewPort = react_1.useCallback(() => {
return {
rowStartIndex,
rowStopIndex,
columnStartIndex,
columnStopIndex,
};
}, [rowStartIndex, rowStopIndex, columnStartIndex, columnStopIndex]);
/**
* When the grid is scrolling,
* 1. Stage does not listen to any mouse events
* 2. Div container does not listen to pointer events
*/
const resetIsScrollingTimeoutID = react_1.useRef(null);
const resetIsScrollingDebounced = react_1.useCallback(() => {
if (resetIsScrollingTimeoutID.current !== null) {
helpers_1.cancelTimeout(resetIsScrollingTimeoutID.current);
}
resetIsScrollingTimeoutID.current = helpers_1.requestTimeout(resetIsScrolling, RESET_SCROLL_EVENTS_DEBOUNCE_INTERVAL);
}, []);
/* Reset isScrolling */
const resetIsScrolling = react_1.useCallback(() => {
resetIsScrollingTimeoutID.current = null;
setScrollState((prev) => {
return Object.assign(Object.assign({}, prev), { isScrolling: false });
});
}, []);
/* Handle vertical scroll */
const handleScroll = react_1.useCallback((e) => {
const { scrollTop } = e.target;
setScrollState((prev) => (Object.assign(Object.assign({}, prev), { isScrolling: true, verticalScrollDirection: prev.scrollTop > scrollTop ? types_1.Direction.Up : types_1.Direction.Down, scrollTop })));
/* Scroll callbacks */
onScroll && onScroll({ scrollTop, scrollLeft });
/* Reset isScrolling if required */
resetIsScrollingDebounced();
}, [scrollLeft]);
/* Handle horizontal scroll */
const handleScrollLeft = react_1.useCallback((e) => {
const { scrollLeft } = e.target;
setScrollState((prev) => (Object.assign(Object.assign({}, prev), { isScrolling: true, horizontalScrollDirection: prev.scrollLeft > scrollLeft ? types_1.Direction.Left : types_1.Direction.Right, scrollLeft })));
/* Scroll callbacks */
onScroll && onScroll({ scrollLeft, scrollTop });
/* Reset isScrolling if required */
resetIsScrollingDebounced();
}, [scrollTop]);
/* Scroll based on left, top position */
const scrollTo = react_1.useCallback(({ scrollTop, scrollLeft }) => {
/* If scrollbar is visible, lets update it which triggers a state change */
if (showScrollbar) {
if (horizontalScrollRef.current && scrollLeft !== void 0)
horizontalScrollRef.current.scrollLeft = scrollLeft;
if (verticalScrollRef.current && scrollTop !== void 0)
verticalScrollRef.current.scrollTop = scrollTop;
}
else {
setScrollState((prev) => {
return Object.assign(Object.assign({}, prev), { scrollLeft: scrollLeft == void 0 ? prev.scrollLeft : scrollLeft, scrollTop: scrollTop == void 0 ? prev.scrollTop : scrollTop });
});
}
}, [showScrollbar]);
/**
* Scrollby utility
*/
const scrollBy = react_1.useCallback(({ x, y }) => {
if (showScrollbar) {
if (horizontalScrollRef.current && x !== void 0)
horizontalScrollRef.current.scrollLeft += x;
if (verticalScrollRef.current && y !== void 0)
verticalScrollRef.current.scrollTop += y;
}
else {
setScrollState((prev) => {
return Object.assign(Object.assign({}, prev), { scrollLeft: x == void 0 ? prev.scrollLeft : prev.scrollLeft + x, scrollTop: y == void 0 ? prev.scrollTop : prev.scrollTop + y });
});
}
}, []);
const scrollToItem = react_1.useCallback(({ rowIndex, columnIndex }, align = helpers_1.Align.smart) => {
/* Do not scroll if the row or column is frozen */
if ((rowIndex && rowIndex < frozenRows) ||
(columnIndex && columnIndex < frozenColumns))
return;
const frozenColumnOffset = helpers_1.getColumnOffset({
index: frozenColumns,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const newScrollLeft = columnIndex !== void 0
? helpers_1.getOffsetForColumnAndAlignment({
index: columnIndex,
containerHeight,
containerWidth,
columnCount,
columnWidth,
rowCount,
rowHeight,
scrollOffset: scrollLeft,
instanceProps: instanceProps.current,
scrollbarSize,
frozenOffset: frozenColumnOffset,
align,
})
: void 0;
const frozenRowOffset = helpers_1.getRowOffset({
index: frozenRows,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const newScrollTop = rowIndex !== void 0
? helpers_1.getOffsetForRowAndAlignment({
index: rowIndex,
containerHeight,
containerWidth,
columnCount,
columnWidth,
rowCount,
rowHeight,
scrollOffset: scrollTop,
instanceProps: instanceProps.current,
scrollbarSize,
frozenOffset: frozenRowOffset,
align,
})
: void 0;
const coords = {
scrollLeft: newScrollLeft,
scrollTop: newScrollTop,
};
const isOutsideViewport = (rowIndex !== void 0 && rowIndex > rowStopIndex) ||
(columnIndex !== void 0 && columnIndex > columnStopIndex);
/* Scroll in the next frame, Useful when user wants to jump from 1st column to last */
if (isOutsideViewport) {
window.requestAnimationFrame(() => {
scrollTo(coords);
});
}
else
scrollTo(coords);
}, [
containerHeight,
containerWidth,
rowCount,
columnCount,
scrollbarSize,
scrollLeft,
scrollTop,
frozenRows,
frozenColumns,
]);
/**
* Fired when user tries to scroll the canvas
*/
const handleWheel = react_1.useCallback((event) => {
var _a, _b;
const { deltaX, deltaY, deltaMode } = event;
/* If snaps are active */
if (snap) {
snapToRowThrottler.current({
deltaY,
rowStartIndex,
rowCount,
frozenRows,
});
snapToColumnThrottler.current({
deltaX,
columnStartIndex,
columnCount,
frozenColumns,
});
return;
}
/* Scroll natively */
if (wheelingRef.current)
return;
let dx = deltaX;
let dy = deltaY;
/* Scroll only in one direction */
const isHorizontal = Math.abs(dx) > Math.abs(dy);
if (deltaMode === 1) {
dy = dy * scrollbarSize;
}
if (!horizontalScrollRef.current || !verticalScrollRef.current)
return;
const currentScroll = isHorizontal
? (_a = horizontalScrollRef.current) === null || _a === void 0 ? void 0 : _a.scrollLeft : (_b = verticalScrollRef.current) === null || _b === void 0 ? void 0 : _b.scrollTop;
wheelingRef.current = window.requestAnimationFrame(() => {
wheelingRef.current = null;
if (isHorizontal) {
if (horizontalScrollRef.current)
horizontalScrollRef.current.scrollLeft = currentScroll + dx;
}
else {
if (verticalScrollRef.current)
verticalScrollRef.current.scrollTop = currentScroll + dy;
}
});
}, [
rowStartIndex,
columnStartIndex,
rowCount,
columnCount,
snap,
frozenColumns,
frozenRows,
]);
/* Callback when visible rows or columns have changed */
react_1.useEffect(() => {
onViewChange &&
onViewChange({
rowStartIndex,
rowStopIndex,
columnStartIndex,
columnStopIndex,
});
}, [rowStartIndex, rowStopIndex, columnStartIndex, columnStopIndex]);
/* Draw all cells */
const cells = [];
if (columnCount > 0 && rowCount) {
for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
/* Skip frozen rows */
if (rowIndex < frozenRows) {
continue;
}
/**
* Do any pre-processing of the row before being renderered.
* Useful for `react-table` to call `prepareRow(row)`
*/
onBeforeRenderRow && onBeforeRenderRow(rowIndex);
for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
/* Skip frozen columns and merged cells */
if (columnIndex < frozenColumns ||
isMergedCell({ rowIndex, columnIndex })) {
continue;
}
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
cells.push(itemRenderer({
x,
y,
width,
height,
rowIndex,
columnIndex,
key: helpers_1.itemKey({ rowIndex, columnIndex }),
}));
}
}
}
/**
* Extend certain cells.
* Mimics google sheets functionality where
* oevrflowed cell content can cover adjacent cells
*/
const ranges = [];
for (const { rowIndex, columnIndex, toColumnIndex } of cellAreas) {
/* Skip merged cells, Merged cell cannot be extended */
if (isMergedCell({ rowIndex, columnIndex })) {
continue;
}
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
const { x: offsetX = 0 } = getCellOffsetFromCoords({
rowIndex,
columnIndex: toColumnIndex + 1,
});
ranges.push(itemRenderer({
x,
y,
width: offsetX - x,
height,
rowIndex,
columnIndex,
key: `range:${helpers_1.itemKey({ rowIndex, columnIndex })}`,
}));
}
/* Draw merged cells */
const mergedCellAreas = [];
const frozenColumnMergedCellAreas = [];
const frozenRowMergedCellAreas = [];
const frozenIntersectionMergedCells = [];
for (let i = 0; i < mergedCells.length; i++) {
const { top: rowIndex, left: columnIndex, right, bottom } = mergedCells[i];
const isLeftBoundFrozen = columnIndex < frozenColumns;
const isTopBoundFrozen = rowIndex < frozenRows;
const isIntersectionFrozen = rowIndex < frozenRows && columnIndex < frozenColumns;
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const width = helpers_1.getColumnOffset({
index: right + 1,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}) - x;
const height = helpers_1.getRowOffset({
index: bottom + 1,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}) - y;
const cellRenderer = itemRenderer({
x,
y,
width,
height,
rowIndex,
columnIndex,
key: helpers_1.itemKey({ rowIndex, columnIndex }),
});
if (isLeftBoundFrozen) {
frozenColumnMergedCellAreas.push(cellRenderer);
}
if (isTopBoundFrozen) {
frozenRowMergedCellAreas.push(cellRenderer);
}
if (isIntersectionFrozen)
frozenIntersectionMergedCells.push(cellRenderer);
mergedCellAreas.push(cellRenderer);
}
/* Draw frozen rows */
const frozenRowCells = [];
for (let rowIndex = 0; rowIndex < Math.min(columnStopIndex, frozenRows); rowIndex++) {
/**
* Do any pre-processing of the row before being renderered.
* Useful for `react-table` to call `prepareRow(row)`
*/
onBeforeRenderRow && onBeforeRenderRow(rowIndex);
for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
/* Skip merged cells columns */
if (isMergedCell({ rowIndex, columnIndex })) {
continue;
}
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
frozenRowCells.push(itemRenderer({
x,
y,
width,
height,
rowIndex,
columnIndex,
key: helpers_1.itemKey({ rowIndex, columnIndex }),
}));
}
}
/* Draw frozen columns */
const frozenColumnCells = [];
for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
/**
* Do any pre-processing of the row before being renderered.
* Useful for `react-table` to call `prepareRow(row)`
*/
onBeforeRenderRow && onBeforeRenderRow(rowIndex);
for (let columnIndex = 0; columnIndex < Math.min(columnStopIndex, frozenColumns); columnIndex++) {
/* Skip merged cells columns */
if (isMergedCell({ rowIndex, columnIndex })) {
continue;
}
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
frozenColumnCells.push(itemRenderer({
x,
y,
width,
height,
rowIndex,
columnIndex,
key: helpers_1.itemKey({ rowIndex, columnIndex }),
}));
}
}
/**
* Frozen column shadow
*/
const frozenColumnShadow = react_1.useMemo(() => {
const frozenColumnLineX = helpers_1.getColumnOffset({
index: frozenColumns,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
return (react_1.default.createElement(ReactKonvaCore_1.Line, Object.assign({ points: [frozenColumnLineX, 0, frozenColumnLineX, containerHeight], offsetX: 1 }, shadowSettings)));
}, [shadowSettings, frozenColumns, containerHeight]);
/**
* Frozen row shadow
*/
const frozenRowShadow = react_1.useMemo(() => {
const frozenRowLineY = helpers_1.getRowOffset({
index: frozenRows,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
return (react_1.default.createElement(ReactKonvaCore_1.Line, Object.assign({ points: [0, frozenRowLineY, containerWidth, frozenRowLineY], offsetY: 1 }, shadowSettings)));
}, [shadowSettings, frozenRows, containerWidth]);
/* Draw frozen intersection cells */
const frozenIntersectionCells = [];
for (let rowIndex = 0; rowIndex < Math.min(rowStopIndex, frozenRows); rowIndex++) {
/**
* Do any pre-processing of the row before being renderered.
* Useful for `react-table` to call `prepareRow(row)`
*/
onBeforeRenderRow && onBeforeRenderRow(rowIndex);
for (let columnIndex = 0; columnIndex < Math.min(columnStopIndex, frozenColumns); columnIndex++) {
/* Skip merged cells columns */
if (isMergedCell({ rowIndex, columnIndex })) {
continue;
}
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
frozenIntersectionCells.push(itemRenderer({
x,
y,
width,
height,
rowIndex,
columnIndex,
key: helpers_1.itemKey({ rowIndex, columnIndex }),
}));
}
}
/**
* Renders active cell
*/
let fillHandleDimension = {};
let activeCellSelection = null;
let activeCellSelectionFrozenColumn = null;
let activeCellSelectionFrozenRow = null;
let activeCellSelectionFrozenIntersection = null;
if (activeCell) {
const bounds = getCellBounds(activeCell);
const { top, left, right, bottom } = bounds;
const actualBottom = Math.min(rowStopIndex, bottom);
const actualRight = Math.min(columnStopIndex, right);
const isInFrozenColumn = left < frozenColumns;
const isInFrozenRow = top < frozenRows;
const isInFrozenIntersection = isInFrozenRow && isInFrozenColumn;
const y = helpers_1.getRowOffset({
index: top,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const height = helpers_1.getRowOffset({
index: actualBottom,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}) -
y +
helpers_1.getRowHeight(actualBottom, instanceProps.current);
const x = helpers_1.getColumnOffset({
index: left,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const width = helpers_1.getColumnOffset({
index: actualRight,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}) -
x +
helpers_1.getColumnWidth(actualRight, instanceProps.current);
const cell = selectionRenderer({
stroke: selectionBorderColor,
strokeWidth: activeCellStrokeWidth,
fill: "transparent",
x: x,
y: y,
width: width,
height: height,
});
if (isInFrozenIntersection) {
activeCellSelectionFrozenIntersection = cell;
}
else if (isInFrozenRow) {
activeCellSelectionFrozenRow = cell;
}
else if (isInFrozenColumn) {
activeCellSelectionFrozenColumn = cell;
}
else {
activeCellSelection = cell;
}
fillHandleDimension = {
x: x + width,
y: y + height,
};
}
/**
* Convert selections to area
* Removed useMemo as changes to lastMeasureRowIndex, lastMeasuredColumnIndex,
* does not trigger useMemo
* Dependencies : [selections, rowStopIndex, columnStopIndex, instanceProps]
*/
let isSelectionInProgress = false;
const selectionAreas = [];
const selectionAreasFrozenColumns = [];
const selectionAreasFrozenRows = [];
const selectionAreasIntersection = [];
for (let i = 0; i < selections.length; i++) {
const { bounds, inProgress, style } = selections[i];
const { top, left, right, bottom } = bounds;
const selectionBounds = { x: 0, y: 0, width: 0, height: 0 };
const actualBottom = Math.min(rowStopIndex, bottom);
const actualRight = Math.min(columnStopIndex, right);
const isLeftBoundFrozen = left < frozenColumns;
const isTopBoundFrozen = top < frozenRows;
const isIntersectionFrozen = top < frozenRows && left < frozenColumns;
const isLast = i === selections.length - 1;
const styles = Object.assign({ stroke: inProgress ? selectionBackgroundColor : selectionBorderColor, fill: selectionBackgroundColor }, style);
if (inProgress)
isSelectionInProgress = true;
selectionBounds.y = helpers_1.getRowOffset({
index: top,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
selectionBounds.height =
helpers_1.getRowOffset({
index: actualBottom,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}) -
selectionBounds.y +
helpers_1.getRowHeight(actualBottom, instanceProps.current);
selectionBounds.x = helpers_1.getColumnOffset({
index: left,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
selectionBounds.width =
helpers_1.getColumnOffset({
index: actualRight,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}) -
selectionBounds.x +
helpers_1.getColumnWidth(actualRight, instanceProps.current);
if (isLeftBoundFrozen) {
const frozenColumnSelectionWidth = Math.min(selectionBounds.width, helpers_1.getColumnOffset({
index: frozenColumns - left,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}));
selectionAreasFrozenColumns.push(selectionRenderer(Object.assign(Object.assign({}, styles), { key: i, x: selectionBounds.x, y: selectionBounds.y, width: frozenColumnSelectionWidth, height: selectionBounds.height, strokeRightWidth: frozenColumnSelectionWidth === selectionBounds.width
? selectionStrokeWidth
: 0 })));
}
if (isTopBoundFrozen) {
const frozenRowSelectionHeight = Math.min(selectionBounds.height, helpers_1.getRowOffset({
index: frozenRows - top,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}));
selectionAreasFrozenRows.push(selectionRenderer(Object.assign(Object.assign({}, styles), { key: i, x: selectionBounds.x, y: selectionBounds.y, width: selectionBounds.width, height: frozenRowSelectionHeight, strokeBottomWidth: frozenRowSelectionHeight === selectionBounds.height
? selectionStrokeWidth
: 0 })));
}
if (isIntersectionFrozen) {
const frozenIntersectionSelectionHeight = Math.min(selectionBounds.height, helpers_1.getRowOffset({
index: frozenRows - top,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}));
const frozenIntersectionSelectionWidth = Math.min(selectionBounds.width, helpers_1.getColumnOffset({
index: frozenColumns - left,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}));
selectionAreasIntersection.push(selectionRenderer(Object.assign(Object.assign({}, styles), { key: i, x: selectionBounds.x, y: selectionBounds.y, width: frozenIntersectionSelectionWidth, height: frozenIntersectionSelectionHeight, strokeBottomWidth: frozenIntersectionSelectionHeight === selectionBounds.height
? selectionStrokeWidth
: 0, strokeRightWidth: frozenIntersectionSelectionWidth === selectionBounds.width
? selectionStrokeWidth
: 0 })));
}
selectionAreas.push(selectionRenderer(Object.assign(Object.assign({}, styles), { key: i, x: selectionBounds.x, y: selectionBounds.y, width: selectionBounds.width, height: selectionBounds.height })));
if (isLast) {
fillHandleDimension = {
x: selectionBounds.x + selectionBounds.width,
y: selectionBounds.y + selectionBounds.height,
};
}
}
/**
* Fillselection
*/
let fillSelections = null;
if (fillSelection) {
const { bounds } = fillSelection;
const { top, left, right, bottom } = bounds;
const actualBottom = Math.min(rowStopIndex, bottom);
const actualRight = Math.min(columnStopIndex, right);
const x = helpers_1.getColumnOffset({
index: left,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const y = helpers_1.getRowOffset({
index: top,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const height = helpers_1.getRowOffset({
index: actualBottom,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}) -
y +
helpers_1.getRowHeight(actualBottom, instanceProps.current);
const width = helpers_1.getColumnOffset({
index: actualRight,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}) -
x +
helpers_1.getColumnWidth(actualRight, instanceProps.current);
fillSelections = selectionRenderer({
x,
y,
width,
height,
stroke: "gray",
strokeStyle: "dashed",
});
}
const borderStylesCells = react_1.useMemo(() => {
const borderStyleCells = [];
for (let i = 0; i < borderStyles.length; i++) {
const { bounds, style } = borderStyles[i];
const { top, right, bottom, left } = bounds;
const x = helpers_1.getColumnOffset({
index: left,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const y = helpers_1.getRowOffset({
index: top,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const width = helpers_1.getColumnOffset({
index: Math.min(columnCount - 1, right + 1),
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}) - x;
const height = helpers_1.getRowOffset({
index: Math.min(rowCount - 1, bottom + 1),
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}) - y;
borderStyleCells.push(utils_1.createHTMLBox(Object.assign({ x,
y,
width,
height }, style)));
}
return borderStyleCells;
}, [borderStyles, columnStopIndex, rowStopIndex, columnCount, rowCount]);
/**
* Prevents drawing hit region when scrolling
*/
const listenToEvents = !isScrolling;
/* Frozen row shadow */
const frozenRowShadowComponent = showFrozenShadow && frozenRows !== 0 && scrollTop !== 0
? frozenRowShadow
: null;
/* Frozen column shadow */
const frozenColumnShadowComponent = showFrozenShadow && frozenColumns !== 0 && scrollLeft !== 0
? frozenColumnShadow
: null;
const stageChildren = (react_1.default.createElement(react_1.default.Fragment, null,
react_1.default.createElement(ReactKonvaCore_1.Layer, { offsetY: scrollTop, offsetX: scrollLeft },
cells,
mergedCellAreas,
ranges),
react_1.default.createElement(ReactKonvaCore_1.Layer, null,
react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: scrollTop, offsetX: scrollLeft, listening: false }),
frozenRowShadowComponent,
frozenColumnShadowComponent,
react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: 0, offsetX: scrollLeft },
frozenRowCells,
frozenRowMergedCellAreas),
react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: scrollTop, offsetX: 0 },
frozenColumnCells,
frozenColumnMergedCellAreas),
react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: 0, offsetX: 0 },
frozenIntersectionCells,
frozenIntersectionMergedCells)),
children && typeof children === "function"
? children({
scrollLeft,
scrollTop,
})
: null));
const fillHandleWidth = 8;
const fillhandleComponent = showFillHandle && !isSelectionInProgress ? (react_1.default.createElement(FillHandle_1.default, Object.assign({}, fillHandleDimension, { stroke: selectionBorderColor, size: fillHandleWidth }, fillHandleProps))) : null;
const selectionChildren = (react_1.default.createElement("div", { style: {
pointerEvents: "none",
} },
react_1.default.createElement("div", { style: {
position: "absolute",
left: frozenColumnWidth,
top: frozenRowHeight,
right: 0,
bottom: 0,
overflow: "hidden",
} },
react_1.default.createElement("div", { style: {
transform: `translate(-${scrollLeft + frozenColumnWidth}px, -${scrollTop + frozenRowHeight}px)`,
} },
borderStylesCells,
fillSelections,
selectionAreas,
activeCellSelection,
fillhandleComponent)),
frozenColumns ? (react_1.default.createElement("div", { style: {
position: "absolute",
width: frozenColumnWidth + fillHandleWidth,
top: frozenRowHeight,
left: 0,
bottom: 0,
overflow: "hidden",
} },
react_1.default.createElement("div", { style: {
transform: `translate(0, -${scrollTop + frozenRowHeight}px)`,
} },
selectionAreasFrozenColumns,
activeCellSelectionFrozenColumn,
fillhandleComponent