@cnamts/synapse 0.0.14-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.
Files changed (108) hide show
  1. package/dist/components/CookiesSelection/CookiesSelection.d.ts +26 -26
  2. package/dist/components/Customs/SyInputSelect/SyInputSelect.d.ts +2 -2
  3. package/dist/components/Customs/SySelect/SySelect.d.ts +24 -12
  4. package/dist/components/Customs/SySelect/locales.d.ts +3 -0
  5. package/dist/components/Customs/SyTextField/SyTextField.d.ts +1393 -3
  6. package/dist/components/DatePicker/DatePicker.d.ts +3532 -22
  7. package/dist/components/DatePicker/DateTextInput.d.ts +1408 -11
  8. package/dist/components/DialogBox/config.d.ts +1 -1
  9. package/dist/components/DownloadBtn/DownloadBtn.d.ts +2 -0
  10. package/dist/components/LangBtn/LangBtn.d.ts +467 -1
  11. package/dist/components/LangBtn/config.d.ts +1 -3
  12. package/dist/components/NirField/NirField.d.ts +2805 -15
  13. package/dist/components/PasswordField/PasswordField.d.ts +2 -2
  14. package/dist/components/PeriodField/PeriodField.d.ts +7345 -325
  15. package/dist/components/PhoneField/PhoneField.d.ts +3 -3
  16. package/dist/components/SelectBtnField/SelectBtnField.d.ts +1 -1
  17. package/dist/components/SkipLink/SkipLink.d.ts +3 -2
  18. package/dist/components/SyAlert/SyAlert.d.ts +72 -1
  19. package/dist/components/UploadWorkflow/UploadWorkflow.d.ts +26 -26
  20. package/dist/components/UserMenuBtn/UserMenuBtn.d.ts +2 -0
  21. package/dist/components/index.d.ts +2 -0
  22. package/dist/composables/date/useDateFormat.d.ts +2 -2
  23. package/dist/composables/date/useDateFormatDayjs.d.ts +23 -0
  24. package/dist/composables/date/useDateInitializationDayjs.d.ts +18 -0
  25. package/dist/design-system-v3.js +4314 -3987
  26. package/dist/design-system-v3.umd.cjs +1 -1
  27. package/dist/style.css +1 -1
  28. package/dist/vuetifyConfig.d.ts +1 -0
  29. package/package.json +1 -1
  30. package/src/components/BackBtn/Accessibilite.stories.ts +4 -0
  31. package/src/components/BackBtn/BackBtn.vue +2 -1
  32. package/src/components/BackToTopBtn/Accessibilite.stories.ts +4 -0
  33. package/src/components/BackToTopBtn/BackToTopBtn.stories.ts +78 -21
  34. package/src/components/BackToTopBtn/BackToTopBtn.vue +15 -0
  35. package/src/components/BackToTopBtn/config.ts +2 -2
  36. package/src/components/BackToTopBtn/tests/__snapshots__/BackToTopBtn.spec.ts.snap +4 -4
  37. package/src/components/CopyBtn/Accessibilite.stories.ts +4 -0
  38. package/src/components/Customs/SyBtnSelect/SyBtnSelect.stories.ts +2 -2
  39. package/src/components/Customs/SyBtnSelect/SyBtnSelect.vue +0 -1
  40. package/src/components/Customs/SyInputSelect/SyInputSelect.stories.ts +3 -3
  41. package/src/components/Customs/SyInputSelect/SyInputSelect.vue +4 -4
  42. package/src/components/Customs/SySelect/SySelect.stories.ts +4 -0
  43. package/src/components/Customs/SySelect/SySelect.vue +75 -10
  44. package/src/components/Customs/SySelect/locales.ts +3 -0
  45. package/src/components/Customs/SySelect/tests/SySelect.spec.ts +24 -2
  46. package/src/components/Customs/SyTextField/Accessibilite.stories.ts +7 -0
  47. package/src/components/Customs/SyTextField/SyTextField.stories.ts +14 -1
  48. package/src/components/Customs/SyTextField/SyTextField.vue +85 -20
  49. package/src/components/DatePicker/ComplexDatePicker/ComplexDatePicker.vue +795 -0
  50. package/src/components/DatePicker/DatePicker.stories.ts +432 -1
  51. package/src/components/DatePicker/DatePicker.vue +143 -76
  52. package/src/components/DatePicker/DatePickerValidation.mdx +338 -0
  53. package/src/components/DatePicker/DatePickerValidation.stories.ts +30 -0
  54. package/src/components/DatePicker/DateTextInput.vue +87 -135
  55. package/src/components/DatePicker/docExamples/DatePickerBidirectionalValidation.vue +282 -0
  56. package/src/components/DatePicker/docExamples/DatePickerValidationExamples.vue +535 -0
  57. package/src/components/DatePicker/tests/DatePicker.spec.ts +33 -32
  58. package/src/components/DatePicker/tests/DateTextInput.spec.ts +83 -35
  59. package/src/components/DialogBox/DialogBox.stories.ts +5 -2
  60. package/src/components/DialogBox/DialogBox.vue +1 -1
  61. package/src/components/DialogBox/config.ts +1 -1
  62. package/src/components/DownloadBtn/Accessibilite.stories.ts +4 -0
  63. package/src/components/DownloadBtn/DownloadBtn.stories.ts +17 -8
  64. package/src/components/DownloadBtn/DownloadBtn.vue +13 -6
  65. package/src/components/DownloadBtn/tests/__snapshots__/DownloadBtn.spec.ts.snap +0 -2
  66. package/src/components/FranceConnectBtn/Accessibilite.stories.ts +4 -0
  67. package/src/components/HeaderBar/HeaderBurgerMenu/HeaderMenuItem/HeaderMenuItem.vue +3 -0
  68. package/src/components/HeaderBar/HeaderBurgerMenu/HeaderSubMenu/HeaderSubMenu.vue +3 -0
  69. package/src/components/HeaderBar/HeaderBurgerMenu/menu.scss +19 -0
  70. package/src/components/HeaderBar/HeaderMenuBtn/HeaderMenuBtn.vue +12 -2
  71. package/src/components/LangBtn/Accessibilite.stories.ts +4 -0
  72. package/src/components/LangBtn/LangBtn.stories.ts +1 -4
  73. package/src/components/LangBtn/LangBtn.vue +68 -9
  74. package/src/components/LangBtn/config.ts +0 -1
  75. package/src/components/LangBtn/tests/LangBtn.spec.ts +30 -2
  76. package/src/components/PageContainer/Accessibilite.stories.ts +36 -23
  77. package/src/components/PaginatedTable/PaginatedTable.stories.ts +144 -18
  78. package/src/components/PasswordField/PasswordField.stories.ts +6 -6
  79. package/src/components/PasswordField/PasswordField.vue +3 -3
  80. package/src/components/PeriodField/PeriodField.vue +4 -4
  81. package/src/components/PhoneField/PhoneField.stories.ts +216 -24
  82. package/src/components/PhoneField/PhoneField.vue +32 -2
  83. package/src/components/PhoneField/tests/PhoneField.spec.ts +161 -14
  84. package/src/components/RatingPicker/NumberPicker/NumberPicker.vue +2 -1
  85. package/src/components/RatingPicker/RatingPicker.stories.ts +1 -1
  86. package/src/components/SkipLink/Accessibilite.stories.ts +8 -0
  87. package/src/components/SkipLink/SkipLink.vue +11 -9
  88. package/src/components/SkipLink/tests/__snapshots__/skipLink.spec.ts.snap +7 -4
  89. package/src/components/SkipLink/tests/skipLink.spec.ts +120 -6
  90. package/src/components/SyAlert/Accessibilite.stories.ts +4 -0
  91. package/src/components/SyAlert/SyAlert.mdx +3 -7
  92. package/src/components/SyAlert/SyAlert.stories.ts +19 -12
  93. package/src/components/SyAlert/SyAlert.vue +88 -51
  94. package/src/components/SyAlert/tests/SyAlert.spec.ts +20 -2
  95. package/src/components/SyAlert/tests/__snapshots__/SyAlert.spec.ts.snap +83 -75
  96. package/src/components/UserMenuBtn/UserMenuBtn.stories.ts +56 -0
  97. package/src/components/UserMenuBtn/UserMenuBtn.vue +4 -2
  98. package/src/components/UserMenuBtn/tests/UserMenuBtn.spec.ts +41 -0
  99. package/src/components/index.ts +2 -0
  100. package/src/composables/date/useDateFormat.ts +17 -1
  101. package/src/composables/date/useDateFormatDayjs.ts +84 -0
  102. package/src/composables/date/useDateInitializationDayjs.ts +133 -0
  103. package/src/composables/rules/useFieldValidation.ts +26 -3
  104. package/src/stories/Accessibilite/Avancement/Avancement.mdx +12 -0
  105. package/src/stories/Accessibilite/Avancement/Avancement.stories.ts +134 -0
  106. package/src/stories/Accessibilite/KitDePreAudit/Echantillonnage.mdx +1 -1
  107. package/src/stories/GuideDuDev/LesBreackingChanges.mdx +31 -2
  108. package/src/components/LangBtn/tests/Config.spec.ts +0 -24
