@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.
@@ -0,0 +1,343 @@
1
+ import { computed, readonly, ref } from "vue";
2
+
3
+ export interface DragDropOptions {
4
+ /**
5
+ * Accept specific file types (MIME types or file extensions)
6
+ */
7
+ accept?: string[];
8
+
9
+ /**
10
+ * Maximum number of files allowed
11
+ */
12
+ maxFiles?: number;
13
+
14
+ /**
15
+ * Maximum file size in bytes
16
+ */
17
+ maxFileSize?: number;
18
+
19
+ /**
20
+ * Whether to allow multiple files
21
+ */
22
+ multiple?: boolean;
23
+
24
+ /**
25
+ * Custom validation function for files
26
+ */
27
+ validator?: (files: File[]) => string[] | null;
28
+
29
+ /**
30
+ * Called when files are dropped or selected
31
+ */
32
+ onFilesReceived?: (files: File[]) => void;
33
+
34
+ /**
35
+ * Called when validation fails
36
+ */
37
+ onValidationError?: (errors: string[]) => void;
38
+
39
+ /**
40
+ * Called when drag state changes
41
+ */
42
+ onDragStateChange?: (isDragging: boolean) => void;
43
+ }
44
+
45
+ export interface DragDropState {
46
+ /**
47
+ * Whether files are currently being dragged over the drop zone
48
+ */
49
+ isDragging: boolean;
50
+
51
+ /**
52
+ * Whether the drag is currently over the drop zone
53
+ */
54
+ isOver: boolean;
55
+
56
+ /**
57
+ * Whether the dragged items are valid files
58
+ */
59
+ isValid: boolean;
60
+
61
+ /**
62
+ * Current validation errors
63
+ */
64
+ errors: string[];
65
+ }
66
+
67
+ const initialState: DragDropState = {
68
+ isDragging: false,
69
+ isOver: false,
70
+ isValid: true,
71
+ errors: [],
72
+ };
73
+
74
+ /**
75
+ * Vue composable for handling drag and drop file uploads with validation.
76
+ * Provides drag state management, file validation, and file picker integration.
77
+ *
78
+ * @param options - Configuration and event handlers
79
+ * @returns Drag and drop state and handlers
80
+ *
81
+ * @example
82
+ * ```vue
83
+ * <script setup lang="ts">
84
+ * import { useDragDrop } from '@uploadista/vue';
85
+ * import { ref } from 'vue';
86
+ *
87
+ * const inputRef = ref<HTMLInputElement>();
88
+ *
89
+ * const dragDrop = useDragDrop({
90
+ * accept: ['image/*', '.pdf'],
91
+ * maxFiles: 5,
92
+ * maxFileSize: 10 * 1024 * 1024, // 10MB
93
+ * multiple: true,
94
+ * onFilesReceived: (files) => {
95
+ * console.log('Received files:', files);
96
+ * // Process files with upload composables
97
+ * },
98
+ * onValidationError: (errors) => {
99
+ * console.error('Validation errors:', errors);
100
+ * },
101
+ * });
102
+ *
103
+ * const openFilePicker = () => {
104
+ * inputRef.value?.click();
105
+ * };
106
+ * </script>
107
+ *
108
+ * <template>
109
+ * <div>
110
+ * <div
111
+ * @dragenter="dragDrop.onDragEnter"
112
+ * @dragover="dragDrop.onDragOver"
113
+ * @dragleave="dragDrop.onDragLeave"
114
+ * @drop="dragDrop.onDrop"
115
+ * @click="openFilePicker"
116
+ * :style="{
117
+ * border: dragDrop.state.isDragging ? '2px dashed #007bff' : '2px dashed #ccc',
118
+ * backgroundColor: dragDrop.state.isOver ? '#f8f9fa' : 'transparent',
119
+ * padding: '2rem',
120
+ * textAlign: 'center',
121
+ * cursor: 'pointer',
122
+ * }"
123
+ * >
124
+ * <p v-if="dragDrop.state.isDragging">Drop files here...</p>
125
+ * <p v-else>Drag files here or click to select</p>
126
+ *
127
+ * <div v-if="dragDrop.state.errors.length > 0" style="color: red; margin-top: 1rem">
128
+ * <p v-for="(error, index) in dragDrop.state.errors" :key="index">{{ error }}</p>
129
+ * </div>
130
+ * </div>
131
+ *
132
+ * <input
133
+ * ref="inputRef"
134
+ * type="file"
135
+ * :multiple="dragDrop.inputProps.multiple"
136
+ * :accept="dragDrop.inputProps.accept"
137
+ * @change="dragDrop.onInputChange"
138
+ * style="display: none"
139
+ * />
140
+ * </div>
141
+ * </template>
142
+ * ```
143
+ */
144
+ export function useDragDrop(options: DragDropOptions = {}) {
145
+ const {
146
+ accept,
147
+ maxFiles,
148
+ maxFileSize,
149
+ multiple = true,
150
+ validator,
151
+ onFilesReceived,
152
+ onValidationError,
153
+ onDragStateChange,
154
+ } = options;
155
+
156
+ const state = ref<DragDropState>({ ...initialState });
157
+ const dragCounter = ref(0);
158
+
159
+ const updateState = (update: Partial<DragDropState>) => {
160
+ state.value = { ...state.value, ...update };
161
+ };
162
+
163
+ const validateFiles = (files: File[]): string[] => {
164
+ const errors: string[] = [];
165
+
166
+ // Check file count
167
+ if (maxFiles && files.length > maxFiles) {
168
+ errors.push(
169
+ `Maximum ${maxFiles} files allowed. You selected ${files.length} files.`,
170
+ );
171
+ }
172
+
173
+ // Check individual files
174
+ for (const file of files) {
175
+ // Check file size
176
+ if (maxFileSize && file.size > maxFileSize) {
177
+ const maxSizeMB = (maxFileSize / (1024 * 1024)).toFixed(1);
178
+ const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1);
179
+ errors.push(
180
+ `File "${file.name}" (${fileSizeMB}MB) exceeds maximum size of ${maxSizeMB}MB.`,
181
+ );
182
+ }
183
+
184
+ // Check file type
185
+ if (accept && accept.length > 0) {
186
+ const isAccepted = accept.some((acceptType) => {
187
+ if (acceptType.startsWith(".")) {
188
+ // File extension check
189
+ return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
190
+ } else {
191
+ // MIME type check (supports wildcards like image/*)
192
+ if (acceptType.endsWith("/*")) {
193
+ const baseType = acceptType.slice(0, -2);
194
+ return file.type.startsWith(baseType);
195
+ } else {
196
+ return file.type === acceptType;
197
+ }
198
+ }
199
+ });
200
+
201
+ if (!isAccepted) {
202
+ errors.push(
203
+ `File "${file.name}" type "${file.type}" is not accepted. Accepted types: ${accept.join(", ")}.`,
204
+ );
205
+ }
206
+ }
207
+ }
208
+
209
+ // Run custom validator
210
+ if (validator) {
211
+ const customErrors = validator(files);
212
+ if (customErrors) {
213
+ errors.push(...customErrors);
214
+ }
215
+ }
216
+
217
+ return errors;
218
+ };
219
+
220
+ const processFiles = (files: File[]) => {
221
+ const fileArray = Array.from(files);
222
+ const errors = validateFiles(fileArray);
223
+
224
+ if (errors.length > 0) {
225
+ updateState({ errors, isValid: false });
226
+ onValidationError?.(errors);
227
+ } else {
228
+ updateState({ errors: [], isValid: true });
229
+ onFilesReceived?.(fileArray);
230
+ }
231
+ };
232
+
233
+ const getFilesFromDataTransfer = (dataTransfer: DataTransfer): File[] => {
234
+ const files: File[] = [];
235
+
236
+ if (dataTransfer.items) {
237
+ // Use DataTransferItemList interface
238
+ for (let i = 0; i < dataTransfer.items.length; i++) {
239
+ const item = dataTransfer.items[i];
240
+ if (item && item.kind === "file") {
241
+ const file = item.getAsFile();
242
+ if (file) {
243
+ files.push(file);
244
+ }
245
+ }
246
+ }
247
+ } else {
248
+ // Fallback to DataTransfer.files
249
+ for (let i = 0; i < dataTransfer.files.length; i++) {
250
+ const file = dataTransfer.files[i];
251
+ if (file) {
252
+ files.push(file);
253
+ }
254
+ }
255
+ }
256
+
257
+ return files;
258
+ };
259
+
260
+ const onDragEnter = (event: DragEvent) => {
261
+ event.preventDefault();
262
+ event.stopPropagation();
263
+
264
+ dragCounter.value++;
265
+
266
+ if (dragCounter.value === 1) {
267
+ updateState({ isDragging: true, isOver: true });
268
+ onDragStateChange?.(true);
269
+ }
270
+ };
271
+
272
+ const onDragOver = (event: DragEvent) => {
273
+ event.preventDefault();
274
+ event.stopPropagation();
275
+
276
+ // Set dropEffect to indicate what operation is allowed
277
+ if (event.dataTransfer) {
278
+ event.dataTransfer.dropEffect = "copy";
279
+ }
280
+ };
281
+
282
+ const onDragLeave = (event: DragEvent) => {
283
+ event.preventDefault();
284
+ event.stopPropagation();
285
+
286
+ dragCounter.value--;
287
+
288
+ if (dragCounter.value === 0) {
289
+ updateState({ isDragging: false, isOver: false, errors: [] });
290
+ onDragStateChange?.(false);
291
+ }
292
+ };
293
+
294
+ const onDrop = (event: DragEvent) => {
295
+ event.preventDefault();
296
+ event.stopPropagation();
297
+
298
+ dragCounter.value = 0;
299
+ updateState({ isDragging: false, isOver: false });
300
+ onDragStateChange?.(false);
301
+
302
+ if (event.dataTransfer) {
303
+ const files = getFilesFromDataTransfer(event.dataTransfer);
304
+ if (files.length > 0) {
305
+ processFiles(files);
306
+ }
307
+ }
308
+ };
309
+
310
+ const onInputChange = (event: Event) => {
311
+ const input = event.target as HTMLInputElement;
312
+ if (input.files && input.files.length > 0) {
313
+ const files = Array.from(input.files);
314
+ processFiles(files);
315
+ }
316
+
317
+ // Reset input value to allow selecting the same files again
318
+ input.value = "";
319
+ };
320
+
321
+ const reset = () => {
322
+ state.value = { ...initialState };
323
+ dragCounter.value = 0;
324
+ };
325
+
326
+ const inputProps = computed(() => ({
327
+ type: "file" as const,
328
+ multiple,
329
+ accept: accept?.join(", "),
330
+ }));
331
+
332
+ return {
333
+ state: readonly(state),
334
+ onDragEnter,
335
+ onDragOver,
336
+ onDragLeave,
337
+ onDrop,
338
+ onInputChange,
339
+ inputProps,
340
+ processFiles,
341
+ reset,
342
+ };
343
+ }