import React = require('react'); import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as Autosuggest from 'react-autosuggest'; import { ObjectField } from '../../fields/ObjectField'; import './EditableView.scss'; import { DocumentIconContainer } from './nodes/DocumentIcon'; import { OverlayView } from './OverlayView'; 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; // whether to display the editable view as a single input line or as a textarea allowCRs?: boolean; // can carriage returns be entered editing?: boolean; isEditingCallback?: (isEditing: boolean) => void; menuCallback?: (x: number, y: number) => void; textCallback?: (char: string) => boolean; showMenuOnLoad?: boolean; background?: string | undefined; placeholder?: string; wrap?: string; // nowrap, pre-wrap, etc } /** * 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: HTMLInputElement | HTMLTextAreaElement | null = null; _overlayDisposer?: () => void; _editingDisposer?: IReactionDisposer; @observable _editing: boolean = false; constructor(props: EditableProps) { super(props); this._editing = this.props.editing ? true : false; } componentDidMount(): void { this._editingDisposer = reaction( () => this._editing, editing => { if (editing) { setTimeout(() => { if (this._inputref?.value.startsWith('=') || this._inputref?.value.startsWith(':=')) { this._overlayDisposer?.(); this._overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); } }); } else { this._overlayDisposer?.(); this._overlayDisposer = undefined; } }, { fireImmediately: true } ); } @action componentDidUpdate() { if (this._editing && this.props.editing === false) { this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false); } else if (this.props.editing !== undefined) { this._editing = this.props.editing; } } componentWillUnmount() { this._overlayDisposer?.(); this._editingDisposer?.(); this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false); } onChange = (e: React.ChangeEvent) => { const targVal = (e.target as any).value; if (!(targVal.startsWith(':=') || targVal.startsWith('='))) { this._overlayDisposer?.(); this._overlayDisposer = undefined; } else if (!this._overlayDisposer) { this._overlayDisposer = OverlayView.Instance.addElement(, { x: 0, y: 0 }); } }; @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': if (this.props.allowCRs !== true) { 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 'ArrowUp': case 'ArrowDown': case 'ArrowLeft': case 'ArrowRight': e.stopPropagation(); break; case 'Shift': case 'Alt': case 'Meta': case 'Control': break; case ':': if (this.props.menuCallback) { e.stopPropagation(); this.props.menuCallback(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); break; } default: if (this.props.textCallback?.(e.key)) { e.stopPropagation(); 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.props.oneLine !== false && this.props.GetValue()?.toString().indexOf('\n') === -1 ? ( (this._inputref = r)} style={{ display: this.props.display, overflow: 'auto', fontSize: this.props.fontSize, minWidth: 20, background: this.props.background }} placeholder={this.props.placeholder} onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} defaultValue={this.props.GetValue()} autoFocus={true} onChange={this.onChange} onKeyDown={this.onKeyDown} onKeyPress={this.stopPropagation} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} /> ) : (