vue-componentes/src/componentes/EliEntrada/EliEntradaNumero.vue
2026-06-06 12:12:24 -03:00

159 lines
5.5 KiB
Vue

<template>
<v-text-field
:model-value="displayValue"
:label="opcoes?.rotulo"
:placeholder="opcoes?.placeholder"
:type="isInteiro ? 'number' : 'text'"
:inputmode="isInteiro ? 'numeric' : 'decimal'"
:pattern="isInteiro ? '[0-9]*' : '[0-9.,]*'"
:readonly="Boolean(opcoes?.apenasLeitura)"
v-bind="attrs"
@update:model-value="onUpdateModelValue"
@focus="() => emit('focus')"
@blur="() => emit('blur')"
>
<!--
Em alguns cenários (ex.: type="number"), o prop `suffix` do Vuetify pode não aparecer.
Usamos slots para garantir exibição consistente de prefixo/sufixo.
-->
<template v-if="opcoes?.prefixo" #prepend-inner>
<span class="eli-entrada__prefixo">{{ opcoes.prefixo }}</span>
</template>
<template v-if="opcoes?.sufixo" #append-inner>
<span class="eli-entrada__sufixo">{{ opcoes.sufixo }}</span>
</template>
</v-text-field>
</template>
<script lang="ts">
import { computed, defineComponent, type PropType, ref, watch } from "vue"
import type { PadroesEntradas } from "./tiposEntradas"
import "./EliEntradaNumero.css"
import {
casasDecimaisFromPrecisao,
formatarNumero,
limitarCasasDecimais,
parseNumero,
somenteNumeros,
somenteNumerosESeparadorDecimal,
valorParcialDecimal,
} from "./EliEntradaNumero.helpers"
type EntradaNumero = PadroesEntradas["numero"]
export default defineComponent({
name: "EliEntradaNumero",
inheritAttrs: false,
props: {
/** Interface padrão (EliEntrada): value + opcoes. */
value: {
type: [Number, null] as unknown as PropType<EntradaNumero["value"]>,
default: undefined,
},
opcoes: {
type: Object as PropType<EntradaNumero["opcoes"]>,
required: true,
},
},
emits: {
"update:value": (_v: EntradaNumero["value"]) => true,
/** Compat Vue2 (v-model padrão: value + input) */
input: (_v: EntradaNumero["value"]) => true,
change: (_v: EntradaNumero["value"]) => true,
focus: () => true,
blur: () => true,
},
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 isInteiro = computed(() => {
// quando não existe precisão, tratamos como decimal livre
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
})
// Controle do texto exibido: impede o campo de ficar com caracteres inválidos.
const displayValue = ref<string>("")
const ultimoEmitido = ref<EntradaNumero["value"] | undefined>(undefined)
watch(
() => props.value,
(v) => {
// 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
// biome-ignore lint/suspicious/noExplicitAny: dynamic format
displayValue.value = formatarNumero(v as any, casasDecimais.value)
ultimoEmitido.value = v
},
{ 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)
// ex.: casas=2, "1" => 0.01 ; "123" => 1.23
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)
// display deve ser sempre o reflexo do value
// 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)
// Emissão:
// - vazio => null
// - decimal parcial ("1,") => 1 (regra B)
// - caso geral => parse normal
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"]
}
ultimoEmitido.value = out
emit("update:value", out)
emit("input", out)
emit("change", out)
// display deve sempre corresponder ao value final
if (casasDecimais.value === null && textoFiltrado) {
displayValue.value = textoFiltrado.replace(/\./g, ",")
} else {
// biome-ignore lint/suspicious/noExplicitAny: dynamic format
displayValue.value = formatarNumero(out as any, casasDecimais.value)
}
}
return { attrs, emit, displayValue, isInteiro, onUpdateModelValue }
},
})
</script>