import { useRef, useState } from 'react'; import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText, } from '@/components/ui/input-group'; type RupiahInputProps = { name: string; id?: string; defaultValue?: string | number | null; placeholder?: string; disabled?: boolean; ariaInvalid?: boolean; }; // Strips everything but digits from a display value (e.g. "1.500.000" -> "1500000"). function digitsOnly(value: string): string { return value.replace(/\D/g, '').replace(/^0+(?=\d)/, ''); } // Parses the initial value coming from the backend, which may be a decimal // string like "3000000.00" - only the integer part before the decimal point // is kept (unlike digitsOnly, which would wrongly treat "." as a thousands // separator here). function parseInitialDigits(value: string | number | null | undefined): string { return digitsOnly(String(value ?? '').split('.')[0]); } function formatThousands(digits: string): string { return digits.replace(/\B(?=(\d{3})+(?!\d))/g, '.'); } // Counts digit characters (ignoring separators) before `caret` in `value`. function digitsBeforeCaret(value: string, caret: number): number { let count = 0; for (let i = 0; i < caret && i < value.length; i++) { if (/\d/.test(value[i])) { count++; } } return count; } // Finds the position in `formatted` right after the `digitCount`-th digit. function caretForDigitCount(formatted: string, digitCount: number): number { if (digitCount <= 0) { return 0; } let seen = 0; for (let i = 0; i < formatted.length; i++) { if (/\d/.test(formatted[i])) { seen++; if (seen === digitCount) { return i + 1; } } } return formatted.length; } /** * The visible 's value IS the formatted "1.000.000" text - there is * no separate overlay, so the caret can never visually drift from the * displayed digits. On every change, the caret's digit-position is measured * first, the value is reformatted, and the caret is restored to the same * digit-position - all synchronously inside the change handler (never via * useLayoutEffect or requestAnimationFrame), so the DOM is already * consistent by the time React commits and fast typing never drops * keystrokes. */ export function RupiahInput({ name, id, defaultValue, placeholder = '0', disabled, ariaInvalid, }: RupiahInputProps) { const hiddenRef = useRef(null); const initialDigits = parseInitialDigits(defaultValue); const [value, setValue] = useState(formatThousands(initialDigits)); function handleChange(e: React.ChangeEvent) { const input = e.target; const caret = input.selectionStart ?? input.value.length; const digitCount = digitsBeforeCaret(input.value, caret); const digits = digitsOnly(input.value); const formatted = formatThousands(digits); const newCaret = caretForDigitCount(formatted, digitCount); input.value = formatted; input.setSelectionRange(newCaret, newCaret); setValue(formatted); if (hiddenRef.current) { hiddenRef.current.value = digits; } } return ( Rp ); }