@cnamts/synapse 0.0.8-alpha → 0.0.9-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/design-system-v3.d.ts +584 -128
- package/dist/design-system-v3.js +4176 -2694
- package/dist/design-system-v3.umd.cjs +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/assets/settings.scss +1 -1
- package/src/components/ContextualMenu/Accessibilite.mdx +14 -0
- package/src/components/ContextualMenu/Accessibilite.stories.ts +191 -0
- package/src/components/ContextualMenu/AccessibiliteItems.ts +89 -0
- package/src/components/ContextualMenu/constants/ExpertiseLevelEnum.ts +4 -0
- package/src/components/Customs/SySelect/SySelect.stories.ts +7 -7
- package/src/components/Customs/SySelect/SySelect.vue +9 -4
- package/src/components/Customs/SySelect/tests/SySelect.spec.ts +2 -2
- package/src/components/Customs/SyTextField/SyTextField.stories.ts +187 -2
- package/src/components/Customs/SyTextField/SyTextField.vue +185 -16
- package/src/components/Customs/SyTextField/tests/SyTextField.spec.ts +2 -4
- package/src/components/Customs/SyTextField/tests/__snapshots__/SyTextField.spec.ts.snap +18 -16
- package/src/components/Customs/SyTextField/types.d.ts +2 -2
- package/src/components/DatePicker/DatePicker.mdx +191 -0
- package/src/components/DatePicker/DatePicker.stories.ts +787 -0
- package/src/components/DatePicker/DatePicker.vue +560 -0
- package/src/components/DatePicker/DateTextInput.vue +409 -0
- package/src/components/DatePicker/tests/DatePicker.spec.ts +266 -0
- package/src/components/DialogBox/DialogBox.stories.ts +1 -1
- package/src/components/ExternalLinks/Accessibilite.mdx +14 -0
- package/src/components/ExternalLinks/Accessibilite.stories.ts +191 -0
- package/src/components/ExternalLinks/AccessibiliteItems.ts +197 -0
- package/src/components/ExternalLinks/constants/ExpertiseLevelEnum.ts +4 -0
- package/src/components/ExternalLinks/tests/__snapshots__/ExternalLinks.spec.ts.snap +9 -9
- package/src/components/FileUpload/FileUpload.mdx +165 -0
- package/src/components/FileUpload/FileUpload.stories.ts +429 -0
- package/src/components/FileUpload/FileUpload.vue +195 -0
- package/src/components/FileUpload/FileUploadContent.vue +109 -0
- package/src/components/FileUpload/locales.ts +10 -0
- package/src/components/FileUpload/tests/FileUpload.spec.ts +332 -0
- package/src/components/FileUpload/tests/__snapshots__/FileUpload.spec.ts.snap +7 -0
- package/src/components/FileUpload/useFileDrop.ts +23 -0
- package/src/components/FileUpload/validateFiles.ts +39 -0
- package/src/components/NirField/NirField.stories.ts +1 -1
- package/src/components/NirField/NirField.vue +2 -1
- package/src/components/PasswordField/Accessibilite.mdx +14 -0
- package/src/components/PasswordField/Accessibilite.stories.ts +191 -0
- package/src/components/PasswordField/AccessibiliteItems.ts +184 -0
- package/src/components/PasswordField/PasswordField.vue +3 -3
- package/src/components/PasswordField/constants/ExpertiseLevelEnum.ts +4 -0
- package/src/components/PhoneField/PhoneField.vue +44 -60
- package/src/components/PhoneField/tests/PhoneField.spec.ts +0 -15
- package/src/components/RangeField/RangeField.mdx +54 -0
- package/src/components/RangeField/RangeField.stories.ts +189 -0
- package/src/components/RangeField/RangeField.vue +157 -0
- package/src/components/RangeField/RangeSlider/RangeSlider.vue +387 -0
- package/src/components/RangeField/RangeSlider/Tooltip/Tooltip.vue +64 -0
- package/src/components/RangeField/RangeSlider/tests/__snapshots__/rangeSlider.spec.ts.snap +27 -0
- package/src/components/RangeField/RangeSlider/tests/rangeSlider.spec.ts +100 -0
- package/src/components/RangeField/RangeSlider/tests/useDoubleSlider.spec.ts +246 -0
- package/src/components/RangeField/RangeSlider/tests/useMouseSlide.spec.ts +204 -0
- package/src/components/RangeField/RangeSlider/tests/useThumb.spec.ts +22 -0
- package/src/components/RangeField/RangeSlider/tests/useThumbKeyboard.spec.ts +233 -0
- package/src/components/RangeField/RangeSlider/tests/useTooltipsNudge.spec.ts +150 -0
- package/src/components/RangeField/RangeSlider/tests/useTrack.spec.ts +314 -0
- package/src/components/RangeField/RangeSlider/tests/vAnimateClick.spec.ts +32 -0
- package/src/components/RangeField/RangeSlider/types.ts +15 -0
- package/src/components/RangeField/RangeSlider/useMouseSlide.ts +109 -0
- package/src/components/RangeField/RangeSlider/useRangeSlider.ts +126 -0
- package/src/components/RangeField/RangeSlider/useThumb.ts +18 -0
- package/src/components/RangeField/RangeSlider/useThumbKeyboard.ts +84 -0
- package/src/components/RangeField/RangeSlider/useTooltipsNudge.ts +92 -0
- package/src/components/RangeField/RangeSlider/useTrack.ts +116 -0
- package/src/components/RangeField/RangeSlider/vAnimateClick.ts +19 -0
- package/src/components/RangeField/config.ts +7 -0
- package/src/components/RangeField/locales.ts +4 -0
- package/src/components/RangeField/tests/RangeField.spec.ts +224 -0
- package/src/components/RangeField/tests/__snapshots__/RangeField.spec.ts.snap +379 -0
- package/src/components/RatingPicker/EmotionPicker/EmotionPicker.vue +205 -0
- package/src/components/RatingPicker/EmotionPicker/locales.ts +3 -0
- package/src/components/RatingPicker/EmotionPicker/tests/EmotionPicker.spec.ts +104 -0
- package/src/components/RatingPicker/EmotionPicker/tests/__snapshots__/EmotionPicker.spec.ts.snap +66 -0
- package/src/components/RatingPicker/NumberPicker/NumberPicker.vue +159 -0
- package/src/components/RatingPicker/NumberPicker/locales.ts +4 -0
- package/src/components/RatingPicker/NumberPicker/tests/NumberPicker.spec.ts +73 -0
- package/src/components/RatingPicker/NumberPicker/tests/__snapshots__/NumberPicker.spec.ts.snap +105 -0
- package/src/components/RatingPicker/Rating.ts +45 -0
- package/src/components/RatingPicker/RatingPicker.mdx +56 -0
- package/src/components/RatingPicker/RatingPicker.stories.ts +515 -0
- package/src/components/RatingPicker/RatingPicker.vue +122 -0
- package/src/components/RatingPicker/StarsPicker/StarsPicker.vue +116 -0
- package/src/components/RatingPicker/StarsPicker/tests/StarsPicker.spec.ts +95 -0
- package/src/components/RatingPicker/StarsPicker/tests/__snapshots__/StarsPicker.spec.ts.snap +36 -0
- package/src/components/RatingPicker/locales.ts +3 -0
- package/src/components/RatingPicker/tests/Rating.spec.ts +104 -0
- package/src/components/RatingPicker/tests/RatingPicker.spec.ts +187 -0
- package/src/components/RatingPicker/tests/__snapshots__/RatingPicker.spec.ts.snap +108 -0
- package/src/components/SearchListField/SearchListField.mdx +74 -0
- package/src/components/SearchListField/SearchListField.stories.ts +126 -0
- package/src/components/SearchListField/SearchListField.vue +194 -0
- package/src/components/SearchListField/locales.ts +5 -0
- package/src/components/SearchListField/tests/SearchListField.spec.ts +323 -0
- package/src/components/SearchListField/types.d.ts +4 -0
- package/src/components/SelectBtnField/SelectBtnField.mdx +50 -0
- package/src/components/SelectBtnField/SelectBtnField.stories.ts +763 -0
- package/src/components/SelectBtnField/SelectBtnField.vue +283 -0
- package/src/components/SelectBtnField/config.ts +11 -0
- package/src/components/SelectBtnField/tests/SelectBtnField.spec.ts +327 -0
- package/src/components/SelectBtnField/tests/__snapshots__/SelectBtnField.spec.ts.snap +125 -0
- package/src/components/SelectBtnField/types.d.ts +11 -0
- package/src/components/index.ts +8 -1
- package/src/composables/rules/useFieldValidation.ts +172 -44
- package/src/designTokens/index.ts +3 -3
- package/src/stories/Fondamentaux/CustomisationEtThemes.mdx +52 -2
- package/src/utils/calcHumanFileSize/index.ts +12 -0
- package/src/utils/calcHumanFileSize/tests/calcHumanFileSize.spec.ts +21 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } 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 { useFieldValidation } from '@/composables/rules/useFieldValidation'
|
|
7
|
+
import type { RuleOptions } from '@/composables/rules/useFieldValidation'
|
|
8
|
+
|
|
9
|
+
type DateValue = string | [string, string]
|
|
10
|
+
type DateInput = string | string[]
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<{
|
|
13
|
+
modelValue?: DateInput
|
|
14
|
+
placeholder?: string
|
|
15
|
+
format?: string
|
|
16
|
+
dateFormatReturn?: string
|
|
17
|
+
isBirthDate?: boolean
|
|
18
|
+
showWeekNumber?: boolean
|
|
19
|
+
required?: boolean
|
|
20
|
+
displayRange?: boolean
|
|
21
|
+
displayIcon?: boolean
|
|
22
|
+
displayAppendIcon?: boolean
|
|
23
|
+
customRules?: { type: string, options: RuleOptions }[]
|
|
24
|
+
customWarningRules?: { type: string, options: RuleOptions }[]
|
|
25
|
+
isDisabled?: boolean
|
|
26
|
+
noIcon?: boolean
|
|
27
|
+
noCalendar?: boolean
|
|
28
|
+
isOutlined?: boolean
|
|
29
|
+
}>(), {
|
|
30
|
+
modelValue: undefined,
|
|
31
|
+
placeholder: 'Sélectionner une date',
|
|
32
|
+
format: 'DD/MM/YYYY',
|
|
33
|
+
dateFormatReturn: '',
|
|
34
|
+
isBirthDate: false,
|
|
35
|
+
showWeekNumber: false,
|
|
36
|
+
required: false,
|
|
37
|
+
displayRange: false,
|
|
38
|
+
displayIcon: true,
|
|
39
|
+
displayAppendIcon: false,
|
|
40
|
+
customRules: () => [],
|
|
41
|
+
customWarningRules: () => [],
|
|
42
|
+
isDisabled: false,
|
|
43
|
+
noIcon: false,
|
|
44
|
+
noCalendar: false,
|
|
45
|
+
isOutlined: true,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const emit = defineEmits<{
|
|
49
|
+
(e: 'update:model-value', value: DateValue): void
|
|
50
|
+
}>()
|
|
51
|
+
|
|
52
|
+
// Fonction pour parser les dates selon le format spécifié
|
|
53
|
+
const parseDate = (dateString: string): Date | null => {
|
|
54
|
+
if (!dateString) return null
|
|
55
|
+
|
|
56
|
+
// Créer un mapping des positions des éléments de date selon le format
|
|
57
|
+
const format = props.format || 'DD/MM/YYYY'
|
|
58
|
+
const separator = format.includes('/') ? '/' : format.includes('-') ? '-' : '.'
|
|
59
|
+
const parts = format.split(separator)
|
|
60
|
+
const dateParts = dateString.split(separator)
|
|
61
|
+
|
|
62
|
+
if (parts.length !== dateParts.length) return null
|
|
63
|
+
|
|
64
|
+
let day = '', month = '', year = ''
|
|
65
|
+
|
|
66
|
+
// Extraire les valeurs selon leur position dans le format
|
|
67
|
+
parts.forEach((part, index) => {
|
|
68
|
+
const value = dateParts[index]
|
|
69
|
+
if (part.includes('DD')) day = value
|
|
70
|
+
else if (part.includes('MM')) month = value
|
|
71
|
+
else if (part.includes('YYYY')) year = value
|
|
72
|
+
else if (part.includes('YY')) year = '20' + value // Assumons que nous sommes au 21ème siècle
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Vérifier que nous avons toutes les parties nécessaires
|
|
76
|
+
if (!day || !month || !year) return null
|
|
77
|
+
|
|
78
|
+
const date = new Date(`${year}-${month}-${day}`)
|
|
79
|
+
return isNaN(date.getTime()) ? null : date
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function initializeSelectedDates(
|
|
83
|
+
modelValue: DateInput | null,
|
|
84
|
+
): Date | Date[] | null {
|
|
85
|
+
if (!modelValue) return null
|
|
86
|
+
|
|
87
|
+
if (Array.isArray(modelValue)) {
|
|
88
|
+
if (modelValue.length >= 2) {
|
|
89
|
+
const dates = [parseDate(modelValue[0]), parseDate(modelValue[1])]
|
|
90
|
+
// Vérifie si l'une des dates est invalide
|
|
91
|
+
if (dates.some(date => date === null)) {
|
|
92
|
+
return []
|
|
93
|
+
}
|
|
94
|
+
// Vérifie si la première date est après la seconde
|
|
95
|
+
if (dates[0] && dates[1] && dates[0] > dates[1]) {
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
// Filtrer les dates nulles et convertir en tableau de Date
|
|
99
|
+
return dates.filter((date): date is Date => date !== null)
|
|
100
|
+
}
|
|
101
|
+
if (modelValue.length === 1) {
|
|
102
|
+
const date = parseDate(modelValue[0])
|
|
103
|
+
return date === null ? [] : [date]
|
|
104
|
+
}
|
|
105
|
+
return []
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const date = parseDate(modelValue)
|
|
109
|
+
return date === null ? null : date
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Utilisation de la fonction pour initialiser `selectedDates`
|
|
113
|
+
const selectedDates = ref<Date | Date[] | null>(
|
|
114
|
+
initializeSelectedDates(props.modelValue as DateInput | null),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const isDatePickerVisible = ref(false)
|
|
118
|
+
const errorMessages = ref<string[]>([])
|
|
119
|
+
const successMessages = ref<string[]>([])
|
|
120
|
+
const warningMessages = ref<string[]>([])
|
|
121
|
+
const displayFormattedDate = ref('')
|
|
122
|
+
|
|
123
|
+
const getMessageClasses = () => ({
|
|
124
|
+
'dp-width': true,
|
|
125
|
+
'v-messages__message--success': successMessages.value.length > 0,
|
|
126
|
+
'v-messages__message--error': errorMessages.value.length > 0,
|
|
127
|
+
'v-messages__message--warning': warningMessages.value.length > 0 && errorMessages.value.length < 1,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Formate une date unique au format spécifié
|
|
131
|
+
const formatDate = (date: Date, format: string): string => {
|
|
132
|
+
if (!date) return ''
|
|
133
|
+
const day = date.getDate().toString().padStart(2, '0')
|
|
134
|
+
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
135
|
+
const year = date.getFullYear().toString()
|
|
136
|
+
const shortYear = year.slice(-2)
|
|
137
|
+
return format.replace('YYYY', year).replace('YY', shortYear).replace('MM', month).replace('DD', day)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Date(s) formatée(s) en chaîne de caractères pour la valeur de retour
|
|
141
|
+
const formattedDate = computed<DateValue>(() => {
|
|
142
|
+
if (!selectedDates.value) return ''
|
|
143
|
+
|
|
144
|
+
const returnFormat = props.dateFormatReturn || props.format
|
|
145
|
+
|
|
146
|
+
if (Array.isArray(selectedDates.value)) {
|
|
147
|
+
if (selectedDates.value.length >= 2) {
|
|
148
|
+
return [
|
|
149
|
+
formatDate(selectedDates.value[0], returnFormat),
|
|
150
|
+
formatDate(selectedDates.value[1], returnFormat),
|
|
151
|
+
] as [string, string]
|
|
152
|
+
}
|
|
153
|
+
return ''
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return formatDate(selectedDates.value, returnFormat)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// Date(s) formatée(s) en chaîne de caractères pour l'affichage
|
|
160
|
+
const displayFormattedDateComputed = computed(() => {
|
|
161
|
+
if (!selectedDates.value) return null
|
|
162
|
+
|
|
163
|
+
if (Array.isArray(selectedDates.value)) {
|
|
164
|
+
if (selectedDates.value.length >= 2) {
|
|
165
|
+
return `${formatDate(selectedDates.value[0], props.format)} - ${formatDate(
|
|
166
|
+
selectedDates.value[selectedDates.value.length - 1],
|
|
167
|
+
props.format,
|
|
168
|
+
)}`
|
|
169
|
+
}
|
|
170
|
+
return formatDate(selectedDates.value[0], props.format)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return formatDate(selectedDates.value, props.format)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const validateDateValue = (value: string | string[]): DateValue => {
|
|
177
|
+
if (Array.isArray(value)) {
|
|
178
|
+
if (value.length >= 2) {
|
|
179
|
+
return [value[0], value[1]] as [string, string]
|
|
180
|
+
}
|
|
181
|
+
return value[0] || ''
|
|
182
|
+
}
|
|
183
|
+
return value
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
watch(formattedDate, (newValue) => {
|
|
187
|
+
const validValue = validateDateValue(newValue)
|
|
188
|
+
emit('update:model-value', validValue)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
watch(displayFormattedDateComputed, (newValue) => {
|
|
192
|
+
if (!props.noCalendar && newValue) {
|
|
193
|
+
displayFormattedDate.value = newValue
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const updateSelectedDates = (input: DateValue) => {
|
|
198
|
+
if (Array.isArray(input)) {
|
|
199
|
+
const dates = input
|
|
200
|
+
.map(date => (date ? parseDate(date) : null))
|
|
201
|
+
.filter((date): date is Date => date !== null)
|
|
202
|
+
|
|
203
|
+
if (dates.length === 0) {
|
|
204
|
+
selectedDates.value = null
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
selectedDates.value = dates
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const date = input ? parseDate(input) : null
|
|
213
|
+
selectedDates.value = date === null ? null : date
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
watch(selectedDates, (newValue) => {
|
|
217
|
+
validateDates()
|
|
218
|
+
if (props.displayRange) {
|
|
219
|
+
if (Array.isArray(newValue) && newValue.length >= 2) {
|
|
220
|
+
isDatePickerVisible.value = false
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
isDatePickerVisible.value = false
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// Gestionnaire de clic en dehors
|
|
229
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
230
|
+
const target = event.target as HTMLElement
|
|
231
|
+
if (!target.closest('.date-picker-container')) {
|
|
232
|
+
isDatePickerVisible.value = false
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const todayInString = computed(() => {
|
|
237
|
+
return (new Date().toLocaleDateString('fr-FR', {
|
|
238
|
+
weekday: 'long',
|
|
239
|
+
month: 'long',
|
|
240
|
+
day: 'numeric',
|
|
241
|
+
})).replace(/\b\w/g, l => l.toUpperCase())
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
onMounted(() => {
|
|
245
|
+
document.addEventListener('click', handleClickOutside)
|
|
246
|
+
if (selectedDates.value !== null) {
|
|
247
|
+
validateDates()
|
|
248
|
+
// Force format application on mount
|
|
249
|
+
emit('update:model-value', formattedDate.value)
|
|
250
|
+
}
|
|
251
|
+
if (displayFormattedDateComputed.value) {
|
|
252
|
+
displayFormattedDate.value = displayFormattedDateComputed.value
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
onBeforeUnmount(() => {
|
|
257
|
+
document.removeEventListener('click', handleClickOutside)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const dateTextInputRef = ref()
|
|
261
|
+
|
|
262
|
+
const validateOnSubmit = () => {
|
|
263
|
+
if (props.noCalendar) {
|
|
264
|
+
return dateTextInputRef.value?.validateOnSubmit()
|
|
265
|
+
}
|
|
266
|
+
validateDates()
|
|
267
|
+
return errorMessages.value.length === 0
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
defineExpose({
|
|
271
|
+
validateOnSubmit,
|
|
272
|
+
isDatePickerVisible,
|
|
273
|
+
selectedDates,
|
|
274
|
+
errorMessages,
|
|
275
|
+
handleClickOutside,
|
|
276
|
+
initializeSelectedDates,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// les btns du date picker ne sont pas accessibles, on les rend accessibles
|
|
280
|
+
watch(isDatePickerVisible, async (newValue) => {
|
|
281
|
+
if (newValue) {
|
|
282
|
+
await nextTick()
|
|
283
|
+
const arrowDown = document.querySelector('.v-btn.v-btn--icon.v-theme--light.v-btn--density-comfortable.v-btn--size-default.v-btn--variant-text.v-date-picker-controls__mode-btn')
|
|
284
|
+
const arrowLeftButtons = document.querySelectorAll('.v-btn.v-btn--icon.v-theme--light.v-btn--density-default.v-btn--size-default.v-btn--variant-text')
|
|
285
|
+
|
|
286
|
+
if (arrowDown) {
|
|
287
|
+
arrowDown.setAttribute('aria-label', 'Fleche vers le bas')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
arrowLeftButtons.forEach((button, index) => {
|
|
291
|
+
if (index === 0) {
|
|
292
|
+
button.setAttribute('aria-label', 'Fleche vers la gauche')
|
|
293
|
+
}
|
|
294
|
+
else if (index === 1) {
|
|
295
|
+
button.setAttribute('aria-label', 'Fleche vers la droite')
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const handlePrependIconClick = () => {
|
|
302
|
+
isDatePickerVisible.value = true
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const handleAppendIconClick = () => {
|
|
306
|
+
isDatePickerVisible.value = true
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
type Rule = { type: string, options: RuleOptions }
|
|
310
|
+
|
|
311
|
+
const customRules = ref<Rule[]>(props.customRules || [])
|
|
312
|
+
const customWarningRules = ref<Rule[]>(props.customWarningRules || [])
|
|
313
|
+
|
|
314
|
+
const { generateRules } = useFieldValidation()
|
|
315
|
+
const validationRules = generateRules(customRules.value)
|
|
316
|
+
const warningValidationRules = generateRules(customWarningRules.value)
|
|
317
|
+
|
|
318
|
+
const validateDates = () => {
|
|
319
|
+
errorMessages.value = []
|
|
320
|
+
successMessages.value = []
|
|
321
|
+
warningMessages.value = []
|
|
322
|
+
|
|
323
|
+
const addMessages = (dates, rules) => {
|
|
324
|
+
dates.forEach((date) => {
|
|
325
|
+
rules.forEach((rule) => {
|
|
326
|
+
const result = rule(date)
|
|
327
|
+
if (result?.error) {
|
|
328
|
+
errorMessages.value.push(result.error)
|
|
329
|
+
errorMessages.value = [...new Set(errorMessages.value)]
|
|
330
|
+
}
|
|
331
|
+
else if (result?.warning) {
|
|
332
|
+
warningMessages.value.push(result.warning)
|
|
333
|
+
warningMessages.value = [...new Set(warningMessages.value)]
|
|
334
|
+
}
|
|
335
|
+
else if (result?.success) {
|
|
336
|
+
successMessages.value.push(result.success)
|
|
337
|
+
successMessages.value = [...new Set(successMessages.value)]
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const handleValidation = (dates) => {
|
|
344
|
+
if (Array.isArray(dates) && dates.length > 1) {
|
|
345
|
+
// Pour une plage, on ne vérifie que le premier et le dernier jour
|
|
346
|
+
const [firstDate, ...rest] = dates
|
|
347
|
+
const lastDate = rest[rest.length - 1]
|
|
348
|
+
const datesToValidate = [firstDate, lastDate]
|
|
349
|
+
|
|
350
|
+
// Validation des règles
|
|
351
|
+
addMessages(datesToValidate, validationRules)
|
|
352
|
+
addMessages(datesToValidate, warningValidationRules)
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// Pour une date unique, on valide normalement
|
|
356
|
+
const datesToValidate = Array.isArray(dates) ? dates : [dates]
|
|
357
|
+
addMessages(datesToValidate, validationRules)
|
|
358
|
+
addMessages(datesToValidate, warningValidationRules)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (
|
|
363
|
+
props.required
|
|
364
|
+
&& (!selectedDates.value || (Array.isArray(selectedDates.value) && selectedDates.value.length === 0))
|
|
365
|
+
) {
|
|
366
|
+
errorMessages.value.push('La date est requise.')
|
|
367
|
+
}
|
|
368
|
+
else if (selectedDates.value) {
|
|
369
|
+
handleValidation(Array.isArray(selectedDates.value) ? selectedDates.value : [selectedDates.value])
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const getIcon = () => {
|
|
374
|
+
if (props.noCalendar) {
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
switch (true) {
|
|
378
|
+
case errorMessages.value.length > 0:
|
|
379
|
+
return 'error'
|
|
380
|
+
case warningMessages.value.length > 0:
|
|
381
|
+
return 'warning'
|
|
382
|
+
case successMessages.value.length > 0:
|
|
383
|
+
return 'success'
|
|
384
|
+
default:
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
</script>
|
|
389
|
+
|
|
390
|
+
<template>
|
|
391
|
+
<div class="date-picker-container">
|
|
392
|
+
<template v-if="props.noCalendar">
|
|
393
|
+
<DateTextInput
|
|
394
|
+
ref="dateTextInputRef"
|
|
395
|
+
v-model="displayFormattedDate"
|
|
396
|
+
:class="[getMessageClasses(), 'label-hidden-on-focus']"
|
|
397
|
+
:date-format-return="props.dateFormatReturn"
|
|
398
|
+
:format="props.format"
|
|
399
|
+
:label="props.placeholder"
|
|
400
|
+
:placeholder="props.placeholder"
|
|
401
|
+
:range="props.displayRange"
|
|
402
|
+
:required="props.required"
|
|
403
|
+
:rules="props.customRules"
|
|
404
|
+
:warning-rules="props.customWarningRules"
|
|
405
|
+
title="Date text input"
|
|
406
|
+
@update:model-value="updateSelectedDates"
|
|
407
|
+
/>
|
|
408
|
+
</template>
|
|
409
|
+
<template v-else>
|
|
410
|
+
<SyTextField
|
|
411
|
+
v-model="displayFormattedDate"
|
|
412
|
+
:append-icon="displayIcon && displayAppendIcon ? 'calendar' : undefined"
|
|
413
|
+
:append-inner-icon="getIcon()"
|
|
414
|
+
:class="[getMessageClasses(), 'label-hidden-on-focus']"
|
|
415
|
+
:error-messages="errorMessages"
|
|
416
|
+
:is-disabled="props.isDisabled"
|
|
417
|
+
:is-read-only="true"
|
|
418
|
+
:label="props.placeholder"
|
|
419
|
+
:messages="[...successMessages, ...warningMessages]"
|
|
420
|
+
:no-icon="props.noIcon"
|
|
421
|
+
:prepend-icon="displayIcon && !displayAppendIcon ? 'calendar' : undefined"
|
|
422
|
+
:variant-style="props.isOutlined ? 'outlined' : 'underlined'"
|
|
423
|
+
color="primary"
|
|
424
|
+
is-clearable
|
|
425
|
+
title="Date Picker"
|
|
426
|
+
@focus="isDatePickerVisible = true"
|
|
427
|
+
@update:model-value="updateSelectedDates"
|
|
428
|
+
@prepend-icon-click="handlePrependIconClick"
|
|
429
|
+
@append-icon-click="handleAppendIconClick"
|
|
430
|
+
/>
|
|
431
|
+
</template>
|
|
432
|
+
<transition name="fade">
|
|
433
|
+
<v-locale-provider locale="fr">
|
|
434
|
+
<VDatePicker
|
|
435
|
+
v-if="isDatePickerVisible && !props.noCalendar"
|
|
436
|
+
v-model="selectedDates"
|
|
437
|
+
:first-day-of-week="1"
|
|
438
|
+
:multiple="props.displayRange ? 'range' : false"
|
|
439
|
+
:show-adjacent-months="true"
|
|
440
|
+
:show-week="props.showWeekNumber"
|
|
441
|
+
:view-mode="props.isBirthDate ? 'year' : 'month'"
|
|
442
|
+
color="primary"
|
|
443
|
+
>
|
|
444
|
+
<template #title>
|
|
445
|
+
Selectionnez une date
|
|
446
|
+
</template>
|
|
447
|
+
<template #header>
|
|
448
|
+
<h3 class="mx-auto my-auto ml-5 mb-4">
|
|
449
|
+
{{ todayInString }}
|
|
450
|
+
</h3>
|
|
451
|
+
</template>
|
|
452
|
+
</VDatePicker>
|
|
453
|
+
</v-locale-provider>
|
|
454
|
+
</transition>
|
|
455
|
+
</div>
|
|
456
|
+
</template>
|
|
457
|
+
|
|
458
|
+
<style lang="scss" scoped>
|
|
459
|
+
@use '@/assets/tokens';
|
|
460
|
+
|
|
461
|
+
.label-hidden-on-focus:focus + label {
|
|
462
|
+
display: none;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.dp-width {
|
|
466
|
+
min-width: 345px;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.v-messages__message--success {
|
|
470
|
+
:deep(.v-input__control),
|
|
471
|
+
:deep(.v-messages__message) {
|
|
472
|
+
color: tokens.$colors-text-success !important;
|
|
473
|
+
|
|
474
|
+
--v-medium-emphasis-opacity: 1;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.v-field--active & {
|
|
478
|
+
color: tokens.$colors-border-success !important;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.v-messages__message--error {
|
|
483
|
+
:deep(.v-input__control),
|
|
484
|
+
:deep(.v-messages__message) {
|
|
485
|
+
color: tokens.$colors-text-error !important;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.v-field--active & {
|
|
489
|
+
color: tokens.$colors-border-error !important;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.v-messages__message--warning {
|
|
494
|
+
:deep(.v-input__control) {
|
|
495
|
+
color: tokens.$colors-text-warning !important;
|
|
496
|
+
|
|
497
|
+
--v-medium-emphasis-opacity: 1;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
:deep(.v-messages__message) {
|
|
501
|
+
color: tokens.$colors-text-warning !important;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.v-field--active & {
|
|
505
|
+
color: tokens.$colors-text-warning !important;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
:deep(.v-btn__content) {
|
|
510
|
+
font-size: tokens.$font-size-body-text + 3;
|
|
511
|
+
font-weight: bold;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
:deep(.v-messages) {
|
|
515
|
+
opacity: 1;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
:deep(.v-field--dirty) {
|
|
519
|
+
opacity: 1 !important;
|
|
520
|
+
|
|
521
|
+
--v-medium-emphasis-opacity: 1;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
:deep(.v-field--focused) {
|
|
525
|
+
opacity: 1 !important;
|
|
526
|
+
|
|
527
|
+
--v-medium-emphasis-opacity: 1;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.date-picker-container {
|
|
531
|
+
max-width: 345px;
|
|
532
|
+
position: relative;
|
|
533
|
+
|
|
534
|
+
:deep(.v-date-picker) {
|
|
535
|
+
width: 345px;
|
|
536
|
+
position: absolute;
|
|
537
|
+
top: 56px;
|
|
538
|
+
left: 0;
|
|
539
|
+
z-index: 2;
|
|
540
|
+
box-shadow:
|
|
541
|
+
0 5px 5px -3px rgb(0 0 0 / 20%),
|
|
542
|
+
0 8px 10px 1px rgb(0 0 0 / 14%),
|
|
543
|
+
0 3px 14px 2px rgb(0 0 0 / 12%) !important;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
:deep(.v-date-picker-month__day--selected, .v-date-picker-month__day--adjacent) {
|
|
548
|
+
opacity: 1;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.fade-enter-active,
|
|
552
|
+
.fade-leave-active {
|
|
553
|
+
transition: opacity 0.5s ease;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.fade-enter-from,
|
|
557
|
+
.fade-leave-to {
|
|
558
|
+
opacity: 0;
|
|
559
|
+
}
|
|
560
|
+
</style>
|