react-konva-grid
Version:
Declarative React Canvas Grid primitive for Data table, Pivot table, Excel Worksheets
329 lines • 13.7 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;
};
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = __importStar(require("react"));
const types_1 = require("./../types");
const helpers_1 = require("../helpers");
/**
* Default cell editor
* @param props
*/
const DefaultEditor = (props) => {
const { rowIndex, columnIndex, onChange, onSubmit, onCancel, position, cell, nextFocusableCell, value = "", activeCell } = props, rest = __rest(props, ["rowIndex", "columnIndex", "onChange", "onSubmit", "onCancel", "position", "cell", "nextFocusableCell", "value", "activeCell"]);
const borderWidth = 2;
const padding = 10; /* 2 + 1 + 1 + 2 + 2 */
const textSizer = react_1.useRef(helpers_1.AutoSizerCanvas("12px Arial"));
const inputRef = react_1.useRef(null);
const { x = 0, y = 0, width = 0, height = 0 } = position;
const getWidth = react_1.useCallback((text) => {
var _a;
const textWidth = ((_a = textSizer.current.measureText(text)) === null || _a === void 0 ? void 0 : _a.width) || 0;
return Math.max(textWidth + padding, width);
}, []);
const [inputWidth, setInputWidth] = react_1.useState(() => getWidth(value));
react_1.useEffect(() => {
if (!inputRef.current)
return;
inputRef.current.focus();
/* Focus cursor at the end */
inputRef.current.selectionStart = value.length;
}, []);
const inputHeight = height;
return (react_1.default.createElement("div", { style: {
top: y - borderWidth / 2,
left: x,
position: "absolute",
width: inputWidth,
height: inputHeight + borderWidth,
padding: borderWidth,
boxShadow: "0 2px 6px 2px rgba(60,64,67,.15)",
border: "2px #1a73e8 solid",
background: "white",
} },
react_1.default.createElement("textarea", Object.assign({ rows: 1, cols: 1, ref: inputRef, defaultValue: value, style: {
font: "12px Arial",
lineHeight: 1.2,
width: "100%",
height: "100%",
padding: "0 1px",
margin: 0,
boxSizing: "border-box",
borderWidth: 0,
outline: "none",
resize: "none",
overflow: "hidden",
}, onChange: (e) => {
setInputWidth(getWidth(e.target.value));
onChange(e.target.value, cell);
}, onKeyDown: (e) => {
if (!inputRef.current)
return;
const isShiftKey = e.nativeEvent.shiftKey;
const value = inputRef.current.value;
// Enter key
if (e.which === types_1.KeyCodes.Enter) {
onSubmit &&
onSubmit(value, cell, nextFocusableCell(cell, isShiftKey ? types_1.Direction.Up : types_1.Direction.Down));
}
if (e.which === types_1.KeyCodes.Escape) {
onCancel && onCancel(e);
}
if (e.which === types_1.KeyCodes.Tab) {
// e.preventDefault();
onSubmit &&
onSubmit(value, cell, nextFocusableCell(cell, isShiftKey ? types_1.Direction.Left : types_1.Direction.Right));
}
} }, rest))));
};
const getDefaultEditor = (cell) => DefaultEditor;
/**
* Hook to make grid editable
* @param param
*/
const useEditable = ({ getEditor = getDefaultEditor, gridRef, getValue, onChange, onSubmit, onCancel, onDelete, selections = [], activeCell, onBeforeEdit, }) => {
const [isEditorShown, setShowEditor] = react_1.useState(false);
const [value, setValue] = react_1.useState("");
const [position, setPosition] = react_1.useState({
x: 0,
y: 0,
width: 0,
height: 0,
});
const currentActiveCellRef = react_1.useRef(null);
const initialActiveCell = react_1.useRef();
const [scrollPosition, setScrollPosition] = react_1.useState({
scrollLeft: 0,
scrollTop: 0,
});
const isDirtyRef = react_1.useRef(false);
const showEditor = () => setShowEditor(true);
const hideEditor = () => {
setShowEditor(false);
currentActiveCellRef.current = null;
};
const focusGrid = () => requestAnimationFrame(() => gridRef.current.focus());
/**
* Make a cell editable
* @param coords
* @param initialValue
*/
const makeEditable = (coords, initialValue) => {
if (!gridRef.current)
return;
/* Call on before edit */
if (onBeforeEdit && !onBeforeEdit(coords))
return;
currentActiveCellRef.current = coords;
const pos = gridRef.current.getCellOffsetFromCoords(coords);
setValue(initialValue || getValue(coords) || "");
showEditor();
setPosition(pos);
};
/* Activate edit mode */
const handleDoubleClick = react_1.useCallback((e) => {
const coords = gridRef.current.getCellCoordsFromOffset(e.clientX, e.clientY);
if (!coords)
return;
const { rowIndex, columnIndex } = coords;
makeEditable({ rowIndex, columnIndex });
}, [getValue]);
const isSelectionKey = (keyCode) => {
return [
types_1.KeyCodes.Right,
types_1.KeyCodes.Left,
types_1.KeyCodes.Up,
types_1.KeyCodes.Down,
types_1.KeyCodes.Meta,
types_1.KeyCodes.Escape,
types_1.KeyCodes.Tab,
types_1.KeyCodes.Home,
types_1.KeyCodes.End,
types_1.KeyCodes.CapsLock,
].includes(keyCode);
};
const handleKeyDown = react_1.useCallback((e) => {
const keyCode = e.nativeEvent.keyCode;
if (keyCode === types_1.KeyCodes.Tab && !initialActiveCell.current) {
initialActiveCell.current = activeCell;
}
if (isSelectionKey(keyCode) ||
e.nativeEvent.ctrlKey ||
(e.nativeEvent.shiftKey &&
(e.nativeEvent.key === "Shift" ||
e.nativeEvent.which === types_1.KeyCodes.SPACE)) ||
e.nativeEvent.metaKey ||
e.nativeEvent.which === types_1.KeyCodes.ALT)
return;
/* If user has not made any selection yet */
if (!activeCell)
return;
const { rowIndex, columnIndex } = activeCell;
if (keyCode === types_1.KeyCodes.Delete || keyCode === types_1.KeyCodes.BackSpace) {
// TODO: onbefore delete
onDelete && onDelete(activeCell, selections);
return;
}
const initialValue = keyCode === types_1.KeyCodes.Enter // Enter key
? undefined
: e.nativeEvent.key;
makeEditable({ rowIndex, columnIndex }, initialValue);
}, [selections, activeCell]);
/**
* Get next focusable cell
* Respects selection bounds
*/
const nextFocusableCell = react_1.useCallback((currentCell, direction = types_1.Direction.Right) => {
var _a, _b, _c, _d;
/* Next immediate cell */
let nextActiveCell = currentCell;
switch (direction) {
case types_1.Direction.Right:
nextActiveCell = {
rowIndex: currentCell.rowIndex,
columnIndex: currentCell.columnIndex + 1,
};
break;
case types_1.Direction.Up:
nextActiveCell = {
rowIndex: currentCell.rowIndex - 1,
columnIndex: currentCell.columnIndex,
};
break;
case types_1.Direction.Left:
nextActiveCell = {
rowIndex: currentCell.rowIndex,
columnIndex: currentCell.columnIndex - 1,
};
break;
default:
nextActiveCell = {
rowIndex: ((_b = (_a = initialActiveCell.current) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : currentCell.rowIndex) + 1,
columnIndex: (_d = (_c = initialActiveCell.current) === null || _c === void 0 ? void 0 : _c.columnIndex) !== null && _d !== void 0 ? _d : currentCell.columnIndex,
};
break;
}
if (direction === types_1.Direction.Right && !initialActiveCell.current) {
initialActiveCell.current = currentCell;
}
if (direction === types_1.Direction.Down) {
/* Move to the next row + cell */
initialActiveCell.current = undefined;
}
/* If user has selected some cells and active cell is within this selection */
if (selections.length && currentCell && gridRef) {
const { bounds } = selections[selections.length - 1];
const activeCellBounds = gridRef.current.getCellBounds(currentCell);
const nextCell = helpers_1.findNextCellWithinBounds(activeCellBounds, bounds, direction);
if (nextCell)
nextActiveCell = nextCell;
}
return nextActiveCell;
}, [selections]);
/* Save the value */
const handleSubmit = react_1.useCallback((value, activeCell, nextActiveCell) => {
/**
* Hide the editor first, so that we can handle onBlur events
* 1. Editor hides -> Submit
* 2. If user clicks outside the grid, onBlur is called, if there is a activeCell, we do another submit
*/
hideEditor();
/* Save the new value */
onSubmit && onSubmit(value, activeCell, nextActiveCell);
/* Keep the focus */
focusGrid();
}, []);
const handleMouseDown = react_1.useCallback((e) => {
if (currentActiveCellRef.current) {
if (isDirtyRef.current) {
handleSubmit(value, currentActiveCellRef.current);
}
else {
handleHide();
}
}
initialActiveCell.current = undefined;
}, [value]);
const handleChange = react_1.useCallback((newValue, activeCell) => {
if (!activeCell)
return;
/* Check if the value has changed. Used to conditionally submit if editor is not in focus */
isDirtyRef.current = newValue !== value;
setValue(newValue);
onChange && onChange(newValue, activeCell);
}, [value]);
/* When the input is blurred out */
const handleHide = react_1.useCallback(() => {
hideEditor();
onCancel && onCancel();
/* Keep the focus back in the grid */
focusGrid();
}, []);
const handleScroll = react_1.useCallback((scrollPos) => {
setScrollPosition(scrollPos);
}, []);
/* Editor */
const editingCell = currentActiveCellRef.current;
const Editor = react_1.useMemo(() => {
return editingCell
? getEditor(editingCell) || getDefaultEditor(editingCell)
: null;
}, [editingCell]);
/**
* Position of the cell
*/
const cellPositon = react_1.useMemo(() => {
return Object.assign(Object.assign({}, position), { x: position.x - scrollPosition.scrollLeft, y: position.y - scrollPosition.scrollTop });
}, [position, scrollPosition]);
const handleBlur = react_1.useCallback((e) => {
if (currentActiveCellRef.current) {
/* Keep the focus */
focusGrid();
}
}, []);
const editorComponent = isEditorShown && Editor ? (react_1.default.createElement(Editor
/* This is the cell that is currently being edited */
, {
/* This is the cell that is currently being edited */
cell: editingCell, activeCell: activeCell, value: value, selections: selections, onChange: handleChange, onSubmit: handleSubmit, onCancel: handleHide, position: cellPositon, nextFocusableCell: nextFocusableCell, onBlur: handleBlur })) : null;
return {
editorComponent,
onDoubleClick: handleDoubleClick,
onScroll: handleScroll,
onKeyDown: handleKeyDown,
onMouseDown: handleMouseDown,
nextFocusableCell,
isEditInProgress: !!editingCell,
editingCell,
};
};
exports.default = useEditable;
//# sourceMappingURL=useEditable.js.map