import React = require('react'); import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as Autosuggest from 'react-autosuggest'; import { ObjectField } from '../../fields/ObjectField'; import './EditableView.scss'; export interface EditableProps { /** * Called to get the initial value for editing * */ GetValue(): string | undefined; /** * Called to apply changes * @param value - The string entered by the user to set the value to * @returns `true` if setting the value was successful, `false` otherwise * */ SetValue(value: string, shiftDown?: boolean, enterKey?: boolean): boolean; OnFillDown?(value: string): void; OnTab?(shift?: boolean): void; OnEmpty?(): void; /** * The contents to render when not editing */ contents: any; fontStyle?: string; fontSize?: number; height?: number | 'auto'; sizeToContent?: boolean; maxHeight?: number; display?: string; overflow?: string; autosuggestProps?: { resetValue: () => void; value: string; onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void; autosuggestProps: Autosuggest.AutosuggestProps; }; oneLine?: boolean; editing?: boolean; isEditingCallback?: (isEditing: boolean) => void; menuCallback?: (x: number, y: number) => void; textCallback?: (char: string) => boolean; showMenuOnLoad?: boolean; background?: string | undefined; placeholder?: string; } /** * Customizable view that can be given an arbitrary view to render normally, * but can also be edited with customizable functions to get a string version * of the content, and set the value based on the entered string. */ @observer export class EditableView extends React.Component { private _ref = React.createRef(); private _inputref = React.createRef(); @observable _editing: boolean = false; constructor(props: EditableProps) { super(props); this._editing = this.props.editing ? true : false; } @action componentDidUpdate() { if (this._editing && this.props.editing === false) { this._inputref.current?.value && this.finalizeEdit(this._inputref.current.value, false, true, false); } else if (this.props.editing !== undefined) { this._editing = this.props.editing; } } componentWillUnmount() { this._inputref.current?.value && this.finalizeEdit(this._inputref.current.value, false, true, false); } @action onKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'Tab': e.stopPropagation(); this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, false); this.props.OnTab?.(e.shiftKey); break; case 'Backspace': e.stopPropagation(); if (!e.currentTarget.value) this.props.OnEmpty?.(); break; case 'Enter': e.stopPropagation(); if (!e.ctrlKey) { this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, true); } else if (this.props.OnFillDown) { this.props.OnFillDown(e.currentTarget.value); this._editing = false; this.props.isEditingCallback?.(false); } break; case 'Escape': e.stopPropagation(); this._editing = false; this.props.isEditingCallback?.(false); break; case ':': this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); break; case 'Shift': case 'Alt': case 'Meta': case 'Control': break; default: if (this.props.textCallback?.(e.key)) { this._editing = false; this.props.isEditingCallback?.(false); } } }; @action onClick = (e: React.MouseEvent) => { if (this.props.editing !== false) { e.nativeEvent.stopPropagation(); if (this._ref.current && this.props.showMenuOnLoad) { this.props.menuCallback?.(this._ref.current.getBoundingClientRect().x, this._ref.current.getBoundingClientRect().y); } else { this._editing = true; this.props.isEditingCallback?.(true); } e.stopPropagation(); } }; @action finalizeEdit(value: string, shiftDown: boolean, lostFocus: boolean, enterKey: boolean) { if (this.props.SetValue(value, shiftDown, enterKey)) { this._editing = false; this.props.isEditingCallback?.(false); } else { this._editing = false; this.props.isEditingCallback?.(false); !lostFocus && setTimeout( action(() => { this._editing = true; this.props.isEditingCallback?.(true); }), 0 ); } } stopPropagation(e: React.SyntheticEvent) { e.stopPropagation(); } @action setIsFocused = (value: boolean) => { const wasFocused = this._editing; this._editing = value; return wasFocused !== this._editing; }; renderEditor() { return this.props.autosuggestProps ? ( this.finalizeEdit(e.currentTarget.value, false, true, false), onPointerDown: this.stopPropagation, onClick: this.stopPropagation, onPointerUp: this.stopPropagation, onKeyPress: this.stopPropagation, value: this.props.autosuggestProps.value, onChange: this.props.autosuggestProps.onChange, }} /> ) : ( this.finalizeEdit(e.currentTarget.value, false, true, false)} defaultValue={this.props.GetValue()} autoFocus={true} onKeyDown={this.onKeyDown} onKeyPress={this.stopPropagation} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} /> ); } render() { if (this._editing && this.props.GetValue() !== undefined) { return this.props.sizeToContent ? (
{this.props.GetValue()}
{this.renderEditor()}
) : ( this.renderEditor() ); } setTimeout(() => this.props.autosuggestProps?.resetValue()); return this.props.contents instanceof ObjectField ? null : (
e.stopPropagation()} onClick={this.onClick} placeholder={this.props.placeholder}> {this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}
); } }