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
);
}