import React, { useCallback, useEffect, useRef, useState, useMemo, } from "react"; import { CellInterface, ScrollCoords, CellPosition, GridRef, SelectionArea, } from "../Grid"; import { KeyCodes, Direction } from "./../types"; import { findNextCellWithinBounds, AutoSizerCanvas } from "../helpers"; export interface UseEditableOptions { /** * Inject custom editors based on a cell */ getEditor?: (cell: CellInterface | null) => React.ElementType; /** * Access grid methods */ gridRef: React.MutableRefObject; /** * Value getter */ getValue: (cell: CellInterface) => any; /** * Callback when user cancels editing */ onCancel?: (e?: React.KeyboardEvent) => void; /** * Callback when user changes a value in editor */ onChange: (value: string, activeCell: CellInterface) => void; /** * Callback when user submits a value. Use this to update state */ onSubmit?: ( value: string, activeCell: CellInterface, nextActiveCell?: CellInterface | null ) => void; /** * Callback when user selects an area and presses delete key */ onDelete?: (activeCell: CellInterface, selections: SelectionArea[]) => void; /** * Currently selected cells, injected by useSelection */ selections: SelectionArea[]; /** * Active selected cell. This can change, if the user is in formula mode */ activeCell: CellInterface | null; /** * Callback fired before editing. Can be used to prevent editing. Do not use it, Can be removed in next release. */ onBeforeEdit?: (coords: CellInterface) => boolean; } export interface EditableResults { /** * Editor component that can be injected */ editorComponent: React.ReactNode; /** * Double click listener, activates the grid */ onDoubleClick: (e: React.MouseEvent) => void; /** * OnScroll listener to align the editor */ onScroll: (props: ScrollCoords) => void; /** * Key down listeners */ onKeyDown: (e: React.KeyboardEvent) => void; /** * Mouse down listener which triggers Blur event on the editors */ onMouseDown: (e: React.MouseEvent) => void; /** * Get next focusable cell based on current activeCell and direction user is moving */ nextFocusableCell: ( currentCell: CellInterface, direction: Direction ) => CellInterface; /** * Is editing in progress */ isEditInProgress: boolean; /** * Currently editing cell */ editingCell: CellInterface | null; } export interface EditorProps extends CellInterface { /** * Currently selected bounds, useful for fomulas */ selections: SelectionArea[]; /** * Initial value of the cell */ value?: string; /** * Callback when a value has changed. */ onChange: (value: string, activeCell: CellInterface) => void; /** * Callback to submit the value back to data store */ onSubmit?: ( value: string, activeCell: CellInterface, nextActiveCell?: CellInterface | null ) => void; /** * On Cancel callbacks. Hides the editor */ onCancel?: (e?: React.KeyboardEvent) => void; /** * Cell position, x, y, width and height */ position: CellPosition; /** * Currently active cell, based on selection */ activeCell: CellInterface; /** * Currrently edited cell */ cell: CellInterface; /** * Next cell that should receive focus */ nextFocusableCell: ( activeCell: CellInterface, direction?: Direction ) => CellInterface | null; } /** * Default cell editor * @param props */ const DefaultEditor: React.FC = (props) => { const { rowIndex, columnIndex, onChange, onSubmit, onCancel, position, cell, nextFocusableCell, value = "", activeCell, ...rest } = props; const borderWidth = 2; const padding = 10; /* 2 + 1 + 1 + 2 + 2 */ const textSizer = useRef(AutoSizerCanvas("12px Arial")); const inputRef = useRef(null); const { x = 0, y = 0, width = 0, height = 0 } = position; const getWidth = useCallback((text) => { const textWidth = textSizer.current.measureText(text)?.width || 0; return Math.max(textWidth + padding, width); }, []); const [inputWidth, setInputWidth] = useState(() => getWidth(value)); useEffect(() => { if (!inputRef.current) return; inputRef.current.focus(); /* Focus cursor at the end */ inputRef.current.selectionStart = value.length; }, []); const inputHeight = height; return (