import { autoBindMethodsForReact } from 'class-autobind-decorator'; import React, { PureComponent } from 'react'; import { AUTOBIND_CFG } from '../../../common/constants'; import { KeydownBinder } from '../keydown-binder'; export const shouldSave = (oldValue, newValue, preventBlank = false) => { // Should not save if length = 0 and we want to prevent blank if (preventBlank && !newValue.length) { return false; } // Should not save if old value and new value is the same if (oldValue === newValue) { return false; } // Should save return true; }; interface Props { onSubmit: (value?: string) => void; value: string; fallbackValue?: string; blankValue?: string; renderReadView?: Function; singleClick?: boolean; onEditStart?: Function; className?: string; preventBlank?: boolean; } interface State { editing: boolean; } @autoBindMethodsForReact(AUTOBIND_CFG) export class Editable extends PureComponent { state: State = { editing: false, }; _input: HTMLInputElement | null = null; _handleSetInputRef(n: HTMLInputElement) { this._input = n; } _handleSingleClickEditStart() { if (this.props.singleClick) { this._handleEditStart(); } } _handleEditStart() { this.setState({ editing: true, }); setTimeout(() => { this._input?.focus(); this._input?.select(); }); if (this.props.onEditStart) { this.props.onEditStart(); } } _handleEditEnd() { const originalValue = this.props.value; const newValue = this._input?.value.trim(); if (shouldSave(originalValue, newValue, this.props.preventBlank)) { // Don't run onSubmit for values that haven't been changed this.props.onSubmit(newValue); } // This timeout prevents the UI from showing the old value after submit. // It should give the UI enough time to redraw the new value. setTimeout( async () => this.setState({ editing: false, }), 100, ); } _handleEditKeyDown(event: KeyboardEvent) { if (event.keyCode === 13) { // Pressed Enter this._handleEditEnd(); } else if (event.keyCode === 27) { // Pressed Escape // Prevent bubbling to modals and other escape listeners. event.stopPropagation(); if (this._input) { // Set the input to the original value this._input.value = this.props.value; this._handleEditEnd(); } } } render() { const { value, fallbackValue, blankValue, singleClick, onEditStart, preventBlank, className, onSubmit, renderReadView, ...extra } = this.props; const { editing } = this.state; const initialValue = value || fallbackValue; if (editing) { return ( // KeydownBinder must be used here to properly stop propagation // from reaching other scoped KeydownBinders ); } else { const readViewProps = { className: `editable ${className} ${!initialValue && 'empty'}`, title: singleClick ? 'Click to edit' : 'Double click to edit', onClick: this._handleSingleClickEditStart, onDoubleClick: this._handleEditStart, ...extra, }; if (renderReadView) { return renderReadView(initialValue, readViewProps); } else { return {initialValue || blankValue}; } } } }