UNPKG

react-konva-grid

Version:

Declarative React Canvas Grid primitive for Data table, Pivot table, Excel Worksheets

329 lines 13.7 kB
"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