@pyreweb/fabric 1.2.6
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/README.md +119 -0
- package/dist/fabric.cjs.js +18109 -0
- package/dist/fabric.css +2180 -0
- package/dist/fabric.esm.js +18062 -0
- package/dist/fabric.min.js +18112 -0
- package/dist/types/components/atoms/FAvatar/FAvatar.test.d.ts +1 -0
- package/dist/types/components/atoms/FBadge/FBadge.test.d.ts +1 -0
- package/dist/types/components/atoms/FButton/FButton.test.d.ts +1 -0
- package/dist/types/components/atoms/FCheckbox/FCheckbox.test.d.ts +1 -0
- package/dist/types/components/atoms/FDivider/FDivider.test.d.ts +1 -0
- package/dist/types/components/atoms/FIcon/FIcon.test.d.ts +1 -0
- package/dist/types/components/atoms/FInput/FInput.test.d.ts +1 -0
- package/dist/types/components/atoms/FLoader/FLoader.test.d.ts +1 -0
- package/dist/types/components/atoms/FRadio/FRadio.test.d.ts +1 -0
- package/dist/types/components/atoms/FTextarea/FTextarea.test.d.ts +1 -0
- package/dist/types/components/atoms/FToggle/FToggle.test.d.ts +1 -0
- package/dist/types/components/atoms/FTypography/FTypography.test.d.ts +1 -0
- package/dist/types/components/atoms/index.d.ts +13 -0
- package/dist/types/components/molecules/FAccordionItem/FAccordionItem.test.d.ts +1 -0
- package/dist/types/components/molecules/FAlert/FAlert.test.d.ts +1 -0
- package/dist/types/components/molecules/FBreadcrumb/FBreadcrumb.test.d.ts +1 -0
- package/dist/types/components/molecules/FButtonGroup/FButtonGroup.test.d.ts +1 -0
- package/dist/types/components/molecules/FCard/FCard.test.d.ts +1 -0
- package/dist/types/components/molecules/FDatePicker/FDatePicker.test.d.ts +1 -0
- package/dist/types/components/molecules/FEmptyState/FEmptyState.test.d.ts +1 -0
- package/dist/types/components/molecules/FFilePreview/FFilePreview.test.d.ts +1 -0
- package/dist/types/components/molecules/FFormField/FFormField.test.d.ts +1 -0
- package/dist/types/components/molecules/FListItem/FListItem.test.d.ts +1 -0
- package/dist/types/components/molecules/FPagination/FPagination.test.d.ts +1 -0
- package/dist/types/components/molecules/FSearchBar/FSearchBar.test.d.ts +1 -0
- package/dist/types/components/molecules/FSelect/FSelect.test.d.ts +1 -0
- package/dist/types/components/molecules/FStatCard/FStatCard.test.d.ts +1 -0
- package/dist/types/components/molecules/FTabs/FTabs.test.d.ts +1 -0
- package/dist/types/components/molecules/FToast/FToast.test.d.ts +1 -0
- package/dist/types/components/molecules/index.d.ts +18 -0
- package/dist/types/components/organisms/FActivityFeed/FActivityFeed.test.d.ts +1 -0
- package/dist/types/components/organisms/FDataTable/FDataTable.test.d.ts +1 -0
- package/dist/types/components/organisms/FDrawer/FDrawer.test.d.ts +1 -0
- package/dist/types/components/organisms/FFileUpload/FFileUpload.test.d.ts +1 -0
- package/dist/types/components/organisms/FFilterSidebar/FFilterSidebar.test.d.ts +1 -0
- package/dist/types/components/organisms/FForm/FForm.test.d.ts +1 -0
- package/dist/types/components/organisms/FModal/FModal.test.d.ts +1 -0
- package/dist/types/components/organisms/FNavigationSidebar/FNavigationSidebar.test.d.ts +1 -0
- package/dist/types/components/organisms/FOnboardingStepper/FOnboardingStepper.test.d.ts +1 -0
- package/dist/types/components/organisms/FOnboardingStepper/FStepperProgress.test.d.ts +1 -0
- package/dist/types/components/organisms/FPageHeader/FPageHeader.test.d.ts +1 -0
- package/dist/types/components/organisms/FProfileSection/FProfileSection.test.d.ts +1 -0
- package/dist/types/components/organisms/FToastProvider/FToastProvider.test.d.ts +1 -0
- package/dist/types/components/organisms/FUserMenu/FUserMenu.test.d.ts +1 -0
- package/dist/types/components/organisms/index.d.ts +14 -0
- package/dist/types/components/utils/FThemeProvider.test.d.ts +1 -0
- package/dist/types/components/utils/index.d.ts +2 -0
- package/dist/types/components.d.ts +602 -0
- package/dist/types/composables/index.d.ts +12 -0
- package/dist/types/composables/useDataTableState.d.ts +106 -0
- package/dist/types/composables/useDataTableState.test.d.ts +1 -0
- package/dist/types/composables/useFormValidation.d.ts +49 -0
- package/dist/types/composables/useFormValidation.test.d.ts +1 -0
- package/dist/types/composables/useSidebarState.d.ts +65 -0
- package/dist/types/composables/useSidebarState.test.d.ts +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/types.d.ts +529 -0
- package/package.json +100 -0
- package/src/components/atoms/FAvatar/FAvatar.stories.js +100 -0
- package/src/components/atoms/FAvatar/FAvatar.test.ts +95 -0
- package/src/components/atoms/FAvatar/FAvatar.vue +190 -0
- package/src/components/atoms/FBadge/FBadge.stories.js +129 -0
- package/src/components/atoms/FBadge/FBadge.test.ts +93 -0
- package/src/components/atoms/FBadge/FBadge.vue +103 -0
- package/src/components/atoms/FButton/FButton.stories.js +122 -0
- package/src/components/atoms/FButton/FButton.test.ts +98 -0
- package/src/components/atoms/FButton/FButton.vue +147 -0
- package/src/components/atoms/FCheckbox/FCheckbox.stories.js +96 -0
- package/src/components/atoms/FCheckbox/FCheckbox.test.ts +64 -0
- package/src/components/atoms/FCheckbox/FCheckbox.vue +76 -0
- package/src/components/atoms/FDivider/FDivider.stories.js +104 -0
- package/src/components/atoms/FDivider/FDivider.test.ts +80 -0
- package/src/components/atoms/FDivider/FDivider.vue +117 -0
- package/src/components/atoms/FIcon/FIcon.stories.js +189 -0
- package/src/components/atoms/FIcon/FIcon.test.ts +99 -0
- package/src/components/atoms/FIcon/FIcon.vue +192 -0
- package/src/components/atoms/FInput/FInput.stories.js +119 -0
- package/src/components/atoms/FInput/FInput.test.ts +79 -0
- package/src/components/atoms/FInput/FInput.vue +88 -0
- package/src/components/atoms/FLoader/FLoader.stories.js +109 -0
- package/src/components/atoms/FLoader/FLoader.test.ts +66 -0
- package/src/components/atoms/FLoader/FLoader.vue +97 -0
- package/src/components/atoms/FRadio/FRadio.stories.js +105 -0
- package/src/components/atoms/FRadio/FRadio.test.ts +75 -0
- package/src/components/atoms/FRadio/FRadio.vue +119 -0
- package/src/components/atoms/FTextarea/FTextarea.stories.js +126 -0
- package/src/components/atoms/FTextarea/FTextarea.test.ts +94 -0
- package/src/components/atoms/FTextarea/FTextarea.vue +156 -0
- package/src/components/atoms/FToggle/FToggle.stories.js +108 -0
- package/src/components/atoms/FToggle/FToggle.test.ts +96 -0
- package/src/components/atoms/FToggle/FToggle.vue +123 -0
- package/src/components/atoms/FTypography/FTypography.stories.js +127 -0
- package/src/components/atoms/FTypography/FTypography.test.ts +93 -0
- package/src/components/atoms/FTypography/FTypography.vue +78 -0
- package/src/components/atoms/index.ts +27 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.stories.js +71 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.test.ts +61 -0
- package/src/components/molecules/FAccordionItem/FAccordionItem.vue +105 -0
- package/src/components/molecules/FAlert/FAlert.stories.js +87 -0
- package/src/components/molecules/FAlert/FAlert.test.ts +59 -0
- package/src/components/molecules/FAlert/FAlert.vue +108 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.stories.js +90 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.test.ts +76 -0
- package/src/components/molecules/FBreadcrumb/FBreadcrumb.vue +117 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.stories.js +82 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.test.ts +44 -0
- package/src/components/molecules/FButtonGroup/FButtonGroup.vue +31 -0
- package/src/components/molecules/FCard/FCard.stories.js +136 -0
- package/src/components/molecules/FCard/FCard.test.ts +87 -0
- package/src/components/molecules/FCard/FCard.vue +75 -0
- package/src/components/molecules/FDatePicker/FDatePicker.stories.js +305 -0
- package/src/components/molecules/FDatePicker/FDatePicker.test.ts +282 -0
- package/src/components/molecules/FDatePicker/FDatePicker.vue +750 -0
- package/src/components/molecules/FEmptyState/FEmptyState.stories.js +98 -0
- package/src/components/molecules/FEmptyState/FEmptyState.test.ts +82 -0
- package/src/components/molecules/FEmptyState/FEmptyState.vue +89 -0
- package/src/components/molecules/FFilePreview/FFilePreview.stories.js +130 -0
- package/src/components/molecules/FFilePreview/FFilePreview.test.ts +70 -0
- package/src/components/molecules/FFilePreview/FFilePreview.vue +125 -0
- package/src/components/molecules/FFormField/FFormField.stories.js +149 -0
- package/src/components/molecules/FFormField/FFormField.test.ts +85 -0
- package/src/components/molecules/FFormField/FFormField.vue +107 -0
- package/src/components/molecules/FListItem/FListItem.stories.js +158 -0
- package/src/components/molecules/FListItem/FListItem.test.ts +93 -0
- package/src/components/molecules/FListItem/FListItem.vue +113 -0
- package/src/components/molecules/FPagination/FPagination.stories.js +132 -0
- package/src/components/molecules/FPagination/FPagination.test.ts +79 -0
- package/src/components/molecules/FPagination/FPagination.vue +206 -0
- package/src/components/molecules/FSearchBar/FSearchBar.stories.js +129 -0
- package/src/components/molecules/FSearchBar/FSearchBar.test.ts +81 -0
- package/src/components/molecules/FSearchBar/FSearchBar.vue +180 -0
- package/src/components/molecules/FSelect/FSelect.stories.js +333 -0
- package/src/components/molecules/FSelect/FSelect.test.ts +478 -0
- package/src/components/molecules/FSelect/FSelect.vue +551 -0
- package/src/components/molecules/FStatCard/FStatCard.stories.js +144 -0
- package/src/components/molecules/FStatCard/FStatCard.test.ts +78 -0
- package/src/components/molecules/FStatCard/FStatCard.vue +106 -0
- package/src/components/molecules/FTabs/FTab.vue +63 -0
- package/src/components/molecules/FTabs/FTabs.stories.js +277 -0
- package/src/components/molecules/FTabs/FTabs.test.ts +264 -0
- package/src/components/molecules/FTabs/FTabs.vue +273 -0
- package/src/components/molecules/FToast/FToast.stories.js +150 -0
- package/src/components/molecules/FToast/FToast.test.ts +157 -0
- package/src/components/molecules/FToast/FToast.vue +283 -0
- package/src/components/molecules/index.ts +37 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.stories.js +217 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.test.ts +134 -0
- package/src/components/organisms/FActivityFeed/FActivityFeed.vue +589 -0
- package/src/components/organisms/FDataTable/FDataTable.stories.js +370 -0
- package/src/components/organisms/FDataTable/FDataTable.test.ts +248 -0
- package/src/components/organisms/FDataTable/FDataTable.vue +808 -0
- package/src/components/organisms/FDrawer/FDrawer.stories.js +296 -0
- package/src/components/organisms/FDrawer/FDrawer.test.ts +142 -0
- package/src/components/organisms/FDrawer/FDrawer.vue +303 -0
- package/src/components/organisms/FFileUpload/FFileUpload.stories.js +162 -0
- package/src/components/organisms/FFileUpload/FFileUpload.test.ts +103 -0
- package/src/components/organisms/FFileUpload/FFileUpload.vue +616 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.stories.js +161 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.test.ts +92 -0
- package/src/components/organisms/FFilterSidebar/FFilterSidebar.vue +458 -0
- package/src/components/organisms/FForm/FForm.stories.js +270 -0
- package/src/components/organisms/FForm/FForm.test.ts +63 -0
- package/src/components/organisms/FForm/FForm.vue +19 -0
- package/src/components/organisms/FModal/FModal.stories.js +227 -0
- package/src/components/organisms/FModal/FModal.test.ts +181 -0
- package/src/components/organisms/FModal/FModal.vue +319 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.stories.js +176 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.test.ts +95 -0
- package/src/components/organisms/FNavigationSidebar/FNavigationSidebar.vue +577 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.stories.js +197 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.test.ts +114 -0
- package/src/components/organisms/FOnboardingStepper/FOnboardingStepper.vue +212 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.stories.js +122 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.test.ts +130 -0
- package/src/components/organisms/FOnboardingStepper/FStepperProgress.vue +146 -0
- package/src/components/organisms/FPageHeader/FPageHeader.stories.js +142 -0
- package/src/components/organisms/FPageHeader/FPageHeader.test.ts +83 -0
- package/src/components/organisms/FPageHeader/FPageHeader.vue +241 -0
- package/src/components/organisms/FProfileSection/FProfileSection.stories.js +190 -0
- package/src/components/organisms/FProfileSection/FProfileSection.test.ts +85 -0
- package/src/components/organisms/FProfileSection/FProfileSection.vue +562 -0
- package/src/components/organisms/FToastProvider/FToastProvider.stories.js +290 -0
- package/src/components/organisms/FToastProvider/FToastProvider.test.ts +215 -0
- package/src/components/organisms/FToastProvider/FToastProvider.vue +214 -0
- package/src/components/organisms/FUserMenu/FUserMenu.stories.js +170 -0
- package/src/components/organisms/FUserMenu/FUserMenu.test.ts +102 -0
- package/src/components/organisms/FUserMenu/FUserMenu.vue +407 -0
- package/src/components/organisms/index.ts +29 -0
- package/src/components/utils/FThemeProvider.stories.js +236 -0
- package/src/components/utils/FThemeProvider.test.ts +244 -0
- package/src/components/utils/FThemeProvider.vue +191 -0
- package/src/components/utils/index.ts +3 -0
- package/src/components.d.ts +602 -0
- package/src/composables/README.md +233 -0
- package/src/composables/index.ts +25 -0
- package/src/composables/useDataTableState.test.ts +378 -0
- package/src/composables/useDataTableState.ts +361 -0
- package/src/composables/useFormValidation.test.ts +198 -0
- package/src/composables/useFormValidation.ts +178 -0
- package/src/composables/useSidebarState.test.ts +307 -0
- package/src/composables/useSidebarState.ts +201 -0
- package/src/env.d.ts +14 -0
- package/src/index.ts +167 -0
- package/src/styles/tailwind.css +173 -0
- package/src/types.ts +740 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="containerClasses">
|
|
3
|
+
<!-- Hidden file input -->
|
|
4
|
+
<input
|
|
5
|
+
ref="fileInput"
|
|
6
|
+
type="file"
|
|
7
|
+
:accept="accept"
|
|
8
|
+
:multiple="multiple"
|
|
9
|
+
:disabled="disabled"
|
|
10
|
+
class="sr-only"
|
|
11
|
+
@change="handleFileChange"
|
|
12
|
+
/>
|
|
13
|
+
|
|
14
|
+
<!-- Drop zone -->
|
|
15
|
+
<div
|
|
16
|
+
:class="dropZoneClasses"
|
|
17
|
+
@click="triggerFileInput"
|
|
18
|
+
@dragenter.prevent="handleDragEnter"
|
|
19
|
+
@dragover.prevent="handleDragOver"
|
|
20
|
+
@dragleave.prevent="handleDragLeave"
|
|
21
|
+
@drop.prevent="handleDrop"
|
|
22
|
+
>
|
|
23
|
+
<f-icon name="upload" size="lg" :class="iconClasses" />
|
|
24
|
+
<f-typography variant="body" :class="textClasses">
|
|
25
|
+
<slot name="label">
|
|
26
|
+
{{ dropZoneLabel }}
|
|
27
|
+
</slot>
|
|
28
|
+
</f-typography>
|
|
29
|
+
<f-typography v-if="hint" variant="caption" class="text-neutral-500">
|
|
30
|
+
{{ hint }}
|
|
31
|
+
</f-typography>
|
|
32
|
+
<f-button
|
|
33
|
+
v-if="showButton"
|
|
34
|
+
variant="outline"
|
|
35
|
+
size="small"
|
|
36
|
+
:disabled="disabled"
|
|
37
|
+
class="mt-2"
|
|
38
|
+
@click.stop="triggerFileInput"
|
|
39
|
+
>
|
|
40
|
+
<template #iconLeft>
|
|
41
|
+
<f-icon name="upload" size="sm" />
|
|
42
|
+
</template>
|
|
43
|
+
{{ buttonLabel }}
|
|
44
|
+
</f-button>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Alert for errors/success -->
|
|
48
|
+
<f-alert
|
|
49
|
+
v-if="alertMessage"
|
|
50
|
+
:variant="alertVariant"
|
|
51
|
+
:message="alertMessage"
|
|
52
|
+
:closable="true"
|
|
53
|
+
class="mt-3"
|
|
54
|
+
@close="clearAlert"
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
<!-- File previews -->
|
|
58
|
+
<div v-if="hasFiles" class="mt-3 space-y-2">
|
|
59
|
+
<f-file-preview
|
|
60
|
+
v-for="file in internalFiles"
|
|
61
|
+
:key="file.id"
|
|
62
|
+
:file-name="file.name"
|
|
63
|
+
:file-type="file.extension"
|
|
64
|
+
:loading="file.status === 'uploading'"
|
|
65
|
+
:disabled="disabled || file.status === 'uploading'"
|
|
66
|
+
:loading-label="loadingLabel"
|
|
67
|
+
@remove="handleRemoveFile(file)"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Progress bar for overall upload -->
|
|
72
|
+
<div v-if="showProgress && isUploading" class="mt-3">
|
|
73
|
+
<div class="flex items-center justify-between mb-1">
|
|
74
|
+
<f-typography variant="caption" class="text-neutral-600">
|
|
75
|
+
{{ progressLabel }}
|
|
76
|
+
</f-typography>
|
|
77
|
+
<f-typography variant="caption" class="text-neutral-600">
|
|
78
|
+
{{ uploadProgress }}%
|
|
79
|
+
</f-typography>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="w-full bg-neutral-200 rounded-full h-2">
|
|
82
|
+
<div
|
|
83
|
+
class="bg-primary-600 h-2 rounded-full transition-all duration-300"
|
|
84
|
+
:style="{ width: `${uploadProgress}%` }"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script>
|
|
92
|
+
import FIcon from '../../atoms/FIcon/FIcon.vue';
|
|
93
|
+
import FTypography from '../../atoms/FTypography/FTypography.vue';
|
|
94
|
+
import FButton from '../../atoms/FButton/FButton.vue';
|
|
95
|
+
import FAlert from '../../molecules/FAlert/FAlert.vue';
|
|
96
|
+
import FFilePreview from '../../molecules/FFilePreview/FFilePreview.vue';
|
|
97
|
+
|
|
98
|
+
let idCounter = 0;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* File state constants
|
|
102
|
+
*/
|
|
103
|
+
const FILE_STATUS = {
|
|
104
|
+
PENDING: 'pending',
|
|
105
|
+
UPLOADING: 'uploading',
|
|
106
|
+
SUCCESS: 'success',
|
|
107
|
+
ERROR: 'error'
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export default {
|
|
111
|
+
name: 'FFileUpload',
|
|
112
|
+
components: {
|
|
113
|
+
FIcon,
|
|
114
|
+
FTypography,
|
|
115
|
+
FButton,
|
|
116
|
+
FAlert,
|
|
117
|
+
FFilePreview
|
|
118
|
+
},
|
|
119
|
+
props: {
|
|
120
|
+
/**
|
|
121
|
+
* Array of files (for v-model support)
|
|
122
|
+
* Each file object should have: { id, name, file, status, progress }
|
|
123
|
+
*/
|
|
124
|
+
value: {
|
|
125
|
+
type: Array,
|
|
126
|
+
default: () => []
|
|
127
|
+
},
|
|
128
|
+
/**
|
|
129
|
+
* Accepted file types (MIME types or extensions)
|
|
130
|
+
* Example: 'image/*,.pdf,.doc,.docx'
|
|
131
|
+
*/
|
|
132
|
+
accept: {
|
|
133
|
+
type: String,
|
|
134
|
+
default: ''
|
|
135
|
+
},
|
|
136
|
+
/**
|
|
137
|
+
* Allow multiple file selection
|
|
138
|
+
*/
|
|
139
|
+
multiple: {
|
|
140
|
+
type: Boolean,
|
|
141
|
+
default: false
|
|
142
|
+
},
|
|
143
|
+
/**
|
|
144
|
+
* Maximum file size in bytes
|
|
145
|
+
*/
|
|
146
|
+
maxSize: {
|
|
147
|
+
type: Number,
|
|
148
|
+
default: 0
|
|
149
|
+
},
|
|
150
|
+
/**
|
|
151
|
+
* Maximum number of files allowed
|
|
152
|
+
*/
|
|
153
|
+
maxFiles: {
|
|
154
|
+
type: Number,
|
|
155
|
+
default: 0
|
|
156
|
+
},
|
|
157
|
+
/**
|
|
158
|
+
* Disable the upload component
|
|
159
|
+
*/
|
|
160
|
+
disabled: {
|
|
161
|
+
type: Boolean,
|
|
162
|
+
default: false
|
|
163
|
+
},
|
|
164
|
+
/**
|
|
165
|
+
* Show the upload button inside the drop zone
|
|
166
|
+
*/
|
|
167
|
+
showButton: {
|
|
168
|
+
type: Boolean,
|
|
169
|
+
default: true
|
|
170
|
+
},
|
|
171
|
+
/**
|
|
172
|
+
* Show progress bar during upload
|
|
173
|
+
*/
|
|
174
|
+
showProgress: {
|
|
175
|
+
type: Boolean,
|
|
176
|
+
default: true
|
|
177
|
+
},
|
|
178
|
+
/**
|
|
179
|
+
* Label for the drop zone
|
|
180
|
+
*/
|
|
181
|
+
dropZoneLabel: {
|
|
182
|
+
type: String,
|
|
183
|
+
default: 'Glissez-déposez vos fichiers ici'
|
|
184
|
+
},
|
|
185
|
+
/**
|
|
186
|
+
* Label for the upload button
|
|
187
|
+
*/
|
|
188
|
+
buttonLabel: {
|
|
189
|
+
type: String,
|
|
190
|
+
default: 'Parcourir'
|
|
191
|
+
},
|
|
192
|
+
/**
|
|
193
|
+
* Hint text displayed below the drop zone label
|
|
194
|
+
*/
|
|
195
|
+
hint: {
|
|
196
|
+
type: String,
|
|
197
|
+
default: ''
|
|
198
|
+
},
|
|
199
|
+
/**
|
|
200
|
+
* Loading label for file preview
|
|
201
|
+
*/
|
|
202
|
+
loadingLabel: {
|
|
203
|
+
type: String,
|
|
204
|
+
default: 'Téléversement en cours'
|
|
205
|
+
},
|
|
206
|
+
/**
|
|
207
|
+
* Progress label shown during upload
|
|
208
|
+
*/
|
|
209
|
+
progressLabel: {
|
|
210
|
+
type: String,
|
|
211
|
+
default: 'Progression'
|
|
212
|
+
},
|
|
213
|
+
/**
|
|
214
|
+
* Error message for file size validation
|
|
215
|
+
*/
|
|
216
|
+
errorSizeMessage: {
|
|
217
|
+
type: String,
|
|
218
|
+
default: 'Le fichier dépasse la taille maximale autorisée'
|
|
219
|
+
},
|
|
220
|
+
/**
|
|
221
|
+
* Error message for file type validation
|
|
222
|
+
*/
|
|
223
|
+
errorTypeMessage: {
|
|
224
|
+
type: String,
|
|
225
|
+
default: "Ce type de fichier n'est pas autorisé"
|
|
226
|
+
},
|
|
227
|
+
/**
|
|
228
|
+
* Error message for max files validation
|
|
229
|
+
*/
|
|
230
|
+
errorMaxFilesMessage: {
|
|
231
|
+
type: String,
|
|
232
|
+
default: 'Nombre maximum de fichiers atteint'
|
|
233
|
+
},
|
|
234
|
+
/**
|
|
235
|
+
* Success message after upload
|
|
236
|
+
*/
|
|
237
|
+
successMessage: {
|
|
238
|
+
type: String,
|
|
239
|
+
default: 'Fichier(s) téléversé(s) avec succès'
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
data() {
|
|
243
|
+
return {
|
|
244
|
+
isDragging: false,
|
|
245
|
+
alertMessage: '',
|
|
246
|
+
alertVariant: 'info',
|
|
247
|
+
uploadProgress: 0
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
computed: {
|
|
251
|
+
/**
|
|
252
|
+
* Internal files list synced with v-model
|
|
253
|
+
*/
|
|
254
|
+
internalFiles: {
|
|
255
|
+
get() {
|
|
256
|
+
return this.value;
|
|
257
|
+
},
|
|
258
|
+
set(val) {
|
|
259
|
+
this.$emit('input', val);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
/**
|
|
263
|
+
* Check if there are files
|
|
264
|
+
*/
|
|
265
|
+
hasFiles() {
|
|
266
|
+
return this.internalFiles.length > 0;
|
|
267
|
+
},
|
|
268
|
+
/**
|
|
269
|
+
* Check if any file is currently uploading
|
|
270
|
+
*/
|
|
271
|
+
isUploading() {
|
|
272
|
+
return this.internalFiles.some((f) => f.status === FILE_STATUS.UPLOADING);
|
|
273
|
+
},
|
|
274
|
+
/**
|
|
275
|
+
* Container classes
|
|
276
|
+
*/
|
|
277
|
+
containerClasses() {
|
|
278
|
+
return 'w-full';
|
|
279
|
+
},
|
|
280
|
+
/**
|
|
281
|
+
* Drop zone classes
|
|
282
|
+
*/
|
|
283
|
+
dropZoneClasses() {
|
|
284
|
+
const baseClasses =
|
|
285
|
+
'flex flex-col items-center justify-center p-6 border-2 border-dashed rounded-lg cursor-pointer transition-colors duration-200';
|
|
286
|
+
const stateClasses = this.isDragging
|
|
287
|
+
? 'border-primary-500 bg-primary-50'
|
|
288
|
+
: 'border-neutral-300 hover:border-neutral-400 bg-neutral-50';
|
|
289
|
+
const disabledClasses = this.disabled
|
|
290
|
+
? 'opacity-50 cursor-not-allowed pointer-events-none'
|
|
291
|
+
: '';
|
|
292
|
+
|
|
293
|
+
return [baseClasses, stateClasses, disabledClasses]
|
|
294
|
+
.filter(Boolean)
|
|
295
|
+
.join(' ');
|
|
296
|
+
},
|
|
297
|
+
/**
|
|
298
|
+
* Icon classes
|
|
299
|
+
*/
|
|
300
|
+
iconClasses() {
|
|
301
|
+
return this.isDragging ? 'text-primary-500' : 'text-neutral-400';
|
|
302
|
+
},
|
|
303
|
+
/**
|
|
304
|
+
* Text classes
|
|
305
|
+
*/
|
|
306
|
+
textClasses() {
|
|
307
|
+
return this.isDragging ? 'text-primary-600' : 'text-neutral-600';
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
methods: {
|
|
311
|
+
/**
|
|
312
|
+
* Trigger the hidden file input
|
|
313
|
+
*/
|
|
314
|
+
triggerFileInput() {
|
|
315
|
+
if (!this.disabled) {
|
|
316
|
+
this.$refs.fileInput.click();
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
/**
|
|
320
|
+
* Handle file input change
|
|
321
|
+
*/
|
|
322
|
+
handleFileChange(event) {
|
|
323
|
+
const files = Array.from(event.target.files);
|
|
324
|
+
this.processFiles(files);
|
|
325
|
+
// Reset input to allow selecting the same file again
|
|
326
|
+
event.target.value = '';
|
|
327
|
+
},
|
|
328
|
+
/**
|
|
329
|
+
* Handle drag enter event
|
|
330
|
+
*/
|
|
331
|
+
handleDragEnter(event) {
|
|
332
|
+
if (!this.disabled) {
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
this.isDragging = true;
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
/**
|
|
338
|
+
* Handle drag over event
|
|
339
|
+
*/
|
|
340
|
+
handleDragOver(event) {
|
|
341
|
+
if (!this.disabled) {
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
this.isDragging = true;
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
/**
|
|
347
|
+
* Handle drag leave event
|
|
348
|
+
*/
|
|
349
|
+
handleDragLeave(event) {
|
|
350
|
+
if (!this.disabled) {
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
this.isDragging = false;
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
/**
|
|
356
|
+
* Handle drop event
|
|
357
|
+
*/
|
|
358
|
+
handleDrop(event) {
|
|
359
|
+
if (!this.disabled) {
|
|
360
|
+
this.isDragging = false;
|
|
361
|
+
const files = Array.from(event.dataTransfer.files);
|
|
362
|
+
this.processFiles(files);
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
/**
|
|
366
|
+
* Process and validate files
|
|
367
|
+
*/
|
|
368
|
+
processFiles(files) {
|
|
369
|
+
this.clearAlert();
|
|
370
|
+
|
|
371
|
+
// Handle empty files array
|
|
372
|
+
if (!files || files.length === 0) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// If not multiple, only take the first file
|
|
377
|
+
const filesToProcess = this.multiple ? files : [files[0]];
|
|
378
|
+
|
|
379
|
+
// Check max files limit (only for multiple mode)
|
|
380
|
+
if (this.multiple && this.maxFiles > 0) {
|
|
381
|
+
const totalFiles = this.internalFiles.length + filesToProcess.length;
|
|
382
|
+
if (totalFiles > this.maxFiles) {
|
|
383
|
+
this.showError(this.errorMaxFilesMessage);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const validFiles = [];
|
|
389
|
+
for (const file of filesToProcess) {
|
|
390
|
+
const validation = this.validateFile(file);
|
|
391
|
+
if (!validation.valid) {
|
|
392
|
+
this.showError(validation.error);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const fileObject = this.createFileObject(file);
|
|
397
|
+
validFiles.push(fileObject);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// If not multiple, replace existing files
|
|
401
|
+
if (!this.multiple) {
|
|
402
|
+
this.internalFiles = validFiles;
|
|
403
|
+
} else {
|
|
404
|
+
this.internalFiles = [...this.internalFiles, ...validFiles];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Emit files-selected event
|
|
408
|
+
this.$emit('files-selected', validFiles);
|
|
409
|
+
},
|
|
410
|
+
/**
|
|
411
|
+
* Validate a single file
|
|
412
|
+
*/
|
|
413
|
+
validateFile(file) {
|
|
414
|
+
// Validate file type
|
|
415
|
+
if (this.accept) {
|
|
416
|
+
const isValid = this.isFileTypeValid(file);
|
|
417
|
+
if (!isValid) {
|
|
418
|
+
return { valid: false, error: this.errorTypeMessage };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Validate file size
|
|
423
|
+
if (this.maxSize > 0 && file.size > this.maxSize) {
|
|
424
|
+
return { valid: false, error: this.errorSizeMessage };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { valid: true };
|
|
428
|
+
},
|
|
429
|
+
/**
|
|
430
|
+
* Check if file type is valid based on accept attribute
|
|
431
|
+
*/
|
|
432
|
+
isFileTypeValid(file) {
|
|
433
|
+
const acceptedTypes = this.accept.split(',').map((t) => t.trim());
|
|
434
|
+
|
|
435
|
+
return acceptedTypes.some((acceptedType) => {
|
|
436
|
+
if (acceptedType.startsWith('.')) {
|
|
437
|
+
// Extension check
|
|
438
|
+
const ext = '.' + file.name.split('.').pop().toLowerCase();
|
|
439
|
+
return ext === acceptedType.toLowerCase();
|
|
440
|
+
} else if (acceptedType.endsWith('/*')) {
|
|
441
|
+
// MIME type wildcard (e.g., image/*)
|
|
442
|
+
const baseType = acceptedType.replace('/*', '');
|
|
443
|
+
return file.type.startsWith(baseType);
|
|
444
|
+
} else {
|
|
445
|
+
// Exact MIME type match
|
|
446
|
+
return file.type === acceptedType;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
},
|
|
450
|
+
/**
|
|
451
|
+
* Create a file object for internal tracking
|
|
452
|
+
*/
|
|
453
|
+
createFileObject(file) {
|
|
454
|
+
const extension = file.name.split('.').pop().toLowerCase();
|
|
455
|
+
return {
|
|
456
|
+
id: `file-${++idCounter}`,
|
|
457
|
+
name: file.name,
|
|
458
|
+
size: file.size,
|
|
459
|
+
type: file.type,
|
|
460
|
+
extension,
|
|
461
|
+
file,
|
|
462
|
+
status: FILE_STATUS.PENDING,
|
|
463
|
+
progress: 0
|
|
464
|
+
};
|
|
465
|
+
},
|
|
466
|
+
/**
|
|
467
|
+
* Remove a file from the list
|
|
468
|
+
*/
|
|
469
|
+
handleRemoveFile(fileToRemove) {
|
|
470
|
+
this.internalFiles = this.internalFiles.filter(
|
|
471
|
+
(f) => f.id !== fileToRemove.id
|
|
472
|
+
);
|
|
473
|
+
this.$emit('file-removed', fileToRemove);
|
|
474
|
+
},
|
|
475
|
+
/**
|
|
476
|
+
* Show error message
|
|
477
|
+
*/
|
|
478
|
+
showError(message) {
|
|
479
|
+
this.alertMessage = message;
|
|
480
|
+
this.alertVariant = 'error';
|
|
481
|
+
},
|
|
482
|
+
/**
|
|
483
|
+
* Show success message
|
|
484
|
+
*/
|
|
485
|
+
showSuccess(message) {
|
|
486
|
+
this.alertMessage = message || this.successMessage;
|
|
487
|
+
this.alertVariant = 'success';
|
|
488
|
+
},
|
|
489
|
+
/**
|
|
490
|
+
* Clear alert message
|
|
491
|
+
*/
|
|
492
|
+
clearAlert() {
|
|
493
|
+
this.alertMessage = '';
|
|
494
|
+
},
|
|
495
|
+
/**
|
|
496
|
+
* Start upload for a specific file (to be called externally)
|
|
497
|
+
*/
|
|
498
|
+
startUpload(fileId) {
|
|
499
|
+
const file = this.internalFiles.find((f) => f.id === fileId);
|
|
500
|
+
if (file) {
|
|
501
|
+
file.status = FILE_STATUS.UPLOADING;
|
|
502
|
+
file.progress = 0;
|
|
503
|
+
this.updateFile(file);
|
|
504
|
+
this.$emit('upload-start', file);
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
/**
|
|
508
|
+
* Update upload progress for a specific file
|
|
509
|
+
*/
|
|
510
|
+
updateProgress(fileId, progress) {
|
|
511
|
+
const file = this.internalFiles.find((f) => f.id === fileId);
|
|
512
|
+
if (file) {
|
|
513
|
+
file.progress = progress;
|
|
514
|
+
this.updateFile(file);
|
|
515
|
+
this.$emit('upload-progress', { file, progress });
|
|
516
|
+
|
|
517
|
+
// Update overall progress
|
|
518
|
+
this.calculateOverallProgress();
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
/**
|
|
522
|
+
* Mark file as successfully uploaded
|
|
523
|
+
*/
|
|
524
|
+
markAsSuccess(fileId) {
|
|
525
|
+
const file = this.internalFiles.find((f) => f.id === fileId);
|
|
526
|
+
if (file) {
|
|
527
|
+
file.status = FILE_STATUS.SUCCESS;
|
|
528
|
+
file.progress = 100;
|
|
529
|
+
this.updateFile(file);
|
|
530
|
+
this.$emit('upload-success', file);
|
|
531
|
+
|
|
532
|
+
// Check if all files are done
|
|
533
|
+
if (this.internalFiles.every((f) => f.status === FILE_STATUS.SUCCESS)) {
|
|
534
|
+
this.showSuccess();
|
|
535
|
+
this.$emit('upload-complete', this.internalFiles);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
/**
|
|
540
|
+
* Mark file as failed
|
|
541
|
+
*/
|
|
542
|
+
markAsError(fileId, errorMessage) {
|
|
543
|
+
const file = this.internalFiles.find((f) => f.id === fileId);
|
|
544
|
+
if (file) {
|
|
545
|
+
file.status = FILE_STATUS.ERROR;
|
|
546
|
+
this.updateFile(file);
|
|
547
|
+
this.showError(errorMessage);
|
|
548
|
+
this.$emit('upload-error', { file, error: errorMessage });
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
/**
|
|
552
|
+
* Update a file in the internal list
|
|
553
|
+
*/
|
|
554
|
+
updateFile(updatedFile) {
|
|
555
|
+
const index = this.internalFiles.findIndex(
|
|
556
|
+
(f) => f.id === updatedFile.id
|
|
557
|
+
);
|
|
558
|
+
if (index !== -1) {
|
|
559
|
+
const newFiles = [...this.internalFiles];
|
|
560
|
+
newFiles[index] = { ...updatedFile };
|
|
561
|
+
this.internalFiles = newFiles;
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
/**
|
|
565
|
+
* Calculate overall upload progress
|
|
566
|
+
*/
|
|
567
|
+
calculateOverallProgress() {
|
|
568
|
+
if (!this.hasFiles) {
|
|
569
|
+
this.uploadProgress = 0;
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const uploadingFiles = this.internalFiles.filter(
|
|
574
|
+
(f) =>
|
|
575
|
+
f.status === FILE_STATUS.UPLOADING || f.status === FILE_STATUS.SUCCESS
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
if (uploadingFiles.length === 0) {
|
|
579
|
+
this.uploadProgress = 0;
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const totalProgress = uploadingFiles.reduce(
|
|
584
|
+
(sum, f) => sum + f.progress,
|
|
585
|
+
0
|
|
586
|
+
);
|
|
587
|
+
this.uploadProgress = Math.round(totalProgress / uploadingFiles.length);
|
|
588
|
+
},
|
|
589
|
+
/**
|
|
590
|
+
* Clear all files
|
|
591
|
+
*/
|
|
592
|
+
clearFiles() {
|
|
593
|
+
this.internalFiles = [];
|
|
594
|
+
this.clearAlert();
|
|
595
|
+
this.uploadProgress = 0;
|
|
596
|
+
this.$emit('files-cleared');
|
|
597
|
+
},
|
|
598
|
+
/**
|
|
599
|
+
* Get all pending files (ready for upload)
|
|
600
|
+
*/
|
|
601
|
+
getPendingFiles() {
|
|
602
|
+
return this.internalFiles.filter((f) => f.status === FILE_STATUS.PENDING);
|
|
603
|
+
},
|
|
604
|
+
/**
|
|
605
|
+
* Start upload for all pending files
|
|
606
|
+
*/
|
|
607
|
+
uploadAll() {
|
|
608
|
+
const pendingFiles = this.getPendingFiles();
|
|
609
|
+
pendingFiles.forEach((file) => {
|
|
610
|
+
this.startUpload(file.id);
|
|
611
|
+
});
|
|
612
|
+
this.$emit('upload-all', pendingFiles);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
</script>
|