nexa-ui-kit 0.6.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/NBadge.nexa +40 -0
- package/dist/NBottomSheet.nexa +124 -0
- package/dist/NButton.nexa +123 -0
- package/dist/NCard.nexa +74 -0
- package/dist/NInput.nexa +116 -0
- package/dist/NModal.nexa +165 -0
- package/dist/NSelect.nexa +169 -0
- package/dist/NToastContainer.nexa +86 -0
- package/dist/NTooltip.nexa +115 -0
- package/dist/components/NAlert.js +134 -0
- package/dist/components/NAlert.nexa +115 -0
- package/dist/components/NAutocomplete.js +94 -0
- package/dist/components/NAutocomplete.nexa +58 -0
- package/dist/components/NAvatar.js +75 -0
- package/dist/components/NAvatar.nexa +67 -0
- package/dist/components/NBadge.js +74 -0
- package/dist/components/NBadge.nexa +61 -0
- package/dist/components/NBottomSheet.js +149 -0
- package/dist/components/NBottomSheet.nexa +145 -0
- package/dist/components/NButton.js +284 -0
- package/dist/components/NButton.nexa +275 -0
- package/dist/components/NCard.js +117 -0
- package/dist/components/NCard.nexa +100 -0
- package/dist/components/NCheckbox.js +108 -0
- package/dist/components/NCheckbox.nexa +90 -0
- package/dist/components/NChips.js +72 -0
- package/dist/components/NChips.nexa +57 -0
- package/dist/components/NDataTable.js +252 -0
- package/dist/components/NDataTable.nexa +186 -0
- package/dist/components/NDatepicker.js +379 -0
- package/dist/components/NDatepicker.nexa +367 -0
- package/dist/components/NForm.js +132 -0
- package/dist/components/NForm.nexa +133 -0
- package/dist/components/NFormField.js +173 -0
- package/dist/components/NFormField.nexa +171 -0
- package/dist/components/NInput.js +311 -0
- package/dist/components/NInput.nexa +311 -0
- package/dist/components/NInputNumber.js +202 -0
- package/dist/components/NInputNumber.nexa +199 -0
- package/dist/components/NModal.js +221 -0
- package/dist/components/NModal.nexa +221 -0
- package/dist/components/NMultiSelect.js +156 -0
- package/dist/components/NMultiSelect.nexa +77 -0
- package/dist/components/NPaginator.js +117 -0
- package/dist/components/NPaginator.nexa +77 -0
- package/dist/components/NPassword.js +193 -0
- package/dist/components/NPassword.nexa +178 -0
- package/dist/components/NProgressBar.js +127 -0
- package/dist/components/NProgressBar.nexa +111 -0
- package/dist/components/NRadio.js +96 -0
- package/dist/components/NRadio.nexa +81 -0
- package/dist/components/NSelect.js +468 -0
- package/dist/components/NSelect.nexa +452 -0
- package/dist/components/NSkeleton.js +98 -0
- package/dist/components/NSkeleton.nexa +74 -0
- package/dist/components/NSwitch.js +92 -0
- package/dist/components/NSwitch.nexa +76 -0
- package/dist/components/NTabs.js +129 -0
- package/dist/components/NTabs.nexa +113 -0
- package/dist/components/NTag.js +108 -0
- package/dist/components/NTag.nexa +93 -0
- package/dist/components/NToastContainer.js +242 -0
- package/dist/components/NToastContainer.nexa +221 -0
- package/dist/components/NTooltip.js +163 -0
- package/dist/components/NTooltip.nexa +166 -0
- package/dist/components/NTreeMenu.js +151 -0
- package/dist/components/NTreeMenu.nexa +142 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +34 -0
- package/dist/services/FloatingOverlay.d.ts +27 -0
- package/dist/services/FloatingOverlay.js +98 -0
- package/dist/services/FormValidation.d.ts +8 -0
- package/dist/services/FormValidation.js +46 -0
- package/dist/services/ToastService.d.ts +16 -0
- package/dist/services/ToastService.js +26 -0
- package/dist/styles/theme.d.ts +1 -0
- package/dist/styles/theme.js +144 -0
- package/package.json +32 -0
- package/src/components/NAlert.nexa +115 -0
- package/src/components/NAutocomplete.nexa +58 -0
- package/src/components/NAvatar.nexa +67 -0
- package/src/components/NBadge.nexa +61 -0
- package/src/components/NBottomSheet.nexa +145 -0
- package/src/components/NButton.nexa +275 -0
- package/src/components/NCard.nexa +100 -0
- package/src/components/NCheckbox.nexa +90 -0
- package/src/components/NChips.nexa +57 -0
- package/src/components/NDataTable.nexa +186 -0
- package/src/components/NDatepicker.nexa +367 -0
- package/src/components/NForm.nexa +133 -0
- package/src/components/NFormField.nexa +171 -0
- package/src/components/NInput.nexa +311 -0
- package/src/components/NInputNumber.nexa +199 -0
- package/src/components/NModal.nexa +221 -0
- package/src/components/NMultiSelect.nexa +77 -0
- package/src/components/NPaginator.nexa +77 -0
- package/src/components/NPassword.nexa +178 -0
- package/src/components/NProgressBar.nexa +111 -0
- package/src/components/NRadio.nexa +81 -0
- package/src/components/NSelect.nexa +452 -0
- package/src/components/NSkeleton.nexa +74 -0
- package/src/components/NSwitch.nexa +76 -0
- package/src/components/NTabs.nexa +113 -0
- package/src/components/NTag.nexa +93 -0
- package/src/components/NToastContainer.nexa +221 -0
- package/src/components/NTooltip.nexa +166 -0
- package/src/components/NTreeMenu.nexa +142 -0
- package/src/index.ts +36 -0
- package/src/services/FloatingOverlay.ts +133 -0
- package/src/services/FormValidation.ts +44 -0
- package/src/services/ToastService.ts +41 -0
- package/src/shims.d.ts +5 -0
- package/src/styles/theme.ts +146 -0
- package/src/styles/tokens.css +170 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { signal, computed, onBeforeUnmount } from 'nexa-framework'
|
|
3
|
+
import NInput from './NInput.nexa'
|
|
4
|
+
import { trackFloatingOverlay } from '../services/FloatingOverlay.js'
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
modelValue: { type: String, default: '' },
|
|
8
|
+
placeholder: { type: String, default: 'Seleccionar fecha' },
|
|
9
|
+
disabled: { type: Boolean, default: false },
|
|
10
|
+
min: { type: String, default: '' },
|
|
11
|
+
max: { type: String, default: '' },
|
|
12
|
+
placement: { type: String, default: 'auto' }
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits(['update:modelValue'])
|
|
16
|
+
|
|
17
|
+
const isOpen = signal(false)
|
|
18
|
+
const viewDate = signal(new Date())
|
|
19
|
+
const instanceId = `n-dp-${Math.random().toString(16).slice(2)}`
|
|
20
|
+
const popupStyle = signal({})
|
|
21
|
+
const resolvedPlacement = signal('bottom')
|
|
22
|
+
let stopTracking = null
|
|
23
|
+
|
|
24
|
+
const today = new Date()
|
|
25
|
+
today.setHours(0, 0, 0, 0)
|
|
26
|
+
|
|
27
|
+
const year = computed(() => viewDate.value.getFullYear())
|
|
28
|
+
const month = computed(() => viewDate.value.getMonth())
|
|
29
|
+
|
|
30
|
+
const daysInMonth = computed(() => new Date(year.value, month.value + 1, 0).getDate())
|
|
31
|
+
const firstDayOfWeek = computed(() => new Date(year.value, month.value, 1).getDay())
|
|
32
|
+
|
|
33
|
+
const calendarDays = computed(() => {
|
|
34
|
+
const days = []
|
|
35
|
+
const totalCells = Math.ceil((firstDayOfWeek.value + daysInMonth.value) / 7) * 7
|
|
36
|
+
for (let i = 0; i < totalCells; i++) {
|
|
37
|
+
const day = i - firstDayOfWeek.value + 1
|
|
38
|
+
if (day > 0 && day <= daysInMonth.value) {
|
|
39
|
+
days.push(day)
|
|
40
|
+
} else {
|
|
41
|
+
days.push(null)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return days
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const formatDate = (d) => {
|
|
48
|
+
const y = d.getFullYear()
|
|
49
|
+
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
50
|
+
const day = String(d.getDate()).padStart(2, '0')
|
|
51
|
+
return `${y}-${m}-${day}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parseDate = (value) => {
|
|
55
|
+
if (!value) return null
|
|
56
|
+
const parts = String(value).split('-')
|
|
57
|
+
if (parts.length !== 3) return null
|
|
58
|
+
const y = Number(parts[0])
|
|
59
|
+
const m = Number(parts[1])
|
|
60
|
+
const d = Number(parts[2])
|
|
61
|
+
if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return null
|
|
62
|
+
const date = new Date(y, m - 1, d)
|
|
63
|
+
if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) return null
|
|
64
|
+
date.setHours(0, 0, 0, 0)
|
|
65
|
+
return date
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const minDate = computed(() => parseDate(props.min))
|
|
69
|
+
const maxDate = computed(() => parseDate(props.max))
|
|
70
|
+
|
|
71
|
+
const isDisabled = (day) => {
|
|
72
|
+
if (day == null) return true
|
|
73
|
+
const d = new Date(year.value, month.value, day)
|
|
74
|
+
d.setHours(0, 0, 0, 0)
|
|
75
|
+
const min = minDate.value
|
|
76
|
+
if (min && d < min) return true
|
|
77
|
+
const max = maxDate.value
|
|
78
|
+
if (max && d > max) return true
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const closePopup = () => {
|
|
83
|
+
if (!isOpen.value) return
|
|
84
|
+
isOpen.value = false
|
|
85
|
+
if (closeHandler) {
|
|
86
|
+
document.removeEventListener('click', closeHandler)
|
|
87
|
+
closeHandler = null
|
|
88
|
+
}
|
|
89
|
+
if (stopTracking) {
|
|
90
|
+
stopTracking()
|
|
91
|
+
stopTracking = null
|
|
92
|
+
}
|
|
93
|
+
rootEl.value = null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const syncViewDateFromModel = () => {
|
|
97
|
+
const selected = parseDate(props.modelValue)
|
|
98
|
+
if (!selected) return
|
|
99
|
+
viewDate.value = new Date(selected.getFullYear(), selected.getMonth(), 1)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const selectDate = (day) => {
|
|
103
|
+
if (day == null) return
|
|
104
|
+
if (isDisabled(day)) return
|
|
105
|
+
const d = new Date(year.value, month.value, day)
|
|
106
|
+
const formatted = formatDate(d)
|
|
107
|
+
emit('update:modelValue', formatted)
|
|
108
|
+
closePopup()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const prevMonth = () => {
|
|
112
|
+
viewDate.value = new Date(year.value, month.value - 1, 1)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const nextMonth = () => {
|
|
116
|
+
viewDate.value = new Date(year.value, month.value + 1, 1)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const monthNames = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre']
|
|
120
|
+
const dayNames = ['Do', 'Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sa']
|
|
121
|
+
|
|
122
|
+
const isSelected = (day) => {
|
|
123
|
+
if (!props.modelValue) return false
|
|
124
|
+
const d = new Date(year.value, month.value, day)
|
|
125
|
+
return props.modelValue === formatDate(d)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const isToday = (day) => {
|
|
129
|
+
const d = new Date(year.value, month.value, day)
|
|
130
|
+
return formatDate(d) === formatDate(today)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let closeHandler = null
|
|
134
|
+
const rootEl = signal(null)
|
|
135
|
+
|
|
136
|
+
const openPopup = (e) => {
|
|
137
|
+
if (props.disabled) return
|
|
138
|
+
if (isOpen.value) {
|
|
139
|
+
closePopup()
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
isOpen.value = true
|
|
143
|
+
syncViewDateFromModel()
|
|
144
|
+
const target = e?.currentTarget || e?.target
|
|
145
|
+
rootEl.value = target?.closest ? target.closest(`[data-datepicker-root="${instanceId}"]`) : null
|
|
146
|
+
stopTracking = trackFloatingOverlay({
|
|
147
|
+
isOpen: () => isOpen.value,
|
|
148
|
+
getAnchor: () => {
|
|
149
|
+
const root = rootEl.value
|
|
150
|
+
return root ? root.querySelector('.n-datepicker-input') : null
|
|
151
|
+
},
|
|
152
|
+
getPopup: () => document.querySelector(`[data-datepicker-popup="${instanceId}"]`),
|
|
153
|
+
placement: props.placement,
|
|
154
|
+
align: 'start',
|
|
155
|
+
matchWidth: true,
|
|
156
|
+
minWidth: 240,
|
|
157
|
+
gap: 6,
|
|
158
|
+
margin: 8,
|
|
159
|
+
zIndex: 9999,
|
|
160
|
+
onUpdate: (result) => {
|
|
161
|
+
popupStyle.value = result.style
|
|
162
|
+
resolvedPlacement.value = result.placement
|
|
163
|
+
},
|
|
164
|
+
isEventInside: (event) => {
|
|
165
|
+
const t = event.target
|
|
166
|
+
if (!t || typeof t.closest !== 'function') return false
|
|
167
|
+
if (t.closest(`[data-datepicker-root="${instanceId}"]`)) return true
|
|
168
|
+
if (t.closest(`[data-datepicker-popup="${instanceId}"]`)) return true
|
|
169
|
+
return false
|
|
170
|
+
},
|
|
171
|
+
onOutside: () => closePopup(),
|
|
172
|
+
})
|
|
173
|
+
if (closeHandler) document.removeEventListener('click', closeHandler)
|
|
174
|
+
closeHandler = (e) => {
|
|
175
|
+
const target = e.target
|
|
176
|
+
if (target && typeof target.closest === 'function') {
|
|
177
|
+
if (target.closest(`[data-datepicker-root="${instanceId}"]`)) return
|
|
178
|
+
if (target.closest(`[data-datepicker-popup="${instanceId}"]`)) return
|
|
179
|
+
}
|
|
180
|
+
closePopup()
|
|
181
|
+
}
|
|
182
|
+
setTimeout(() => document.addEventListener('click', closeHandler), 0)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
onBeforeUnmount(() => {
|
|
186
|
+
closePopup()
|
|
187
|
+
})
|
|
188
|
+
</script>
|
|
189
|
+
|
|
190
|
+
<template>
|
|
191
|
+
<div class="n-datepicker" :data-datepicker-root="instanceId">
|
|
192
|
+
<div class="n-datepicker-input" @click="openPopup">
|
|
193
|
+
<NInput
|
|
194
|
+
:modelValue="modelValue"
|
|
195
|
+
:placeholder="placeholder"
|
|
196
|
+
:disabled="disabled"
|
|
197
|
+
readonly
|
|
198
|
+
/>
|
|
199
|
+
<span class="n-datepicker-icon">📅</span>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<Teleport to="body">
|
|
203
|
+
<div v-if="isOpen.value" class="n-datepicker-dropdown" :class="{ 'is-top': resolvedPlacement.value === 'top' }" :data-datepicker-popup="instanceId" :style="popupStyle.value">
|
|
204
|
+
<div class="n-datepicker-header">
|
|
205
|
+
<button type="button" class="n-datepicker-nav" @click="prevMonth">‹</button>
|
|
206
|
+
<span class="n-datepicker-title">{{ monthNames[month.value] }} {{ year.value }}</span>
|
|
207
|
+
<button type="button" class="n-datepicker-nav" @click="nextMonth">›</button>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div class="n-datepicker-grid">
|
|
211
|
+
<div v-for="d in dayNames" :key="d" class="n-datepicker-day-header">{{ d }}</div>
|
|
212
|
+
<div
|
|
213
|
+
v-for="(day, i) in calendarDays.value"
|
|
214
|
+
:key="i"
|
|
215
|
+
:class="{
|
|
216
|
+
'n-datepicker-day': true,
|
|
217
|
+
'is-empty': day === null,
|
|
218
|
+
'is-disabled': day !== null && isDisabled(day),
|
|
219
|
+
'is-selected': day !== null && isSelected(day),
|
|
220
|
+
'is-today': day !== null && isToday(day)
|
|
221
|
+
}"
|
|
222
|
+
@click="selectDate(day)"
|
|
223
|
+
>{{ day ?? '' }}</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</Teleport>
|
|
227
|
+
</div>
|
|
228
|
+
</template>
|
|
229
|
+
|
|
230
|
+
<style scoped>
|
|
231
|
+
.n-datepicker {
|
|
232
|
+
position: relative;
|
|
233
|
+
display: inline-block;
|
|
234
|
+
font-family: var(--n-font-sans);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.n-datepicker-input {
|
|
238
|
+
position: relative;
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.n-datepicker-input .n-input {
|
|
243
|
+
cursor: pointer;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.n-datepicker-icon {
|
|
247
|
+
position: absolute;
|
|
248
|
+
right: 0.75rem;
|
|
249
|
+
top: 50%;
|
|
250
|
+
transform: translateY(-50%);
|
|
251
|
+
font-size: 1rem;
|
|
252
|
+
pointer-events: none;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.n-datepicker-dropdown {
|
|
256
|
+
position: absolute;
|
|
257
|
+
top: calc(100% + 4px);
|
|
258
|
+
left: 0;
|
|
259
|
+
z-index: var(--n-z-dropdown);
|
|
260
|
+
background: var(--n-color-surface-elevated);
|
|
261
|
+
border: 1px solid var(--n-color-border);
|
|
262
|
+
border-radius: var(--n-radius-lg);
|
|
263
|
+
box-shadow: var(--n-shadow-lg);
|
|
264
|
+
padding: 1rem;
|
|
265
|
+
width: 280px;
|
|
266
|
+
animation: fade-in 0.15s ease;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@keyframes fade-in {
|
|
270
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
271
|
+
to { opacity: 1; transform: translateY(0); }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.n-datepicker-dropdown.is-top {
|
|
275
|
+
animation: fade-in-top 0.15s ease;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@keyframes fade-in-top {
|
|
279
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
280
|
+
to { opacity: 1; transform: translateY(0); }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.n-datepicker-header {
|
|
284
|
+
display: flex;
|
|
285
|
+
align-items: center;
|
|
286
|
+
justify-content: space-between;
|
|
287
|
+
margin-bottom: 0.75rem;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.n-datepicker-title {
|
|
291
|
+
font-weight: var(--n-weight-semibold);
|
|
292
|
+
font-size: var(--n-text-sm);
|
|
293
|
+
color: var(--n-color-text);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.n-datepicker-nav {
|
|
297
|
+
background: none;
|
|
298
|
+
border: 1px solid var(--n-color-border);
|
|
299
|
+
color: var(--n-color-text-secondary);
|
|
300
|
+
width: 28px;
|
|
301
|
+
height: 28px;
|
|
302
|
+
border-radius: var(--n-radius-sm);
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
font-size: 1.1rem;
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
justify-content: center;
|
|
308
|
+
transition: all var(--n-transition-fast);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.n-datepicker-nav:hover {
|
|
312
|
+
background: var(--n-color-surface-hover);
|
|
313
|
+
color: var(--n-color-text);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.n-datepicker-grid {
|
|
317
|
+
display: grid;
|
|
318
|
+
grid-template-columns: repeat(7, 1fr);
|
|
319
|
+
gap: 2px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.n-datepicker-day-header {
|
|
323
|
+
text-align: center;
|
|
324
|
+
font-size: var(--n-text-xs);
|
|
325
|
+
color: var(--n-color-text-muted);
|
|
326
|
+
font-weight: var(--n-weight-semibold);
|
|
327
|
+
padding: 0.35rem 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.n-datepicker-day {
|
|
331
|
+
text-align: center;
|
|
332
|
+
padding: 0.4rem 0;
|
|
333
|
+
font-size: var(--n-text-sm);
|
|
334
|
+
color: var(--n-color-text);
|
|
335
|
+
border-radius: var(--n-radius-sm);
|
|
336
|
+
cursor: pointer;
|
|
337
|
+
transition: all var(--n-transition-fast);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.n-datepicker-day:hover:not(.is-empty) {
|
|
341
|
+
background: var(--n-color-primary-light);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.n-datepicker-day.is-empty {
|
|
345
|
+
cursor: default;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.n-datepicker-day.is-disabled {
|
|
349
|
+
opacity: 0.4;
|
|
350
|
+
cursor: not-allowed;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.n-datepicker-day.is-disabled:hover {
|
|
354
|
+
background: transparent;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.n-datepicker-day.is-today {
|
|
358
|
+
font-weight: var(--n-weight-bold);
|
|
359
|
+
color: var(--n-color-primary);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.n-datepicker-day.is-selected {
|
|
363
|
+
background: var(--n-color-primary);
|
|
364
|
+
color: white;
|
|
365
|
+
font-weight: var(--n-weight-semibold);
|
|
366
|
+
}
|
|
367
|
+
</style>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { signal, computed, provide, h, hText, effect, defineComponent, registerComponent, reloadComponent, injectStyle } from 'nexa-framework'
|
|
2
|
+
|
|
3
|
+
const _sfc_main = defineComponent({
|
|
4
|
+
__scopeId: 'data-v-38e99ca2',
|
|
5
|
+
__hmrId: 'NForm_nexa',
|
|
6
|
+
props: {
|
|
7
|
+
validateOn: { type: String, default: 'submit' },
|
|
8
|
+
disabled: { type: Boolean, default: false }
|
|
9
|
+
},
|
|
10
|
+
emits: ['submit', 'invalid'],
|
|
11
|
+
setup(props, setupContext) {
|
|
12
|
+
const { emit, slots, slots: $slots } = setupContext
|
|
13
|
+
const fieldNames = signal([])
|
|
14
|
+
const fields = Object.create(null)
|
|
15
|
+
const registerField = (name, field) => {
|
|
16
|
+
if (!name) return () => {}
|
|
17
|
+
fields[name] = field
|
|
18
|
+
if (!fieldNames.value.includes(name)) fieldNames.value = [...fieldNames.value, name]
|
|
19
|
+
return () => {
|
|
20
|
+
delete fields[name]
|
|
21
|
+
if (fieldNames.value.includes(name)) {
|
|
22
|
+
fieldNames.value = fieldNames.value.filter(n => n !== name)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const getValues = () => {
|
|
27
|
+
const out = {}
|
|
28
|
+
for (const name of fieldNames.value) {
|
|
29
|
+
const field = fields[name]
|
|
30
|
+
if (!field) continue
|
|
31
|
+
out[name] = field.value.value
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
const values = computed(() => {
|
|
36
|
+
const names = fieldNames.value
|
|
37
|
+
const out = {}
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
const field = fields[name]
|
|
40
|
+
if (!field) continue
|
|
41
|
+
out[name] = field.value.value
|
|
42
|
+
}
|
|
43
|
+
return out
|
|
44
|
+
})
|
|
45
|
+
const errors = computed(() => {
|
|
46
|
+
const names = fieldNames.value
|
|
47
|
+
const out = {}
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
const field = fields[name]
|
|
50
|
+
if (!field) continue
|
|
51
|
+
out[name] = field.errors.value
|
|
52
|
+
}
|
|
53
|
+
return out
|
|
54
|
+
})
|
|
55
|
+
const pending = computed(() => {
|
|
56
|
+
const names = fieldNames.value
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
const field = fields[name]
|
|
59
|
+
if (!field) continue
|
|
60
|
+
if (field.pending.value) return true
|
|
61
|
+
}
|
|
62
|
+
return false
|
|
63
|
+
})
|
|
64
|
+
const valid = computed(() => {
|
|
65
|
+
const names = fieldNames.value
|
|
66
|
+
for (const name of names) {
|
|
67
|
+
const field = fields[name]
|
|
68
|
+
if (!field) continue
|
|
69
|
+
if (field.errors.value.length > 0) return false
|
|
70
|
+
}
|
|
71
|
+
return true
|
|
72
|
+
})
|
|
73
|
+
const validateAll = async () => {
|
|
74
|
+
const tasks = fieldNames.value.map(async (name) => {
|
|
75
|
+
const field = fields[name]
|
|
76
|
+
if (!field) return true
|
|
77
|
+
const errs = await field.validate('submit')
|
|
78
|
+
return errs.length === 0
|
|
79
|
+
})
|
|
80
|
+
const results = await Promise.all(tasks)
|
|
81
|
+
return results.every(Boolean)
|
|
82
|
+
}
|
|
83
|
+
const reset = () => {
|
|
84
|
+
for (const name of fieldNames.value) {
|
|
85
|
+
const field = fields[name]
|
|
86
|
+
if (!field) continue
|
|
87
|
+
field.reset()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const submit = async () => {
|
|
91
|
+
if (props.disabled) return
|
|
92
|
+
const ok = await validateAll()
|
|
93
|
+
if (!ok) {
|
|
94
|
+
emit('invalid', errors.value)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
emit('submit', values.value)
|
|
98
|
+
}
|
|
99
|
+
const onSubmit = (e) => {
|
|
100
|
+
if (e && typeof e.preventDefault === 'function') e.preventDefault()
|
|
101
|
+
submit()
|
|
102
|
+
}
|
|
103
|
+
provide('nexa-ui:form', {
|
|
104
|
+
validateOn: props.validateOn,
|
|
105
|
+
disabled: props.disabled,
|
|
106
|
+
registerField,
|
|
107
|
+
getValues,
|
|
108
|
+
})
|
|
109
|
+
return { fieldNames, fields, registerField, getValues, values, errors, pending, valid, validateAll, reset, submit, onSubmit, provide, $slots, emit }
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
// Injected render function
|
|
113
|
+
_sfc_main.render = function(ctx) {
|
|
114
|
+
const { fieldNames, fields, registerField, getValues, values, errors, pending, valid, validateAll, reset, submit, onSubmit, provide, $slots, emit, validateOn, disabled, Fragment: _ntc_Fragment } = ctx
|
|
115
|
+
return h('form', { class: "n-form", onSubmit: onSubmit, "data-v-38e99ca2": "" }, [
|
|
116
|
+
"\n ",
|
|
117
|
+
ctx.$slots.default ? ctx.$slots.default({ values: values, errors: errors, valid: valid, pending: pending, submit: submit, reset: reset }) : null,
|
|
118
|
+
"\n "
|
|
119
|
+
])
|
|
120
|
+
}
|
|
121
|
+
_sfc_main.__scopeId = 'data-v-38e99ca2'
|
|
122
|
+
_sfc_main.__hmrId = 'NForm_nexa'
|
|
123
|
+
|
|
124
|
+
export default _sfc_main
|
|
125
|
+
|
|
126
|
+
const __style = `.n-form[data-v-38e99ca2]{
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
gap: var(--n-space-4);
|
|
130
|
+
width: 100%;
|
|
131
|
+
}`
|
|
132
|
+
injectStyle('data-v-38e99ca2', __style)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { signal, computed, provide } from 'nexa-framework'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
validateOn: { type: String, default: 'submit' },
|
|
6
|
+
disabled: { type: Boolean, default: false }
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits(['submit', 'invalid'])
|
|
10
|
+
|
|
11
|
+
const fieldNames = signal([])
|
|
12
|
+
const fields = Object.create(null)
|
|
13
|
+
|
|
14
|
+
const registerField = (name, field) => {
|
|
15
|
+
if (!name) return () => {}
|
|
16
|
+
fields[name] = field
|
|
17
|
+
if (!fieldNames.value.includes(name)) fieldNames.value = [...fieldNames.value, name]
|
|
18
|
+
return () => {
|
|
19
|
+
delete fields[name]
|
|
20
|
+
if (fieldNames.value.includes(name)) {
|
|
21
|
+
fieldNames.value = fieldNames.value.filter(n => n !== name)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getValues = () => {
|
|
27
|
+
const out = {}
|
|
28
|
+
for (const name of fieldNames.value) {
|
|
29
|
+
const field = fields[name]
|
|
30
|
+
if (!field) continue
|
|
31
|
+
out[name] = field.value.value
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const values = computed(() => {
|
|
37
|
+
const names = fieldNames.value
|
|
38
|
+
const out = {}
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
const field = fields[name]
|
|
41
|
+
if (!field) continue
|
|
42
|
+
out[name] = field.value.value
|
|
43
|
+
}
|
|
44
|
+
return out
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const errors = computed(() => {
|
|
48
|
+
const names = fieldNames.value
|
|
49
|
+
const out = {}
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
const field = fields[name]
|
|
52
|
+
if (!field) continue
|
|
53
|
+
out[name] = field.errors.value
|
|
54
|
+
}
|
|
55
|
+
return out
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const pending = computed(() => {
|
|
59
|
+
const names = fieldNames.value
|
|
60
|
+
for (const name of names) {
|
|
61
|
+
const field = fields[name]
|
|
62
|
+
if (!field) continue
|
|
63
|
+
if (field.pending.value) return true
|
|
64
|
+
}
|
|
65
|
+
return false
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const valid = computed(() => {
|
|
69
|
+
const names = fieldNames.value
|
|
70
|
+
for (const name of names) {
|
|
71
|
+
const field = fields[name]
|
|
72
|
+
if (!field) continue
|
|
73
|
+
if (field.errors.value.length > 0) return false
|
|
74
|
+
}
|
|
75
|
+
return true
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const validateAll = async () => {
|
|
79
|
+
const tasks = fieldNames.value.map(async (name) => {
|
|
80
|
+
const field = fields[name]
|
|
81
|
+
if (!field) return true
|
|
82
|
+
const errs = await field.validate('submit')
|
|
83
|
+
return errs.length === 0
|
|
84
|
+
})
|
|
85
|
+
const results = await Promise.all(tasks)
|
|
86
|
+
return results.every(Boolean)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const reset = () => {
|
|
90
|
+
for (const name of fieldNames.value) {
|
|
91
|
+
const field = fields[name]
|
|
92
|
+
if (!field) continue
|
|
93
|
+
field.reset()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const submit = async () => {
|
|
98
|
+
if (props.disabled) return
|
|
99
|
+
const ok = await validateAll()
|
|
100
|
+
if (!ok) {
|
|
101
|
+
emit('invalid', errors.value)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
emit('submit', values.value)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const onSubmit = (e) => {
|
|
108
|
+
if (e && typeof e.preventDefault === 'function') e.preventDefault()
|
|
109
|
+
submit()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
provide('nexa-ui:form', {
|
|
113
|
+
validateOn: props.validateOn,
|
|
114
|
+
disabled: props.disabled,
|
|
115
|
+
registerField,
|
|
116
|
+
getValues,
|
|
117
|
+
})
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<template>
|
|
121
|
+
<form class="n-form" @submit="onSubmit">
|
|
122
|
+
<slot :values="values" :errors="errors" :valid="valid" :pending="pending" :submit="submit" :reset="reset" />
|
|
123
|
+
</form>
|
|
124
|
+
</template>
|
|
125
|
+
|
|
126
|
+
<style scoped>
|
|
127
|
+
.n-form {
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-direction: column;
|
|
130
|
+
gap: var(--n-space-4);
|
|
131
|
+
width: 100%;
|
|
132
|
+
}
|
|
133
|
+
</style>
|