@innertia-solutions/innertia-nuxt 0.1.42 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, watch, nextTick } from 'vue'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* CurrencyInput — input de moneda con formato SOLO visual.
|
|
6
|
+
* El `v-model` es SIEMPRE un número puro (o null); lo que se muestra es el valor
|
|
7
|
+
* agrupado con separador de miles y prefijo (ej. "$2.234.234"). Pensado para CLP
|
|
8
|
+
* (entero, sin decimales); `locale`/`prefix` lo hacen reusable.
|
|
9
|
+
*/
|
|
10
|
+
const props = withDefaults(defineProps<{
|
|
11
|
+
label?: string
|
|
12
|
+
placeholder?: string
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
error?: string | null
|
|
15
|
+
hint?: string
|
|
16
|
+
size?: 'sm' | 'md'
|
|
17
|
+
prefix?: string
|
|
18
|
+
locale?: string
|
|
19
|
+
min?: number | null
|
|
20
|
+
}>(), {
|
|
21
|
+
prefix: '$',
|
|
22
|
+
locale: 'es-CL',
|
|
23
|
+
min: null,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const model = defineModel<number | null>({ default: null })
|
|
27
|
+
|
|
28
|
+
const inputEl = ref<HTMLInputElement | null>(null)
|
|
29
|
+
const focused = ref(false)
|
|
30
|
+
|
|
31
|
+
const nf = computed(() => new Intl.NumberFormat(props.locale, {
|
|
32
|
+
minimumFractionDigits: 0,
|
|
33
|
+
maximumFractionDigits: 0,
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
function format(n: number | null | undefined): string {
|
|
37
|
+
if (n === null || n === undefined || Number.isNaN(n)) return ''
|
|
38
|
+
return props.prefix + nf.value.format(n)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parse(str: string): number | null {
|
|
42
|
+
const digits = (str || '').replace(/\D/g, '')
|
|
43
|
+
if (!digits) return null
|
|
44
|
+
return Number(digits)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const display = ref(format(model.value))
|
|
48
|
+
|
|
49
|
+
// Mantener el display en sync cuando el valor cambia desde fuera (no mientras escribe).
|
|
50
|
+
watch(model, (v) => {
|
|
51
|
+
if (!focused.value) display.value = format(v)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
function onInput(e: Event) {
|
|
55
|
+
const el = e.target as HTMLInputElement
|
|
56
|
+
let n = parse(el.value)
|
|
57
|
+
if (n !== null && props.min !== null && n < props.min) n = props.min
|
|
58
|
+
model.value = n
|
|
59
|
+
display.value = format(n)
|
|
60
|
+
// Caret al final tras reformatear (evita saltos raros al agrupar).
|
|
61
|
+
nextTick(() => {
|
|
62
|
+
const len = inputEl.value?.value.length ?? 0
|
|
63
|
+
inputEl.value?.setSelectionRange(len, len)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onBlur() {
|
|
68
|
+
focused.value = false
|
|
69
|
+
display.value = format(model.value)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const baseClasses = computed(() =>
|
|
73
|
+
`innertia-field${props.size === 'sm' ? ' innertia-field-sm' : ''}`,
|
|
74
|
+
)
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<div class="w-full">
|
|
79
|
+
<label v-if="label" class="block text-sm font-medium text-foreground mb-1.5">
|
|
80
|
+
{{ label }}
|
|
81
|
+
</label>
|
|
82
|
+
|
|
83
|
+
<input
|
|
84
|
+
ref="inputEl"
|
|
85
|
+
type="text"
|
|
86
|
+
inputmode="numeric"
|
|
87
|
+
:value="display"
|
|
88
|
+
:placeholder="placeholder"
|
|
89
|
+
:disabled="disabled"
|
|
90
|
+
:class="[baseClasses, error ? '!border-red-400 dark:!border-red-500' : '']"
|
|
91
|
+
@input="onInput"
|
|
92
|
+
@focus="focused = true"
|
|
93
|
+
@blur="onBlur"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<p v-if="error" class="text-xs text-red-500 dark:text-red-400 mt-1">{{ error }}</p>
|
|
97
|
+
<p v-else-if="hint" class="text-xs text-muted-foreground mt-1">{{ hint }}</p>
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|