import React, { ComponentProps, DetailedHTMLProps, InputHTMLAttributes, LabelHTMLAttributes, ReactNode, RefObject, TextareaHTMLAttributes, } from "react"; import { twMerge } from "tailwind-merge"; import Span from "../../layout/Span"; import Button from "../../layout/Button"; import { Eye, EyeOff, Info, InfoIcon, X } from "lucide-react"; import { AutocompleteOptions } from "../../types"; import twuiNumberfy from "../../utils/numberfy"; import Dropdown from "../../elements/Dropdown"; import Card from "../../elements/Card"; import Stack from "../../layout/Stack"; import NumberInputButtons from "./NumberInputButtons"; import twuiSlugToNormalText from "../../utils/slug-to-normal-text"; import twuiUseReady from "../../hooks/useReady"; import Row from "../../layout/Row"; import Paper from "../../elements/Paper"; import { TWUISelectValidityObject } from "../Select"; let timeout: any; let validationFnTimeout: any; let externalValueChangeTimeout: any; export type InputProps = DetailedHTMLProps< InputHTMLAttributes, HTMLInputElement > & DetailedHTMLProps< TextareaHTMLAttributes, HTMLTextAreaElement > & { label?: string; variant?: "normal" | "warning" | "error" | "inactive"; prefix?: string | ReactNode; suffix?: string | ReactNode; suffixProps?: React.DetailedHTMLProps< React.HTMLAttributes, HTMLDivElement >; showLabel?: boolean; istextarea?: boolean; wrapperProps?: DetailedHTMLProps< InputHTMLAttributes, HTMLDivElement >; wrapperWrapperProps?: ComponentProps; labelProps?: DetailedHTMLProps< LabelHTMLAttributes, HTMLLabelElement >; componentRef?: RefObject; validationRegex?: RegExp; debounce?: number; invalidMessage?: string; validationFunction?: ( value: string, element?: HTMLInputElement | HTMLTextAreaElement ) => Promise; changeHandler?: ( value: string, element?: HTMLInputElement | HTMLTextAreaElement ) => void; autoComplete?: (typeof AutocompleteOptions)[number]; name?: KeyType; valueUpdate?: string; numberText?: boolean; setReady?: React.Dispatch>; decimal?: number; info?: string | ReactNode; ready?: boolean; validity?: TWUISelectValidityObject; }; let refreshes = 0; /** * # Input Element * @className twui-input * @className twui-input-wrapper * @className twui-input-invalid * @className twui-clear-input-field-button */ export default function Input( inputProps: InputProps ) { const { label, variant, prefix, suffix, componentRef, labelProps, wrapperProps, wrapperWrapperProps, showLabel, istextarea, debounce, invalidMessage: initialInvalidMessage, autoComplete, validationFunction, validationRegex, valueUpdate, numberText, decimal, suffixProps, ready: existingReady, setReady: externalSetReady, info, changeHandler, validity: existingValidity, ...props } = inputProps; function getFinalValue(v: any) { if (numberText) { return ( twuiNumberfy(v, decimal).toLocaleString() + (String(v).match(/\.$/) ? "." : "") ); } return v; } const defaultInitialValue = props.defaultValue || props.value ? getFinalValue(props.defaultValue || props.value) : undefined; const [validity, setValidity] = React.useState( existingValidity || { isValid: true, } ); const inputRef = componentRef || React.useRef(null); const textAreaRef = componentRef || React.useRef(null); const buttonDownRef = React.useRef(false); const [focus, setFocus] = React.useState(false); const [inputType, setInputType] = React.useState( numberText ? "text" : props.type ); const DEFAULT_DEBOUNCE = 500; const finalDebounce = debounce || DEFAULT_DEBOUNCE; const finalLabel = label || props.title || props.placeholder || (props.name ? twuiSlugToNormalText(props.name) : undefined); function getNormalizedValue(value: string) { if (numberText) { if (props.max && twuiNumberfy(value) > twuiNumberfy(props.max)) return getFinalValue(props.max); if (props.min && twuiNumberfy(value) < twuiNumberfy(props.min)) return getFinalValue(props.min); return getFinalValue(value); } else { return value; } } React.useEffect(() => { if (!existingValidity) return; setValidity(existingValidity); }, [existingValidity]); const updateValueFn = ( val: string, el?: HTMLInputElement | HTMLTextAreaElement ) => { if (buttonDownRef.current) return; if (changeHandler) { window.clearTimeout(externalValueChangeTimeout); externalValueChangeTimeout = setTimeout(() => { changeHandler(val, el); }, finalDebounce); } if (typeof val == "string") { if (!val.match(/./)) { setValidity({ isValid: true }); props.value = ""; if (istextarea && textAreaRef.current) { textAreaRef.current.value = ""; } else if (inputRef?.current) { inputRef.current.value = ""; } return; } window.clearTimeout(timeout); if (validationRegex && !validationFunction) { timeout = setTimeout(() => { setValidity({ isValid: validationRegex.test(val), msg: "Value mismatch", }); }, finalDebounce); } else if (validationFunction) { window.clearTimeout(validationFnTimeout); validationFnTimeout = setTimeout(() => { if (validationRegex && !validationRegex.test(val)) { return; } validationFunction(val, el).then((res) => { setValidity(res); }); }, finalDebounce); } } }; React.useEffect(() => { if (typeof props.value !== "string" || !props.value.match(/./)) return; updateValueFn(String(props.value)); }, [props.value]); function handleValueChange( e: React.ChangeEvent & React.ChangeEvent ) { const newValue = e.target.value; updateValue(newValue, e.target); props.onChange?.(e); } function updateValue( v: string, el?: HTMLInputElement | HTMLTextAreaElement ) { if (istextarea && textAreaRef.current) { } else if (inputRef?.current) { inputRef.current.value = getFinalValue(v); } updateValueFn(v, el); } const targetComponent = istextarea ? (