react-konva-grid
Version:
Canvas grid to render large set of tabular data with virtualization.
548 lines • 22.3 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 (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
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 defaultRowHeight = () => 20;
const defaultColumnWidth = () => 100;
const defaultProps = {
width: 800,
height: 800,
rowCount: 200,
columnCount: 200,
rowHeight: defaultRowHeight,
columnWidth: defaultColumnWidth,
scrollbarSize: 17,
showScrollbar: true,
selectionBackgroundColor: "rgb(14, 101, 235, 0.1)",
selectionBorderColor: "#1a73e8",
selections: [],
mergedCells: [],
frozenRows: 0,
frozenColumns: 0,
};
const DEFAULT_ESTIMATED_ITEM_SIZE = 50;
/**
* Grid component using React Konva
* @param props
*/
const Grid = react_1.memo(react_1.forwardRef((props, forwardedRef) => {
const { width: containerWidth, height: containerHeight, estimatedColumnWidth, estimatedRowHeight, rowHeight, columnWidth, rowCount, columnCount, scrollbarSize, onScroll, showScrollbar, selectionBackgroundColor, selectionBorderColor, selections, frozenRows, frozenColumns, itemRenderer, mergedCells, onViewChange, ...rest } = props;
/* Expose some methods in ref */
react_1.useImperativeHandle(forwardedRef, () => {
return {
scrollTo,
stage: stageRef.current,
resetAfterIndices,
getScrollPosition,
isMergedCell,
getCellBounds,
getCellCoordsFromOffsets,
getCellOffsetFromCoords,
};
});
const instanceProps = react_1.useRef({
columnMetadataMap: {},
rowMetadataMap: {},
lastMeasuredColumnIndex: -1,
lastMeasuredRowIndex: -1,
estimatedColumnWidth: estimatedColumnWidth || DEFAULT_ESTIMATED_ITEM_SIZE,
estimatedRowHeight: estimatedRowHeight || DEFAULT_ESTIMATED_ITEM_SIZE,
});
const stageRef = 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 [scrollTop, setScrollTop] = react_1.useState(0);
const [scrollLeft, setScrollLeft] = react_1.useState(0);
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]);
/* Handle vertical scroll */
const handleScroll = react_1.useCallback((e) => {
setScrollTop(e.target.scrollTop);
/* Scroll callbacks */
onScroll && onScroll({ scrollTop: e.target.scrollTop, scrollLeft });
}, [scrollLeft]);
/* Handle horizontal scroll */
const handleScrollLeft = react_1.useCallback((e) => {
setScrollLeft(e.target.scrollLeft);
/* Scroll callbacks */
onScroll && onScroll({ scrollLeft: e.target.scrollLeft, scrollTop });
}, [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)
horizontalScrollRef.current.scrollLeft = scrollLeft;
if (verticalScrollRef.current)
verticalScrollRef.current.scrollTop = scrollTop;
}
else {
scrollLeft !== void 0 && setScrollLeft(scrollLeft);
scrollTop !== void 0 && setScrollTop(scrollTop);
}
}, [showScrollbar]);
const handleWheel = react_1.useCallback((event) => {
if (wheelingRef.current)
return;
const { deltaX, deltaY, deltaMode } = event.nativeEvent;
let dx = deltaX;
let dy = deltaY;
if (deltaMode === 1) {
dy = dy * 17;
}
if (!horizontalScrollRef.current || !verticalScrollRef.current)
return;
const x = horizontalScrollRef.current?.scrollLeft;
const y = verticalScrollRef.current?.scrollTop;
wheelingRef.current = window.requestAnimationFrame(() => {
wheelingRef.current = null;
if (horizontalScrollRef.current)
horizontalScrollRef.current.scrollLeft = x + dx;
if (verticalScrollRef.current)
verticalScrollRef.current.scrollTop = y + dy;
});
}, []);
const rowStartIndex = helpers_1.getRowStartIndexForOffset({
rowHeight,
columnWidth,
rowCount,
columnCount,
instanceProps: instanceProps.current,
offset: scrollTop,
});
const rowStopIndex = helpers_1.getRowStopIndexForStartIndex({
startIndex: rowStartIndex,
rowCount,
rowHeight,
columnWidth,
scrollTop,
containerHeight,
instanceProps: instanceProps.current,
});
const columnStartIndex = helpers_1.getColumnStartIndexForOffset({
rowHeight,
columnWidth,
rowCount,
columnCount,
instanceProps: instanceProps.current,
offset: scrollLeft,
});
const columnStopIndex = helpers_1.getColumnStopIndexForStartIndex({
startIndex: columnStartIndex,
columnCount,
rowHeight,
columnWidth,
scrollLeft,
containerWidth,
instanceProps: instanceProps.current,
});
const estimatedTotalHeight = helpers_1.getEstimatedTotalHeight(rowCount, instanceProps.current.estimatedRowHeight, instanceProps.current);
const estimatedTotalWidth = helpers_1.getEstimatedTotalWidth(columnCount, instanceProps.current.estimatedColumnWidth, instanceProps.current);
/* 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;
}
for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
/* Skip frozen columns */
if (columnIndex < frozenColumns ||
isMergedCell(rowIndex, columnIndex)) {
continue;
}
const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
cells.push(itemRenderer({
x,
y,
width,
height,
rowIndex,
columnIndex,
key: helpers_1.itemKey({ rowIndex, columnIndex }),
}));
}
}
}
/* Draw merged cells */
const mergedCellAreas = react_1.useMemo(() => {
const areas = [];
for (let i = 0; i < mergedCells.length; i++) {
const { top: rowIndex, left: columnIndex, right, bottom } = mergedCells[i];
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;
areas.push(itemRenderer({
x,
y,
width,
height,
rowIndex,
columnIndex,
key: helpers_1.itemKey({ rowIndex, columnIndex }),
}));
}
return areas;
}, [
mergedCells,
rowStartIndex,
rowStopIndex,
columnStartIndex,
columnStopIndex,
]);
/* Draw frozen rows */
const frozenRowCells = [];
for (let rowIndex = 0; rowIndex < Math.min(columnStopIndex, frozenRows); rowIndex++) {
for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) {
const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
frozenRowCells.push(itemRenderer({
x,
y,
width,
height,
rowIndex,
columnIndex,
key: helpers_1.itemKey({ rowIndex, columnIndex }),
}));
}
}
/* Draw frozen columns */
const frozenColumnCells = [];
for (let columnIndex = 0; columnIndex < Math.min(columnStopIndex, frozenColumns); columnIndex++) {
for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) {
const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
frozenColumnCells.push(itemRenderer({
x,
y,
width,
height,
rowIndex,
columnIndex,
key: helpers_1.itemKey({ rowIndex, columnIndex }),
}));
}
}
/* Draw frozen intersection cells */
const frozenIntersectionCells = [];
for (let rowIndex = 0; rowIndex < Math.min(rowStopIndex, frozenRows); rowIndex++) {
for (let columnIndex = 0; columnIndex < Math.min(columnStopIndex, frozenColumns); columnIndex++) {
const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
frozenIntersectionCells.push(itemRenderer({
x,
y,
width,
height,
rowIndex,
columnIndex,
key: helpers_1.itemKey({ rowIndex, columnIndex }),
}));
}
}
/**
* Convert selections to area
* Removed useMemo as changes to lastMeasureRowIndex, lastMeasuredColumnIndex,
* does not trigger useMemo
* Dependencies : [selections, rowStopIndex, columnStopIndex, instanceProps]
*/
const selectionAreas = [];
for (let i = 0; i < selections.length; i++) {
const { top, left, right, bottom } = selections[i];
const selectionBounds = { x: 0, y: 0, width: 0, height: 0 };
const actualBottom = Math.min(rowStopIndex, bottom);
const actualRight = Math.min(columnStopIndex, right);
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);
selectionAreas.push(react_1.default.createElement(ReactKonvaCore_1.Rect, { key: i, stroke: selectionBorderColor, x: selectionBounds.x, y: selectionBounds.y, width: selectionBounds.width, height: selectionBounds.height, fill: selectionBackgroundColor, shadowForStrokeEnabled: false, listening: false, hitStrokeWidth: 0 }));
}
/**
* Get cell offset position from rowIndex, columnIndex
*/
const getCellOffsetFromCoords = react_1.useCallback(({ rowIndex, columnIndex }) => {
const width = helpers_1.getColumnWidth(columnIndex, instanceProps.current);
const x = helpers_1.getColumnOffset({
index: columnIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
const height = helpers_1.getRowHeight(rowIndex, instanceProps.current);
const y = helpers_1.getRowOffset({
index: rowIndex,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
});
return {
x,
y,
width,
height,
};
}, []);
/* Find frozen column boundary */
const isWithinFrozenColumnBoundary = (x) => {
return (frozenColumns > 0 &&
x <
helpers_1.getColumnOffset({
index: frozenColumns,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}));
};
/* Find frozen row boundary */
const isWithinFrozenRowBoundary = (y) => {
return (frozenRows > 0 &&
y <
helpers_1.getRowOffset({
index: frozenRows,
rowHeight,
columnWidth,
instanceProps: instanceProps.current,
}));
};
/**
* Get cell cordinates from current mouse x/y positions
*/
const getCellCoordsFromOffsets = react_1.useCallback((x, y) => {
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,
});
return { rowIndex, columnIndex };
}, [scrollLeft, scrollTop, rowCount, columnCount]);
return (react_1.default.createElement("div", { style: { position: "relative", width: containerWidth } },
react_1.default.createElement("div", Object.assign({ onWheel: handleWheel, tabIndex: -1 }, rest),
react_1.default.createElement(ReactKonvaCore_1.Stage, { width: containerWidth, height: containerHeight, ref: stageRef },
react_1.default.createElement(ReactKonvaCore_1.Layer, null,
react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: scrollTop, offsetX: scrollLeft },
cells,
mergedCellAreas),
react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: scrollTop, offsetX: 0 }, frozenColumnCells),
react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: 0, offsetX: scrollLeft }, frozenRowCells),
react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: 0, offsetX: 0 }, frozenIntersectionCells)),
react_1.default.createElement(ReactKonvaCore_1.Layer, { listening: false },
react_1.default.createElement(ReactKonvaCore_1.Group, { offsetY: scrollTop, offsetX: scrollLeft, listening: false }, selectionAreas)))),
showScrollbar ? (react_1.default.createElement(react_1.default.Fragment, null,
react_1.default.createElement("div", { style: {
height: containerHeight,
overflow: "scroll",
position: "absolute",
right: 0,
top: 0,
width: scrollbarSize,
}, onScroll: handleScroll, ref: verticalScrollRef },
react_1.default.createElement("div", { style: {
position: "absolute",
height: estimatedTotalHeight,
width: 1,
} })),
react_1.default.createElement("div", { style: {
overflow: "scroll",
position: "absolute",
bottom: 0,
left: 0,
width: containerWidth,
height: scrollbarSize,
}, onScroll: handleScrollLeft, ref: horizontalScrollRef },
react_1.default.createElement("div", { style: {
position: "absolute",
width: estimatedTotalWidth,
height: 1,
} })))) : null));
}));
Grid.defaultProps = defaultProps;
exports.default = Grid;
//# sourceMappingURL=Grid.js.map