@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,195 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useWidthable, type Widthable } from '@/composables/widthable'
|
|
3
|
+
import { ref, useId, watch } from 'vue'
|
|
4
|
+
import { useTheme } from 'vuetify'
|
|
5
|
+
import FileUploadContent, { type FileUploadContentSlots } from './FileUploadContent.vue'
|
|
6
|
+
import { locales as defaultLocales } from './locales'
|
|
7
|
+
import useFileDrop from './useFileDrop'
|
|
8
|
+
import validateFiles from './validateFiles'
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<{
|
|
11
|
+
modelValue: File[]
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
multiple?: boolean
|
|
14
|
+
fileSizeMax?: number
|
|
15
|
+
fileSizeUnits?: Array<string>
|
|
16
|
+
allowedExtensions?: Array<string>
|
|
17
|
+
locales?: typeof defaultLocales
|
|
18
|
+
} & Widthable>(), {
|
|
19
|
+
disabled: false,
|
|
20
|
+
multiple: false,
|
|
21
|
+
fileSizeMax: 10_485_760,
|
|
22
|
+
fileSizeUnits: () => defaultLocales.fileSizeUnits,
|
|
23
|
+
allowedExtensions: () => ['pdf', 'jpg', 'jpeg', 'png'],
|
|
24
|
+
locales: () => defaultLocales,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const emits = defineEmits<{
|
|
28
|
+
(e: 'update:modelValue', value: File[]): void
|
|
29
|
+
(e: 'error', value: string[]): void
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
defineSlots<{
|
|
33
|
+
default(): void
|
|
34
|
+
} & FileUploadContentSlots>()
|
|
35
|
+
|
|
36
|
+
const dragover = ref(false)
|
|
37
|
+
const id = 'file-upload-' + useId()
|
|
38
|
+
const dropZone = ref<HTMLElement | null>(null)
|
|
39
|
+
const fileInput = ref<HTMLInputElement | null>(null)
|
|
40
|
+
const fileList = ref<File[]>([])
|
|
41
|
+
const isDarkMode = useTheme().current.value.dark
|
|
42
|
+
|
|
43
|
+
defineExpose({
|
|
44
|
+
fileInput,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
watch(() => props.modelValue, (value) => {
|
|
48
|
+
fileList.value = value
|
|
49
|
+
}, {
|
|
50
|
+
immediate: true,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
watch(fileList, (value: File[]) => {
|
|
54
|
+
emits('update:modelValue', value)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const { widthStyles } = useWidthable(props)
|
|
58
|
+
|
|
59
|
+
function newFiles(files: File[]) {
|
|
60
|
+
if (props.disabled) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
if (!props.multiple) {
|
|
64
|
+
files = files.slice(0, 1)
|
|
65
|
+
}
|
|
66
|
+
const { errors, validFiles } = validateFiles(
|
|
67
|
+
files, props.fileSizeMax, props.allowedExtensions, props.fileSizeUnits,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if (errors.length) {
|
|
71
|
+
emits('error', errors)
|
|
72
|
+
}
|
|
73
|
+
if (props.multiple) {
|
|
74
|
+
fileList.value = [...fileList.value, ...validFiles]
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
fileList.value = validFiles
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
useFileDrop(
|
|
82
|
+
dropZone,
|
|
83
|
+
newFiles,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
function onFileChange(e: Event) {
|
|
87
|
+
const files = (e.target as HTMLInputElement).files
|
|
88
|
+
if (!files) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
newFiles(Array.from(files))
|
|
92
|
+
;(e.target as HTMLInputElement).value = ''
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
|
|
96
|
+
<template>
|
|
97
|
+
<label
|
|
98
|
+
ref="dropZone"
|
|
99
|
+
:for="id"
|
|
100
|
+
:class="[
|
|
101
|
+
{
|
|
102
|
+
dragover: dragover,
|
|
103
|
+
'sy-file-upload--disabled': disabled,
|
|
104
|
+
'dark-mode': isDarkMode,
|
|
105
|
+
},
|
|
106
|
+
]"
|
|
107
|
+
:style="widthStyles"
|
|
108
|
+
class="sy-file-upload d-block pa-4"
|
|
109
|
+
@dragover.prevent="dragover = true"
|
|
110
|
+
@dragleave="dragover = false"
|
|
111
|
+
>
|
|
112
|
+
<input
|
|
113
|
+
:id="id"
|
|
114
|
+
ref="fileInput"
|
|
115
|
+
type="file"
|
|
116
|
+
:disabled="disabled"
|
|
117
|
+
:multiple="multiple"
|
|
118
|
+
:accept="allowedExtensions.map(el=>`.${el}`).join(', ')"
|
|
119
|
+
class="sy-file-upload-input"
|
|
120
|
+
@change="onFileChange"
|
|
121
|
+
>
|
|
122
|
+
<slot>
|
|
123
|
+
<FileUploadContent
|
|
124
|
+
:allowed-extensions="allowedExtensions"
|
|
125
|
+
:multiple="multiple"
|
|
126
|
+
:file-size-max="fileSizeMax"
|
|
127
|
+
:file-size-units="fileSizeUnits"
|
|
128
|
+
>
|
|
129
|
+
<template
|
|
130
|
+
v-for="(_, slotName) in $slots"
|
|
131
|
+
#[slotName]="slotProps"
|
|
132
|
+
>
|
|
133
|
+
<slot
|
|
134
|
+
:name="slotName"
|
|
135
|
+
v-bind="slotProps || {}"
|
|
136
|
+
/>
|
|
137
|
+
</template>
|
|
138
|
+
</FileUploadContent>
|
|
139
|
+
</slot>
|
|
140
|
+
</label>
|
|
141
|
+
</template>
|
|
142
|
+
|
|
143
|
+
<style lang="scss" scoped>
|
|
144
|
+
@use '@/assets/tokens';
|
|
145
|
+
|
|
146
|
+
.sy-file-upload {
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
position: relative;
|
|
149
|
+
border: 1px dashed tokens.$colors-border-accent;
|
|
150
|
+
border-radius: tokens.$radius-rounded-lg;
|
|
151
|
+
transition: background 0.25s;
|
|
152
|
+
|
|
153
|
+
&:hover,
|
|
154
|
+
&:focus-within,
|
|
155
|
+
&.dragover {
|
|
156
|
+
background: tokens.$colors-background-surface-alt;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
&.dark-mode {
|
|
160
|
+
&:hover,
|
|
161
|
+
&:focus-within,
|
|
162
|
+
&.dragover {
|
|
163
|
+
background: #303030;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
&.sy-file-upload--disabled {
|
|
168
|
+
opacity: 0.5;
|
|
169
|
+
cursor: default;
|
|
170
|
+
|
|
171
|
+
&:hover,
|
|
172
|
+
&:focus-within,
|
|
173
|
+
&.dragover {
|
|
174
|
+
background: inherit;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
&.sy-file-upload--disabled.dark-mode {
|
|
179
|
+
&:hover,
|
|
180
|
+
&:focus-within,
|
|
181
|
+
&.dragover {
|
|
182
|
+
background: #303030;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.sy-file-upload-input {
|
|
188
|
+
position: absolute;
|
|
189
|
+
width: 1px;
|
|
190
|
+
height: 1px;
|
|
191
|
+
white-space: nowrap;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
clip: rect(1px, 1px, 1px, 1px);
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { calcHumanFileSize } from '@/utils/calcHumanFileSize'
|
|
3
|
+
import { locales } from './locales'
|
|
4
|
+
import { mdiCloudUpload } from '@mdi/js'
|
|
5
|
+
import { computed } from 'vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
allowedExtensions: string[]
|
|
9
|
+
fileSizeUnits: Array<string>
|
|
10
|
+
fileSizeMax: number
|
|
11
|
+
multiple: boolean
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
export interface FileUploadContentSlots {
|
|
15
|
+
'icon'(): void
|
|
16
|
+
'action-text'(): void
|
|
17
|
+
'or'(): void
|
|
18
|
+
'button-text'(): void
|
|
19
|
+
'info-text'(): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
defineSlots<FileUploadContentSlots>()
|
|
23
|
+
|
|
24
|
+
const maxSizeReadable = computed(() => {
|
|
25
|
+
return calcHumanFileSize(props.fileSizeMax, props.fileSizeUnits)
|
|
26
|
+
})
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<span class="sy-file-upload-placeholder">
|
|
31
|
+
<slot
|
|
32
|
+
name="icon"
|
|
33
|
+
>
|
|
34
|
+
<VIcon
|
|
35
|
+
size="40"
|
|
36
|
+
color="primary"
|
|
37
|
+
>
|
|
38
|
+
{{ mdiCloudUpload }}
|
|
39
|
+
</VIcon>
|
|
40
|
+
</slot>
|
|
41
|
+
|
|
42
|
+
<span
|
|
43
|
+
class="mt-1 font-weight-medium text-black"
|
|
44
|
+
>
|
|
45
|
+
<slot
|
|
46
|
+
name="action-text"
|
|
47
|
+
:multiple="multiple"
|
|
48
|
+
>
|
|
49
|
+
<span>{{ locales.dropFilesHere(multiple) }}</span>
|
|
50
|
+
</slot>
|
|
51
|
+
</span>
|
|
52
|
+
|
|
53
|
+
<span
|
|
54
|
+
class="mb-2 sy-file-upload-caption"
|
|
55
|
+
>
|
|
56
|
+
<slot name="or">
|
|
57
|
+
{{ locales.or }}
|
|
58
|
+
</slot>
|
|
59
|
+
</span>
|
|
60
|
+
|
|
61
|
+
<span
|
|
62
|
+
class="sy-file-upload-btn bg-primary text-white elevation-2"
|
|
63
|
+
>
|
|
64
|
+
<slot name="button-text">
|
|
65
|
+
{{ locales.chooseFile(multiple) }}
|
|
66
|
+
</slot>
|
|
67
|
+
</span>
|
|
68
|
+
|
|
69
|
+
<span
|
|
70
|
+
class="mt-4 sy-file-upload-caption"
|
|
71
|
+
>
|
|
72
|
+
<slot
|
|
73
|
+
name="info-text"
|
|
74
|
+
:max-size="maxSizeReadable"
|
|
75
|
+
:extensions="allowedExtensions"
|
|
76
|
+
>
|
|
77
|
+
{{
|
|
78
|
+
locales.infoText(
|
|
79
|
+
maxSizeReadable,
|
|
80
|
+
allowedExtensions,
|
|
81
|
+
)
|
|
82
|
+
}}
|
|
83
|
+
</slot>
|
|
84
|
+
</span>
|
|
85
|
+
</span>
|
|
86
|
+
</template>
|
|
87
|
+
|
|
88
|
+
<style lang="scss" scoped>
|
|
89
|
+
@use '@/assets/tokens';
|
|
90
|
+
|
|
91
|
+
.sy-file-upload-placeholder {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.sy-file-upload-caption {
|
|
98
|
+
font-size: 0.875rem;
|
|
99
|
+
color: tokens.$colors-text-subdued;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.sy-file-upload-btn {
|
|
103
|
+
border-radius: tokens.$radius-rounded;
|
|
104
|
+
transition: background 0.25s;
|
|
105
|
+
font-weight: 700 !important;
|
|
106
|
+
font-size: tokens.$heading-3-font-size;
|
|
107
|
+
padding: 10px 16px;
|
|
108
|
+
}
|
|
109
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const locales = {
|
|
2
|
+
or: 'Ou',
|
|
3
|
+
chooseFile: (multiple: boolean) => multiple ? 'Choisir des fichiers' : 'Choisir un fichier',
|
|
4
|
+
infoText: (max: string, ext: string[]): string =>
|
|
5
|
+
`Taille max. : ${max}. ${ext.length === 1 ? 'Format accepté' : 'Formats acceptés'} : ${ext.join(', ')}`,
|
|
6
|
+
fileSizeUnits: ['o', 'Ko', 'Mo', 'Go', 'To'],
|
|
7
|
+
dropFilesHere: (multiple: boolean): string => (!multiple ? 'Déposer votre fichier ici' : 'Déposer vos fichiers ici'),
|
|
8
|
+
errorSize: (fileName: string, max: string): string => `Le fichier ${fileName} est trop volumineux. Taille max. : ${max}`,
|
|
9
|
+
errorExtension: (fileName: string, ext: string[]): string => `Le fichier ${fileName} a une extension invalide. Extensions acceptées : ${ext.join(', ')}`,
|
|
10
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { vuetify } from '@tests/unit/setup'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import FileUpload from '../FileUpload.vue'
|
|
5
|
+
|
|
6
|
+
const file: File = new File([''], 'filename', { type: 'text/html' })
|
|
7
|
+
|
|
8
|
+
describe('FileUpload', () => {
|
|
9
|
+
const consoleMock = vi
|
|
10
|
+
.spyOn(console, 'warn')
|
|
11
|
+
.mockImplementation(() => undefined)
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
consoleMock.mockReset()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('renders correctly', () => {
|
|
18
|
+
const wrapper = mount(FileUpload, {
|
|
19
|
+
global: {
|
|
20
|
+
plugins: [vuetify],
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(wrapper.text()).toMatchSnapshot()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('renders correctly in multiple mode', () => {
|
|
28
|
+
const wrapper = mount(FileUpload, {
|
|
29
|
+
global: {
|
|
30
|
+
plugins: [vuetify],
|
|
31
|
+
},
|
|
32
|
+
propsData: {
|
|
33
|
+
multiple: true,
|
|
34
|
+
modelValue: [file, file],
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(wrapper.text()).toMatchSnapshot()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('renders correctly with only one extension allowed', () => {
|
|
42
|
+
const wrapper = mount(FileUpload, {
|
|
43
|
+
global: {
|
|
44
|
+
plugins: [vuetify],
|
|
45
|
+
},
|
|
46
|
+
propsData: {
|
|
47
|
+
allowedExtensions: ['pdf'],
|
|
48
|
+
modelValue: [file],
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(wrapper.text()).toMatchSnapshot()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('change the style when dragging', async () => {
|
|
56
|
+
const wrapper = mount(FileUpload, {
|
|
57
|
+
global: {
|
|
58
|
+
plugins: [vuetify],
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const label = wrapper.find('label')
|
|
63
|
+
const labelClasses = label.classes()
|
|
64
|
+
|
|
65
|
+
await wrapper.find('input').trigger('dragover')
|
|
66
|
+
expect(label.classes()).not.toEqual(labelClasses)
|
|
67
|
+
|
|
68
|
+
await wrapper.find('input').trigger('dragleave')
|
|
69
|
+
expect(label.classes()).toEqual(labelClasses)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('accepts the drop of a file with the correct extension', async () => {
|
|
73
|
+
const wrapper = mount(FileUpload, {
|
|
74
|
+
global: {
|
|
75
|
+
plugins: [vuetify],
|
|
76
|
+
},
|
|
77
|
+
props: {
|
|
78
|
+
modelValue: [],
|
|
79
|
+
allowedExtensions: ['pdf'],
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const file: File = new File([''], 'filename.pdf', { type: 'application/pdf' })
|
|
84
|
+
|
|
85
|
+
await wrapper.find('input').trigger('drop', {
|
|
86
|
+
dataTransfer: {
|
|
87
|
+
files: [file],
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
|
92
|
+
expect(wrapper.emitted('error')).toBeFalsy()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('accepts the drop of many files', async () => {
|
|
96
|
+
const wrapper = mount(FileUpload, {
|
|
97
|
+
global: {
|
|
98
|
+
plugins: [vuetify],
|
|
99
|
+
},
|
|
100
|
+
props: {
|
|
101
|
+
modelValue: [],
|
|
102
|
+
multiple: true,
|
|
103
|
+
allowedExtensions: ['pdf'],
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const file: File = new File([''], 'filename.pdf', { type: 'application/pdf' })
|
|
108
|
+
|
|
109
|
+
await wrapper.find('label').trigger('drop', {
|
|
110
|
+
dataTransfer: {
|
|
111
|
+
files: [file],
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
await wrapper.find('label').trigger('drop', {
|
|
116
|
+
dataTransfer: {
|
|
117
|
+
files: [file, file],
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([[file, file, file]])
|
|
122
|
+
expect(wrapper.emitted('error')).toBeFalsy()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('rejects the drop of a file with the wrong extension', async () => {
|
|
126
|
+
const wrapper = mount(FileUpload, {
|
|
127
|
+
global: {
|
|
128
|
+
plugins: [vuetify],
|
|
129
|
+
},
|
|
130
|
+
props: {
|
|
131
|
+
modelValue: [],
|
|
132
|
+
allowedExtensions: ['pdf'],
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const file: File = new File([''], 'filename.jpg', { type: 'image/jpeg' })
|
|
137
|
+
|
|
138
|
+
await wrapper.find('input').trigger('drop', {
|
|
139
|
+
dataTransfer: {
|
|
140
|
+
files: [file],
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([[]])
|
|
145
|
+
expect(wrapper.emitted('error')?.[0]).toEqual([['Le fichier filename.jpg a une extension invalide. Extensions acceptées : pdf']])
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('rejects the drop of a file with the wrong extension in multiple mode', async () => {
|
|
149
|
+
const pdfFile: File = new File([''], 'filename.pdf', { type: 'application/pdf' })
|
|
150
|
+
const jpgFile: File = new File([''], 'filename.jpg', { type: 'image/jpeg' })
|
|
151
|
+
|
|
152
|
+
const wrapper = mount(FileUpload, {
|
|
153
|
+
global: {
|
|
154
|
+
plugins: [vuetify],
|
|
155
|
+
},
|
|
156
|
+
props: {
|
|
157
|
+
multiple: true,
|
|
158
|
+
modelValue: [pdfFile],
|
|
159
|
+
allowedExtensions: ['pdf'],
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
await wrapper.find('label').trigger('drop', {
|
|
164
|
+
dataTransfer: {
|
|
165
|
+
files: [jpgFile],
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([[pdfFile]])
|
|
170
|
+
expect(wrapper.emitted('error')?.[0]).toEqual([['Le fichier filename.jpg a une extension invalide. Extensions acceptées : pdf']])
|
|
171
|
+
|
|
172
|
+
await wrapper.find('label').trigger('drop', {
|
|
173
|
+
dataTransfer: {
|
|
174
|
+
files: [pdfFile],
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
expect(wrapper.emitted('error')?.[1]).toBeFalsy()
|
|
179
|
+
expect(wrapper.emitted('update:modelValue')?.[1]).toEqual([[pdfFile, pdfFile]])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('rejects the drop of a file that is too big', async () => {
|
|
183
|
+
const wrapper = mount(FileUpload, {
|
|
184
|
+
global: {
|
|
185
|
+
plugins: [vuetify],
|
|
186
|
+
},
|
|
187
|
+
props: {
|
|
188
|
+
modelValue: [],
|
|
189
|
+
fileSizeMax: 1,
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const file: File = new File(['42'], 'filename.jpg', { type: 'image/jpeg' })
|
|
194
|
+
|
|
195
|
+
await wrapper.find('label').trigger('drop', {
|
|
196
|
+
dataTransfer: {
|
|
197
|
+
files: [file],
|
|
198
|
+
},
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([[]])
|
|
202
|
+
expect(wrapper.emitted('error')?.[0]).toEqual([['Le fichier filename.jpg est trop volumineux. Taille max. : 1 o']])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('do nothing if no file is dropped', async () => {
|
|
206
|
+
const wrapper = mount(FileUpload, {
|
|
207
|
+
global: {
|
|
208
|
+
plugins: [vuetify],
|
|
209
|
+
},
|
|
210
|
+
props: {
|
|
211
|
+
modelValue: [],
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
await wrapper.find('label').trigger('drop', {
|
|
216
|
+
dataTransfer: {
|
|
217
|
+
files: [],
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
|
222
|
+
expect(wrapper.emitted('error')).toBeFalsy()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('allow any extension if allowedExtensions is empty', async () => {
|
|
226
|
+
const wrapper = mount(FileUpload, {
|
|
227
|
+
global: {
|
|
228
|
+
plugins: [vuetify],
|
|
229
|
+
},
|
|
230
|
+
props: {
|
|
231
|
+
modelValue: [],
|
|
232
|
+
allowedExtensions: [],
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const file: File = new File([''], 'filename.xyz', { type: 'unknown' })
|
|
237
|
+
|
|
238
|
+
await wrapper.find('label').trigger('drop', {
|
|
239
|
+
dataTransfer: {
|
|
240
|
+
files: [file],
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([[file]])
|
|
245
|
+
expect(wrapper.emitted('error')).toBeFalsy()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('do nothing if the field is disabled', async () => {
|
|
249
|
+
const wrapper = mount(FileUpload, {
|
|
250
|
+
global: {
|
|
251
|
+
plugins: [vuetify],
|
|
252
|
+
},
|
|
253
|
+
props: {
|
|
254
|
+
modelValue: [],
|
|
255
|
+
disabled: true,
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
await wrapper.find('label').trigger('drop', {
|
|
260
|
+
dataTransfer: {
|
|
261
|
+
files: [file],
|
|
262
|
+
},
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
|
266
|
+
expect(wrapper.emitted('error')).toBeFalsy()
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('if many files are dropped in single mode, only the first one is kept', async () => {
|
|
270
|
+
const wrapper = mount(FileUpload, {
|
|
271
|
+
global: {
|
|
272
|
+
plugins: [vuetify],
|
|
273
|
+
},
|
|
274
|
+
props: {
|
|
275
|
+
modelValue: [],
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const file1: File = new File([''], 'filename1.jpg', { type: 'image/jpeg' })
|
|
280
|
+
const file2: File = new File([''], 'filename2.jpg', { type: 'image/jpeg' })
|
|
281
|
+
|
|
282
|
+
await wrapper.find('label').trigger('drop', {
|
|
283
|
+
dataTransfer: {
|
|
284
|
+
files: [file1, file2],
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([[file1]])
|
|
289
|
+
expect(wrapper.emitted('error')).toBeFalsy()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('update the field when the input changes', async () => {
|
|
293
|
+
const file1 = new File([''], 'filename1.jpg', { type: 'image/jpeg' })
|
|
294
|
+
const file2 = new File([''], 'filename2.jpg', { type: 'image/jpeg' })
|
|
295
|
+
const wrapper = mount(FileUpload, {
|
|
296
|
+
global: {
|
|
297
|
+
plugins: [vuetify],
|
|
298
|
+
},
|
|
299
|
+
props: {
|
|
300
|
+
modelValue: [file1],
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const input = wrapper.find('input')
|
|
305
|
+
input.element.files = [file2] as unknown as FileList
|
|
306
|
+
await input.trigger('change')
|
|
307
|
+
|
|
308
|
+
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([[file2]])
|
|
309
|
+
expect(wrapper.emitted('error')).toBeFalsy()
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('add the new files to the existing ones when the input changes in multiple mode', async () => {
|
|
313
|
+
const file1 = new File([''], 'filename1.jpg', { type: 'image/jpeg' })
|
|
314
|
+
const file2 = new File([''], 'filename2.jpg', { type: 'image/jpeg' })
|
|
315
|
+
const wrapper = mount(FileUpload, {
|
|
316
|
+
global: {
|
|
317
|
+
plugins: [vuetify],
|
|
318
|
+
},
|
|
319
|
+
props: {
|
|
320
|
+
modelValue: [file1],
|
|
321
|
+
multiple: true,
|
|
322
|
+
},
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
const input = wrapper.find('input')
|
|
326
|
+
input.element.files = [file2] as unknown as FileList
|
|
327
|
+
await input.trigger('change')
|
|
328
|
+
|
|
329
|
+
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([[file1, file2]])
|
|
330
|
+
expect(wrapper.emitted('error')).toBeFalsy()
|
|
331
|
+
})
|
|
332
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`FileUpload > renders correctly 1`] = `"Déposer votre fichier iciOuChoisir un fichierTaille max. : 10 Mo. Formats acceptés : pdf, jpg, jpeg, png"`;
|
|
4
|
+
|
|
5
|
+
exports[`FileUpload > renders correctly in multiple mode 1`] = `"Déposer vos fichiers iciOuChoisir des fichiersTaille max. : 10 Mo. Formats acceptés : pdf, jpg, jpeg, png"`;
|
|
6
|
+
|
|
7
|
+
exports[`FileUpload > renders correctly with only one extension allowed 1`] = `"Déposer votre fichier iciOuChoisir un fichierTaille max. : 10 Mo. Format accepté : pdf"`;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { onMounted, toValue, type MaybeRef } from 'vue'
|
|
2
|
+
|
|
3
|
+
export default function useFileDrop(
|
|
4
|
+
dropZone: MaybeRef<HTMLElement | null>,
|
|
5
|
+
callback: (files: File[]) => void,
|
|
6
|
+
) {
|
|
7
|
+
onMounted(() => {
|
|
8
|
+
const inputEl = toValue(dropZone)
|
|
9
|
+
|
|
10
|
+
if (!inputEl) return
|
|
11
|
+
|
|
12
|
+
inputEl.addEventListener('drop', (e) => {
|
|
13
|
+
e.preventDefault()
|
|
14
|
+
e.stopPropagation()
|
|
15
|
+
|
|
16
|
+
const droppedFiles = e.dataTransfer?.files
|
|
17
|
+
|
|
18
|
+
if (!droppedFiles?.length) return
|
|
19
|
+
|
|
20
|
+
callback(Array.from(droppedFiles))
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { calcHumanFileSize } from '@/utils/calcHumanFileSize'
|
|
2
|
+
import { locales } from './locales'
|
|
3
|
+
|
|
4
|
+
export default function validateFiles(
|
|
5
|
+
files: File[],
|
|
6
|
+
maxFileSize: number,
|
|
7
|
+
allowedExtensions: string[],
|
|
8
|
+
fileSizeUnits: string[],
|
|
9
|
+
) {
|
|
10
|
+
const errors: string[] = []
|
|
11
|
+
const validFiles: File[] = []
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
let isValid = true
|
|
14
|
+
if (file.size > maxFileSize) {
|
|
15
|
+
errors.push(
|
|
16
|
+
locales.errorSize(file.name, calcHumanFileSize(maxFileSize, fileSizeUnits)),
|
|
17
|
+
)
|
|
18
|
+
isValid = false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
!allowedExtensions.includes(file.name.split('.').pop() || '')
|
|
23
|
+
&& allowedExtensions.length > 0
|
|
24
|
+
) {
|
|
25
|
+
errors.push(
|
|
26
|
+
locales.errorExtension(file.name, allowedExtensions),
|
|
27
|
+
)
|
|
28
|
+
isValid = false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isValid) {
|
|
32
|
+
validFiles.push(file)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
errors,
|
|
37
|
+
validFiles,
|
|
38
|
+
}
|
|
39
|
+
}
|