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 = Omit< DetailedHTMLProps, HTMLInputElement>, "prefix" | "suffix" > & Omit< DetailedHTMLProps< TextareaHTMLAttributes, HTMLTextAreaElement >, "prefix" | "suffix" > & { 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) => void; autoComplete?: (typeof AutocompleteOptions)[number]; name?: KeyType; valueUpdate?: string; numberText?: boolean; rawNumber?: boolean; setReady?: React.Dispatch>; decimal?: number; info?: string | ReactNode; ready?: boolean; validity?: TWUISelectValidityObject; clearInputProps?: React.DetailedHTMLProps< React.HTMLAttributes, HTMLDivElement >; }; 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, clearInputProps, rawNumber, ...props } = inputProps; function getFinalValue(v: any) { if (rawNumber) return twuiNumberfy(v); 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 [value, setValue] = React.useState( props.defaultValue ? String(props.defaultValue) : "", ); 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) => { if (buttonDownRef.current) return; if (changeHandler) { window.clearTimeout(externalValueChangeTimeout); externalValueChangeTimeout = setTimeout(() => { changeHandler(val); }, finalDebounce); } if (typeof val == "string") { if (!val.match(/./)) { setValidity({ isValid: true }); setValue(""); if (istextarea && textAreaRef.current) { textAreaRef.current.value = ""; } else if (inputRef?.current) { inputRef.current.value = ""; } return; } window.clearTimeout(timeout); if (validationRegex) { timeout = setTimeout(() => { setValidity({ isValid: validationRegex.test(val), msg: "Value mismatch", }); }, finalDebounce); } if (validationFunction) { window.clearTimeout(validationFnTimeout); validationFnTimeout = setTimeout(() => { if (validationRegex && !validationRegex.test(val)) { return; } validationFunction(val).then((res) => { setValidity(res); }); }, finalDebounce); } } }; React.useEffect(() => { if (typeof props.value !== "string" || !props.value.match(/./)) return; setValue(String(props.value)); }, [props.value]); React.useEffect(() => { if (istextarea && textAreaRef.current) { } else if (inputRef?.current) { inputRef.current.value = getFinalValue(value); } updateValueFn(value); }, [value]); function handleValueChange( e: React.ChangeEvent & React.ChangeEvent, ) { const newValue = e.target.value; setValue(newValue); 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); // } const targetComponent = istextarea ? (