@@ -2,10 +2,12 @@
2
2
  import { ref, computed, watch, onMounted } from 'vue'
3
3
  import { nextTick } from 'vue'
4
4
  import SyTextField from '@/components/Customs/SyTextField/SyTextField.vue'
5
- import { useDateFormat } from '@/composables/date/useDateFormat'
6
5
  import { useValidation, type ValidationRule } from '@/composables/validation/useValidation'
6
+ import dayjs from 'dayjs'
7
+ import customParseFormat from 'dayjs/plugin/customParseFormat'
7
8
 
8
- const { parseDate } = useDateFormat()
9
+ // Initialiser les plugins dayjs
10
+ dayjs.extend(customParseFormat)
9
11
 
10
12
  type DateValue = string | null
11
13
 
@@ -17,7 +19,7 @@
17
19
  label?: string
18
20
  required?: boolean
19
21
  disabled?: boolean
20
- readOnly?: boolean
22
+ readonly?: boolean
21
23
  isOutlined?: boolean
22
24
  displayIcon?: boolean
23
25
  displayAppendIcon?: boolean
@@ -27,6 +29,7 @@
27
29
  displayPrependIcon?: boolean
28
30
  disableErrorHandling?: boolean
29
31
  showSuccessMessages?: boolean
32
+ bgColor?: string
30
33
  }>(), {
31
34
  modelValue: null,
32
35
  placeholder: 'Sélectionner une date',
@@ -35,7 +38,7 @@
35
38
  label: undefined,
36
39
  required: false,
37
40
  disabled: false,
38
- readOnly: false,
41
+ readonly: false,
39
42
  isOutlined: true,
40
43
  displayIcon: true,
41
44
  displayAppendIcon: false,
@@ -45,6 +48,7 @@
45
48
  displayPrependIcon: true,
46
49
  disableErrorHandling: false,
47
50
  showSuccessMessages: true,
51
+ bgColor: undefined,
48
52
  })
