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; toggle?: () => void; 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) :
{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}
; } }