159 lines
5.5 KiB
Vue
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>
|
|
|