49
53
 
50
54
  const emit = defineEmits<{
@@ -74,101 +78,37 @@
74
78
  const isFocused = ref(false)
75
79
  const hasInteracted = ref(false)
76
80
 
77
- const formatDateToString = (date: Date, format: string): string => {
78
- const day = date.getDate().toString().padStart(2, '0')
79
- const month = (date.getMonth() + 1).toString().padStart(2, '0')
80
- const year = date.getFullYear()
81
- const shortYear = (year % 100).toString().padStart(2, '0')
82
-
83
- const separator = format.includes('/') ? '/' : format.includes('-') ? '-' : '.'
84
- const parts: string[] = []
85
-
86
- format.split(/[-/.]/).forEach((part) => {
87
- switch (part.toUpperCase()) {
88
- case 'DD':
89
- parts.push(day)
90
- break
91
- case 'MM':
92
- parts.push(month)
93
- break
94
- case 'YY':
95
- parts.push(shortYear)
96
- break
97
- case 'YYYY':
98
- parts.push(year.toString())
99
- break
100
- }
101
- })
102
-
103
- return parts.join(separator)
104
- }
105
-
106
81
  const formatDateInput = (input: string, cursorPosition?: number): { formatted: string, cursorPos: number } => {
107
- const separator = props.format.includes('/') ? '/' : props.format.includes('-') ? '-' : '.'
108
-
109
- const formatParts = props.format.split(/[/.-]/)
110
- const dayIndex = formatParts.findIndex(part => part.toUpperCase().includes('D'))
111
- const monthIndex = formatParts.findIndex(part => part.toUpperCase().includes('M'))
112
- const yearIndex = formatParts.findIndex(part => part.toUpperCase().includes('Y'))
113
-
114
- const parts = Array(3).fill('__')
115
- parts[yearIndex] = '____'
116
- const mask = parts.join(separator)
117
-
118
- let result = mask
119
- let pos = cursorPosition || 0
120
-
121
- let cleanInput = input
122
-
123
- if (input.includes(separator)) {
124
- const parts = input.split(separator)
125
- cleanInput = parts.map(part => part.replace(/\D/g, '')).join(separator)
126
- }
127
- else {
128
- cleanInput = input.replace(/\D/g, '')
129
- }
130
-
131
- if (cleanInput.includes(separator)) {
132
- const parts = cleanInput.split(separator)
133
- const formattedParts = Array(3).fill('__')
134
- formattedParts[yearIndex] = (parts[yearIndex] || '').padEnd(4, '_')
135
- formattedParts[monthIndex] = (parts[monthIndex] || '').padEnd(2, '_')
136
- formattedParts[dayIndex] = (parts[dayIndex] || '').padEnd(2, '_')
137
-
138
- result = formattedParts.join(separator)
139
- }
140
- else {
141
- const formatOrder = [dayIndex, monthIndex, yearIndex]
142
- let currentDigit = 0
143
-
144
- for (let partIndex = 0; currentDigit < Math.min(cleanInput.length, 8); partIndex++) {
145
- const formatPartIndex = formatOrder[partIndex % 3]
146
- const isYear = formatParts[formatPartIndex].toUpperCase().includes('Y')
147
- const partLength = isYear ? 4 : 2
148
- const targetStartPos = formatPartIndex * 3
149
-
150
- for (let j = 0; j < partLength && currentDigit < cleanInput.length; j++) {
151
- const digit = cleanInput[currentDigit]
152
- const targetPos = targetStartPos + j
153
- result = result.substring(0, targetPos) + digit + result.substring(targetPos + 1)
154
- currentDigit++
82
+ const cleanedInput = input.replace(/[^\d]/g, '')
83
+ let cursor = cursorPosition || 0
84
+ let result = ''
85
+
86
+ let i = 0
87
+ for (const char of props.format) {
88
+ if (['D', 'M', 'Y'].includes(char.toUpperCase())) {
89
+ if (cleanedInput[i]) {
90
+ result += cleanedInput[i]
91
+ i++
92
+ }
93
+ else {
94
+ result += '_'
155
95
  }
156
96
  }
97
+ else {
98
+ result += char
99
+ }
157
100
  }
158
101
 
159
- if (cursorPosition !== undefined) {
160
- pos = cursorPosition
161
- if (mask[pos] === separator) {
162
- pos++
163
- }
102
+ const nextChartIsSeparator = props.format[cursor] === result[cursor]
103
+ if (nextChartIsSeparator) {
104
+ cursor++
164
105
  }
165
106
 
166
107
  return {
167
108
  formatted: result,
168
- cursorPos: pos,
109
+ cursorPos: cursor,
169
110
  }
170
111
  }
171
-
172
112
  const cleanDateString = (input: string): string => {
173
113
  return input.replace(/[^\d/.-]/g, '')
174
114
  }
@@ -184,20 +124,17 @@
184
124
  if (!/^[\d/.-]*$/.test(dateStr)) {
185
125
  return {
186
126
  isValid: props.disableErrorHandling,
187
- message: props.disableErrorHandling ? '' : 'Format de date invalide',
127
+ message: props.disableErrorHandling ? '' : `Format de date invalide (${props.format})`,
188
128
  }
189
129
  }
190
130
 
191
- let date = parseDate(dateStr, props.format)
131
+ const isValid = dayjs(dateStr, props.format, true).isValid()
132
+ || (props.dateFormatReturn ? dayjs(dateStr, props.dateFormatReturn, true).isValid() : false)
192
133
 
193
- if (!date && props.dateFormatReturn) {
194
- date = parseDate(dateStr, props.dateFormatReturn)
195
- }
196
-
197
- if (!date) {
134
+ if (!isValid) {
198
135
  return {
199
136
  isValid: props.disableErrorHandling,
200
- message: props.disableErrorHandling ? '' : 'Format de date invalide',
137
+ message: props.disableErrorHandling ? '' : `Format de date invalide (${props.format})`,
201
138
  }
202
139
  }
203
140
 
@@ -253,9 +190,17 @@
253
190
  return undefined
254
191
  })
255
192
 
256
- const handleKeydown = (event: KeyboardEvent) => {
257
- if (event.ctrlKey || event.metaKey) {
258
- return
193
+ const handleKeydown = (event: KeyboardEvent & { target: HTMLInputElement }) => {
194
+ // the cursor have to be set to the previous character if the user delete a non digit character
195
+ if (event.key === 'Backspace') {
196
+ const input = event.target
197
+ if (!input.selectionStart || input.selectionStart !== input.selectionEnd) {
198
+ return
199
+ }
200
+ const charBeforeCursor = input.value[input.selectionStart - 1]
201
+ if (!/\d/.test(charBeforeCursor)) {
202
+ input.setSelectionRange(input.selectionStart - 1, input.selectionStart - 1)
203
+ }
259
204
  }
260
205
  }
261
206
 
@@ -305,10 +250,13 @@
305
250
  if (isDateComplete) {
306
251
  const validation = validateDateFormat(formatted)
307
252
  if (validation.isValid) {
308
- const date = parseDate(formatted, props.format)
253
+ const date = dayjs(formatted, props.format, true).isValid()
254
+ ? dayjs(formatted, props.format).toDate()
255
+ : null
256
+
309
257
  if (date) {
310
258
  const formattedDate = props.dateFormatReturn
311
- ? formatDateToString(date, props.dateFormatReturn)
259
+ ? dayjs(date).format(props.dateFormatReturn)
312
260
  : formatted
313
261
  await nextTick()
314
262
  emit('update:model-value', formattedDate)
@@ -334,14 +282,17 @@
334
282
  return
335
283
  }
336
284
 
337
- const date = parseDate(newValue, props.format)
285
+ const date = dayjs(newValue, props.format, true).isValid()
286
+ ? dayjs(newValue, props.format).toDate()
287
+ : null
288
+
338
289
  if (date) {
339
290
  if (props.dateFormatReturn && props.dateFormatReturn !== props.format) {
340
- const formattedForReturn = formatDateToString(date, props.dateFormatReturn)
291
+ const formattedForReturn = dayjs(date).format(props.dateFormatReturn)
341
292
  emit('update:model-value', formattedForReturn)
342
293
  }
343
294
 
344
- inputValue.value = formatDateToString(date, props.format)
295
+ inputValue.value = dayjs(date).format(props.format)
345
296
  validateRules(inputValue.value)
346
297
  }
347
298
  else {
@@ -363,10 +314,13 @@
363
314
  if (inputValue.value) {
364
315
  const validation = validateDateFormat(inputValue.value)
365
316
  if (validation.isValid) {
366
- const date = parseDate(inputValue.value, props.format)
317
+ const date = dayjs(inputValue.value, props.format, true).isValid()
318
+ ? dayjs(inputValue.value, props.format).toDate()
319
+ : null
320
+
367
321
  if (date) {
368
322
  const formattedDate = props.dateFormatReturn
369
- ? formatDateToString(date, props.dateFormatReturn)
323
+ ? dayjs(date).format(props.dateFormatReturn)
370
324
  : inputValue.value
371
325
  emit('update:model-value', formattedDate)
372
326
  }
@@ -407,14 +361,17 @@
407
361
 
408
362
  defineExpose({
409
363
  validateOnSubmit,
410
- focus: () => {
411
- const input = document.querySelector('input')
364
+ focus() {
365
+ // Utiliser un sélecteur plus spécifique pour cibler l'input principal
366
+ // SyTextField peut contenir plusieurs inputs, donc on cible le premier qui n'est pas caché
367
+ const input = inputRef.value?.$el.querySelector('input:not([type="hidden"])')
412
368
  if (input) {
413
369
  input.focus()
414
370
  }
415
371
  },
416
- blur: () => {
417
- const input = document.querySelector('input')
372
+ blur() {
373
+ // Utiliser un sélecteur plus spécifique pour cibler l'input principal
374
+ const input = inputRef.value?.$el.querySelector('input:not([type="hidden"])')
418
375
  if (input) {
419
376
  input.blur()
420
377
  }
@@ -426,51 +383,46 @@
426
383
  return
427
384
  }
428
385
 
429
- const date = parseDate(props.modelValue, props.format)
430
- if (date) {
431
- if (props.dateFormatReturn && props.dateFormatReturn !== props.format) {
432
- const formattedForReturn = formatDateToString(date, props.dateFormatReturn)
433
- emit('update:model-value', formattedForReturn)
434
- }
386
+ const date = dayjs(props.modelValue, props.format, true).isValid()
387
+ ? dayjs(props.modelValue, props.format).toDate()
388
+ : null
435
389
 
436
- inputValue.value = formatDateToString(date, props.format)
437
- validateRules(inputValue.value)
390
+ if (date) {
391
+ inputValue.value = dayjs(date).format(props.format)
438
392
  }
439
393
  else {
440
394
  inputValue.value = props.modelValue
441
- validateRules(props.modelValue)
442
395
  }
443
396
  })
444
-
445
397
  </script>
446
398
 
447
399
  <template>
448
400
  <SyTextField
449
401
  ref="inputRef"
450
402
  v-model="inputValue"
451
- :placeholder="placeholder"
452
- :label="label"
453
- :error-messages="errorMessages"
454
- :warning-messages="warningMessages"
455
- :success-messages="showSuccessMessages ? successMessages : []"
456
- :is-on-error="isOnError"
457
- :disabled="disabled"
458
- :read-only="readOnly"
459
- :display-icon="displayIcon"
460
- :display-append-icon="displayAppendIcon"
461
- :no-icon="noIcon"
462
- :prepend-icon="props.displayPrependIcon && !props.displayAppendIcon ? 'calendar' : undefined"
463
- :append-icon="props.displayAppendIcon ? 'calendar' : undefined"
403
+ :append-icon="displayIcon && displayAppendIcon ? 'calendar' : undefined"
464
404
  :append-inner-icon="getIcon"
465
- :variant-style="isOutlined ? 'outlined' : 'filled'"
466
405
  :class="{
467
406
  'error-field': isOnError,
468
407
  'warning-field': isOnWarning,
469
408
  'success-field': isOnSuccess
470
409
  }"
471
- @keydown="handleKeydown"
410
+ :disabled="props.disabled"
411
+ :error-messages="errorMessages"
412
+ :label="props.label || props.placeholder"
413
+ :no-icon="props.noIcon"
414
+ :prepend-icon="displayIcon && displayPrependIcon ? 'calendar' : undefined"
415
+ :readonly="props.readonly"
416
+ :variant-style="props.isOutlined ? 'outlined' : 'underlined'"
417
+ :warning-messages="warningMessages"
418
+ :success-messages="props.showSuccessMessages ? successMessages : []"
419
+ :bg-color="props.bgColor"
420
+ color="primary"
421
+ is-clearable
422
+ title="Date text input"
472
423
  @focus="handleFocus"
473
424
  @blur="handleBlur"
425
+ @keydown="handleKeydown"
474
426
  @paste="handlePaste"
475
427
  />
476
428
  </template>
@@ -0,0 +1,282 @@
1
+ <script lang="ts" setup>
2
+ import { ref, computed, watch } from 'vue'
3
+ import DatePicker from '@/components/DatePicker/DatePicker.vue'
4
+ import { useDateFormat } from '@/composables/date/useDateFormat'
5
+
6
+ const { parseDate } = useDateFormat()
7
+
8
+ // État des dates
9
+ const startDate = ref<string | null>(null)
10
+ const endDate = ref<string | null>(null)
11
+
12
+ // Références aux composants DatePicker pour accéder à leurs méthodes
13
+ const startDatePickerRef = ref<InstanceType<typeof DatePicker> | null>(null)
14
+ const endDatePickerRef = ref<InstanceType<typeof DatePicker> | null>(null)
15
+
16
+ // Règle de validation pour vérifier que la date de fin n'est pas avant la date de début
17
+ const createEndDateValidationRule = () => ({
18
+ type: 'custom',
19
+ options: {
20
+ validate: (value: string) => {
21
+ // Si pas de valeur pour la date de fin, pas besoin de validation
22
+ if (!value) return true
23
+
24
+ // Si pas de date de début mais une date de fin, afficher l'erreur
25
+ if (!startDate.value) return 'Veuillez d\'abord sélectionner une date de début'
26
+
27
+ const start = parseDate(startDate.value, 'DD/MM/YYYY')
28
+ const end = parseDate(value, 'DD/MM/YYYY')
29
+
30
+ if (!start || !end) return true
31
+
32
+ return end >= start || 'La date de fin ne peut pas être antérieure à la date de début'
33
+ },
34
+ message: 'La date de fin ne peut pas être antérieure à la date de début',
35
+ },
36
+ })
37
+
38
+ // Règle de validation pour vérifier que la date de début n'est pas après la date de fin
39
+ const createStartDateValidationRule = () => ({
40
+ type: 'custom',
41
+ options: {
42
+ validate: (value: string) => {
43
+ // Si pas de valeur pour la date de début ou pas de date de fin, pas besoin de validation
44
+ if (!value || !endDate.value) return true
45
+
46
+ const start = parseDate(value, 'DD/MM/YYYY')
47
+ const end = parseDate(endDate.value, 'DD/MM/YYYY')
48
+
49
+ if (!start || !end) return true
50
+
51
+ return start <= end || 'La date de début ne peut pas être postérieure à la date de fin'
52
+ },
53
+ message: 'La date de début ne peut pas être postérieure à la date de fin',
54
+ },
55
+ })
56
+
57
+ // Règles de validation pour la date de début
58
+ const startDateRules = computed(() => [
59
+ {
60
+ type: 'required',
61
+ options: {
62
+ message: 'La date de début est requise.',
63
+ },
64
+ },
65
+ createStartDateValidationRule(),
66
+ ])
67
+
68
+ // Règles de validation pour la date de fin
69
+ const endDateRules = computed(() => [
70
+ {
71
+ type: 'required',
72
+ options: {
73
+ message: 'La date de fin est requise.',
74
+ },
75
+ },
76
+ createEndDateValidationRule(),
77
+ ])
78
+
79
+ // Fonction pour forcer la validation de la date de fin quand la date de début change
80
+ const validateEndDate = () => {
81
+ if (endDatePickerRef.value && endDate.value) {
82
+ // On utilise validateOnSubmit pour forcer la validation complète
83
+ endDatePickerRef.value.validateOnSubmit()
84
+ }
85
+ }
86
+
87
+ // Fonction pour forcer la validation de la date de début quand la date de fin change
88
+ const validateStartDate = () => {
89
+ if (startDatePickerRef.value && startDate.value) {
90
+ // On utilise validateOnSubmit pour forcer la validation complète
91
+ startDatePickerRef.value.validateOnSubmit()
92
+ }
93
+ }
94
+
95
+ // Watcher pour la date de début qui force la revalidation de la date de fin
96
+ watch(startDate, () => {
97
+ // Laisser le temps au système de mettre à jour les valeurs
98
+ setTimeout(() => {
99
+ validateEndDate()
100
+ }, 0)
101
+ })
102
+
103
+ // Watcher pour la date de fin qui force la revalidation de la date de début
104
+ watch(endDate, () => {
105
+ // Laisser le temps au système de mettre à jour les valeurs
106
+ setTimeout(() => {
107
+ validateStartDate()
108
+ }, 0)
109
+ })
110
+
111
+ // Fonction pour définir des dates de test
112
+ const setTestDates = () => {
113
+ // Définir une date de début (aujourd'hui)
114
+ const today = new Date()
115
+ const day = String(today.getDate()).padStart(2, '0')
116
+ const month = String(today.getMonth() + 1).padStart(2, '0')
117
+ const year = today.getFullYear()
118
+ startDate.value = `${day}/${month}/${year}`
119
+
120
+ // Définir une date de fin (demain) - pour démontrer une validation valide
121
+ const tomorrow = new Date(today)
122
+ tomorrow.setDate(today.getDate() + 1)
123
+ const tomorrowDay = String(tomorrow.getDate()).padStart(2, '0')
124
+ const tomorrowMonth = String(tomorrow.getMonth() + 1).padStart(2, '0')
125
+ const tomorrowYear = tomorrow.getFullYear()
126
+ endDate.value = `${tomorrowDay}/${tomorrowMonth}/${tomorrowYear}`
127
+ }
128
+
129
+ // Fonction pour définir des dates invalides (date de fin avant date de début)
130
+ const setInvalidDates = () => {
131
+ // Définir une date de début (aujourd'hui)
132
+ const today = new Date()
133
+ const day = String(today.getDate()).padStart(2, '0')
134
+ const month = String(today.getMonth() + 1).padStart(2, '0')
135
+ const year = today.getFullYear()
136
+ startDate.value = `${day}/${month}/${year}`
137
+
138
+ // Définir une date de fin (hier) - pour démontrer une validation invalide
139
+ const yesterday = new Date(today)
140
+ yesterday.setDate(today.getDate() - 1)
141
+ const yesterdayDay = String(yesterday.getDate()).padStart(2, '0')
142
+ const yesterdayMonth = String(yesterday.getMonth() + 1).padStart(2, '0')
143
+ const yesterdayYear = yesterday.getFullYear()
144
+ endDate.value = `${yesterdayDay}/${yesterdayMonth}/${yesterdayYear}`
145
+ }
146
+
147
+ // Fonction pour réinitialiser les dates
148
+ const resetDates = () => {
149
+ startDate.value = null
150
+ endDate.value = null
151
+ }
152
+ </script>
153
+
154
+ <template>
155
+ <div class="date-picker-bidirectional-validation">
156
+ <h1 class="text-h5 mb-6">
157
+ Validation bidirectionnelle entre deux DatePickers
158
+ </h1>
159
+
160
+ <p class="text-body-2 mb-4">
161
+ Ce composant démontre la validation bidirectionnelle entre deux DatePickers. Les règles de validation sont appliquées dans les deux sens :
162
+ <ul>
163
+ <li>La date de fin doit être postérieure ou égale à la date de début</li>
164
+ <li>La date de début doit être antérieure ou égale à la date de fin</li>
165
+ <li>Lorsque la date de début change, la validation de la date de fin est mise à jour</li>
166
+ <li>Lorsque la date de fin change, la validation de la date de début est mise à jour</li>
167
+ </ul>
168
+ </p>
169
+
170
+ <div class="date-range-container mb-6">
171
+ <div class="date-picker-wrapper">
172
+ <h3 class="text-subtitle-1 mb-2">
173
+ Date de début
174
+ </h3>
175
+ <DatePicker
176
+ ref="startDatePickerRef"
177
+ v-model="startDate"
178
+ placeholder="Date de début"
179
+ :custom-rules="startDateRules"
180
+ required
181
+ @update:model-value="validateEndDate"
182
+ />
183
+ </div>
184
+ <div class="date-picker-wrapper">
185
+ <h3 class="text-subtitle-1 mb-2">
186
+ Date de fin
187
+ </h3>
188
+ <DatePicker
189
+ ref="endDatePickerRef"
190
+ v-model="endDate"
191
+ placeholder="Date de fin"
192
+ :custom-rules="endDateRules"
193
+ required
194
+ @update:model-value="validateStartDate"
195
+ />
196
+ </div>
197
+ </div>
198
+
199
+ <div class="actions mb-4">
200
+ <v-btn
201
+ size="small"
202
+ color="primary"
203
+ class="mr-2"
204
+ @click="resetDates"
205
+ >
206
+ Réinitialiser
207
+ </v-btn>
208
+
209
+ <v-btn
210
+ size="small"
211
+ color="success"
212
+ class="mr-2"
213
+ @click="setTestDates"
214
+ >
215
+ Dates valides
216
+ </v-btn>
217
+
218
+ <v-btn
219
+ size="small"
220
+ color="error"
221
+ @click="setInvalidDates"
222
+ >
223
+ Dates invalides
224
+ </v-btn>
225
+ </div>
226
+
227
+ <div class="current-values mt-4">
228
+ <p><strong>Date de début :</strong> {{ startDate || 'Non sélectionnée' }}</p>
229
+ <p><strong>Date de fin :</strong> {{ endDate || 'Non sélectionnée' }}</p>
230
+ </div>
231
+
232
+ <div class="mt-6 pa-4 bg-grey-lighten-4 rounded">
233
+ <h3 class="text-subtitle-1 mb-2">
234
+ Comment fonctionne la validation bidirectionnelle
235
+ </h3>
236
+ <p class="text-body-2">
237
+ La validation bidirectionnelle entre les DatePickers est implémentée grâce à des règles de validation personnalisées
238
+ qui vérifient la relation entre les deux dates. Chaque DatePicker a sa propre règle qui vérifie sa valeur par rapport à l'autre.
239
+ </p>
240
+ <p class="text-body-2 mt-2">
241
+ Lorsqu'une date change, un watcher déclenche la validation de l'autre DatePicker. Cela garantit que les messages d'erreur
242
+ sont toujours à jour, même lorsque les dates sont modifiées dans n'importe quel ordre.
243
+ </p>
244
+ <p class="text-body-2 mt-2">
245
+ Les messages d'erreur apparaissent directement dans les composants DatePicker, ce qui améliore l'expérience utilisateur
246
+ en fournissant un retour immédiat sur la validité des dates sélectionnées.
247
+ </p>
248
+ <p class="text-body-2 mt-2">
249
+ Vous trouverez le code source dans la story Bidirectional Validation.
250
+ </p>
251
+ </div>
252
+ </div>
253
+ </template>
254
+
255
+ <style scoped>
256
+ .date-picker-bidirectional-validation {
257
+ padding: 20px;
258
+ max-width: 800px;
259
+ margin: 0 auto;
260
+ }
261
+
262
+ .date-range-container {
263
+ display: flex;
264
+ gap: 20px;
265
+ }
266
+
267
+ .date-picker-wrapper {
268
+ flex: 1;
269
+ }
270
+
271
+ .actions {
272
+ display: flex;
273
+ flex-wrap: wrap;
274
+ gap: 8px;
275
+ }
276
+
277
+ .current-values {
278
+ padding: 15px;
279
+ background-color: #f5f5f5;
280
+ border-radius: 4px;
281
+ }
282
+ </style>