);
}
window.ToastHost = ToastHost;
// =====================================================================
// NumInput: input numérico tolerante (aceita vírgula, ponto e milhar BR)
//
// Correção importante:
// - O texto digitado fica em estado local enquanto o campo está em edição.
// - O componente só sincroniza com o valor externo quando o usuário NÃO está
// digitando. Isso evita o efeito de “digitei, saí do campo e voltou para 0”.
// - No blur, o valor confirmado é mantido visualmente e enviado ao pai sem
// depender da ordem de renderização do React.
// =====================================================================
const OBJ_STR_RE = /\[object[^\]]*\]/g;
const isCorrupt = (s) => typeof s === 'string' && s.includes('[object');
function parseLocaleNumber(raw) {
if (raw === null || raw === undefined) return null;
if (typeof raw === 'number') return isFinite(raw) ? raw : null;
let s = String(raw)
.replace(OBJ_STR_RE, '')
.replace(/R\$|%/g, '')
.replace(/\s+/g, '')
.trim();
if (!s || s === '-' || s === ',' || s === '.' || s === '-,' || s === '-.') return null;
const hasComma = s.includes(',');
const hasDot = s.includes('.');
if (hasComma && hasDot) {
// O último separador digitado é tratado como decimal.
if (s.lastIndexOf(',') > s.lastIndexOf('.')) {
s = s.replace(/\./g, '').replace(',', '.'); // 19.813,24 -> 19813.24
} else {
s = s.replace(/,/g, ''); // 19,813.24 -> 19813.24
}
} else if (hasComma) {
s = s.replace(',', '.');
} else if (hasDot) {
// 19.813 deve ser aceito como milhar BR; 19.8 como decimal.
const sign = s.startsWith('-') ? '-' : '';
const body = sign ? s.slice(1) : s;
if (/^\d{1,3}(\.\d{3})+$/.test(body)) s = sign + body.replace(/\./g, '');
}
const n = Number(s);
return Number.isFinite(n) ? n : null;
}
function numToDraft(value) {
if (value === null || value === undefined || value === '') return '';
const n = parseLocaleNumber(value);
return n === null ? '' : String(n);
}
function NumInput({ value, onChange, ...rest }) {
const [draft, setDraft] = useState(() => numToDraft(value));
const [editing, setEditing] = useState(false);
const committedRef = useRef(null);
const emit = useCallback((n) => {
if (typeof onChange === 'function') onChange(n);
}, [onChange]);
useEffect(() => {
if (editing) return;
const external = parseLocaleNumber(value);
const committed = committedRef.current;
// Após blur, pode existir um render intermediário com o valor antigo do pai.
// Nesse caso, preserva o texto confirmado em vez de voltar ao valor anterior.
if (committed !== null) {
if (external === null || Math.abs(external - committed) > 0.0000001) return;
committedRef.current = null;
}
setDraft(numToDraft(value));
}, [value, editing]);
const safeDraft = (typeof draft === 'string' ? draft : '').replace(OBJ_STR_RE, '');
return (
setEditing(true)}
onBlur={() => {
const num = parseLocaleNumber(safeDraft);
setEditing(false);
if (num === null) {
committedRef.current = 0;
setDraft(numToDraft(0));
emit(0);
return;
}
committedRef.current = num;
setDraft(numToDraft(num));
emit(num);
}}
onChange={(ev) => {
const text = ev.target.value;
if (isCorrupt(text)) {
setDraft('');
return;
}
setDraft(text);
const num = parseLocaleNumber(text);
// Atualiza o estado do pai sempre que o texto já representa um número.
// O draft local preserva formatos intermediários como "12," enquanto digita.
if (num !== null) emit(num);
}}
/>
);
}
window.NumInput = NumInput;
window.__isCorrupt = isCorrupt;
window.__parseLocaleNumber = parseLocaleNumber;