insomnia/packages/insomnia-app/app/ui/components/base/editable.tsx

157 lines
3.7 KiB
TypeScript

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<Props, State> {
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
<KeydownBinder onKeydown={this._handleEditKeyDown} scoped>
<input
{...extra}
className={`editable ${className || ''}`}
type="text"
ref={this._handleSetInputRef}
defaultValue={initialValue}
onBlur={this._handleEditEnd}
/>
</KeydownBinder>
);
} 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 <span {...readViewProps}>{initialValue || blankValue}</span>;
}
}
}
}