This commit is contained in:
Luiz Silva 2026-02-15 15:17:08 -03:00
parent 8a5596e860
commit 57325f6744
100 changed files with 16153 additions and 4623 deletions

View file

@ -25,87 +25,90 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref, watch } from "vue";
import type { PadroesEntradas } from "./tiposEntradas";
import { computed, defineComponent, type PropType, ref, watch } from "vue"
import type { PadroesEntradas } from "./tiposEntradas"
type EntradaNumero = PadroesEntradas["numero"];
type EntradaNumero = PadroesEntradas["numero"]
function casasDecimaisFromPrecisao(precisao: number): number {
if (!Number.isFinite(precisao) || precisao <= 0) return 0;
if (precisao >= 1) return 0;
if (!Number.isFinite(precisao) || precisao <= 0) return 0
if (precisao >= 1) return 0
// Preferência por contar casas decimais na representação (ex.: 0.01 -> 2)
const texto = precisao.toString();
const texto = precisao.toString()
if (texto.includes("e-")) {
const [, exp] = texto.split("e-");
const n = Number(exp);
return Number.isFinite(n) ? n : 0;
const [, exp] = texto.split("e-")
const n = Number(exp)
return Number.isFinite(n) ? n : 0
}
const idx = texto.indexOf(".");
if (idx === -1) return 0;
const dec = texto.slice(idx + 1).replace(/0+$/, "");
return dec.length;
const idx = texto.indexOf(".")
if (idx === -1) return 0
const dec = texto.slice(idx + 1).replace(/0+$/, "")
return dec.length
}
function parseNumero(texto: string): number | null {
const normalizado = (texto ?? "").trim().replace(/,/g, ".");
if (!normalizado) return null;
const n = Number(normalizado);
if (Number.isNaN(n)) return null;
return n;
const normalizado = (texto ?? "").trim().replace(/,/g, ".")
if (!normalizado) return null
const n = Number(normalizado)
if (Number.isNaN(n)) return null
return n
}
function formatarNumero(value: number | null | undefined, casas: number | null) {
if (value === null || value === undefined) return "";
if (casas === null) return String(value);
function formatarNumero(
value: number | null | undefined,
casas: number | null,
) {
if (value === null || value === undefined) return ""
if (casas === null) return String(value)
// Garantimos que o texto visual corresponda ao value (fixed).
const fixed = Number(value).toFixed(Math.max(0, casas));
const fixed = Number(value).toFixed(Math.max(0, casas))
// Exibe com vírgula (pt-BR)
return fixed.replace(/\./g, ",");
return fixed.replace(/\./g, ",")
}
function somenteNumeros(texto: string) {
return (texto ?? "").replace(/\D+/g, "");
return (texto ?? "").replace(/\D+/g, "")
}
function somenteNumerosESeparadorDecimal(texto: string) {
// Mantém apenas números e UM separador decimal ("," ou "."), sem sinais.
// Implementação baseada em regex + colapso de separadores.
const limpo = (texto ?? "").replace(/[^0-9.,]+/g, "");
const matchSep = limpo.match(/[.,]/);
if (!matchSep) return limpo;
const limpo = (texto ?? "").replace(/[^0-9.,]+/g, "")
const matchSep = limpo.match(/[.,]/)
if (!matchSep) return limpo
const sep = matchSep[0];
const idx = limpo.indexOf(sep);
const antesRaw = limpo.slice(0, idx).replace(/[.,]/g, "");
const depois = limpo.slice(idx + 1).replace(/[.,]/g, "");
const sep = matchSep[0]
const idx = limpo.indexOf(sep)
const antesRaw = limpo.slice(0, idx).replace(/[.,]/g, "")
const depois = limpo.slice(idx + 1).replace(/[.,]/g, "")
// Se o usuário começa pelo separador (",4"), normalizamos para "0,4".
const antes = antesRaw.length ? antesRaw : "0";
return `${antes}${sep}${depois}`;
const antes = antesRaw.length ? antesRaw : "0"
return `${antes}${sep}${depois}`
}
function limitarCasasDecimais(texto: string, casas: number | null) {
if (casas === null) return texto;
if (casas <= 0) return texto.replace(/[.,]/g, "");
if (casas === null) return texto
if (casas <= 0) return texto.replace(/[.,]/g, "")
const matchSep = texto.match(/[.,]/);
if (!matchSep) return texto;
const sep = matchSep[0];
const idx = texto.indexOf(sep);
const matchSep = texto.match(/[.,]/)
if (!matchSep) return texto
const sep = matchSep[0]
const idx = texto.indexOf(sep)
const inteiro = texto.slice(0, idx);
const frac = texto.slice(idx + 1);
return `${inteiro}${sep}${frac.slice(0, casas)}`;
const inteiro = texto.slice(0, idx)
const frac = texto.slice(idx + 1)
return `${inteiro}${sep}${frac.slice(0, casas)}`
}
function valorParcialDecimal(texto: string): number | null {
// Regra (B): se o usuário digitou "1," ou "1." emite 1.
const m = texto.match(/^(\d+)[.,]$/);
if (!m) return null;
const n = Number(m[1]);
return Number.isNaN(n) ? null : n;
const m = texto.match(/^(\d+)[.,]$/)
if (!m) return null
const n = Number(m[1])
return Number.isNaN(n) ? null : n
}
export default defineComponent({
@ -133,25 +136,25 @@ export default defineComponent({
setup(props, { attrs, emit }) {
// Se `precisao` não existir => não limitamos casas (mas ainda bloqueamos caracteres inválidos).
const casasDecimais = computed<number | null>(() => {
const p = props.opcoes?.precisao;
if (p === undefined || p === null) return null;
return casasDecimaisFromPrecisao(p);
});
const p = props.opcoes?.precisao
if (p === undefined || p === null) return null
return casasDecimaisFromPrecisao(p)
})
const isInteiro = computed(() => {
// quando não existe precisão, tratamos como decimal livre
return casasDecimais.value === 0;
});
return casasDecimais.value === 0
})
const isFixedPoint = computed(() => {
// Quando precisao existe e é < 1, controlamos a vírgula automaticamente.
const casas = casasDecimais.value;
return casas !== null && casas > 0;
});
const casas = casasDecimais.value
return casas !== null && casas > 0
})
// Controle do texto exibido: impede o campo de ficar com caracteres inválidos.
const displayValue = ref<string>("");
const ultimoEmitido = ref<EntradaNumero["value"] | undefined>(undefined);
const displayValue = ref<string>("")
const ultimoEmitido = ref<EntradaNumero["value"] | undefined>(undefined)
watch(
() => props.value,
@ -159,63 +162,71 @@ export default defineComponent({
// Se foi uma atualização resultante do que acabamos de emitir, não sobrescreve.
// Isso evita o efeito de "apagar" o texto ao digitar algo parcial (ex.: "1,")
// quando o valor emitido é `1` ou `null`.
if (v === ultimoEmitido.value) return;
displayValue.value = formatarNumero(v as any, casasDecimais.value);
ultimoEmitido.value = v;
if (v === ultimoEmitido.value) return
// biome-ignore lint/suspicious/noExplicitAny: dynamic format
displayValue.value = formatarNumero(v as any, casasDecimais.value)
ultimoEmitido.value = v
},
{ immediate: true }
);
{ immediate: true },
)
function onUpdateModelValue(texto: string) {
// Modo fixed-point: usuário digita números continuamente e a vírgula é inserida automaticamente.
if (isFixedPoint.value) {
const casas = casasDecimais.value ?? 0;
const digitos = somenteNumeros(texto);
const casas = casasDecimais.value ?? 0
const digitos = somenteNumeros(texto)
// ex.: casas=2, "1" => 0.01 ; "123" => 1.23
const baseInt = digitos ? Number(digitos) : 0;
const divisor = Math.pow(10, casas);
const n = digitos ? baseInt / divisor : null;
const baseInt = digitos ? Number(digitos) : 0
const divisor = 10 ** casas
const n = digitos ? baseInt / divisor : null
const out = (n === null ? null : n) as EntradaNumero["value"];
ultimoEmitido.value = out;
emit("update:value", out);
emit("input", out);
emit("change", out);
const out = (n === null ? null : n) as EntradaNumero["value"]
ultimoEmitido.value = out
emit("update:value", out)
emit("input", out)
emit("change", out)
// display deve ser sempre o reflexo do value
displayValue.value = formatarNumero(out as any, casas);
return;
// biome-ignore lint/suspicious/noExplicitAny: dynamic format
displayValue.value = formatarNumero(out as any, casas)
return
}
// Modo livre (sem precisao) ou inteiro: aceita números e 1 separador (no caso livre)
const base = isInteiro.value ? somenteNumeros(texto) : somenteNumerosESeparadorDecimal(texto);
const textoFiltrado = isInteiro.value ? base : limitarCasasDecimais(base, casasDecimais.value);
const base = isInteiro.value
? somenteNumeros(texto)
: somenteNumerosESeparadorDecimal(texto)
const textoFiltrado = isInteiro.value
? base
: limitarCasasDecimais(base, casasDecimais.value)
// Emissão:
// - vazio => null
// - decimal parcial ("1,") => 1 (regra B)
// - caso geral => parse normal
let out: EntradaNumero["value"] = null;
let out: EntradaNumero["value"] = null
if (textoFiltrado) {
const parcial = isInteiro.value ? null : valorParcialDecimal(textoFiltrado);
const n = parcial ?? parseNumero(textoFiltrado);
out = (n === null ? null : n) as EntradaNumero["value"];
const parcial = isInteiro.value
? null
: valorParcialDecimal(textoFiltrado)
const n = parcial ?? parseNumero(textoFiltrado)
out = (n === null ? null : n) as EntradaNumero["value"]
}
ultimoEmitido.value = out;
ultimoEmitido.value = out
emit("update:value", out);
emit("input", out);
emit("change", out);
emit("update:value", out)
emit("input", out)
emit("change", out)
// display deve sempre corresponder ao value final
displayValue.value = formatarNumero(out as any, casasDecimais.value);
// biome-ignore lint/suspicious/noExplicitAny: dynamic format
displayValue.value = formatarNumero(out as any, casasDecimais.value)
}
return { attrs, emit, displayValue, isInteiro, onUpdateModelValue };
return { attrs, emit, displayValue, isInteiro, onUpdateModelValue }
},
});
})
</script>
<style>