Farmbot-Web-App/frontend/ui/blurable_input.tsx

132 lines
3.8 KiB
TypeScript

import * as React from "react";
import { equals, parseIntInput } from "../util";
import { isNumber } from "lodash";
import { InputError } from "./input_error";
import { t } from "../i18next_wrapper";
import { error } from "../toast/toast";
export interface BIProps {
value: string | number;
onCommit(e: React.SyntheticEvent<HTMLInputElement>): void;
min?: number;
max?: number;
type?:
| "text"
| "number"
| "email"
| "time"
| "date"
| "hidden";
name?: string;
id?: string;
/** Allow the user to empty out the form control. If unset, form control
* will reset itself to previous defaultValue. */
allowEmpty?: boolean;
disabled?: boolean;
className?: string;
placeholder?: string;
hidden?: boolean;
error?: string;
title?: string;
autoFocus?: boolean;
autoSelect?: boolean;
}
interface BIState {
buffer: string;
isEditing: boolean;
error: string | undefined;
}
export class BlurableInput extends React.Component<BIProps, Partial<BIState>> {
state: BIState = { buffer: "", isEditing: false, error: undefined };
get error() { return this.props.error || this.state.error; }
withinLimits = (options?: { toasts?: boolean }): boolean => {
const onError = (msg: string) => {
this.setState({ error: msg });
options?.toasts && error(msg);
};
if (this.props.type === "number") {
const value = parseIntInput(this.state.buffer);
if (isNumber(this.props.min) && value < this.props.min) {
onError(t("Value must be greater than or equal to {{min}}.",
{ min: this.props.min }));
return false;
}
if (isNumber(this.props.max) && value > this.props.max) {
onError(t("Value must be less than or equal to {{max}}.",
{ max: this.props.max }));
return false;
}
/** `e.currentTarget.value` is "" for any invalid number input. */
if ((this.state.buffer === "") && !this.props.allowEmpty) {
onError(t("Please enter a number."));
return false;
}
}
this.setState({ error: undefined });
return true;
}
/** Called on blur. */
maybeCommit = (e: React.SyntheticEvent<HTMLInputElement>) => {
const bufferOk = this.state.buffer || this.props.allowEmpty;
const shouldPassToParent = bufferOk && this.withinLimits({ toasts: true });
shouldPassToParent && this.props.onCommit(e);
this.setState({ isEditing: false, buffer: "", error: undefined });
}
focus = (e: React.FocusEvent<HTMLInputElement>) => {
const { value } = this.props;
this.props.autoSelect &&
e.target.setSelectionRange(0, e.target.value.length);
this.setState({
isEditing: true,
buffer: "" + (value || ""),
error: undefined
});
}
updateBuffer = (e: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ buffer: e.currentTarget.value }, this.withinLimits);
}
usualProps = () => {
const value = this.state.isEditing ?
this.state.buffer : this.props.value;
return {
value,
hidden: !!this.props.hidden,
onFocus: this.focus,
onChange: this.updateBuffer,
onSubmit: this.maybeCommit,
onBlur: this.maybeCommit,
name: this.props.name,
id: this.props.id,
min: this.props.min,
max: this.props.max,
type: this.props.type || "text",
disabled: this.props.disabled,
className: (this.props.className || "") + (this.error ? " error" : ""),
title: this.props.title || "",
placeholder: this.props.placeholder,
autoFocus: this.props.autoFocus,
};
}
shouldComponentUpdate(nextProps: BIProps, nextState: Partial<BIState>) {
return !equals(this.props, nextProps) || !equals(this.state, nextState);
}
render() {
return <div className="input">
<InputError error={this.error} />
<input {...this.usualProps()} />
</div>;
}
}