nexa-ui-kit 0.7.10 → 0.8.0
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.
- package/dist/components/NAlert.js +1 -1
- package/dist/components/NAutocomplete.js +1 -1
- package/dist/components/NAvatar.js +1 -1
- package/dist/components/NBadge.js +1 -1
- package/dist/components/NBottomSheet.js +1 -1
- package/dist/components/NButton.js +225 -226
- package/dist/components/NButton.nexa +274 -275
- package/dist/components/NCard.js +3 -3
- package/dist/components/NCheckbox.js +1 -1
- package/dist/components/NChips.js +1 -1
- package/dist/components/NDataTable.js +59 -61
- package/dist/components/NDataTable.nexa +203 -204
- package/dist/components/NDatepicker.js +1 -1
- package/dist/components/NForm.js +1 -1
- package/dist/components/NFormField.js +1 -1
- package/dist/components/NImage.js +1 -1
- package/dist/components/NInput.js +1 -1
- package/dist/components/NInputNumber.js +17 -17
- package/dist/components/NInputNumber.nexa +232 -232
- package/dist/components/NModal.js +131 -131
- package/dist/components/NModal.nexa +226 -226
- package/dist/components/NMultiSelect.js +1 -1
- package/dist/components/NPaginator.js +1 -1
- package/dist/components/NPassword.js +1 -1
- package/dist/components/NProgressBar.js +1 -1
- package/dist/components/NRadio.js +1 -1
- package/dist/components/NScrollView.js +1 -1
- package/dist/components/NSelect.js +1 -1
- package/dist/components/NSkeleton.js +1 -1
- package/dist/components/NSwitch.js +1 -1
- package/dist/components/NTabs.js +1 -1
- package/dist/components/NTag.js +1 -1
- package/dist/components/NToastContainer.js +1 -1
- package/dist/components/NTooltip.js +1 -1
- package/dist/components/NTreeMenu.js +1 -1
- package/dist/components/NVirtualList.js +1 -1
- package/package.json +4 -4
- package/src/components/NButton.nexa +274 -275
- package/src/components/NDataTable.nexa +203 -204
- package/src/components/NInputNumber.nexa +232 -232
- package/src/components/NModal.nexa +226 -226
|
@@ -1,232 +1,232 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { signal, computed, inject, effect, batch } from 'nexa-framework'
|
|
3
|
-
|
|
4
|
-
const props = defineProps({
|
|
5
|
-
modelValue: { type: Number, default: 0 },
|
|
6
|
-
min: { type: Number, default: null },
|
|
7
|
-
max: { type: Number, default: null },
|
|
8
|
-
step: { type: Number, default: 1 },
|
|
9
|
-
bindField: { type: Boolean, default: false },
|
|
10
|
-
disabled: { type: Boolean, default: false },
|
|
11
|
-
readonly: { type: Boolean, default: false },
|
|
12
|
-
label: { type: String, default: '' },
|
|
13
|
-
placeholder: { type: String, default: '' },
|
|
14
|
-
currency: { type: String, default: undefined },
|
|
15
|
-
locale: { type: String, default: undefined },
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
|
19
|
-
|
|
20
|
-
const field = inject('nexa-ui:form-field', undefined)
|
|
21
|
-
|
|
22
|
-
const effectiveValue = computed(() => {
|
|
23
|
-
if (props.bindField && field?.value) {
|
|
24
|
-
const v = field.value.value
|
|
25
|
-
return typeof v === 'number' ? v : Number(v)
|
|
26
|
-
}
|
|
27
|
-
return props.modelValue
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
const effectiveDisabled = computed(() => {
|
|
31
|
-
if (props.disabled) return true
|
|
32
|
-
if (props.bindField && field?.disabled) return !!field.disabled.value
|
|
33
|
-
return false
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
const text = signal(String(effectiveValue.value ?? ''))
|
|
37
|
-
|
|
38
|
-
effect(() => {
|
|
39
|
-
const next = effectiveValue.value
|
|
40
|
-
const nextText = next === null || next === undefined ? '' : String(next)
|
|
41
|
-
if (text.value === nextText) return
|
|
42
|
-
text.value = nextText
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
const canEdit = computed(() => !effectiveDisabled.value && !props.readonly)
|
|
46
|
-
|
|
47
|
-
const isFocused = signal(false)
|
|
48
|
-
|
|
49
|
-
const nfSignal = signal(null)
|
|
50
|
-
|
|
51
|
-
effect(() => {
|
|
52
|
-
if (props.currency) {
|
|
53
|
-
nfSignal.value = new Intl.NumberFormat(props.locale || undefined, {
|
|
54
|
-
style: 'currency',
|
|
55
|
-
currency: props.currency,
|
|
56
|
-
minimumFractionDigits: 2,
|
|
57
|
-
maximumFractionDigits: 2,
|
|
58
|
-
})
|
|
59
|
-
} else {
|
|
60
|
-
nfSignal.value = null
|
|
61
|
-
}
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
const displayText = computed(() => {
|
|
65
|
-
const raw = text.value
|
|
66
|
-
const nf = nfSignal.value
|
|
67
|
-
if (!nf || isFocused.value) return raw
|
|
68
|
-
if (!raw) return raw
|
|
69
|
-
const n = parseFloat(raw)
|
|
70
|
-
if (Number.isNaN(n)) return raw
|
|
71
|
-
return nf.format(n)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
const sanitize = (raw) => {
|
|
75
|
-
const input = String(raw ?? '')
|
|
76
|
-
if (!input) return ''
|
|
77
|
-
const allowMinus = typeof props.min === 'number' ? props.min < 0 : true
|
|
78
|
-
let out = ''
|
|
79
|
-
let hasDot = false
|
|
80
|
-
let hasMinus = false
|
|
81
|
-
for (let i = 0; i < input.length; i++) {
|
|
82
|
-
const ch = input[i] === ',' ? '.' : input[i]
|
|
83
|
-
const isDigit = ch >= '0' && ch <= '9'
|
|
84
|
-
if (isDigit) {
|
|
85
|
-
out += ch
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
|
-
if (ch === '.' && !hasDot) {
|
|
89
|
-
hasDot = true
|
|
90
|
-
out += ch
|
|
91
|
-
continue
|
|
92
|
-
}
|
|
93
|
-
if (ch === '-' && allowMinus && !hasMinus && out.length === 0) {
|
|
94
|
-
hasMinus = true
|
|
95
|
-
out += ch
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return out
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const clamp = (n) => {
|
|
102
|
-
if (n == null || Number.isNaN(n)) return null
|
|
103
|
-
let out = n
|
|
104
|
-
if (typeof props.min === 'number' && !Number.isNaN(props.min)) out = Math.max(out, props.min)
|
|
105
|
-
if (typeof props.max === 'number' && !Number.isNaN(props.max)) out = Math.min(out, props.max)
|
|
106
|
-
return out
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const parse = (raw) => {
|
|
110
|
-
const s = sanitize(raw).trim()
|
|
111
|
-
if (!s) return null
|
|
112
|
-
const n = Number(s)
|
|
113
|
-
if (Number.isNaN(n)) return null
|
|
114
|
-
return n
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const setValue = (next) => {
|
|
118
|
-
if (!canEdit.value) return
|
|
119
|
-
const clamped = clamp(next)
|
|
120
|
-
if (clamped == null) return
|
|
121
|
-
batch(() => {
|
|
122
|
-
if (props.bindField && field?.setValue) field.setValue(clamped)
|
|
123
|
-
else emit('update:modelValue', clamped)
|
|
124
|
-
text.value = String(clamped)
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const inc = () => {
|
|
129
|
-
if (!canEdit.value) return
|
|
130
|
-
const base = typeof effectiveValue.value === 'number' && !Number.isNaN(effectiveValue.value) ? effectiveValue.value : 0
|
|
131
|
-
setValue(base + (props.step || 1))
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const dec = () => {
|
|
135
|
-
if (!canEdit.value) return
|
|
136
|
-
const base = typeof effectiveValue.value === 'number' && !Number.isNaN(effectiveValue.value) ? effectiveValue.value : 0
|
|
137
|
-
setValue(base - (props.step || 1))
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const onInput = (e) => {
|
|
141
|
-
if (!canEdit.value) return
|
|
142
|
-
const nextRaw = e.target.value
|
|
143
|
-
const nextText = sanitize(nextRaw)
|
|
144
|
-
if (e?.target && e.target.value !== nextText) {
|
|
145
|
-
e.target.value = nextText
|
|
146
|
-
}
|
|
147
|
-
batch(() => {
|
|
148
|
-
text.value = nextText
|
|
149
|
-
const n = parse(nextText)
|
|
150
|
-
const clamped = clamp(n)
|
|
151
|
-
if (clamped == null) return
|
|
152
|
-
if (props.bindField && field?.setValue) field.setValue(clamped)
|
|
153
|
-
else emit('update:modelValue', clamped)
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const onBeforeInput = (e) => {
|
|
158
|
-
if (!canEdit.value) return
|
|
159
|
-
const inputType = e?.inputType
|
|
160
|
-
if (typeof inputType === 'string' && inputType.includes('Composition')) return
|
|
161
|
-
if (typeof inputType === 'string' && !inputType.startsWith('insert')) return
|
|
162
|
-
const data = e?.data
|
|
163
|
-
if (typeof data !== 'string' || !data) return
|
|
164
|
-
for (let i = 0; i < data.length; i++) {
|
|
165
|
-
const ch = data[i] === ',' ? '.' : data[i]
|
|
166
|
-
const isDigit = ch >= '0' && ch <= '9'
|
|
167
|
-
if (isDigit || ch === '.' || ch === '-') continue
|
|
168
|
-
e.preventDefault()
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const onPaste = (e) => {
|
|
174
|
-
if (!canEdit.value) return
|
|
175
|
-
const pasted = e?.clipboardData?.getData?.('text')
|
|
176
|
-
if (typeof pasted !== 'string') return
|
|
177
|
-
e.preventDefault()
|
|
178
|
-
const nextText = sanitize(pasted)
|
|
179
|
-
const target = e?.target
|
|
180
|
-
if (!target) return
|
|
181
|
-
target.value = nextText
|
|
182
|
-
onInput({ target })
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const onKeydown = (e) => {
|
|
186
|
-
if (!canEdit.value) return
|
|
187
|
-
if (e.ctrlKey || e.metaKey || e.altKey) return
|
|
188
|
-
const key = e.key
|
|
189
|
-
if (typeof key !== 'string' || key.length !== 1) return
|
|
190
|
-
const ch = key === ',' ? '.' : key
|
|
191
|
-
const isDigit = ch >= '0' && ch <= '9'
|
|
192
|
-
if (isDigit || ch === '.' || ch === '-') return
|
|
193
|
-
e.preventDefault()
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const onBlur = () => {
|
|
197
|
-
emit('blur')
|
|
198
|
-
if (props.bindField && field?.onBlur) field.onBlur()
|
|
199
|
-
isFocused.value = false
|
|
200
|
-
const n = parse(text.value)
|
|
201
|
-
const clamped = clamp(n)
|
|
202
|
-
if (clamped == null) {
|
|
203
|
-
text.value = String(effectiveValue.value ?? '')
|
|
204
|
-
return
|
|
205
|
-
}
|
|
206
|
-
batch(() => {
|
|
207
|
-
text.value = String(clamped)
|
|
208
|
-
if (props.bindField && field?.setValue) field.setValue(clamped)
|
|
209
|
-
else emit('update:modelValue', clamped)
|
|
210
|
-
})
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const onFocus = () => {
|
|
214
|
-
isFocused.value = true
|
|
215
|
-
emit('focus')
|
|
216
|
-
}
|
|
217
|
-
</script>
|
|
218
|
-
|
|
219
|
-
<template>
|
|
220
|
-
<div class="n-inum">
|
|
221
|
-
<label v-if="label" class="n-inum-label">{{ label }}</label>
|
|
222
|
-
<div class="n-inum-wrap" :class="{ 'is-disabled': effectiveDisabled.value }">
|
|
223
|
-
<button type="button" class="n-inum-btn n-inum-dec" :disabled="effectiveDisabled.value || readonly" aria-label="Decrement" @click="dec">−</button>
|
|
224
|
-
<input class="n-inum-input" type="text" :value="displayText.value" :placeholder="placeholder" :disabled="effectiveDisabled.value" :readonly="readonly" inputmode="decimal" autocomplete="off" @beforeinput="onBeforeInput" @keydown="onKeydown" @paste="onPaste" @input="onInput" @focus="onFocus" @blur="onBlur" />
|
|
225
|
-
<button type="button" class="n-inum-btn n-inum-inc" :disabled="effectiveDisabled.value || readonly" aria-label="Increment" @click="inc">+</button>
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
</template>
|
|
229
|
-
|
|
230
|
-
<style scoped>
|
|
231
|
-
.n-inum{display:flex;flex-direction:column;gap:var(--n-space-2);width:100%;font-family:var(--n-font-sans)}.n-inum-label{display:block;font-size:var(--n-text-sm);font-weight:var(--n-weight-medium);color:var(--n-color-text-secondary);margin-bottom:var(--n-space-2)}.n-inum-wrap{display:flex;align-items:stretch;min-height:44px;background:var(--n-color-surface);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);overflow:hidden;transition:all var(--n-transition-fast)}.n-inum-wrap:focus-within{border-color:var(--n-color-primary);box-shadow:0 0 0 3px var(--n-color-primary-light)}.n-inum-input{flex:1;background:transparent;border:none;outline:none;padding:0.75rem 0.75rem;color:var(--n-color-text);font-size:var(--n-text-base);font-family:inherit;text-align:center;line-height:1.2;box-sizing:border-box}.n-inum-btn{width:2.5rem;background:transparent;border:none;color:var(--n-color-text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:var(--n-text-base);transition:all var(--n-transition-fast)}.n-inum-btn:hover:not(:disabled){background:var(--n-color-glass);color:var(--n-color-text)}.n-inum-btn:disabled{opacity:0.5;cursor:not-allowed}.is-disabled{opacity:0.6;cursor:not-allowed;background:var(--n-color-surface-alt)}
|
|
232
|
-
</style>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { signal, computed, inject, effect, batch } from 'nexa-framework'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
modelValue: { type: Number, default: 0 },
|
|
6
|
+
min: { type: Number, default: null },
|
|
7
|
+
max: { type: Number, default: null },
|
|
8
|
+
step: { type: Number, default: 1 },
|
|
9
|
+
bindField: { type: Boolean, default: false },
|
|
10
|
+
disabled: { type: Boolean, default: false },
|
|
11
|
+
readonly: { type: Boolean, default: false },
|
|
12
|
+
label: { type: String, default: '' },
|
|
13
|
+
placeholder: { type: String, default: '' },
|
|
14
|
+
currency: { type: String, default: undefined },
|
|
15
|
+
locale: { type: String, default: undefined },
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
|
19
|
+
|
|
20
|
+
const field = inject('nexa-ui:form-field', undefined)
|
|
21
|
+
|
|
22
|
+
const effectiveValue = computed(() => {
|
|
23
|
+
if (props.bindField && field?.value) {
|
|
24
|
+
const v = field.value.value
|
|
25
|
+
return typeof v === 'number' ? v : Number(v)
|
|
26
|
+
}
|
|
27
|
+
return props.modelValue
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const effectiveDisabled = computed(() => {
|
|
31
|
+
if (props.disabled) return true
|
|
32
|
+
if (props.bindField && field?.disabled) return !!field.disabled.value
|
|
33
|
+
return false
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const text = signal(String(effectiveValue.value ?? ''))
|
|
37
|
+
|
|
38
|
+
effect(() => {
|
|
39
|
+
const next = effectiveValue.value
|
|
40
|
+
const nextText = next === null || next === undefined ? '' : String(next)
|
|
41
|
+
if (text.value === nextText) return
|
|
42
|
+
text.value = nextText
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const canEdit = computed(() => !effectiveDisabled.value && !props.readonly)
|
|
46
|
+
|
|
47
|
+
const isFocused = signal(false)
|
|
48
|
+
|
|
49
|
+
const nfSignal = signal(null)
|
|
50
|
+
|
|
51
|
+
effect(() => {
|
|
52
|
+
if (props.currency) {
|
|
53
|
+
nfSignal.value = new Intl.NumberFormat(props.locale || undefined, {
|
|
54
|
+
style: 'currency',
|
|
55
|
+
currency: props.currency,
|
|
56
|
+
minimumFractionDigits: 2,
|
|
57
|
+
maximumFractionDigits: 2,
|
|
58
|
+
})
|
|
59
|
+
} else {
|
|
60
|
+
nfSignal.value = null
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const displayText = computed(() => {
|
|
65
|
+
const raw = text.value
|
|
66
|
+
const nf = nfSignal.value
|
|
67
|
+
if (!nf || isFocused.value) return raw
|
|
68
|
+
if (!raw) return raw
|
|
69
|
+
const n = parseFloat(raw)
|
|
70
|
+
if (Number.isNaN(n)) return raw
|
|
71
|
+
return nf.format(n)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const sanitize = (raw) => {
|
|
75
|
+
const input = String(raw ?? '')
|
|
76
|
+
if (!input) return ''
|
|
77
|
+
const allowMinus = typeof props.min === 'number' ? props.min < 0 : true
|
|
78
|
+
let out = ''
|
|
79
|
+
let hasDot = false
|
|
80
|
+
let hasMinus = false
|
|
81
|
+
for (let i = 0; i < input.length; i++) {
|
|
82
|
+
const ch = input[i] === ',' ? '.' : input[i]
|
|
83
|
+
const isDigit = ch >= '0' && ch <= '9'
|
|
84
|
+
if (isDigit) {
|
|
85
|
+
out += ch
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
if (ch === '.' && !hasDot) {
|
|
89
|
+
hasDot = true
|
|
90
|
+
out += ch
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
if (ch === '-' && allowMinus && !hasMinus && out.length === 0) {
|
|
94
|
+
hasMinus = true
|
|
95
|
+
out += ch
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const clamp = (n) => {
|
|
102
|
+
if (n == null || Number.isNaN(n)) return null
|
|
103
|
+
let out = n
|
|
104
|
+
if (typeof props.min === 'number' && !Number.isNaN(props.min)) out = Math.max(out, props.min)
|
|
105
|
+
if (typeof props.max === 'number' && !Number.isNaN(props.max)) out = Math.min(out, props.max)
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const parse = (raw) => {
|
|
110
|
+
const s = sanitize(raw).trim()
|
|
111
|
+
if (!s) return null
|
|
112
|
+
const n = Number(s)
|
|
113
|
+
if (Number.isNaN(n)) return null
|
|
114
|
+
return n
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const setValue = (next) => {
|
|
118
|
+
if (!canEdit.value) return
|
|
119
|
+
const clamped = clamp(next)
|
|
120
|
+
if (clamped == null) return
|
|
121
|
+
batch(() => {
|
|
122
|
+
if (props.bindField && field?.setValue) field.setValue(clamped)
|
|
123
|
+
else emit('update:modelValue', clamped)
|
|
124
|
+
text.value = String(clamped)
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const inc = () => {
|
|
129
|
+
if (!canEdit.value) return
|
|
130
|
+
const base = typeof effectiveValue.value === 'number' && !Number.isNaN(effectiveValue.value) ? effectiveValue.value : 0
|
|
131
|
+
setValue(base + (props.step || 1))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const dec = () => {
|
|
135
|
+
if (!canEdit.value) return
|
|
136
|
+
const base = typeof effectiveValue.value === 'number' && !Number.isNaN(effectiveValue.value) ? effectiveValue.value : 0
|
|
137
|
+
setValue(base - (props.step || 1))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const onInput = (e) => {
|
|
141
|
+
if (!canEdit.value) return
|
|
142
|
+
const nextRaw = e.target.value
|
|
143
|
+
const nextText = sanitize(nextRaw)
|
|
144
|
+
if (e?.target && e.target.value !== nextText) {
|
|
145
|
+
e.target.value = nextText
|
|
146
|
+
}
|
|
147
|
+
batch(() => {
|
|
148
|
+
text.value = nextText
|
|
149
|
+
const n = parse(nextText)
|
|
150
|
+
const clamped = clamp(n)
|
|
151
|
+
if (clamped == null) return
|
|
152
|
+
if (props.bindField && field?.setValue) field.setValue(clamped)
|
|
153
|
+
else emit('update:modelValue', clamped)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const onBeforeInput = (e) => {
|
|
158
|
+
if (!canEdit.value) return
|
|
159
|
+
const inputType = e?.inputType
|
|
160
|
+
if (typeof inputType === 'string' && inputType.includes('Composition')) return
|
|
161
|
+
if (typeof inputType === 'string' && !inputType.startsWith('insert')) return
|
|
162
|
+
const data = e?.data
|
|
163
|
+
if (typeof data !== 'string' || !data) return
|
|
164
|
+
for (let i = 0; i < data.length; i++) {
|
|
165
|
+
const ch = data[i] === ',' ? '.' : data[i]
|
|
166
|
+
const isDigit = ch >= '0' && ch <= '9'
|
|
167
|
+
if (isDigit || ch === '.' || ch === '-') continue
|
|
168
|
+
e.preventDefault()
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const onPaste = (e) => {
|
|
174
|
+
if (!canEdit.value) return
|
|
175
|
+
const pasted = e?.clipboardData?.getData?.('text')
|
|
176
|
+
if (typeof pasted !== 'string') return
|
|
177
|
+
e.preventDefault()
|
|
178
|
+
const nextText = sanitize(pasted)
|
|
179
|
+
const target = e?.target
|
|
180
|
+
if (!target) return
|
|
181
|
+
target.value = nextText
|
|
182
|
+
onInput({ target })
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const onKeydown = (e) => {
|
|
186
|
+
if (!canEdit.value) return
|
|
187
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return
|
|
188
|
+
const key = e.key
|
|
189
|
+
if (typeof key !== 'string' || key.length !== 1) return
|
|
190
|
+
const ch = key === ',' ? '.' : key
|
|
191
|
+
const isDigit = ch >= '0' && ch <= '9'
|
|
192
|
+
if (isDigit || ch === '.' || ch === '-') return
|
|
193
|
+
e.preventDefault()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const onBlur = () => {
|
|
197
|
+
emit('blur')
|
|
198
|
+
if (props.bindField && field?.onBlur) field.onBlur()
|
|
199
|
+
isFocused.value = false
|
|
200
|
+
const n = parse(text.value)
|
|
201
|
+
const clamped = clamp(n)
|
|
202
|
+
if (clamped == null) {
|
|
203
|
+
text.value = String(effectiveValue.value ?? '')
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
batch(() => {
|
|
207
|
+
text.value = String(clamped)
|
|
208
|
+
if (props.bindField && field?.setValue) field.setValue(clamped)
|
|
209
|
+
else emit('update:modelValue', clamped)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const onFocus = () => {
|
|
214
|
+
isFocused.value = true
|
|
215
|
+
emit('focus')
|
|
216
|
+
}
|
|
217
|
+
</script>
|
|
218
|
+
|
|
219
|
+
<template>
|
|
220
|
+
<div class="n-inum">
|
|
221
|
+
<label v-if="label" class="n-inum-label">{{ label }}</label>
|
|
222
|
+
<div class="n-inum-wrap" :class="{ 'is-disabled': effectiveDisabled.value }">
|
|
223
|
+
<button type="button" class="n-inum-btn n-inum-dec" :disabled="effectiveDisabled.value || readonly" aria-label="Decrement" @click="dec">−</button>
|
|
224
|
+
<input class="n-inum-input" type="text" :value="displayText.value" :placeholder="placeholder" :disabled="effectiveDisabled.value" :readonly="readonly" inputmode="decimal" autocomplete="off" @beforeinput="onBeforeInput" @keydown="onKeydown" @paste="onPaste" @input="onInput" @focus="onFocus" @blur="onBlur" />
|
|
225
|
+
<button type="button" class="n-inum-btn n-inum-inc" :disabled="effectiveDisabled.value || readonly" aria-label="Increment" @click="inc">+</button>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</template>
|
|
229
|
+
|
|
230
|
+
<style scoped>
|
|
231
|
+
.n-inum{display:flex;flex-direction:column;gap:var(--n-space-2);width:100%;font-family:var(--n-font-sans)}.n-inum-label{display:block;font-size:var(--n-text-sm);font-weight:var(--n-weight-medium);color:var(--n-color-text-secondary);margin-bottom:var(--n-space-2)}.n-inum-wrap{display:flex;align-items:stretch;min-height:44px;background:var(--n-color-surface);border:1px solid var(--n-color-border);border-radius:var(--n-radius-md);overflow:hidden;transition:all var(--n-transition-fast)}.n-inum-wrap:focus-within{border-color:var(--n-color-primary);box-shadow:0 0 0 3px var(--n-color-primary-light)}.n-inum-input{flex:1;background:transparent;border:none;outline:none;padding:0.75rem 0.75rem;color:var(--n-color-text);font-size:var(--n-text-base);font-family:inherit;text-align:center;line-height:1.2;box-sizing:border-box}.n-inum-btn{width:2.5rem;background:transparent;border:none;color:var(--n-color-text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:var(--n-text-base);transition:all var(--n-transition-fast)}.n-inum-btn:hover:not(:disabled){background:var(--n-color-glass);color:var(--n-color-text)}.n-inum-btn:disabled{opacity:0.5;cursor:not-allowed}.is-disabled{opacity:0.6;cursor:not-allowed;background:var(--n-color-surface-alt)}
|
|
232
|
+
</style>
|