import React, { ComponentProps, DetailedHTMLProps, InputHTMLAttributes, LabelHTMLAttributes, ReactNode, RefObject, TextareaHTMLAttributes, useRef, } from "react"; import { twMerge } from "tailwind-merge"; import Span from "../../layout/Span"; import { Eye, EyeOff, Info, InfoIcon, X } from "lucide-react"; import { AutocompleteOptions } from "../../types"; import twuiNumberfy from "../../utils/numberfy"; import Dropdown from "../../elements/Dropdown"; import Stack from "../../layout/Stack"; import NumberInputButtons from "./NumberInputButtons"; import twuiSlugToNormalText from "../../utils/slug-to-normal-text"; import Row from "../../layout/Row"; import Paper from "../../elements/Paper"; import { TWUISelectValidityObject } from "../Select"; 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 >; // refreshDefaultValue?: number; }; 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, // refreshDefaultValue, ...props } = inputProps; const componentRefreshesRef = useRef(0); let timeoutRef = useRef(null); let validationFnTimeoutRef = useRef(null); let externalValueChangeTimeoutRef = useRef(null); refreshes++; componentRefreshesRef.current++; 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); React.useEffect(() => { // if (!existingReady) return; if (!existingValidity) return; setValidity(existingValidity); }, [existingValidity]); const updateValueFn = (val: string) => { if (buttonDownRef.current) return; if (changeHandler) { window.clearTimeout(externalValueChangeTimeoutRef.current); externalValueChangeTimeoutRef.current = 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(timeoutRef.current); if (validationRegex) { timeoutRef.current = setTimeout(() => { setValidity({ isValid: validationRegex.test(val), msg: "Value mismatch", }); }, finalDebounce); } if (validationFunction) { window.clearTimeout(validationFnTimeoutRef.current); validationFnTimeoutRef.current = setTimeout(() => { if (validationRegex && !validationRegex.test(val)) { return; } validationFunction(val).then((res) => { setValidity(res); }); }, finalDebounce); } } }; React.useEffect(() => { // if (!existingReady) return; if (typeof props.value !== "string" || !props.value.match(/./)) return; setValue(String(props.value)); }, [props.value]); // React.useEffect(() => { // if (!refreshDefaultValue) return; // console.log("Name:", props.title || props.name); // console.log("props.defaultValue", props.defaultValue); // // setValue(String(props.defaultValue || "")); // }, [refreshDefaultValue]); React.useEffect(() => { // if (!existingReady) return; 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 ? (