@cnamts/synapse 0.0.15-alpha → 0.0.16-alpha
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/CookiesSelection/CookiesSelection.d.ts +26 -26
- package/dist/components/Customs/SyTextField/SyTextField.d.ts +1391 -1
- package/dist/components/DatePicker/DatePicker.d.ts +2810 -16
- package/dist/components/DatePicker/DateTextInput.d.ts +1401 -4
- package/dist/components/LangBtn/LangBtn.d.ts +4 -4
- package/dist/components/NirField/NirField.d.ts +2794 -4
- package/dist/components/PeriodField/PeriodField.d.ts +5636 -48
- package/dist/components/SyAlert/SyAlert.d.ts +72 -1
- package/dist/components/UploadWorkflow/UploadWorkflow.d.ts +26 -26
- package/dist/components/index.d.ts +1 -0
- package/dist/composables/date/useDateFormat.d.ts +2 -2
- package/dist/composables/date/useDateFormatDayjs.d.ts +23 -0
- package/dist/composables/date/useDateInitializationDayjs.d.ts +18 -0
- package/dist/design-system-v3.js +3953 -3728
- package/dist/design-system-v3.umd.cjs +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Customs/SyTextField/Accessibilite.stories.ts +7 -0
- package/src/components/Customs/SyTextField/SyTextField.stories.ts +13 -0
- package/src/components/Customs/SyTextField/SyTextField.vue +82 -17
- package/src/components/DatePicker/ComplexDatePicker/ComplexDatePicker.vue +795 -0
- package/src/components/DatePicker/DatePicker.stories.ts +432 -1
- package/src/components/DatePicker/DatePicker.vue +66 -24
- package/src/components/DatePicker/DatePickerValidation.stories.ts +9 -1
- package/src/components/DatePicker/DateTextInput.vue +85 -133
- package/src/components/DatePicker/docExamples/DatePickerBidirectionalValidation.vue +282 -0
- package/src/components/DatePicker/tests/DatePicker.spec.ts +33 -32
- package/src/components/DatePicker/tests/DateTextInput.spec.ts +81 -33
- package/src/components/SyAlert/Accessibilite.stories.ts +4 -0
- package/src/components/SyAlert/SyAlert.mdx +3 -7
- package/src/components/SyAlert/SyAlert.stories.ts +19 -12
- package/src/components/SyAlert/SyAlert.vue +88 -51
- package/src/components/SyAlert/tests/SyAlert.spec.ts +20 -2
- package/src/components/SyAlert/tests/__snapshots__/SyAlert.spec.ts.snap +83 -75
- package/src/components/index.ts +1 -0
- package/src/composables/date/useDateFormat.ts +17 -1
- package/src/composables/date/useDateFormatDayjs.ts +84 -0
- package/src/composables/date/useDateInitializationDayjs.ts +133 -0
- package/src/stories/Accessibilite/Avancement/Avancement.mdx +12 -0
- package/src/stories/Accessibilite/Avancement/Avancement.stories.ts +134 -0
- /package/src/components/DatePicker/{DatePickerValidationExamples.vue → docExamples/DatePickerValidationExamples.vue} +0 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick, type ComponentPublicInstance } from 'vue'
|
|
3
|
+
import SyTextField from '@/components/Customs/SyTextField/SyTextField.vue'
|
|
4
|
+
import DateTextInput from '../DateTextInput.vue'
|
|
5
|
+
import { VDatePicker } from 'vuetify/components'
|
|
6
|
+
import { useValidation } from '@/composables/validation/useValidation'
|
|
7
|
+
import { useDateFormat } from '@/composables/date/useDateFormatDayjs'
|
|
8
|
+
import { useDateInitialization, type DateValue, type DateInput } from '@/composables/date/useDateInitializationDayjs'
|
|
9
|
+
import { useDatePickerAccessibility } from '@/composables/date/useDatePickerAccessibility'
|
|
10
|
+
import dayjs from 'dayjs'
|
|
11
|
+
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
|
12
|
+
|
|
13
|
+
// Initialiser les plugins dayjs
|
|
14
|
+
dayjs.extend(customParseFormat)
|
|
15
|
+
|
|
16
|
+
const { parseDate, formatDate } = useDateFormat()
|
|
17
|
+
const { initializeSelectedDates } = useDateInitialization()
|
|
18
|
+
const { updateAccessibility } = useDatePickerAccessibility()
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<{
|
|
21
|
+
modelValue?: DateInput
|
|
22
|
+
placeholder?: string
|
|
23
|
+
format?: string
|
|
24
|
+
dateFormatReturn?: string
|
|
25
|
+
isBirthDate?: boolean
|
|
26
|
+
showWeekNumber?: boolean
|
|
27
|
+
required?: boolean
|
|
28
|
+
displayRange?: boolean
|
|
29
|
+
displayIcon?: boolean
|
|
30
|
+
displayAppendIcon?: boolean
|
|
31
|
+
displayPrependIcon?: boolean
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mock Axios headers
|
|
33
|
+
customRules?: { type: string, options: any }[]
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mock Axios headers
|
|
35
|
+
customWarningRules?: { type: string, options: any }[]
|
|
36
|
+
disabled?: boolean
|
|
37
|
+
noIcon?: boolean
|
|
38
|
+
noCalendar?: boolean
|
|
39
|
+
isOutlined?: boolean
|
|
40
|
+
readonly?: boolean
|
|
41
|
+
width?: string
|
|
42
|
+
disableErrorHandling?: boolean
|
|
43
|
+
showSuccessMessages?: boolean
|
|
44
|
+
bgColor?: string
|
|
45
|
+
textFieldActivator?: boolean
|
|
46
|
+
}>(), {
|
|
47
|
+
modelValue: undefined,
|
|
48
|
+
placeholder: 'Sélectionner une date',
|
|
49
|
+
format: 'DD/MM/YYYY',
|
|
50
|
+
dateFormatReturn: '',
|
|
51
|
+
isBirthDate: false,
|
|
52
|
+
showWeekNumber: false,
|
|
53
|
+
required: false,
|
|
54
|
+
displayRange: false,
|
|
55
|
+
displayIcon: true,
|
|
56
|
+
displayAppendIcon: false,
|
|
57
|
+
displayPrependIcon: true,
|
|
58
|
+
customRules: () => [],
|
|
59
|
+
customWarningRules: () => [],
|
|
60
|
+
disabled: false,
|
|
61
|
+
noIcon: false,
|
|
62
|
+
noCalendar: false,
|
|
63
|
+
isOutlined: true,
|
|
64
|
+
readonly: false,
|
|
65
|
+
width: '100%',
|
|
66
|
+
disableErrorHandling: false,
|
|
67
|
+
showSuccessMessages: true,
|
|
68
|
+
bgColor: undefined,
|
|
69
|
+
textFieldActivator: false,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const emit = defineEmits<{
|
|
73
|
+
(e: 'update:modelValue', value: DateValue): void
|
|
74
|
+
(e: 'closed'): void
|
|
75
|
+
(e: 'focus'): void
|
|
76
|
+
(e: 'blur'): void
|
|
77
|
+
}>()
|
|
78
|
+
|
|
79
|
+
const selectedDates = ref<Date | Date[] | null>(
|
|
80
|
+
initializeSelectedDates(props.modelValue as DateInput | null, props.format, props.dateFormatReturn),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const isDatePickerVisible = ref(false)
|
|
84
|
+
const validation = useValidation({
|
|
85
|
+
showSuccessMessages: props.showSuccessMessages,
|
|
86
|
+
fieldIdentifier: 'Date',
|
|
87
|
+
customRules: props.customRules,
|
|
88
|
+
warningRules: props.customWarningRules,
|
|
89
|
+
disableErrorHandling: props.disableErrorHandling,
|
|
90
|
+
})
|
|
91
|
+
const { errors, warnings, successes, validateField, clearValidation } = validation
|
|
92
|
+
|
|
93
|
+
const errorMessages = errors
|
|
94
|
+
const warningMessages = warnings
|
|
95
|
+
const successMessages = successes
|
|
96
|
+
const displayFormattedDate = ref('')
|
|
97
|
+
|
|
98
|
+
const textInputValue = ref<string>('')
|
|
99
|
+
|
|
100
|
+
// Variable pour éviter les mises à jour récursives
|
|
101
|
+
const isUpdatingFromInternal = ref(false)
|
|
102
|
+
|
|
103
|
+
// Fonction pour valider les dates
|
|
104
|
+
const validateDates = (forceValidation = false) => {
|
|
105
|
+
if (props.noCalendar) {
|
|
106
|
+
// En mode no-calendar, on délègue la validation au DateTextInput
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Réinitialiser la validation
|
|
111
|
+
clearValidation()
|
|
112
|
+
|
|
113
|
+
// Si la gestion des erreurs est désactivée, on effectue la validation interne
|
|
114
|
+
// mais on n'ajoute pas les messages d'erreur
|
|
115
|
+
const shouldDisplayErrors = !props.disableErrorHandling
|
|
116
|
+
|
|
117
|
+
// Vérifier si le champ est requis et vide
|
|
118
|
+
if ((forceValidation || !isUpdatingFromInternal.value) && props.required && (!selectedDates.value || (Array.isArray(selectedDates.value) && selectedDates.value.length === 0))) {
|
|
119
|
+
if (shouldDisplayErrors) {
|
|
120
|
+
errors.value.push('La date est requise.')
|
|
121
|
+
}
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!selectedDates.value) return
|
|
126
|
+
|
|
127
|
+
// Préparer les dates à valider
|
|
128
|
+
const datesToValidate = Array.isArray(selectedDates.value)
|
|
129
|
+
? selectedDates.value
|
|
130
|
+
: [selectedDates.value]
|
|
131
|
+
|
|
132
|
+
// Valider chaque date
|
|
133
|
+
if (shouldDisplayErrors) {
|
|
134
|
+
datesToValidate.forEach((date) => {
|
|
135
|
+
validateField(
|
|
136
|
+
date,
|
|
137
|
+
props.customRules,
|
|
138
|
+
props.customWarningRules,
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Dédoublonner les messages (au cas où plusieurs dates auraient les mêmes messages)
|
|
143
|
+
errors.value = [...new Set(errors.value)]
|
|
144
|
+
warnings.value = [...new Set(warnings.value)]
|
|
145
|
+
successes.value = [...new Set(successes.value)]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Fonction centralisée pour mettre à jour le modèle
|
|
150
|
+
const updateModel = (value: DateValue) => {
|
|
151
|
+
// Éviter les mises à jour inutiles
|
|
152
|
+
if (JSON.stringify(value) === JSON.stringify(props.modelValue)) return
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
isUpdatingFromInternal.value = true
|
|
156
|
+
emit('update:modelValue', value)
|
|
157
|
+
}
|
|
158
|
+
finally {
|
|
159
|
+
// S'assurer que le flag est toujours réinitialisé
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
isUpdatingFromInternal.value = false
|
|
162
|
+
}, 0)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Watcher pour mettre à jour le modèle lorsque les dates sélectionnées changent
|
|
167
|
+
watch(selectedDates, (newValue) => {
|
|
168
|
+
// Valider les dates
|
|
169
|
+
validateDates()
|
|
170
|
+
|
|
171
|
+
// Mettre à jour le modèle si nécessaire
|
|
172
|
+
if (newValue !== null) {
|
|
173
|
+
updateModel(formattedDate.value)
|
|
174
|
+
|
|
175
|
+
// Mettre à jour textInputValue pour le DateTextInput
|
|
176
|
+
try {
|
|
177
|
+
isUpdatingFromInternal.value = true
|
|
178
|
+
if (Array.isArray(newValue)) {
|
|
179
|
+
// Pour les plages de dates, utiliser la première date
|
|
180
|
+
if (newValue.length > 0) {
|
|
181
|
+
textInputValue.value = formatDate(newValue[0], props.format)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// Pour une date unique
|
|
186
|
+
textInputValue.value = formatDate(newValue, props.format)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
isUpdatingFromInternal.value = false
|
|
192
|
+
}, 0)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
updateModel(null)
|
|
197
|
+
// Réinitialiser textInputValue
|
|
198
|
+
textInputValue.value = ''
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const getMessageClasses = () => ({
|
|
203
|
+
'dp-width': true,
|
|
204
|
+
'v-messages__message--success': successMessages.value.length > 0,
|
|
205
|
+
'v-messages__message--error': errorMessages.value.length > 0,
|
|
206
|
+
'v-messages__message--warning': warningMessages.value.length > 0 && errorMessages.value.length < 1,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const inputStyle = computed(() => ({
|
|
210
|
+
'min-width': '100%',
|
|
211
|
+
}))
|
|
212
|
+
|
|
213
|
+
// Date(s) formatée(s) en chaîne de caractères pour la valeur de retour
|
|
214
|
+
const formattedDate = computed<DateValue>(() => {
|
|
215
|
+
if (!selectedDates.value) return ''
|
|
216
|
+
|
|
217
|
+
const returnFormat = props.dateFormatReturn || props.format
|
|
218
|
+
|
|
219
|
+
if (Array.isArray(selectedDates.value)) {
|
|
220
|
+
if (selectedDates.value.length >= 2) {
|
|
221
|
+
return [
|
|
222
|
+
formatDate(selectedDates.value[0], returnFormat),
|
|
223
|
+
formatDate(selectedDates.value[1], returnFormat),
|
|
224
|
+
] as [string, string]
|
|
225
|
+
}
|
|
226
|
+
return ''
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return formatDate(selectedDates.value, returnFormat)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
watch(formattedDate, (newValue) => {
|
|
233
|
+
if (!newValue || newValue === '') {
|
|
234
|
+
textInputValue.value = ''
|
|
235
|
+
}
|
|
236
|
+
else if (typeof newValue === 'string') {
|
|
237
|
+
// Si on a un format de retour différent, on doit convertir la date
|
|
238
|
+
if (props.dateFormatReturn) {
|
|
239
|
+
const date = parseDate(newValue, props.dateFormatReturn)
|
|
240
|
+
if (date) {
|
|
241
|
+
textInputValue.value = formatDate(date, props.format)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
textInputValue.value = newValue
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}, { immediate: true })
|
|
249
|
+
|
|
250
|
+
watch(textInputValue, (newValue) => {
|
|
251
|
+
// Éviter les mises à jour récursives
|
|
252
|
+
if (isUpdatingFromInternal.value) return
|
|
253
|
+
|
|
254
|
+
// Parse la date avec le format d'affichage
|
|
255
|
+
const date = parseDate(newValue, props.format)
|
|
256
|
+
if (date) {
|
|
257
|
+
// Si on a un format de retour, formater la date dans ce format
|
|
258
|
+
const formattedValue = props.dateFormatReturn
|
|
259
|
+
? formatDate(date, props.dateFormatReturn)
|
|
260
|
+
: formatDate(date, props.format)
|
|
261
|
+
updateModel(formattedValue)
|
|
262
|
+
|
|
263
|
+
// Mettre à jour selectedDates sans déclencher de watchers supplémentaires
|
|
264
|
+
try {
|
|
265
|
+
isUpdatingFromInternal.value = true
|
|
266
|
+
selectedDates.value = date
|
|
267
|
+
// Mettre à jour l'affichage formaté
|
|
268
|
+
displayFormattedDate.value = formatDate(date, props.format)
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
isUpdatingFromInternal.value = false
|
|
273
|
+
}, 0)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (newValue) {
|
|
277
|
+
// Même si la date n'est pas valide, conserver la valeur saisie
|
|
278
|
+
// pour éviter que la date ne disparaisse
|
|
279
|
+
updateModel(newValue)
|
|
280
|
+
// Mettre à jour l'affichage formaté pour qu'il corresponde à ce qui est saisi
|
|
281
|
+
try {
|
|
282
|
+
isUpdatingFromInternal.value = true
|
|
283
|
+
displayFormattedDate.value = newValue
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
setTimeout(() => {
|
|
287
|
+
isUpdatingFromInternal.value = false
|
|
288
|
+
}, 0)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
updateModel(null)
|
|
293
|
+
// Réinitialiser l'affichage formaté
|
|
294
|
+
try {
|
|
295
|
+
isUpdatingFromInternal.value = true
|
|
296
|
+
displayFormattedDate.value = ''
|
|
297
|
+
selectedDates.value = null
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
setTimeout(() => {
|
|
301
|
+
isUpdatingFromInternal.value = false
|
|
302
|
+
}, 0)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
// Date(s) formatée(s) en chaîne de caractères pour l'affichage
|
|
308
|
+
const displayFormattedDateComputed = computed(() => {
|
|
309
|
+
if (!selectedDates.value) return null
|
|
310
|
+
|
|
311
|
+
if (Array.isArray(selectedDates.value)) {
|
|
312
|
+
if (selectedDates.value.length >= 2) {
|
|
313
|
+
return `${formatDate(selectedDates.value[0], props.format)} - ${formatDate(
|
|
314
|
+
selectedDates.value[selectedDates.value.length - 1],
|
|
315
|
+
props.format,
|
|
316
|
+
)}`
|
|
317
|
+
}
|
|
318
|
+
return formatDate(selectedDates.value[0], props.format)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return formatDate(selectedDates.value, props.format)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
watch(displayFormattedDateComputed, (newValue) => {
|
|
325
|
+
if (!props.noCalendar && newValue) {
|
|
326
|
+
displayFormattedDate.value = newValue
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
// Fonction pour mettre à jour displayFormattedDate quand le VDatePicker change
|
|
331
|
+
const updateDisplayFormattedDate = () => {
|
|
332
|
+
if (displayFormattedDateComputed.value) {
|
|
333
|
+
displayFormattedDate.value = displayFormattedDateComputed.value
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const updateSelectedDates = (input: DateValue) => {
|
|
338
|
+
if (Array.isArray(input)) {
|
|
339
|
+
const dates = input
|
|
340
|
+
.map(date => (date ? parseDate(date, props.format) : null))
|
|
341
|
+
.filter((date): date is Date => date !== null)
|
|
342
|
+
|
|
343
|
+
if (dates.length === 0) {
|
|
344
|
+
selectedDates.value = null
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
selectedDates.value = dates
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const date = input ? parseDate(input, props.format) : null
|
|
353
|
+
selectedDates.value = date === null ? null : date
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Gestionnaire de clic en dehors
|
|
357
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
358
|
+
if (!isDatePickerVisible.value) return
|
|
359
|
+
|
|
360
|
+
const target = event.target as HTMLElement
|
|
361
|
+
const container = target.closest('.date-picker-container')
|
|
362
|
+
|
|
363
|
+
// Si on clique dans le conteneur du DatePicker, on ne fait rien
|
|
364
|
+
if (container) return
|
|
365
|
+
emit('closed')
|
|
366
|
+
// Déclencher la validation à la fermeture
|
|
367
|
+
validateDates()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const todayInString = computed(() => {
|
|
371
|
+
return dayjs().locale('fr').format('dddd D MMMM').replace(/\b\w/g, l => l.toUpperCase())
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
onMounted(() => {
|
|
375
|
+
document.addEventListener('click', handleClickOutside)
|
|
376
|
+
|
|
377
|
+
// Initialiser l'affichage formaté
|
|
378
|
+
if (displayFormattedDateComputed.value) {
|
|
379
|
+
displayFormattedDate.value = displayFormattedDateComputed.value
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Valider les dates au montage
|
|
383
|
+
validateDates()
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
onBeforeUnmount(() => {
|
|
387
|
+
document.removeEventListener('click', handleClickOutside)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
const dateTextInputRef = ref<null | ComponentPublicInstance<typeof DateTextInput>>()
|
|
391
|
+
const dateCalendarTextInputRef = ref<null | ComponentPublicInstance<typeof SyTextField>>()
|
|
392
|
+
const datePickerRef = ref<null | ComponentPublicInstance<typeof VDatePicker>>()
|
|
393
|
+
|
|
394
|
+
const validateOnSubmit = () => {
|
|
395
|
+
if (props.noCalendar) {
|
|
396
|
+
return dateTextInputRef.value?.validateOnSubmit()
|
|
397
|
+
}
|
|
398
|
+
// Forcer la validation pour ignorer les conditions de validation interactive
|
|
399
|
+
validateDates(true)
|
|
400
|
+
// Retourner directement un booléen pour maintenir la compatibilité avec les tests existants
|
|
401
|
+
return errors.value.length === 0
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const openDatePicker = () => {
|
|
405
|
+
if (!isDatePickerVisible.value) {
|
|
406
|
+
toggleDatePicker()
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
type ViewMode = 'month' | 'year' | 'months' | undefined
|
|
411
|
+
|
|
412
|
+
// Variable pour suivre le mode d'affichage actuel du DatePicker
|
|
413
|
+
const currentViewMode = ref<ViewMode>(props.isBirthDate ? 'year' : 'month')
|
|
414
|
+
|
|
415
|
+
watch(() => props.isBirthDate, (newValue) => {
|
|
416
|
+
currentViewMode.value = newValue ? 'year' : 'month'
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
// Fonction pour gérer le changement de mode d'affichage
|
|
420
|
+
const handleViewModeUpdate = (newMode: ViewMode) => {
|
|
421
|
+
currentViewMode.value = newMode
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Fonction pour gérer la sélection de l'année quand isBirthDate est true
|
|
425
|
+
const handleYearUpdate = () => {
|
|
426
|
+
if (props.isBirthDate) {
|
|
427
|
+
// Après la sélection de l'année, passer automatiquement à la sélection du mois
|
|
428
|
+
currentViewMode.value = 'months'
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Fonction pour gérer la sélection du mois quand isBirthDate est true
|
|
433
|
+
const handleMonthUpdate = () => {
|
|
434
|
+
if (props.isBirthDate) {
|
|
435
|
+
// Après la sélection du mois, passer automatiquement à la sélection du jour
|
|
436
|
+
currentViewMode.value = undefined
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const handleInputBlur = () => {
|
|
441
|
+
emit('blur')
|
|
442
|
+
validateDates(true)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
watch(isDatePickerVisible, async (isVisible) => {
|
|
446
|
+
if (!isVisible && props.isBirthDate) {
|
|
447
|
+
// Réinitialiser le mode d'affichage au type birthdate
|
|
448
|
+
currentViewMode.value = 'year'
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (isVisible) {
|
|
452
|
+
// set the focus on the date picker
|
|
453
|
+
await nextTick()
|
|
454
|
+
const firstButton = datePickerRef.value?.$el.querySelector('button')
|
|
455
|
+
if (firstButton) {
|
|
456
|
+
firstButton.focus()
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
// set the focus on the text input
|
|
461
|
+
// wait for VMenu to finish DOM updates & transition
|
|
462
|
+
setTimeout(() => {
|
|
463
|
+
requestAnimationFrame(() => {
|
|
464
|
+
const inputElement = dateCalendarTextInputRef.value?.$el?.querySelector('input')
|
|
465
|
+
if (inputElement) {
|
|
466
|
+
inputElement.focus()
|
|
467
|
+
isDatePickerVisible.value = false
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
}, 0)
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
const getIcon = () => {
|
|
475
|
+
if (props.noCalendar || props.disableErrorHandling) {
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
switch (true) {
|
|
479
|
+
case errorMessages.value.length > 0:
|
|
480
|
+
return 'error'
|
|
481
|
+
case warningMessages.value.length > 0:
|
|
482
|
+
return 'warning'
|
|
483
|
+
case successMessages.value.length > 0:
|
|
484
|
+
return 'success'
|
|
485
|
+
default:
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const syncFromModelValue = (newValue: DateInput | undefined) => {
|
|
491
|
+
if (!newValue || newValue === '') {
|
|
492
|
+
selectedDates.value = null
|
|
493
|
+
textInputValue.value = ''
|
|
494
|
+
displayFormattedDate.value = ''
|
|
495
|
+
validateDates()
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
selectedDates.value = initializeSelectedDates(newValue, props.format, props.dateFormatReturn)
|
|
500
|
+
|
|
501
|
+
if (selectedDates.value) {
|
|
502
|
+
const firstDate = Array.isArray(selectedDates.value)
|
|
503
|
+
? selectedDates.value[0]
|
|
504
|
+
: selectedDates.value
|
|
505
|
+
|
|
506
|
+
textInputValue.value = formatDate(firstDate, props.format)
|
|
507
|
+
displayFormattedDate.value = displayFormattedDateComputed.value || ''
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
validateDates()
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
watch(() => props.modelValue, (newValue) => {
|
|
514
|
+
if (isUpdatingFromInternal.value) {
|
|
515
|
+
if (props.displayRange) {
|
|
516
|
+
if (Array.isArray(newValue) && newValue.length >= 2) {
|
|
517
|
+
isDatePickerVisible.value = false
|
|
518
|
+
emit('closed')
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
isDatePickerVisible.value = false
|
|
523
|
+
emit('closed')
|
|
524
|
+
}
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
isUpdatingFromInternal.value = true
|
|
530
|
+
syncFromModelValue(newValue)
|
|
531
|
+
}
|
|
532
|
+
finally {
|
|
533
|
+
setTimeout(() => {
|
|
534
|
+
isUpdatingFromInternal.value = false
|
|
535
|
+
}, 0)
|
|
536
|
+
}
|
|
537
|
+
}, { immediate: true })
|
|
538
|
+
|
|
539
|
+
const toggleDatePicker = () => {
|
|
540
|
+
if (props.disabled || props.readonly) return
|
|
541
|
+
|
|
542
|
+
isDatePickerVisible.value = !isDatePickerVisible.value
|
|
543
|
+
|
|
544
|
+
if (isDatePickerVisible.value) {
|
|
545
|
+
// Mettre à jour l'accessibilité après l'ouverture du DatePicker
|
|
546
|
+
nextTick(() => {
|
|
547
|
+
updateAccessibility()
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
emit('closed')
|
|
552
|
+
validateDates()
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const openDatePickerOnClick = () => {
|
|
557
|
+
if (props.textFieldActivator) {
|
|
558
|
+
openDatePicker()
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const openDatePickerOnFocus = () => {
|
|
563
|
+
// Only open the DatePicker if textFieldActivator is true
|
|
564
|
+
if (props.textFieldActivator) {
|
|
565
|
+
openDatePicker()
|
|
566
|
+
}
|
|
567
|
+
// Always emit the focus event
|
|
568
|
+
emit('focus')
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const openDatePickerOnIconClick = () => {
|
|
572
|
+
toggleDatePicker()
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
defineExpose({
|
|
576
|
+
validateOnSubmit,
|
|
577
|
+
isDatePickerVisible,
|
|
578
|
+
selectedDates,
|
|
579
|
+
errorMessages,
|
|
580
|
+
handleClickOutside,
|
|
581
|
+
initializeSelectedDates,
|
|
582
|
+
updateAccessibility,
|
|
583
|
+
openDatePicker,
|
|
584
|
+
})
|
|
585
|
+
</script>
|
|
586
|
+
|
|
587
|
+
<template>
|
|
588
|
+
<div
|
|
589
|
+
class="date-picker-container"
|
|
590
|
+
:style="inputStyle"
|
|
591
|
+
>
|
|
592
|
+
<template v-if="props.noCalendar">
|
|
593
|
+
<DateTextInput
|
|
594
|
+
ref="dateTextInputRef"
|
|
595
|
+
v-model="textInputValue"
|
|
596
|
+
:class="[getMessageClasses(), 'label-hidden-on-focus']"
|
|
597
|
+
:date-format-return="props.dateFormatReturn"
|
|
598
|
+
:format="props.format"
|
|
599
|
+
:label="props.placeholder"
|
|
600
|
+
:placeholder="props.placeholder"
|
|
601
|
+
:required="props.required"
|
|
602
|
+
:custom-rules="props.customRules"
|
|
603
|
+
:custom-warning-rules="props.customWarningRules"
|
|
604
|
+
:disabled="props.disabled"
|
|
605
|
+
:readonly="props.readonly"
|
|
606
|
+
:is-outlined="props.isOutlined"
|
|
607
|
+
:display-icon="props.displayIcon"
|
|
608
|
+
:display-append-icon="props.displayAppendIcon"
|
|
609
|
+
:display-prepend-icon="props.displayPrependIcon"
|
|
610
|
+
:no-icon="props.noIcon"
|
|
611
|
+
:disable-error-handling="props.disableErrorHandling"
|
|
612
|
+
:show-success-messages="props.showSuccessMessages"
|
|
613
|
+
:bg-color="props.bgColor"
|
|
614
|
+
title="Date text input"
|
|
615
|
+
@focus="emit('focus')"
|
|
616
|
+
@blur="emit('blur')"
|
|
617
|
+
/>
|
|
618
|
+
</template>
|
|
619
|
+
<template v-else>
|
|
620
|
+
<VMenu
|
|
621
|
+
v-if="!props.noCalendar"
|
|
622
|
+
v-model="isDatePickerVisible"
|
|
623
|
+
activator="parent"
|
|
624
|
+
:min-width="0"
|
|
625
|
+
location="bottom"
|
|
626
|
+
:close-on-content-click="false"
|
|
627
|
+
:open-on-click="false"
|
|
628
|
+
scroll-strategy="none"
|
|
629
|
+
transition="fade-transition"
|
|
630
|
+
attach="body"
|
|
631
|
+
:offset="[-20, 5]"
|
|
632
|
+
>
|
|
633
|
+
<template #activator="{ props: menuProps }">
|
|
634
|
+
<SyTextField
|
|
635
|
+
v-bind="menuProps"
|
|
636
|
+
ref="dateCalendarTextInputRef"
|
|
637
|
+
v-model="displayFormattedDate"
|
|
638
|
+
:append-icon="displayIcon && displayAppendIcon ? 'calendar' : undefined"
|
|
639
|
+
:append-inner-icon="getIcon()"
|
|
640
|
+
:class="[getMessageClasses(), 'label-hidden-on-focus']"
|
|
641
|
+
:error-messages="errorMessages"
|
|
642
|
+
:warning-messages="warningMessages"
|
|
643
|
+
:success-messages="props.showSuccessMessages ? successMessages : []"
|
|
644
|
+
:disabled="props.disabled"
|
|
645
|
+
:disable-click-button="false"
|
|
646
|
+
:readonly="true"
|
|
647
|
+
:label="props.placeholder"
|
|
648
|
+
:no-icon="props.noIcon"
|
|
649
|
+
:prepend-icon="displayIcon && !displayAppendIcon ? 'calendar' : undefined"
|
|
650
|
+
:variant-style="props.isOutlined ? 'outlined' : 'underlined'"
|
|
651
|
+
color="primary"
|
|
652
|
+
:show-success-messages="props.showSuccessMessages"
|
|
653
|
+
:bg-color="props.bgColor"
|
|
654
|
+
is-clearable
|
|
655
|
+
title="Date Picker"
|
|
656
|
+
@click="openDatePickerOnClick"
|
|
657
|
+
@focus="openDatePickerOnFocus"
|
|
658
|
+
@blur="handleInputBlur"
|
|
659
|
+
@update:model-value="updateSelectedDates"
|
|
660
|
+
@prepend-icon-click="openDatePickerOnIconClick"
|
|
661
|
+
@append-icon-click="openDatePickerOnIconClick"
|
|
662
|
+
/>
|
|
663
|
+
</template>
|
|
664
|
+
<VDatePicker
|
|
665
|
+
v-if="isDatePickerVisible && !props.noCalendar"
|
|
666
|
+
ref="datePickerRef"
|
|
667
|
+
v-model="selectedDates"
|
|
668
|
+
color="primary"
|
|
669
|
+
:first-day-of-week="1"
|
|
670
|
+
:multiple="props.displayRange ? 'range' : false"
|
|
671
|
+
:show-adjacent-months="true"
|
|
672
|
+
:show-week="props.showWeekNumber"
|
|
673
|
+
:view-mode="currentViewMode"
|
|
674
|
+
@update:view-mode="handleViewModeUpdate"
|
|
675
|
+
@update:year="handleYearUpdate"
|
|
676
|
+
@update:month="handleMonthUpdate"
|
|
677
|
+
@update:model-value="updateDisplayFormattedDate"
|
|
678
|
+
>
|
|
679
|
+
<template #title>
|
|
680
|
+
Sélectionnez une date
|
|
681
|
+
</template>
|
|
682
|
+
<template #header>
|
|
683
|
+
<h3 class="mx-auto my-auto ml-5 mb-4">
|
|
684
|
+
{{ todayInString }}
|
|
685
|
+
</h3>
|
|
686
|
+
</template>
|
|
687
|
+
</VDatePicker>
|
|
688
|
+
</VMenu>
|
|
689
|
+
</template>
|
|
690
|
+
</div>
|
|
691
|
+
</template>
|
|
692
|
+
|
|
693
|
+
<style lang="scss" scoped>
|
|
694
|
+
@use '@/assets/tokens';
|
|
695
|
+
|
|
696
|
+
.label-hidden-on-focus:focus + label {
|
|
697
|
+
display: none;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.dp-width {
|
|
701
|
+
width: v-bind('props.width');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.v-messages__message--success {
|
|
705
|
+
:deep(.v-input__control),
|
|
706
|
+
:deep(.v-messages__message) {
|
|
707
|
+
color: tokens.$colors-text-success !important;
|
|
708
|
+
|
|
709
|
+
--v-medium-emphasis-opacity: 1;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.v-field--active & {
|
|
713
|
+
color: tokens.$colors-border-success !important;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.v-messages__message--error {
|
|
718
|
+
:deep(.v-input__control),
|
|
719
|
+
:deep(.v-messages__message) {
|
|
720
|
+
color: tokens.$colors-text-error !important;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.v-field--active & {
|
|
724
|
+
color: tokens.$colors-border-error !important;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.v-messages__message--warning {
|
|
729
|
+
:deep(.v-input__control) {
|
|
730
|
+
color: tokens.$colors-text-warning !important;
|
|
731
|
+
|
|
732
|
+
--v-medium-emphasis-opacity: 1;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
:deep(.v-messages__message) {
|
|
736
|
+
color: tokens.$colors-text-warning !important;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.v-field--active & {
|
|
740
|
+
color: tokens.$colors-text-warning !important;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
:deep(.v-btn__content) {
|
|
745
|
+
font-size: tokens.$font-size-body-text + 3;
|
|
746
|
+
font-weight: bold;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
:deep(.v-messages) {
|
|
750
|
+
opacity: 1;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
:deep(.v-field--dirty) {
|
|
754
|
+
opacity: 1 !important;
|
|
755
|
+
|
|
756
|
+
--v-medium-emphasis-opacity: 1;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
:deep(.v-field--focused) {
|
|
760
|
+
opacity: 1 !important;
|
|
761
|
+
|
|
762
|
+
--v-medium-emphasis-opacity: 1;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.date-picker-container {
|
|
766
|
+
max-width: 100%;
|
|
767
|
+
position: relative;
|
|
768
|
+
|
|
769
|
+
:deep(.v-date-picker) {
|
|
770
|
+
max-width: 445px;
|
|
771
|
+
position: absolute;
|
|
772
|
+
top: 56px;
|
|
773
|
+
left: 0;
|
|
774
|
+
z-index: 2;
|
|
775
|
+
box-shadow:
|
|
776
|
+
0 5px 5px -3px rgb(0 0 0 / 20%),
|
|
777
|
+
0 8px 10px 1px rgb(0 0 0 / 14%),
|
|
778
|
+
0 3px 14px 2px rgb(0 0 0 / 12%) !important;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
:deep(.v-date-picker-month__day--selected, .v-date-picker-month__day--adjacent) {
|
|
783
|
+
opacity: 1;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.fade-enter-active,
|
|
787
|
+
.fade-leave-active {
|
|
788
|
+
transition: opacity 0.5s ease;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.fade-enter-from,
|
|
792
|
+
.fade-leave-to {
|
|
793
|
+
opacity: 0;
|
|
794
|
+
}
|
|
795
|
+
</style>
|