@uploadista/vue 0.0.3
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/.turbo/turbo-check.log +240 -0
- package/LICENSE +21 -0
- package/README.md +554 -0
- package/package.json +36 -0
- package/src/components/FlowUploadList.vue +342 -0
- package/src/components/FlowUploadZone.vue +305 -0
- package/src/components/UploadList.vue +303 -0
- package/src/components/UploadZone.vue +254 -0
- package/src/components/index.ts +11 -0
- package/src/composables/index.ts +44 -0
- package/src/composables/plugin.ts +76 -0
- package/src/composables/useDragDrop.ts +343 -0
- package/src/composables/useFlowUpload.ts +431 -0
- package/src/composables/useMultiFlowUpload.ts +322 -0
- package/src/composables/useMultiUpload.ts +546 -0
- package/src/composables/useUpload.ts +300 -0
- package/src/composables/useUploadMetrics.ts +502 -0
- package/src/composables/useUploadistaClient.ts +73 -0
- package/src/index.ts +28 -0
- package/src/providers/UploadistaProvider.vue +69 -0
- package/src/providers/index.ts +1 -0
- package/src/utils/index.ts +156 -0
- package/src/utils/is-browser-file.ts +2 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* UploadList - Display a list of uploads with customizable status grouping and sorting
|
|
4
|
+
*
|
|
5
|
+
* Shows the progress and status of multiple file uploads. Supports filtering, sorting,
|
|
6
|
+
* and status-based grouping. Provides flexible slot-based customization for rendering each item.
|
|
7
|
+
*
|
|
8
|
+
* @component
|
|
9
|
+
* @example
|
|
10
|
+
* // Basic upload list
|
|
11
|
+
* <UploadList :uploads="uploads" />
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Custom item rendering with status indicators
|
|
15
|
+
* <UploadList :uploads="uploads">
|
|
16
|
+
* <template #item="{ item, isSuccess, isError }">
|
|
17
|
+
* <div class="upload-item">
|
|
18
|
+
* <span>{{ item.filename }}</span>
|
|
19
|
+
* <progress :value="item.progress" max="100"></progress>
|
|
20
|
+
* <span :class="{ success: isSuccess, error: isError }">
|
|
21
|
+
* {{ item.state.status }}
|
|
22
|
+
* </span>
|
|
23
|
+
* </div>
|
|
24
|
+
* </template>
|
|
25
|
+
* </UploadList>
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // With filtering and sorting
|
|
29
|
+
* <UploadList
|
|
30
|
+
* :uploads="uploads"
|
|
31
|
+
* :filter="item => item.state.status === 'success'"
|
|
32
|
+
* :sort-by="(a, b) => a.uploadedAt - b.uploadedAt"
|
|
33
|
+
* />
|
|
34
|
+
*/
|
|
35
|
+
import { computed } from "vue";
|
|
36
|
+
import type { UploadItem } from "../composables";
|
|
37
|
+
import { isBrowserFile } from "../utils";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Props for the UploadList component
|
|
41
|
+
* @property {UploadItem[]} uploads - Array of upload items to display
|
|
42
|
+
* @property {Function} filter - Optional filter for which items to display
|
|
43
|
+
* @property {Function} sortBy - Optional sorting function for items (a, b) => number
|
|
44
|
+
*/
|
|
45
|
+
export interface UploadListProps {
|
|
46
|
+
/**
|
|
47
|
+
* Array of upload items to display
|
|
48
|
+
*/
|
|
49
|
+
uploads: UploadItem[];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Optional filter for which items to display
|
|
53
|
+
*/
|
|
54
|
+
filter?: (item: UploadItem) => boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Optional sorting function for items
|
|
58
|
+
*/
|
|
59
|
+
sortBy?: (a: UploadItem, b: UploadItem) => number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const props = defineProps<UploadListProps>();
|
|
63
|
+
|
|
64
|
+
defineSlots<{
|
|
65
|
+
item(props: {
|
|
66
|
+
item: UploadItem;
|
|
67
|
+
index: number;
|
|
68
|
+
isUploading: boolean;
|
|
69
|
+
isSuccess: boolean;
|
|
70
|
+
isError: boolean;
|
|
71
|
+
formatFileSize: (bytes: number) => string;
|
|
72
|
+
}): any;
|
|
73
|
+
default?(props: {
|
|
74
|
+
items: UploadItem[];
|
|
75
|
+
itemsByStatus: {
|
|
76
|
+
idle: UploadItem[];
|
|
77
|
+
uploading: UploadItem[];
|
|
78
|
+
success: UploadItem[];
|
|
79
|
+
error: UploadItem[];
|
|
80
|
+
aborted: UploadItem[];
|
|
81
|
+
};
|
|
82
|
+
}): any;
|
|
83
|
+
}>();
|
|
84
|
+
|
|
85
|
+
// Apply filtering and sorting
|
|
86
|
+
const filteredItems = computed(() => {
|
|
87
|
+
let items = props.uploads;
|
|
88
|
+
|
|
89
|
+
if (props.filter) {
|
|
90
|
+
items = items.filter(props.filter);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (props.sortBy) {
|
|
94
|
+
items = [...items].sort(props.sortBy);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return items;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Group items by status
|
|
101
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
102
|
+
const itemsByStatus = computed(() => ({
|
|
103
|
+
idle: filteredItems.value.filter((item) => item.state.status === "idle"),
|
|
104
|
+
uploading: filteredItems.value.filter(
|
|
105
|
+
(item) => item.state.status === "uploading",
|
|
106
|
+
),
|
|
107
|
+
success: filteredItems.value.filter(
|
|
108
|
+
(item) => item.state.status === "success",
|
|
109
|
+
),
|
|
110
|
+
error: filteredItems.value.filter((item) => item.state.status === "error"),
|
|
111
|
+
aborted: filteredItems.value.filter(
|
|
112
|
+
(item) => item.state.status === "aborted",
|
|
113
|
+
),
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
// Helper function to format file sizes
|
|
117
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
118
|
+
const formatFileSize = (bytes: number): string => {
|
|
119
|
+
if (bytes === 0) return "0 Bytes";
|
|
120
|
+
const k = 1024;
|
|
121
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
122
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
123
|
+
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Helper function to get status icon
|
|
127
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
128
|
+
const getStatusIcon = (status: string): string => {
|
|
129
|
+
switch (status) {
|
|
130
|
+
case "idle":
|
|
131
|
+
return "⏳";
|
|
132
|
+
case "uploading":
|
|
133
|
+
return "📤";
|
|
134
|
+
case "success":
|
|
135
|
+
return "✅";
|
|
136
|
+
case "error":
|
|
137
|
+
return "❌";
|
|
138
|
+
case "aborted":
|
|
139
|
+
return "⏹️";
|
|
140
|
+
default:
|
|
141
|
+
return "❓";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Helper function to get status color
|
|
146
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
147
|
+
const getStatusColor = (status: string): string => {
|
|
148
|
+
switch (status) {
|
|
149
|
+
case "idle":
|
|
150
|
+
return "#6c757d";
|
|
151
|
+
case "uploading":
|
|
152
|
+
return "#007bff";
|
|
153
|
+
case "success":
|
|
154
|
+
return "#28a745";
|
|
155
|
+
case "error":
|
|
156
|
+
return "#dc3545";
|
|
157
|
+
case "aborted":
|
|
158
|
+
return "#6c757d";
|
|
159
|
+
default:
|
|
160
|
+
return "#6c757d";
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
</script>
|
|
164
|
+
|
|
165
|
+
<template>
|
|
166
|
+
<div class="upload-list">
|
|
167
|
+
<slot :items="filteredItems" :items-by-status="itemsByStatus">
|
|
168
|
+
<!-- Default rendering: simple list of upload items -->
|
|
169
|
+
<div
|
|
170
|
+
v-for="(item, index) in filteredItems"
|
|
171
|
+
:key="item.id"
|
|
172
|
+
class="upload-list__item"
|
|
173
|
+
:class="`upload-list__item--${item.state.status}`"
|
|
174
|
+
>
|
|
175
|
+
<slot
|
|
176
|
+
name="item"
|
|
177
|
+
:item="item"
|
|
178
|
+
:index="index"
|
|
179
|
+
:is-uploading="item.state.status === 'uploading'"
|
|
180
|
+
:is-success="item.state.status === 'success'"
|
|
181
|
+
:is-error="item.state.status === 'error'"
|
|
182
|
+
:format-file-size="formatFileSize"
|
|
183
|
+
>
|
|
184
|
+
<!-- Default item template -->
|
|
185
|
+
<div class="upload-list__item-header">
|
|
186
|
+
<span class="upload-list__item-icon">
|
|
187
|
+
{{ getStatusIcon(item.state.status) }}
|
|
188
|
+
</span>
|
|
189
|
+
<span class="upload-list__item-name">
|
|
190
|
+
{{ isBrowserFile(item.file) ? item.file.name : 'File' }}
|
|
191
|
+
</span>
|
|
192
|
+
<span
|
|
193
|
+
class="upload-list__item-status"
|
|
194
|
+
:style="{ color: getStatusColor(item.state.status) }"
|
|
195
|
+
>
|
|
196
|
+
{{ item.state.status.toUpperCase() }}
|
|
197
|
+
</span>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div v-if="item.state.totalBytes" class="upload-list__item-size">
|
|
201
|
+
{{ formatFileSize(item.state.totalBytes) }}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div v-if="item.state.status === 'uploading'" class="upload-list__item-progress">
|
|
205
|
+
<div class="upload-list__progress-bar">
|
|
206
|
+
<div
|
|
207
|
+
class="upload-list__progress-fill"
|
|
208
|
+
:style="{ width: `${item.state.progress}%` }"
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
211
|
+
<span class="upload-list__progress-text">{{ item.state.progress }}%</span>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div v-if="item.state.status === 'error' && item.state.error" class="upload-list__item-error">
|
|
215
|
+
{{ item.state.error.message }}
|
|
216
|
+
</div>
|
|
217
|
+
</slot>
|
|
218
|
+
</div>
|
|
219
|
+
</slot>
|
|
220
|
+
</div>
|
|
221
|
+
</template>
|
|
222
|
+
|
|
223
|
+
<style scoped>
|
|
224
|
+
.upload-list {
|
|
225
|
+
display: flex;
|
|
226
|
+
flex-direction: column;
|
|
227
|
+
gap: 0.5rem;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.upload-list__item {
|
|
231
|
+
padding: 0.75rem;
|
|
232
|
+
border: 1px solid #e0e0e0;
|
|
233
|
+
border-radius: 0.375rem;
|
|
234
|
+
background-color: #fff;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.upload-list__item-header {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: 0.5rem;
|
|
241
|
+
margin-bottom: 0.5rem;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.upload-list__item-icon {
|
|
245
|
+
font-size: 1rem;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.upload-list__item-name {
|
|
249
|
+
flex: 1;
|
|
250
|
+
font-weight: 500;
|
|
251
|
+
overflow: hidden;
|
|
252
|
+
text-overflow: ellipsis;
|
|
253
|
+
white-space: nowrap;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.upload-list__item-status {
|
|
257
|
+
font-size: 0.75rem;
|
|
258
|
+
font-weight: 600;
|
|
259
|
+
text-transform: uppercase;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.upload-list__item-size {
|
|
263
|
+
font-size: 0.75rem;
|
|
264
|
+
color: #666;
|
|
265
|
+
margin-bottom: 0.5rem;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.upload-list__item-progress {
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
gap: 0.5rem;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.upload-list__progress-bar {
|
|
275
|
+
flex: 1;
|
|
276
|
+
height: 0.375rem;
|
|
277
|
+
background-color: #e0e0e0;
|
|
278
|
+
border-radius: 0.1875rem;
|
|
279
|
+
overflow: hidden;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.upload-list__progress-fill {
|
|
283
|
+
height: 100%;
|
|
284
|
+
background-color: #007bff;
|
|
285
|
+
transition: width 0.2s ease;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.upload-list__progress-text {
|
|
289
|
+
font-size: 0.75rem;
|
|
290
|
+
color: #666;
|
|
291
|
+
min-width: 3rem;
|
|
292
|
+
text-align: right;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.upload-list__item-error {
|
|
296
|
+
margin-top: 0.5rem;
|
|
297
|
+
padding: 0.5rem;
|
|
298
|
+
background-color: #f8d7da;
|
|
299
|
+
color: #721c24;
|
|
300
|
+
font-size: 0.75rem;
|
|
301
|
+
border-radius: 0.25rem;
|
|
302
|
+
}
|
|
303
|
+
</style>
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* UploadZone - A flexible file upload component with drag-and-drop support
|
|
4
|
+
*
|
|
5
|
+
* Provides a drag-and-drop zone and file picker for uploading files. Supports both single
|
|
6
|
+
* and multiple file uploads with validation. Emits events for file selection and upload events.
|
|
7
|
+
*
|
|
8
|
+
* @component
|
|
9
|
+
* @example
|
|
10
|
+
* // Basic single file upload
|
|
11
|
+
* <UploadZone @file-select="handleFiles" />
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Multiple files with validation
|
|
15
|
+
* <UploadZone
|
|
16
|
+
* multiple
|
|
17
|
+
* accept={["image/*"]}
|
|
18
|
+
* :max-file-size="10 * 1024 * 1024"
|
|
19
|
+
* @file-select="handleFiles"
|
|
20
|
+
* @validation-error="handleErrors"
|
|
21
|
+
* >
|
|
22
|
+
* <template #default="{ isDragging, errors, openFilePicker }">
|
|
23
|
+
* <div :class="{ dragging: isDragging }" @click="openFilePicker">
|
|
24
|
+
* <p>{{ isDragging ? 'Drop files here' : 'Click or drag files here' }}</p>
|
|
25
|
+
* <div v-if="errors.length">
|
|
26
|
+
* <p v-for="error in errors" :key="error">{{ error }}</p>
|
|
27
|
+
* </div>
|
|
28
|
+
* </div>
|
|
29
|
+
* </template>
|
|
30
|
+
* </UploadZone>
|
|
31
|
+
*
|
|
32
|
+
* @emits file-select - When files are selected/dropped
|
|
33
|
+
* @emits upload-start - When upload begins
|
|
34
|
+
* @emits validation-error - When validation fails
|
|
35
|
+
*/
|
|
36
|
+
import type { UploadOptions } from "@uploadista/client-browser";
|
|
37
|
+
import { computed, ref } from "vue";
|
|
38
|
+
import type { MultiUploadOptions } from "../composables";
|
|
39
|
+
import { useDragDrop, useMultiUpload, useUpload } from "../composables";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Props for the UploadZone component
|
|
43
|
+
* @property {string[]} accept - Accepted file types (MIME types or file extensions)
|
|
44
|
+
* @property {boolean} multiple - Whether to allow multiple files (default: true)
|
|
45
|
+
* @property {boolean} disabled - Whether the upload zone is disabled (default: false)
|
|
46
|
+
* @property {number} maxFileSize - Maximum file size in bytes
|
|
47
|
+
* @property {Function} validator - Custom validation function for files
|
|
48
|
+
* @property {MultiUploadOptions} multiUploadOptions - Multi-upload options (only used when multiple=true)
|
|
49
|
+
* @property {UploadOptions} uploadOptions - Single upload options (only used when multiple=false)
|
|
50
|
+
*/
|
|
51
|
+
export interface UploadZoneProps {
|
|
52
|
+
/**
|
|
53
|
+
* Accepted file types (MIME types or file extensions)
|
|
54
|
+
*/
|
|
55
|
+
accept?: string[];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether to allow multiple files
|
|
59
|
+
*/
|
|
60
|
+
multiple?: boolean;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether the upload zone is disabled
|
|
64
|
+
*/
|
|
65
|
+
disabled?: boolean;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Maximum file size in bytes
|
|
69
|
+
*/
|
|
70
|
+
maxFileSize?: number;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Custom validation function for files
|
|
74
|
+
*/
|
|
75
|
+
validator?: (files: File[]) => string[] | null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Multi-upload options (only used when multiple=true)
|
|
79
|
+
*/
|
|
80
|
+
multiUploadOptions?: MultiUploadOptions;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Single upload options (only used when multiple=false)
|
|
84
|
+
*/
|
|
85
|
+
uploadOptions?: UploadOptions;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const props = withDefaults(defineProps<UploadZoneProps>(), {
|
|
89
|
+
multiple: true,
|
|
90
|
+
disabled: false,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const emit = defineEmits<{
|
|
94
|
+
"file-select": [files: File[]];
|
|
95
|
+
"upload-start": [files: File[]];
|
|
96
|
+
"validation-error": [errors: string[]];
|
|
97
|
+
}>();
|
|
98
|
+
|
|
99
|
+
defineSlots<{
|
|
100
|
+
// biome-ignore lint/suspicious/noExplicitAny: Vue slot definition requires any
|
|
101
|
+
default(props: {
|
|
102
|
+
isDragging: boolean;
|
|
103
|
+
isOver: boolean;
|
|
104
|
+
isUploading: boolean;
|
|
105
|
+
errors: string[];
|
|
106
|
+
openFilePicker: () => void;
|
|
107
|
+
}): any;
|
|
108
|
+
}>();
|
|
109
|
+
|
|
110
|
+
// Initialize upload composables
|
|
111
|
+
const singleUpload = props.multiple
|
|
112
|
+
? null
|
|
113
|
+
: useUpload(props.uploadOptions || {});
|
|
114
|
+
const multiUpload = props.multiple
|
|
115
|
+
? useMultiUpload(props.multiUploadOptions || {})
|
|
116
|
+
: null;
|
|
117
|
+
|
|
118
|
+
// Handle files received from drag-drop or file picker
|
|
119
|
+
const handleFilesReceived = (files: File[]) => {
|
|
120
|
+
emit("file-select", files);
|
|
121
|
+
emit("upload-start", files);
|
|
122
|
+
|
|
123
|
+
if (props.multiple && multiUpload) {
|
|
124
|
+
multiUpload.addFiles(files);
|
|
125
|
+
setTimeout(() => multiUpload.startAll(), 0);
|
|
126
|
+
} else if (!props.multiple && singleUpload && files[0]) {
|
|
127
|
+
singleUpload.upload(files[0]);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Handle validation errors
|
|
132
|
+
const handleValidationError = (errors: string[]) => {
|
|
133
|
+
emit("validation-error", errors);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Initialize drag-drop
|
|
137
|
+
const dragDrop = useDragDrop({
|
|
138
|
+
accept: props.accept,
|
|
139
|
+
multiple: props.multiple,
|
|
140
|
+
maxFileSize: props.maxFileSize,
|
|
141
|
+
validator: props.validator,
|
|
142
|
+
onFilesReceived: handleFilesReceived,
|
|
143
|
+
onValidationError: handleValidationError,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// File input ref
|
|
147
|
+
const fileInputRef = ref<HTMLInputElement>();
|
|
148
|
+
|
|
149
|
+
// Open file picker
|
|
150
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
151
|
+
const openFilePicker = () => {
|
|
152
|
+
if (!props.disabled) {
|
|
153
|
+
fileInputRef.value?.click();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Computed states
|
|
158
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
159
|
+
const isActive = computed(
|
|
160
|
+
() => dragDrop.state.value.isDragging || dragDrop.state.value.isOver,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// biome-ignore lint/correctness/noUnusedVariables: Used in slot templates
|
|
164
|
+
const isUploading = computed(() => {
|
|
165
|
+
if (props.multiple && multiUpload) {
|
|
166
|
+
return multiUpload.state.value.isUploading;
|
|
167
|
+
} else if (!props.multiple && singleUpload) {
|
|
168
|
+
return singleUpload.state.value.status === "uploading";
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
});
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<template>
|
|
175
|
+
<div
|
|
176
|
+
class="upload-zone"
|
|
177
|
+
:class="{ 'upload-zone--active': isActive, 'upload-zone--disabled': disabled }"
|
|
178
|
+
@dragenter="!disabled && dragDrop.onDragEnter"
|
|
179
|
+
@dragover="!disabled && dragDrop.onDragOver"
|
|
180
|
+
@dragleave="!disabled && dragDrop.onDragLeave"
|
|
181
|
+
@drop="!disabled && dragDrop.onDrop"
|
|
182
|
+
@click="openFilePicker"
|
|
183
|
+
role="button"
|
|
184
|
+
:tabindex="disabled ? -1 : 0"
|
|
185
|
+
:aria-disabled="disabled"
|
|
186
|
+
:aria-label="multiple ? 'Upload multiple files' : 'Upload a file'"
|
|
187
|
+
@keydown.enter="openFilePicker"
|
|
188
|
+
@keydown.space.prevent="openFilePicker"
|
|
189
|
+
>
|
|
190
|
+
<slot
|
|
191
|
+
:is-dragging="dragDrop.state.value.isDragging"
|
|
192
|
+
:is-over="dragDrop.state.value.isOver"
|
|
193
|
+
:is-uploading="isUploading"
|
|
194
|
+
:errors="[...dragDrop.state.value.errors]"
|
|
195
|
+
:open-file-picker="openFilePicker"
|
|
196
|
+
>
|
|
197
|
+
<!-- Default slot content -->
|
|
198
|
+
<div class="upload-zone__content">
|
|
199
|
+
<p v-if="dragDrop.state.value.isDragging">
|
|
200
|
+
{{ multiple ? 'Drop files here...' : 'Drop file here...' }}
|
|
201
|
+
</p>
|
|
202
|
+
<p v-else>
|
|
203
|
+
{{ multiple ? 'Drag files here or click to select' : 'Drag a file here or click to select' }}
|
|
204
|
+
</p>
|
|
205
|
+
|
|
206
|
+
<div v-if="dragDrop.state.value.errors.length > 0" class="upload-zone__errors">
|
|
207
|
+
<p v-for="(error, index) in dragDrop.state.value.errors" :key="index">
|
|
208
|
+
{{ error }}
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</slot>
|
|
213
|
+
|
|
214
|
+
<input
|
|
215
|
+
ref="fileInputRef"
|
|
216
|
+
type="file"
|
|
217
|
+
:multiple="dragDrop.inputProps.value.multiple"
|
|
218
|
+
:accept="dragDrop.inputProps.value.accept"
|
|
219
|
+
:disabled="disabled"
|
|
220
|
+
@change="dragDrop.onInputChange"
|
|
221
|
+
style="display: none"
|
|
222
|
+
aria-hidden="true"
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
</template>
|
|
226
|
+
|
|
227
|
+
<style scoped>
|
|
228
|
+
.upload-zone {
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
user-select: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.upload-zone--disabled {
|
|
234
|
+
cursor: not-allowed;
|
|
235
|
+
opacity: 0.6;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.upload-zone__content {
|
|
239
|
+
display: flex;
|
|
240
|
+
flex-direction: column;
|
|
241
|
+
align-items: center;
|
|
242
|
+
justify-content: center;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.upload-zone__errors {
|
|
246
|
+
margin-top: 0.5rem;
|
|
247
|
+
color: #dc3545;
|
|
248
|
+
font-size: 0.875rem;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.upload-zone__errors p {
|
|
252
|
+
margin: 0.25rem 0;
|
|
253
|
+
}
|
|
254
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue 3 Components for Uploadista
|
|
3
|
+
*
|
|
4
|
+
* These components provide ready-to-use UI elements for file uploads and flow processing.
|
|
5
|
+
* They integrate with the Uploadista composables and are designed to be flexible and customizable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { default as FlowUploadList } from "./FlowUploadList.vue";
|
|
9
|
+
export { default as FlowUploadZone } from "./FlowUploadZone.vue";
|
|
10
|
+
export { default as UploadList } from "./UploadList.vue";
|
|
11
|
+
export { default as UploadZone } from "./UploadZone.vue";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Plugin
|
|
2
|
+
|
|
3
|
+
export type { UploadistaPluginOptions } from "./plugin";
|
|
4
|
+
export { createUploadistaPlugin, UPLOADISTA_CLIENT_KEY } from "./plugin";
|
|
5
|
+
export type {
|
|
6
|
+
DragDropOptions,
|
|
7
|
+
DragDropState,
|
|
8
|
+
} from "./useDragDrop";
|
|
9
|
+
// Drag and drop composable
|
|
10
|
+
export { useDragDrop } from "./useDragDrop";
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
FlowUploadState,
|
|
14
|
+
FlowUploadStatus,
|
|
15
|
+
} from "./useFlowUpload";
|
|
16
|
+
// Flow upload composables
|
|
17
|
+
export { useFlowUpload } from "./useFlowUpload";
|
|
18
|
+
export { useMultiFlowUpload } from "./useMultiFlowUpload";
|
|
19
|
+
export type {
|
|
20
|
+
MultiUploadOptions,
|
|
21
|
+
MultiUploadState,
|
|
22
|
+
UploadItem,
|
|
23
|
+
} from "./useMultiUpload";
|
|
24
|
+
export { useMultiUpload } from "./useMultiUpload";
|
|
25
|
+
export type {
|
|
26
|
+
ChunkMetrics,
|
|
27
|
+
PerformanceInsights,
|
|
28
|
+
UploadInput,
|
|
29
|
+
UploadMetrics,
|
|
30
|
+
UploadSessionMetrics,
|
|
31
|
+
UploadState,
|
|
32
|
+
UploadStatus,
|
|
33
|
+
} from "./useUpload";
|
|
34
|
+
// Upload composables
|
|
35
|
+
export { useUpload } from "./useUpload";
|
|
36
|
+
export type { UseUploadistaClientReturn } from "./useUploadistaClient";
|
|
37
|
+
// Core client composable
|
|
38
|
+
export { useUploadistaClient } from "./useUploadistaClient";
|
|
39
|
+
export type {
|
|
40
|
+
FileUploadMetrics,
|
|
41
|
+
UseUploadMetricsOptions,
|
|
42
|
+
} from "./useUploadMetrics";
|
|
43
|
+
// Metrics composable
|
|
44
|
+
export { useUploadMetrics } from "./useUploadMetrics";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createUploadistaClient,
|
|
3
|
+
type UploadistaClientOptions,
|
|
4
|
+
type UploadistaEvent,
|
|
5
|
+
} from "@uploadista/client-browser";
|
|
6
|
+
import type { App, InjectionKey, Ref } from "vue";
|
|
7
|
+
import { ref } from "vue";
|
|
8
|
+
|
|
9
|
+
export interface UploadistaPluginOptions extends UploadistaClientOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Global event handler for all upload and flow events from this client
|
|
12
|
+
*/
|
|
13
|
+
onEvent?: UploadistaClientOptions["onEvent"];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const UPLOADISTA_CLIENT_KEY: InjectionKey<
|
|
17
|
+
ReturnType<typeof createUploadistaClient>
|
|
18
|
+
> = Symbol("uploadista-client");
|
|
19
|
+
|
|
20
|
+
export const UPLOADISTA_EVENT_SUBSCRIBERS_KEY: InjectionKey<
|
|
21
|
+
Ref<Set<(event: UploadistaEvent) => void>>
|
|
22
|
+
> = Symbol("uploadista-event-subscribers");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Vue plugin for providing Uploadista client instance globally.
|
|
26
|
+
* Uses Vue's provide/inject pattern to make the client available
|
|
27
|
+
* throughout the component tree.
|
|
28
|
+
*
|
|
29
|
+
* @param options - Uploadista client configuration options
|
|
30
|
+
* @returns Vue plugin object
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { createApp } from 'vue';
|
|
35
|
+
* import { createUploadistaPlugin } from '@uploadista/vue';
|
|
36
|
+
* import App from './App.vue';
|
|
37
|
+
*
|
|
38
|
+
* const app = createApp(App);
|
|
39
|
+
*
|
|
40
|
+
* app.use(createUploadistaPlugin({
|
|
41
|
+
* baseUrl: 'https://api.example.com',
|
|
42
|
+
* storageId: 'my-storage',
|
|
43
|
+
* chunkSize: 1024 * 1024, // 1MB
|
|
44
|
+
* storeFingerprintForResuming: true,
|
|
45
|
+
* onEvent: (event) => {
|
|
46
|
+
* console.log('Upload event:', event);
|
|
47
|
+
* }
|
|
48
|
+
* }));
|
|
49
|
+
*
|
|
50
|
+
* app.mount('#app');
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function createUploadistaPlugin(options: UploadistaPluginOptions) {
|
|
54
|
+
return {
|
|
55
|
+
install(app: App) {
|
|
56
|
+
// Create a shared set of event subscribers
|
|
57
|
+
const eventSubscribers = ref(new Set<(event: UploadistaEvent) => void>());
|
|
58
|
+
|
|
59
|
+
const client = createUploadistaClient({
|
|
60
|
+
...options,
|
|
61
|
+
onEvent: (event) => {
|
|
62
|
+
// Dispatch to all subscribers registered via subscribeToEvents
|
|
63
|
+
eventSubscribers.value.forEach((subscriber) => {
|
|
64
|
+
subscriber(event);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Call the original onEvent handler if provided
|
|
68
|
+
options.onEvent?.(event);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
app.provide(UPLOADISTA_CLIENT_KEY, client);
|
|
73
|
+
app.provide(UPLOADISTA_EVENT_SUBSCRIBERS_KEY, eventSubscribers);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|