@uploadista/react 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 +89 -0
- package/FLOW_UPLOAD.md +307 -0
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/package.json +35 -0
- package/src/components/flow-upload-list.tsx +614 -0
- package/src/components/flow-upload-zone.tsx +441 -0
- package/src/components/upload-list.tsx +626 -0
- package/src/components/upload-zone.tsx +545 -0
- package/src/components/uploadista-provider.tsx +190 -0
- package/src/hooks/use-drag-drop.ts +404 -0
- package/src/hooks/use-flow-upload.ts +568 -0
- package/src/hooks/use-multi-flow-upload.ts +477 -0
- package/src/hooks/use-multi-upload.ts +691 -0
- package/src/hooks/use-upload-metrics.ts +585 -0
- package/src/hooks/use-upload.ts +411 -0
- package/src/hooks/use-uploadista-client.ts +145 -0
- package/src/index.ts +87 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { UploadistaEvent } from "@uploadista/client-browser";
|
|
3
|
+
import type React from "react";
|
|
4
|
+
import { createContext, useCallback, useContext, useMemo, useRef } from "react";
|
|
5
|
+
import {
|
|
6
|
+
type UseUploadistaClientOptions,
|
|
7
|
+
type UseUploadistaClientReturn,
|
|
8
|
+
useUploadistaClient,
|
|
9
|
+
} from "../hooks/use-uploadista-client";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Props for the UploadistaProvider component.
|
|
13
|
+
* Combines client configuration options with React children.
|
|
14
|
+
*
|
|
15
|
+
* @property children - React components that will have access to the upload client context
|
|
16
|
+
* @property baseUrl - API base URL for uploads
|
|
17
|
+
* @property storageId - Default storage identifier
|
|
18
|
+
* @property chunkSize - Upload chunk size in bytes
|
|
19
|
+
* @property onEvent - Global event handler for all upload events
|
|
20
|
+
* @property ... - All other UploadistaClientOptions
|
|
21
|
+
*/
|
|
22
|
+
export interface UploadistaProviderProps extends UseUploadistaClientOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Children components that will have access to the upload client
|
|
25
|
+
*/
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type UploadistaContextValue = UseUploadistaClientReturn & {
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to events (used internally by hooks)
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
subscribeToEvents: (handler: (event: UploadistaEvent) => void) => () => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const UploadistaContext = createContext<UploadistaContextValue | null>(null);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Context provider that provides uploadista client functionality to child components.
|
|
41
|
+
* This eliminates the need to pass upload client configuration down through props
|
|
42
|
+
* and ensures a single, shared upload client instance across your application.
|
|
43
|
+
*
|
|
44
|
+
* @param props - Upload client options and children
|
|
45
|
+
* @returns Provider component with upload client context
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* // Wrap your app with the upload provider
|
|
50
|
+
* function App() {
|
|
51
|
+
* return (
|
|
52
|
+
* <UploadistaProvider
|
|
53
|
+
* baseUrl="https://api.example.com"
|
|
54
|
+
* storageId="my-storage"
|
|
55
|
+
* chunkSize={1024 * 1024} // 1MB chunks
|
|
56
|
+
* onEvent={(event) => {
|
|
57
|
+
* console.log('Global upload event:', event);
|
|
58
|
+
* }}
|
|
59
|
+
* >
|
|
60
|
+
* <UploadInterface />
|
|
61
|
+
* </UploadistaProvider>
|
|
62
|
+
* );
|
|
63
|
+
* }
|
|
64
|
+
*
|
|
65
|
+
* // Use the upload client in any child component
|
|
66
|
+
* function UploadInterface() {
|
|
67
|
+
* const uploadClient = useUploadistaContext();
|
|
68
|
+
* const upload = useUpload(uploadClient);
|
|
69
|
+
* const dragDrop = useDragDrop({
|
|
70
|
+
* onFilesReceived: (files) => {
|
|
71
|
+
* files.forEach(file => upload.upload(file));
|
|
72
|
+
* }
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* return (
|
|
76
|
+
* <div {...dragDrop.dragHandlers}>
|
|
77
|
+
* <p>Drop files here to upload</p>
|
|
78
|
+
* {upload.isUploading && <p>Progress: {upload.state.progress}%</p>}
|
|
79
|
+
* </div>
|
|
80
|
+
* );
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function UploadistaProvider({
|
|
85
|
+
children,
|
|
86
|
+
...options
|
|
87
|
+
}: UploadistaProviderProps) {
|
|
88
|
+
const eventSubscribersRef = useRef<Set<(event: UploadistaEvent) => void>>(
|
|
89
|
+
new Set(),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Wrap the original onEvent to broadcast to subscribers
|
|
93
|
+
const wrappedOnEvent = useCallback(
|
|
94
|
+
(event: UploadistaEvent) => {
|
|
95
|
+
console.log("[UploadistaProvider] Received event:", event);
|
|
96
|
+
|
|
97
|
+
// Call original handler if provided
|
|
98
|
+
options.onEvent?.(event);
|
|
99
|
+
|
|
100
|
+
// Broadcast to all subscribers
|
|
101
|
+
console.log(
|
|
102
|
+
"[UploadistaProvider] Broadcasting to",
|
|
103
|
+
eventSubscribersRef.current.size,
|
|
104
|
+
"subscribers",
|
|
105
|
+
);
|
|
106
|
+
eventSubscribersRef.current.forEach((handler) => {
|
|
107
|
+
try {
|
|
108
|
+
handler(event);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error("Error in event subscriber:", err);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
[options.onEvent],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const uploadClient = useUploadistaClient({
|
|
118
|
+
...options,
|
|
119
|
+
onEvent: wrappedOnEvent,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const subscribeToEvents = useCallback(
|
|
123
|
+
(handler: (event: UploadistaEvent) => void) => {
|
|
124
|
+
eventSubscribersRef.current.add(handler);
|
|
125
|
+
return () => {
|
|
126
|
+
eventSubscribersRef.current.delete(handler);
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
[],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Memoize the context value to prevent unnecessary re-renders
|
|
133
|
+
const contextValue = useMemo(
|
|
134
|
+
() => ({
|
|
135
|
+
...uploadClient,
|
|
136
|
+
subscribeToEvents,
|
|
137
|
+
}),
|
|
138
|
+
[uploadClient, subscribeToEvents],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<UploadistaContext.Provider value={contextValue}>
|
|
143
|
+
{children}
|
|
144
|
+
</UploadistaContext.Provider>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Hook to access the uploadista client from the UploadistaProvider context.
|
|
150
|
+
* Must be used within an UploadistaProvider component.
|
|
151
|
+
*
|
|
152
|
+
* @returns Upload client instance from context
|
|
153
|
+
* @throws Error if used outside of UploadistaProvider
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```tsx
|
|
157
|
+
* function FileUploader() {
|
|
158
|
+
* const uploadClient = useUploadistaContext();
|
|
159
|
+
* const upload = useUpload(uploadClient);
|
|
160
|
+
*
|
|
161
|
+
* return (
|
|
162
|
+
* <button
|
|
163
|
+
* onClick={() => {
|
|
164
|
+
* const input = document.createElement('input');
|
|
165
|
+
* input.type = 'file';
|
|
166
|
+
* input.onchange = (e) => {
|
|
167
|
+
* const file = (e.target as HTMLInputElement).files?.[0];
|
|
168
|
+
* if (file) upload.upload(file);
|
|
169
|
+
* };
|
|
170
|
+
* input.click();
|
|
171
|
+
* }}
|
|
172
|
+
* >
|
|
173
|
+
* Upload File
|
|
174
|
+
* </button>
|
|
175
|
+
* );
|
|
176
|
+
* }
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export function useUploadistaContext(): UploadistaContextValue {
|
|
180
|
+
const context = useContext(UploadistaContext);
|
|
181
|
+
|
|
182
|
+
if (context === null) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
"useUploadistaContext must be used within an UploadistaProvider. " +
|
|
185
|
+
"Make sure to wrap your component tree with <UploadistaProvider>.",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return context;
|
|
190
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
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
|
+
export interface UseDragDropReturn {
|
|
68
|
+
/**
|
|
69
|
+
* Current drag and drop state
|
|
70
|
+
*/
|
|
71
|
+
state: DragDropState;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Event handlers for the drop zone element
|
|
75
|
+
*/
|
|
76
|
+
dragHandlers: {
|
|
77
|
+
onDragEnter: (event: React.DragEvent) => void;
|
|
78
|
+
onDragOver: (event: React.DragEvent) => void;
|
|
79
|
+
onDragLeave: (event: React.DragEvent) => void;
|
|
80
|
+
onDrop: (event: React.DragEvent) => void;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Props for a file input element
|
|
85
|
+
*/
|
|
86
|
+
inputProps: {
|
|
87
|
+
type: "file";
|
|
88
|
+
multiple: boolean;
|
|
89
|
+
accept?: string;
|
|
90
|
+
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
91
|
+
style: { display: "none" };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Open file picker dialog
|
|
96
|
+
*/
|
|
97
|
+
openFilePicker: () => void;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Manually process files (useful for programmatic file handling)
|
|
101
|
+
*/
|
|
102
|
+
processFiles: (files: File[]) => void;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reset drag state
|
|
106
|
+
*/
|
|
107
|
+
reset: () => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const initialState: DragDropState = {
|
|
111
|
+
isDragging: false,
|
|
112
|
+
isOver: false,
|
|
113
|
+
isValid: true,
|
|
114
|
+
errors: [],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* React hook for handling drag and drop file uploads with validation.
|
|
119
|
+
* Provides drag state management, file validation, and file picker integration.
|
|
120
|
+
*
|
|
121
|
+
* @param options - Configuration and event handlers
|
|
122
|
+
* @returns Drag and drop state and handlers
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```tsx
|
|
126
|
+
* const dragDrop = useDragDrop({
|
|
127
|
+
* accept: ['image/*', '.pdf'],
|
|
128
|
+
* maxFiles: 5,
|
|
129
|
+
* maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
130
|
+
* multiple: true,
|
|
131
|
+
* onFilesReceived: (files) => {
|
|
132
|
+
* console.log('Received files:', files);
|
|
133
|
+
* // Process files with upload hooks
|
|
134
|
+
* },
|
|
135
|
+
* onValidationError: (errors) => {
|
|
136
|
+
* console.error('Validation errors:', errors);
|
|
137
|
+
* },
|
|
138
|
+
* });
|
|
139
|
+
*
|
|
140
|
+
* return (
|
|
141
|
+
* <div>
|
|
142
|
+
* <div
|
|
143
|
+
* {...dragDrop.dragHandlers}
|
|
144
|
+
* style={{
|
|
145
|
+
* border: dragDrop.state.isDragging ? '2px dashed #007bff' : '2px dashed #ccc',
|
|
146
|
+
* backgroundColor: dragDrop.state.isOver ? '#f8f9fa' : 'transparent',
|
|
147
|
+
* padding: '2rem',
|
|
148
|
+
* textAlign: 'center',
|
|
149
|
+
* cursor: 'pointer',
|
|
150
|
+
* }}
|
|
151
|
+
* onClick={dragDrop.openFilePicker}
|
|
152
|
+
* >
|
|
153
|
+
* {dragDrop.state.isDragging ? (
|
|
154
|
+
* <p>Drop files here...</p>
|
|
155
|
+
* ) : (
|
|
156
|
+
* <p>Drag files here or click to select</p>
|
|
157
|
+
* )}
|
|
158
|
+
*
|
|
159
|
+
* {dragDrop.state.errors.length > 0 && (
|
|
160
|
+
* <div style={{ color: 'red', marginTop: '1rem' }}>
|
|
161
|
+
* {dragDrop.state.errors.map((error, index) => (
|
|
162
|
+
* <p key={index}>{error}</p>
|
|
163
|
+
* ))}
|
|
164
|
+
* </div>
|
|
165
|
+
* )}
|
|
166
|
+
* </div>
|
|
167
|
+
*
|
|
168
|
+
* <input {...dragDrop.inputProps} />
|
|
169
|
+
* </div>
|
|
170
|
+
* );
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export function useDragDrop(options: DragDropOptions = {}): UseDragDropReturn {
|
|
174
|
+
const {
|
|
175
|
+
accept,
|
|
176
|
+
maxFiles,
|
|
177
|
+
maxFileSize,
|
|
178
|
+
multiple = true,
|
|
179
|
+
validator,
|
|
180
|
+
onFilesReceived,
|
|
181
|
+
onValidationError,
|
|
182
|
+
onDragStateChange,
|
|
183
|
+
} = options;
|
|
184
|
+
|
|
185
|
+
const [state, setState] = useState<DragDropState>(initialState);
|
|
186
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
187
|
+
const dragCounterRef = useRef(0);
|
|
188
|
+
|
|
189
|
+
const updateState = useCallback((update: Partial<DragDropState>) => {
|
|
190
|
+
setState((prev) => ({ ...prev, ...update }));
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
const validateFiles = useCallback(
|
|
194
|
+
(files: File[]): string[] => {
|
|
195
|
+
const errors: string[] = [];
|
|
196
|
+
|
|
197
|
+
// Check file count
|
|
198
|
+
if (maxFiles && files.length > maxFiles) {
|
|
199
|
+
errors.push(
|
|
200
|
+
`Maximum ${maxFiles} files allowed. You selected ${files.length} files.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check individual files
|
|
205
|
+
for (const file of files) {
|
|
206
|
+
// Check file size
|
|
207
|
+
if (maxFileSize && file.size > maxFileSize) {
|
|
208
|
+
const maxSizeMB = (maxFileSize / (1024 * 1024)).toFixed(1);
|
|
209
|
+
const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1);
|
|
210
|
+
errors.push(
|
|
211
|
+
`File "${file.name}" (${fileSizeMB}MB) exceeds maximum size of ${maxSizeMB}MB.`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check file type
|
|
216
|
+
if (accept && accept.length > 0) {
|
|
217
|
+
const isAccepted = accept.some((acceptType) => {
|
|
218
|
+
if (acceptType.startsWith(".")) {
|
|
219
|
+
// File extension check
|
|
220
|
+
return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
|
|
221
|
+
} else {
|
|
222
|
+
// MIME type check (supports wildcards like image/*)
|
|
223
|
+
if (acceptType.endsWith("/*")) {
|
|
224
|
+
const baseType = acceptType.slice(0, -2);
|
|
225
|
+
return file.type.startsWith(baseType);
|
|
226
|
+
} else {
|
|
227
|
+
return file.type === acceptType;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (!isAccepted) {
|
|
233
|
+
errors.push(
|
|
234
|
+
`File "${file.name}" type "${file.type}" is not accepted. Accepted types: ${accept.join(", ")}.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Run custom validator
|
|
241
|
+
if (validator) {
|
|
242
|
+
const customErrors = validator(files);
|
|
243
|
+
if (customErrors) {
|
|
244
|
+
errors.push(...customErrors);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return errors;
|
|
249
|
+
},
|
|
250
|
+
[accept, maxFiles, maxFileSize, validator],
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const processFiles = useCallback(
|
|
254
|
+
(files: File[]) => {
|
|
255
|
+
const fileArray = Array.from(files);
|
|
256
|
+
const errors = validateFiles(fileArray);
|
|
257
|
+
|
|
258
|
+
if (errors.length > 0) {
|
|
259
|
+
updateState({ errors, isValid: false });
|
|
260
|
+
onValidationError?.(errors);
|
|
261
|
+
} else {
|
|
262
|
+
updateState({ errors: [], isValid: true });
|
|
263
|
+
onFilesReceived?.(fileArray);
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
[validateFiles, updateState, onFilesReceived, onValidationError],
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const getFilesFromDataTransfer = useCallback(
|
|
270
|
+
(dataTransfer: DataTransfer): File[] => {
|
|
271
|
+
const files: File[] = [];
|
|
272
|
+
|
|
273
|
+
if (dataTransfer.items) {
|
|
274
|
+
// Use DataTransferItemList interface
|
|
275
|
+
for (let i = 0; i < dataTransfer.items.length; i++) {
|
|
276
|
+
const item = dataTransfer.items[i];
|
|
277
|
+
if (item && item.kind === "file") {
|
|
278
|
+
const file = item.getAsFile();
|
|
279
|
+
if (file) {
|
|
280
|
+
files.push(file);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
// Fallback to DataTransfer.files
|
|
286
|
+
for (let i = 0; i < dataTransfer.files.length; i++) {
|
|
287
|
+
const file = dataTransfer.files[i];
|
|
288
|
+
if (file) {
|
|
289
|
+
files.push(file);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return files;
|
|
295
|
+
},
|
|
296
|
+
[],
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const onDragEnter = useCallback(
|
|
300
|
+
(event: React.DragEvent) => {
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
event.stopPropagation();
|
|
303
|
+
|
|
304
|
+
dragCounterRef.current++;
|
|
305
|
+
|
|
306
|
+
if (dragCounterRef.current === 1) {
|
|
307
|
+
updateState({ isDragging: true, isOver: true });
|
|
308
|
+
onDragStateChange?.(true);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
[updateState, onDragStateChange],
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const onDragOver = useCallback((event: React.DragEvent) => {
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
event.stopPropagation();
|
|
317
|
+
|
|
318
|
+
// Set dropEffect to indicate what operation is allowed
|
|
319
|
+
if (event.dataTransfer) {
|
|
320
|
+
event.dataTransfer.dropEffect = "copy";
|
|
321
|
+
}
|
|
322
|
+
}, []);
|
|
323
|
+
|
|
324
|
+
const onDragLeave = useCallback(
|
|
325
|
+
(event: React.DragEvent) => {
|
|
326
|
+
event.preventDefault();
|
|
327
|
+
event.stopPropagation();
|
|
328
|
+
|
|
329
|
+
dragCounterRef.current--;
|
|
330
|
+
|
|
331
|
+
if (dragCounterRef.current === 0) {
|
|
332
|
+
updateState({ isDragging: false, isOver: false, errors: [] });
|
|
333
|
+
onDragStateChange?.(false);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
[updateState, onDragStateChange],
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const onDrop = useCallback(
|
|
340
|
+
(event: React.DragEvent) => {
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
event.stopPropagation();
|
|
343
|
+
|
|
344
|
+
dragCounterRef.current = 0;
|
|
345
|
+
updateState({ isDragging: false, isOver: false });
|
|
346
|
+
onDragStateChange?.(false);
|
|
347
|
+
|
|
348
|
+
if (event.dataTransfer) {
|
|
349
|
+
const files = getFilesFromDataTransfer(event.dataTransfer);
|
|
350
|
+
if (files.length > 0) {
|
|
351
|
+
processFiles(files);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
[updateState, onDragStateChange, getFilesFromDataTransfer, processFiles],
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const openFilePicker = useCallback(() => {
|
|
359
|
+
inputRef.current?.click();
|
|
360
|
+
}, []);
|
|
361
|
+
|
|
362
|
+
const onInputChange = useCallback(
|
|
363
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
364
|
+
if (event.target.files && event.target.files.length > 0) {
|
|
365
|
+
const files = Array.from(event.target.files);
|
|
366
|
+
processFiles(files);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Reset input value to allow selecting the same files again
|
|
370
|
+
event.target.value = "";
|
|
371
|
+
},
|
|
372
|
+
[processFiles],
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const reset = useCallback(() => {
|
|
376
|
+
setState(initialState);
|
|
377
|
+
dragCounterRef.current = 0;
|
|
378
|
+
}, []);
|
|
379
|
+
|
|
380
|
+
const dragHandlers = {
|
|
381
|
+
onDragEnter,
|
|
382
|
+
onDragOver,
|
|
383
|
+
onDragLeave,
|
|
384
|
+
onDrop,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const inputProps = {
|
|
388
|
+
type: "file" as const,
|
|
389
|
+
multiple,
|
|
390
|
+
accept: accept?.join(", "),
|
|
391
|
+
onChange: onInputChange,
|
|
392
|
+
style: { display: "none" as const },
|
|
393
|
+
ref: inputRef,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
state,
|
|
398
|
+
dragHandlers,
|
|
399
|
+
inputProps,
|
|
400
|
+
openFilePicker,
|
|
401
|
+
processFiles,
|
|
402
|
+
reset,
|
|
403
|
+
};
|
|
404
|
+
}
|