react-konva-grid
Version:
Declarative React Canvas Grid primitive for Data table, Pivot table, Excel Worksheets
157 lines (142 loc) • 4.15 kB
text/typescript
import React, { useCallback, useState, useRef, useEffect } from "react";
import { ViewPortProps, GridRef, CellInterface, ItemSizer } from "./../Grid";
import { debounce } from "./../helpers";
import invariant from "tiny-invariant";
interface IProps {
gridRef: React.MutableRefObject<GridRef>;
getValue: (cell: CellInterface) => any;
initialVisibleRows?: number;
minColumnWidth?: number;
cellSpacing?: number;
timeout?: number;
resizeOnScroll?: boolean;
font?: string;
resizeStrategy?: ResizeStrategy;
rowCount?: number;
}
enum ResizeStrategy {
"lazy" = "lazy",
"full" = "full",
}
interface AutoResizerResults {
columnWidth: ItemSizer;
onViewChange: (cells: ViewPortProps) => void;
}
/**
* Auto sizer hook
* @param param
*
* TODO
* Dynamically resize columns after user has scrolled down/view port changed ?
*/
const useAutoSizer = ({
gridRef,
getValue,
initialVisibleRows = 20,
cellSpacing = 10,
minColumnWidth = 60,
timeout = 300,
resizeStrategy = ResizeStrategy.lazy,
rowCount,
resizeOnScroll = true,
font = "12px Arial",
}: IProps): AutoResizerResults => {
invariant(
!(resizeStrategy === ResizeStrategy.full && rowCount === void 0),
"Row count should be specified if resize stragtegy is full"
);
const autoSizer = useRef(AutoSizerCanvas(font));
const [viewport, setViewport] = useState<ViewPortProps>({
rowStartIndex: 0,
rowStopIndex: 0,
columnStartIndex: 0,
columnStopIndex: 0,
});
const isMounted = useRef(false);
const debounceResizer = useRef(
debounce(
({ rowIndex, columnIndex }: CellInterface) =>
gridRef.current.resetAfterIndices({ rowIndex, columnIndex }),
timeout
)
);
useEffect(() => {
isMounted.current = true;
}, []);
/* Update any styles, fonts if necessary */
useEffect(() => {
autoSizer.current.setFont(font);
}, [font]);
const getColumnWidth = useCallback(
(columnIndex: number) => {
const { rowStartIndex, rowStopIndex } = viewport;
const visibleRows =
resizeStrategy === ResizeStrategy.full
? (rowCount as number)
: rowStopIndex || initialVisibleRows;
let start = resizeStrategy === ResizeStrategy.full ? 0 : rowStartIndex;
let maxWidth = minColumnWidth;
while (start < visibleRows) {
const value =
getValue({
rowIndex: start,
columnIndex,
}) ?? null;
if (value !== null) {
const metrics = autoSizer.current.measureText(value);
if (metrics) {
const width = Math.ceil(metrics.width) + cellSpacing;
if (width > maxWidth) maxWidth = width;
}
}
start++;
}
return maxWidth;
},
[viewport, getValue, initialVisibleRows]
);
const handleViewChange = useCallback(
(cells: ViewPortProps) => {
/* Update viewport cells */
setViewport(cells);
/* Check if viewport has changed */
if (
resizeStrategy === ResizeStrategy.full ||
!resizeOnScroll ||
(cells.rowStartIndex === viewport.rowStartIndex &&
cells.columnStartIndex === viewport.columnStartIndex)
)
return;
if (gridRef.current) {
/* During first mount, column width is calculated. Do not re-calculate */
if (!isMounted.current) return;
debounceResizer.current({
rowIndex: cells.rowStartIndex,
columnIndex: cells.columnStartIndex,
});
}
},
[resizeOnScroll, viewport, resizeStrategy]
);
return {
columnWidth: getColumnWidth,
onViewChange: handleViewChange,
};
};
/* Canvas element */
const AutoSizerCanvas = (defaultFont: string) => {
const canvas = <HTMLCanvasElement>document.createElement("canvas");
const context = canvas.getContext("2d");
const setFont = (font: string = defaultFont) => {
if (context) context.font = font;
};
const measureText = (text: string) => context?.measureText(text);
/* Set font in constructor */
setFont(defaultFont);
return {
context,
measureText,
setFont,
};
};
export default useAutoSizer;