/* * Copyright 2016 Palantir Technologies, Inc. All rights reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import classNames from "classnames"; import { Component, createRef } from "react"; import { Utils as CoreUtils, DISPLAYNAME_PREFIX, EditableText, type EditableTextProps, type HotkeyConfig, HotkeysTarget, type UseHotkeysReturnValue, } from "@blueprintjs/core"; import * as Classes from "../common/classes"; import { Draggable } from "../interactions/draggable"; import { Cell, type CellProps } from "./cell"; export interface EditableCellProps extends Omit { /** * Whether the given cell is the current active/focused cell. */ isFocused?: boolean; /** * The value displayed in the text box. Be sure to update this value when * rendering this component after a confirmed change. */ value?: string; /** * A listener that is triggered if the user cancels the edit. This is * important to listen to if you are doing anything with `onChange` events, * since you'll likely want to revert whatever changes you made. The * callback will also receive the row index and column index if they were * originally provided via props. */ onCancel?: (value: string, rowIndex?: number, columnIndex?: number) => void; /** * A listener that is triggered as soon as the editable name is modified. * This can be due, for example, to keyboard input or the clipboard. The * callback will also receive the row index and column index if they were * originally provided via props. */ onChange?: (value: string, rowIndex?: number, columnIndex?: number) => void; /** * A listener that is triggered once the editing is confirmed. This is * usually due to the return (or enter) key press. * The callback will also receive the row index and column index if they * were originally provided via props. */ onConfirm?: (value: string, rowIndex?: number, columnIndex?: number) => void; /** * Props that should be passed to the EditableText when it is used to edit */ editableTextProps?: Omit; } export interface EditableCellState { isEditing?: boolean; savedValue?: string; dirtyValue?: string; } /** * Editable cell component. * * @see https://blueprintjs.com/docs/#table/api.editablecell */ export class EditableCell extends Component { public static displayName = `${DISPLAYNAME_PREFIX}.EditableCell`; public static defaultProps = { truncated: true, wrapText: false, }; private cellRef = createRef(); private contentsRef = createRef(); public state: EditableCellState = { isEditing: false, savedValue: this.props.value, }; public componentDidMount() { this.checkShouldFocus(); } public componentDidUpdate(prevProps: EditableCellProps) { const didPropsChange = !CoreUtils.shallowCompareKeys(this.props, prevProps, { exclude: ["style"] }) || !CoreUtils.deepCompareKeys(this.props, prevProps, ["style"]); const { value } = this.props; if (didPropsChange && value != null) { this.setState({ dirtyValue: value, savedValue: value }); } this.checkShouldFocus(); } public shouldComponentUpdate(nextProps: EditableCellProps, nextState: EditableCellState) { return ( !CoreUtils.shallowCompareKeys(this.props, nextProps, { exclude: ["style"] }) || !CoreUtils.shallowCompareKeys(this.state, nextState) || !CoreUtils.deepCompareKeys(this.props, nextProps, ["style"]) ); } public render() { return {this.renderCell}; } private renderCell = ({ handleKeyDown, handleKeyUp }: UseHotkeysReturnValue) => { const { editableTextProps, onCancel, onChange, onConfirm, tabIndex = 0, truncated, wrapText, ...spreadableProps } = this.props; const { isEditing, dirtyValue, savedValue } = this.state; const interactive = spreadableProps.interactive || isEditing; let cellContents: React.JSX.Element | undefined; if (isEditing) { const className = editableTextProps ? editableTextProps.className : null; cellContents = ( ); } else { const textClasses = classNames(Classes.TABLE_EDITABLE_TEXT, { [Classes.TABLE_TRUNCATED_TEXT]: truncated, [Classes.TABLE_NO_WRAP_TEXT]: !wrapText, }); cellContents = (
{savedValue}
); } return ( {cellContents} ); }; private checkShouldFocus() { if (this.props.isFocused && !this.state.isEditing) { // don't focus if we're editing -- we'll lose the fact that we're editing this.cellRef.current?.focus(); } } private handleKeyPress = () => { if (this.state.isEditing || !this.props.isFocused) { return; } // setting dirty value to empty string because apparently the text field will pick up the key and write it in there this.setState({ dirtyValue: "", isEditing: true, savedValue: this.state.savedValue }); }; private handleEdit = () => { this.setState({ dirtyValue: this.state.savedValue, isEditing: true }); }; private handleCancel = (value: string) => { // don't strictly need to clear the dirtyValue, but it's better hygiene this.setState({ dirtyValue: undefined, isEditing: false }); this.invokeCallback(this.props.onCancel, value); }; private handleChange = (value: string) => { this.setState({ dirtyValue: value }); this.invokeCallback(this.props.onChange, value); }; private handleConfirm = (value: string) => { this.setState({ dirtyValue: undefined, isEditing: false, savedValue: value }); this.invokeCallback(this.props.onConfirm, value); }; private invokeCallback( callback: ((value: string, rowIndex?: number, columnIndex?: number) => void) | undefined, value: string, ) { // pass through the row and column indices if they were provided as props by the consumer const { rowIndex, columnIndex } = this.props; callback?.(value, rowIndex, columnIndex); } private handleCellActivate = (_event: MouseEvent) => { return true; }; private handleCellDoubleClick = (_event: MouseEvent) => { this.handleEdit(); }; private hotkeys: HotkeyConfig[] = [ { combo: "f2", group: "Table", label: "Edit the currently focused cell", onKeyDown: this.handleEdit, }, ]; } /** @deprecated Use `EditableCell` instead */ export const EditableCell2 = EditableCell; // eslint-disable-next-line @typescript-eslint/no-deprecated export type EditableCell2 = InstanceType; /** @deprecated Use `EditableCellProps` instead */ export type EditableCell2Props = EditableCellProps;