react-konva-grid
Version:
Declarative React Canvas Grid primitive for Data table, Pivot table, Excel Worksheets
577 lines • 20.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.isNull = exports.AutoSizerCanvas = exports.mergedCellBounds = exports.findNextCellWithinBounds = exports.prepareClipboardData = exports.numberToAlphabet = exports.selectionFromActiveCell = exports.requestTimeout = exports.cancelTimeout = exports.getOffsetForRowAndAlignment = exports.getOffsetForColumnAndAlignment = exports.getOffsetForIndexAndAlignment = exports.rafThrottle = exports.debounce = exports.throttle = exports.cellIndentifier = exports.getEstimatedTotalWidth = exports.getEstimatedTotalHeight = exports.getItemMetadata = exports.getColumnWidth = exports.getRowHeight = exports.getColumnOffset = exports.getRowOffset = exports.itemKey = exports.getBoundedCells = exports.getColumnStopIndexForStartIndex = exports.getColumnStartIndexForOffset = exports.getRowStopIndexForStartIndex = exports.getRowStartIndexForOffset = exports.ItemType = exports.Align = void 0;
const types_1 = require("./types");
var Align;
(function (Align) {
Align["start"] = "start";
Align["end"] = "end";
Align["center"] = "center";
Align["auto"] = "auto";
Align["smart"] = "smart";
})(Align = exports.Align || (exports.Align = {}));
var ItemType;
(function (ItemType) {
ItemType["row"] = "row";
ItemType["column"] = "column";
})(ItemType = exports.ItemType || (exports.ItemType = {}));
exports.getRowStartIndexForOffset = ({ rowHeight, columnWidth, rowCount, columnCount, instanceProps, offset, }) => {
return findNearestItem({
itemType: ItemType.row,
rowHeight,
columnWidth,
rowCount,
columnCount,
instanceProps,
offset,
});
};
exports.getRowStopIndexForStartIndex = ({ startIndex, rowCount, rowHeight, columnWidth, scrollTop, containerHeight, instanceProps, }) => {
const itemMetadata = exports.getItemMetadata({
itemType: ItemType.row,
rowHeight,
columnWidth,
index: startIndex,
instanceProps,
});
const maxOffset = scrollTop + containerHeight;
let offset = itemMetadata.offset + itemMetadata.size;
let stopIndex = startIndex;
while (stopIndex < rowCount - 1 && offset < maxOffset) {
stopIndex++;
offset += exports.getItemMetadata({
itemType: ItemType.row,
rowHeight,
columnWidth,
index: stopIndex,
instanceProps,
}).size;
}
return stopIndex;
};
exports.getColumnStartIndexForOffset = ({ rowHeight, columnWidth, rowCount, columnCount, instanceProps, offset, }) => {
return findNearestItem({
itemType: ItemType.column,
rowHeight,
columnWidth,
rowCount,
columnCount,
instanceProps,
offset,
});
};
exports.getColumnStopIndexForStartIndex = ({ startIndex, rowHeight, columnWidth, instanceProps, containerWidth, scrollLeft, columnCount, }) => {
const itemMetadata = exports.getItemMetadata({
itemType: ItemType.column,
index: startIndex,
rowHeight,
columnWidth,
instanceProps,
});
const maxOffset = scrollLeft + containerWidth;
let offset = itemMetadata.offset + itemMetadata.size;
let stopIndex = startIndex;
while (stopIndex < columnCount - 1 && offset < maxOffset) {
stopIndex++;
offset += exports.getItemMetadata({
itemType: ItemType.column,
rowHeight,
columnWidth,
index: stopIndex,
instanceProps,
}).size;
}
return stopIndex;
};
exports.getBoundedCells = (area) => {
const cells = new Set();
if (!area)
return cells;
const { top, bottom, left, right } = area;
for (let i = top; i <= bottom; i++) {
for (let j = left; j <= right; j++) {
cells.add(exports.cellIndentifier(i, j));
}
}
return cells;
};
exports.itemKey = ({ rowIndex, columnIndex }) => `${rowIndex}:${columnIndex}`;
exports.getRowOffset = ({ index, rowHeight, columnWidth, instanceProps, }) => {
return exports.getItemMetadata({
itemType: ItemType.row,
index,
rowHeight,
columnWidth,
instanceProps,
}).offset;
};
exports.getColumnOffset = ({ index, rowHeight, columnWidth, instanceProps, }) => {
return exports.getItemMetadata({
itemType: ItemType.column,
index,
rowHeight,
columnWidth,
instanceProps,
}).offset;
};
exports.getRowHeight = (index, instanceProps) => {
return instanceProps.rowMetadataMap[index].size;
};
exports.getColumnWidth = (index, instanceProps) => {
return instanceProps.columnMetadataMap[index].size;
};
exports.getItemMetadata = ({ itemType, index, rowHeight, columnWidth, instanceProps, }) => {
var _a;
let itemMetadataMap, itemSize, lastMeasuredIndex, recalcIndices;
if (itemType === "column") {
itemMetadataMap = instanceProps.columnMetadataMap;
itemSize = columnWidth;
lastMeasuredIndex = instanceProps.lastMeasuredColumnIndex;
recalcIndices = instanceProps.recalcColumnIndices;
}
else {
itemMetadataMap = instanceProps.rowMetadataMap;
itemSize = rowHeight;
lastMeasuredIndex = instanceProps.lastMeasuredRowIndex;
recalcIndices = instanceProps.recalcRowIndices;
}
const recalcWithinBoundsOnly = recalcIndices.length > 0;
if (index > lastMeasuredIndex) {
let offset = 0;
if (lastMeasuredIndex >= 0) {
const itemMetadata = itemMetadataMap[lastMeasuredIndex];
offset = itemMetadata.offset + itemMetadata.size;
}
for (let i = lastMeasuredIndex + 1; i <= index; i++) {
// Only recalculates specified columns
let size = recalcWithinBoundsOnly
? recalcIndices.includes(i)
? itemSize(i)
: ((_a = itemMetadataMap[i]) === null || _a === void 0 ? void 0 : _a.size) || itemSize(i)
: itemSize(i);
itemMetadataMap[i] = {
offset,
size,
};
offset += size;
}
if (itemType === "column") {
instanceProps.lastMeasuredColumnIndex = index;
}
else {
instanceProps.lastMeasuredRowIndex = index;
}
}
return itemMetadataMap[index];
};
const findNearestItem = ({ itemType, rowHeight, columnWidth, rowCount, columnCount, instanceProps, offset, }) => {
let itemMetadataMap, lastMeasuredIndex;
if (itemType === "column") {
itemMetadataMap = instanceProps.columnMetadataMap;
lastMeasuredIndex = instanceProps.lastMeasuredColumnIndex;
}
else {
itemMetadataMap = instanceProps.rowMetadataMap;
lastMeasuredIndex = instanceProps.lastMeasuredRowIndex;
}
const lastMeasuredItemOffset = lastMeasuredIndex > 0 ? itemMetadataMap[lastMeasuredIndex].offset : 0;
if (lastMeasuredItemOffset >= offset) {
// If we've already measured items within this range just use a binary search as it's faster.
return findNearestItemBinarySearch({
itemType,
rowHeight,
columnWidth,
instanceProps,
high: lastMeasuredIndex,
low: 0,
offset,
});
}
else {
// If we haven't yet measured this high, fallback to an exponential search with an inner binary search.
// The exponential search avoids pre-computing sizes for the full set of items as a binary search would.
// The overall complexity for this approach is O(log n).
return findNearestItemExponentialSearch({
itemType,
rowHeight,
rowCount,
columnCount,
columnWidth,
instanceProps,
index: Math.max(0, lastMeasuredIndex),
offset,
});
}
};
const findNearestItemBinarySearch = ({ itemType, rowHeight, columnWidth, instanceProps, high, low, offset, }) => {
while (low <= high) {
const middle = low + Math.floor((high - low) / 2);
const currentOffset = exports.getItemMetadata({
itemType,
rowHeight,
columnWidth,
index: middle,
instanceProps,
}).offset;
if (currentOffset === offset) {
return middle;
}
else if (currentOffset < offset) {
low = middle + 1;
}
else if (currentOffset > offset) {
high = middle - 1;
}
}
if (low > 0) {
return low - 1;
}
else {
return 0;
}
};
const findNearestItemExponentialSearch = ({ itemType, rowHeight, columnWidth, rowCount, columnCount, instanceProps, index, offset, }) => {
const itemCount = itemType === "column" ? columnCount : rowCount;
let interval = 1;
while (index < itemCount &&
exports.getItemMetadata({
itemType,
rowHeight,
columnWidth,
index,
instanceProps,
}).offset < offset) {
index += interval;
interval *= 2;
}
return findNearestItemBinarySearch({
itemType,
rowHeight,
columnWidth,
instanceProps,
high: Math.min(index, itemCount - 1),
low: Math.floor(index / 2),
offset,
});
};
exports.getEstimatedTotalHeight = (rowCount, instanceProps) => {
const { estimatedRowHeight } = instanceProps;
let totalSizeOfMeasuredRows = 0;
let { lastMeasuredRowIndex, rowMetadataMap } = instanceProps;
// Edge case check for when the number of items decreases while a scroll is in progress.
// https://github.com/bvaughn/react-window/pull/138
if (lastMeasuredRowIndex >= rowCount) {
lastMeasuredRowIndex = rowCount - 1;
}
if (lastMeasuredRowIndex >= 0) {
const itemMetadata = rowMetadataMap[lastMeasuredRowIndex];
totalSizeOfMeasuredRows = itemMetadata.offset + itemMetadata.size;
}
const numUnmeasuredItems = rowCount - lastMeasuredRowIndex - 1;
const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedRowHeight;
return totalSizeOfMeasuredRows + totalSizeOfUnmeasuredItems;
};
exports.getEstimatedTotalWidth = (columnCount, instanceProps) => {
const { estimatedColumnWidth } = instanceProps;
let totalSizeOfMeasuredRows = 0;
let { lastMeasuredColumnIndex, columnMetadataMap } = instanceProps;
// Edge case check for when the number of items decreases while a scroll is in progress.
// https://github.com/bvaughn/react-window/pull/138
if (lastMeasuredColumnIndex >= columnCount) {
lastMeasuredColumnIndex = columnCount - 1;
}
if (lastMeasuredColumnIndex >= 0) {
const itemMetadata = columnMetadataMap[lastMeasuredColumnIndex];
totalSizeOfMeasuredRows = itemMetadata.offset + itemMetadata.size;
}
const numUnmeasuredItems = columnCount - lastMeasuredColumnIndex - 1;
const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedColumnWidth;
return totalSizeOfMeasuredRows + totalSizeOfUnmeasuredItems;
};
/* Create a stringified cell identifier */
exports.cellIndentifier = (rowIndex, columnIndex) => [rowIndex, columnIndex].toString();
/**
* @desc Throttle fn
* @param func function
* @param limit Delay in milliseconds
*/
function throttle(func, limit) {
let inThrottle;
return function () {
const args = arguments;
const context = this;
if (!inThrottle) {
inThrottle = true;
func.apply(context, args);
setTimeout(() => (inThrottle = false), limit);
}
};
}
exports.throttle = throttle;
function debounce(cb, wait = 20) {
let h = 0;
let callable = (...args) => {
clearTimeout(h);
h = window.setTimeout(() => cb(...args), wait);
};
return callable;
}
exports.debounce = debounce;
function rafThrottle(callback) {
var active = false; // a simple flag
var evt; // to keep track of the last event
var handler = function () {
// fired only when screen has refreshed
active = false; // release our flag
callback(evt);
};
return function handleEvent(e) {
// the actual event handler
evt = e; // save our event at each call
evt && evt.persist();
if (!active) {
// only if we weren't already doing it
active = true; // raise the flag
requestAnimationFrame(handler); // wait for next screen refresh
}
};
}
exports.rafThrottle = rafThrottle;
exports.getOffsetForIndexAndAlignment = ({ itemType, containerHeight, containerWidth, rowHeight, columnWidth, columnCount, rowCount, index, align = Align.smart, scrollOffset, instanceProps, scrollbarSize, frozenOffset = 0, }) => {
const size = itemType === "column" ? containerWidth : containerHeight;
const itemMetadata = exports.getItemMetadata({
itemType,
rowHeight,
columnWidth,
index,
instanceProps,
});
// Get estimated total size after ItemMetadata is computed,
// To ensure it reflects actual measurements instead of just estimates.
const estimatedTotalSize = itemType === "column"
? exports.getEstimatedTotalWidth(columnCount, instanceProps)
: exports.getEstimatedTotalHeight(rowCount, instanceProps);
const maxOffset = Math.max(0, Math.min(estimatedTotalSize - size, itemMetadata.offset - frozenOffset));
const minOffset = Math.max(0, itemMetadata.offset - size + scrollbarSize + itemMetadata.size);
if (align === Align.smart) {
if (scrollOffset >= minOffset - size && scrollOffset <= maxOffset + size) {
align = Align.auto;
}
else {
align = Align.center;
}
}
switch (align) {
case Align.start:
return maxOffset;
case Align.end:
return minOffset;
case Align.center:
return Math.round(minOffset + (maxOffset - minOffset) / 2);
case Align.auto:
default:
if (scrollOffset >= minOffset && scrollOffset <= maxOffset) {
return scrollOffset;
}
else if (minOffset > maxOffset) {
// Because we only take into account the scrollbar size when calculating minOffset
// this value can be larger than maxOffset when at the end of the list
return minOffset;
}
else if (scrollOffset < minOffset) {
return minOffset;
}
else {
return maxOffset;
}
}
};
exports.getOffsetForColumnAndAlignment = (props) => {
return exports.getOffsetForIndexAndAlignment(Object.assign({ itemType: ItemType.column }, props));
};
exports.getOffsetForRowAndAlignment = (props) => {
return exports.getOffsetForIndexAndAlignment(Object.assign({ itemType: ItemType.row }, props));
};
// Animation frame based implementation of setTimeout.
// Inspired by Joe Lambert, https://gist.github.com/joelambert/1002116#file-requesttimeout-js
const hasNativePerformanceNow = typeof performance === "object" && typeof performance.now === "function";
const now = hasNativePerformanceNow
? () => performance.now()
: () => Date.now();
function cancelTimeout(timeoutID) {
cancelAnimationFrame(timeoutID.id);
}
exports.cancelTimeout = cancelTimeout;
/**
* Create a throttler based on RAF
* @param callback
* @param delay
*/
function requestTimeout(callback, delay) {
const start = now();
function tick() {
if (now() - start >= delay) {
callback.call(null);
}
else {
timeoutID.id = requestAnimationFrame(tick);
}
}
const timeoutID = {
id: requestAnimationFrame(tick),
};
return timeoutID;
}
exports.requestTimeout = requestTimeout;
exports.selectionFromActiveCell = (activeCell) => {
if (!activeCell)
return [];
return [
{
bounds: {
top: activeCell.rowIndex,
left: activeCell.columnIndex,
bottom: activeCell.rowIndex,
right: activeCell.columnIndex,
},
},
];
};
/**
* Converts a number to alphabet
* @param i
*/
exports.numberToAlphabet = (i) => {
return ((i >= 26 ? exports.numberToAlphabet(((i / 26) >> 0) - 1) : "") +
"abcdefghijklmnopqrstuvwxyz"[i % 26 >> 0]).toUpperCase();
};
/**
* Convert selections to html and csv data
* @param rows
*/
exports.prepareClipboardData = (rows) => {
const html = ["<table>"];
const csv = [];
rows.forEach((row) => {
html.push("<tr>");
const csvRow = [];
row.forEach((cell) => {
html.push(`<td>${cell}</td>`);
csvRow.push(`"${cell.replace(/"/g, '""')}"`);
});
csv.push(csvRow.join(","));
html.push("</tr>");
});
html.push("</table>");
return [html.join(""), csv.join("\n")];
};
/**
* Cycles active cell within selecton bounds
* @param activeCellBounds
* @param selectionBounds
* @param direction
*/
exports.findNextCellWithinBounds = (activeCellBounds, selectionBounds, direction = types_1.Direction.Right) => {
let rowIndex, columnIndex;
let nextActiveCell = null;
if (direction === types_1.Direction.Right) {
rowIndex = activeCellBounds.top;
columnIndex = activeCellBounds.left + 1;
if (columnIndex > selectionBounds.right) {
rowIndex = rowIndex + 1;
columnIndex = selectionBounds.left;
if (rowIndex > selectionBounds.bottom) {
rowIndex = selectionBounds.top;
}
}
nextActiveCell = { rowIndex, columnIndex };
}
if (direction === types_1.Direction.Left) {
rowIndex = activeCellBounds.bottom;
columnIndex = activeCellBounds.left - 1;
if (columnIndex < selectionBounds.left) {
rowIndex = rowIndex - 1;
columnIndex = selectionBounds.right;
if (rowIndex < selectionBounds.top) {
rowIndex = selectionBounds.bottom;
}
}
nextActiveCell = { rowIndex, columnIndex };
}
if (direction === types_1.Direction.Down) {
rowIndex = activeCellBounds.bottom + 1;
columnIndex = activeCellBounds.left;
if (rowIndex > selectionBounds.bottom) {
columnIndex = activeCellBounds.left + 1;
rowIndex = selectionBounds.top;
if (columnIndex > selectionBounds.right) {
columnIndex = selectionBounds.left;
}
}
nextActiveCell = { rowIndex, columnIndex };
}
if (direction === types_1.Direction.Up) {
rowIndex = activeCellBounds.top - 1;
columnIndex = activeCellBounds.left;
if (rowIndex < selectionBounds.top) {
columnIndex = activeCellBounds.left - 1;
rowIndex = selectionBounds.bottom;
if (columnIndex < selectionBounds.left) {
columnIndex = selectionBounds.right;
}
}
nextActiveCell = { rowIndex, columnIndex };
}
return nextActiveCell;
};
/**
* Get maximum bound of an area, caters to merged cells
* @param area
* @param boundGetter
*/
exports.mergedCellBounds = (area, boundGetter) => {
for (let i = area.top; i <= area.bottom; i++) {
for (let j = area.left; j <= area.right; j++) {
const bounds = boundGetter({ rowIndex: i, columnIndex: j });
if (bounds.bottom > area.bottom)
area.bottom = bounds.bottom;
if (bounds.right > area.right)
area.right = bounds.right;
if (bounds.left < area.left)
area.left = bounds.left;
if (bounds.top < area.top)
area.top = bounds.top;
}
}
return area;
};
/**
* Simple Canvas element to measure text size
* @param defaultFont
*
* Usage
*
* ```
* const textSizer = new AutoSizer('12px Arial')
* textSizer.measureText('Hello world').width
* ```
*/
exports.AutoSizerCanvas = (defaultFont) => {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const setFont = (font = defaultFont) => {
if (context)
context.font = font;
};
const measureText = (text) => context === null || context === void 0 ? void 0 : context.measureText(text);
/* Set font in constructor */
setFont(defaultFont);
return {
context,
measureText,
setFont,
};
};
/* Check if a value is null */
exports.isNull = (value) => value === void 0 || value === null || value === "";
//# sourceMappingURL=helpers.js.map