react-dropzone 16.0.0 → 18.0.0
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 +130 -113
- package/dist/index.cjs +43 -435
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +117 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +46 -436
- package/dist/index.js.map +1 -1
- package/package.json +36 -39
- package/src/index.tsx +774 -0
- package/src/utils/index.ts +305 -0
- package/src/index.jsx +0 -1103
- package/src/utils/index.js +0 -362
- package/typings/react-dropzone.d.ts +0 -102
- package/typings/tests/accept.tsx +0 -54
- package/typings/tests/all.tsx +0 -46
- package/typings/tests/basic.tsx +0 -53
- package/typings/tests/events.tsx +0 -31
- package/typings/tests/file-dialog.tsx +0 -20
- package/typings/tests/hook.tsx +0 -15
- package/typings/tests/plugin.tsx +0 -87
- package/typings/tests/refs.tsx +0 -18
- package/typings/tests/tsconfig.json +0 -23
package/src/index.tsx
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import {fromEvent} from "file-selector";
|
|
2
|
+
import type {FileWithPath} from "file-selector";
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
import {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useReducer, useRef} from "react";
|
|
5
|
+
import {
|
|
6
|
+
acceptPropAsAcceptAttr,
|
|
7
|
+
allFilesAccepted,
|
|
8
|
+
canUseFileSystemAccessAPI,
|
|
9
|
+
composeEventHandlers,
|
|
10
|
+
ErrorCode,
|
|
11
|
+
fileAccepted,
|
|
12
|
+
fileMatchSize,
|
|
13
|
+
isAbort,
|
|
14
|
+
isEvtWithFiles,
|
|
15
|
+
isIeOrEdge,
|
|
16
|
+
isPropagationStopped,
|
|
17
|
+
isSecurityError,
|
|
18
|
+
onDocumentDragOver,
|
|
19
|
+
pickerOptionsFromAccept,
|
|
20
|
+
TOO_MANY_FILES_REJECTION
|
|
21
|
+
} from "./utils";
|
|
22
|
+
import type {Accept, FileError} from "./utils";
|
|
23
|
+
|
|
24
|
+
export type {Accept, FileError, FileWithPath};
|
|
25
|
+
export {ErrorCode};
|
|
26
|
+
|
|
27
|
+
export interface DropzoneProps extends DropzoneOptions {
|
|
28
|
+
children?: (state: DropzoneState) => React.ReactElement;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FileRejection {
|
|
32
|
+
file: FileWithPath;
|
|
33
|
+
errors: readonly FileError[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type SharedProps = "multiple" | "onDragEnter" | "onDragOver" | "onDragLeave";
|
|
37
|
+
|
|
38
|
+
export type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> & {
|
|
39
|
+
accept?: Accept;
|
|
40
|
+
minSize?: number;
|
|
41
|
+
maxSize?: number;
|
|
42
|
+
maxFiles?: number;
|
|
43
|
+
preventDropOnDocument?: boolean;
|
|
44
|
+
noClick?: boolean;
|
|
45
|
+
noKeyboard?: boolean;
|
|
46
|
+
noDrag?: boolean;
|
|
47
|
+
noDragEventsBubbling?: boolean;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
onDrop?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
|
|
50
|
+
onDropAccepted?: <T extends File>(files: T[], event: DropEvent) => void;
|
|
51
|
+
onDropRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
|
|
52
|
+
getFilesFromEvent?: (event: DropEvent) => Promise<Array<File | DataTransferItem>>;
|
|
53
|
+
onFileDialogCancel?: () => void;
|
|
54
|
+
onFileDialogOpen?: () => void;
|
|
55
|
+
onError?: (err: Error) => void;
|
|
56
|
+
validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
|
|
57
|
+
useFsAccessApi?: boolean;
|
|
58
|
+
autoFocus?: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type DropEvent =
|
|
62
|
+
| React.DragEvent<HTMLElement>
|
|
63
|
+
| React.ChangeEvent<HTMLInputElement>
|
|
64
|
+
| DragEvent
|
|
65
|
+
| Event
|
|
66
|
+
| Array<FileSystemFileHandle>;
|
|
67
|
+
|
|
68
|
+
export interface DropzoneRef {
|
|
69
|
+
open: () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type DropzoneState = DropzoneRef & {
|
|
73
|
+
isFocused: boolean;
|
|
74
|
+
isDragActive: boolean;
|
|
75
|
+
isDragAccept: boolean;
|
|
76
|
+
isDragReject: boolean;
|
|
77
|
+
isDragGlobal: boolean;
|
|
78
|
+
isFileDialogActive: boolean;
|
|
79
|
+
acceptedFiles: readonly FileWithPath[];
|
|
80
|
+
fileRejections: readonly FileRejection[];
|
|
81
|
+
rootRef: React.RefObject<HTMLElement>;
|
|
82
|
+
inputRef: React.RefObject<HTMLInputElement>;
|
|
83
|
+
getRootProps: <T extends DropzoneRootProps>(props?: T) => T;
|
|
84
|
+
getInputProps: <T extends DropzoneInputProps>(props?: T) => T;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export interface DropzoneRootProps extends React.HTMLAttributes<HTMLElement> {
|
|
88
|
+
refKey?: string;
|
|
89
|
+
[key: string]: any;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface DropzoneInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
93
|
+
refKey?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Convenience wrapper component for the `useDropzone` hook
|
|
98
|
+
*
|
|
99
|
+
* ```jsx
|
|
100
|
+
* <Dropzone>
|
|
101
|
+
* {({getRootProps, getInputProps}) => (
|
|
102
|
+
* <div {...getRootProps()}>
|
|
103
|
+
* <input {...getInputProps()} />
|
|
104
|
+
* <p>Drag 'n' drop some files here, or click to select files</p>
|
|
105
|
+
* </div>
|
|
106
|
+
* )}
|
|
107
|
+
* </Dropzone>
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
const Dropzone: React.ForwardRefExoticComponent<DropzoneProps & React.RefAttributes<DropzoneRef>> = forwardRef<
|
|
111
|
+
DropzoneRef,
|
|
112
|
+
DropzoneProps
|
|
113
|
+
>(({children, ...params}, ref) => {
|
|
114
|
+
const {open, ...props} = useDropzone(params);
|
|
115
|
+
|
|
116
|
+
useImperativeHandle(ref, () => ({open}), [open]);
|
|
117
|
+
|
|
118
|
+
return <>{children?.({...props, open})}</>;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
Dropzone.displayName = "Dropzone";
|
|
122
|
+
|
|
123
|
+
export default Dropzone;
|
|
124
|
+
|
|
125
|
+
interface DropzoneInternalState {
|
|
126
|
+
isFocused: boolean;
|
|
127
|
+
isFileDialogActive: boolean;
|
|
128
|
+
isDragActive: boolean;
|
|
129
|
+
isDragAccept: boolean;
|
|
130
|
+
isDragReject: boolean;
|
|
131
|
+
isDragGlobal: boolean;
|
|
132
|
+
acceptedFiles: FileWithPath[];
|
|
133
|
+
fileRejections: FileRejection[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const initialState: DropzoneInternalState = {
|
|
137
|
+
isFocused: false,
|
|
138
|
+
isFileDialogActive: false,
|
|
139
|
+
isDragActive: false,
|
|
140
|
+
isDragAccept: false,
|
|
141
|
+
isDragReject: false,
|
|
142
|
+
isDragGlobal: false,
|
|
143
|
+
acceptedFiles: [],
|
|
144
|
+
fileRejections: []
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A React hook that creates a drag 'n' drop area.
|
|
149
|
+
*
|
|
150
|
+
* ```jsx
|
|
151
|
+
* function MyDropzone(props) {
|
|
152
|
+
* const {getRootProps, getInputProps} = useDropzone({
|
|
153
|
+
* onDrop: acceptedFiles => {
|
|
154
|
+
* // do something with the File objects, e.g. upload to some server
|
|
155
|
+
* }
|
|
156
|
+
* });
|
|
157
|
+
* return (
|
|
158
|
+
* <div {...getRootProps()}>
|
|
159
|
+
* <input {...getInputProps()} />
|
|
160
|
+
* <p>Drag and drop some files here, or click to select files</p>
|
|
161
|
+
* </div>
|
|
162
|
+
* )
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export function useDropzone(props: DropzoneOptions = {}): DropzoneState {
|
|
167
|
+
const {
|
|
168
|
+
accept,
|
|
169
|
+
disabled = false,
|
|
170
|
+
getFilesFromEvent = fromEvent,
|
|
171
|
+
maxSize = Number.POSITIVE_INFINITY,
|
|
172
|
+
minSize = 0,
|
|
173
|
+
multiple = true,
|
|
174
|
+
maxFiles = 0,
|
|
175
|
+
onDragEnter,
|
|
176
|
+
onDragLeave,
|
|
177
|
+
onDragOver,
|
|
178
|
+
onDrop,
|
|
179
|
+
onDropAccepted,
|
|
180
|
+
onDropRejected,
|
|
181
|
+
onFileDialogCancel,
|
|
182
|
+
onFileDialogOpen,
|
|
183
|
+
useFsAccessApi = false,
|
|
184
|
+
autoFocus = false,
|
|
185
|
+
preventDropOnDocument = true,
|
|
186
|
+
noClick = false,
|
|
187
|
+
noKeyboard = false,
|
|
188
|
+
noDrag = false,
|
|
189
|
+
noDragEventsBubbling = false,
|
|
190
|
+
onError,
|
|
191
|
+
validator
|
|
192
|
+
} = props;
|
|
193
|
+
|
|
194
|
+
const acceptAttr = useMemo(() => acceptPropAsAcceptAttr(accept), [accept]);
|
|
195
|
+
const pickerTypes = useMemo(() => pickerOptionsFromAccept(accept), [accept]);
|
|
196
|
+
|
|
197
|
+
const onFileDialogOpenCb = useMemo<(...args: any[]) => void>(
|
|
198
|
+
() => (typeof onFileDialogOpen === "function" ? onFileDialogOpen : noop),
|
|
199
|
+
[onFileDialogOpen]
|
|
200
|
+
);
|
|
201
|
+
const onFileDialogCancelCb = useMemo<(...args: any[]) => void>(
|
|
202
|
+
() => (typeof onFileDialogCancel === "function" ? onFileDialogCancel : noop),
|
|
203
|
+
[onFileDialogCancel]
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const rootRef = useRef<HTMLElement>(null);
|
|
207
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
208
|
+
|
|
209
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
210
|
+
const {isFocused, isFileDialogActive} = state;
|
|
211
|
+
|
|
212
|
+
const fsAccessApiWorksRef = useRef(
|
|
213
|
+
typeof window !== "undefined" && window.isSecureContext && useFsAccessApi && canUseFileSystemAccessAPI()
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Update file dialog active state when the window is focused on
|
|
217
|
+
const onWindowFocus = () => {
|
|
218
|
+
// Execute the timeout only if the file dialog is opened in the browser
|
|
219
|
+
if (!fsAccessApiWorksRef.current && isFileDialogActive) {
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
if (inputRef.current) {
|
|
222
|
+
const {files} = inputRef.current;
|
|
223
|
+
|
|
224
|
+
if (!files?.length) {
|
|
225
|
+
dispatch({type: "closeDialog"});
|
|
226
|
+
onFileDialogCancelCb();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}, 300);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
window.addEventListener("focus", onWindowFocus, false);
|
|
234
|
+
return () => {
|
|
235
|
+
window.removeEventListener("focus", onWindowFocus, false);
|
|
236
|
+
};
|
|
237
|
+
}, [inputRef, isFileDialogActive, onFileDialogCancelCb, fsAccessApiWorksRef]);
|
|
238
|
+
|
|
239
|
+
const dragTargetsRef = useRef<EventTarget[]>([]);
|
|
240
|
+
const globalDragTargetsRef = useRef<EventTarget[]>([]);
|
|
241
|
+
const onDocumentDrop = (event: DragEvent) => {
|
|
242
|
+
if (rootRef.current && event.target && rootRef.current.contains(event.target as Node)) {
|
|
243
|
+
// If we intercepted an event for our instance, let it propagate down to the instance's onDrop handler
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
dragTargetsRef.current = [];
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (preventDropOnDocument) {
|
|
252
|
+
document.addEventListener("dragover", onDocumentDragOver, false);
|
|
253
|
+
document.addEventListener("drop", onDocumentDrop, false);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return () => {
|
|
257
|
+
if (preventDropOnDocument) {
|
|
258
|
+
document.removeEventListener("dragover", onDocumentDragOver);
|
|
259
|
+
document.removeEventListener("drop", onDocumentDrop);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}, [rootRef, preventDropOnDocument]);
|
|
263
|
+
|
|
264
|
+
// Track global drag state for document-level drag events
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
const onDocumentDragEnter = (event: DragEvent) => {
|
|
267
|
+
if (event.target) {
|
|
268
|
+
globalDragTargetsRef.current = [...globalDragTargetsRef.current, event.target];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (isEvtWithFiles(event)) {
|
|
272
|
+
dispatch({isDragGlobal: true, type: "setDragGlobal"});
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const onDocumentDragLeave = (event: DragEvent) => {
|
|
277
|
+
// Only deactivate once we've left all children
|
|
278
|
+
globalDragTargetsRef.current = globalDragTargetsRef.current.filter(el => el !== event.target && el !== null);
|
|
279
|
+
|
|
280
|
+
if (globalDragTargetsRef.current.length > 0) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
dispatch({isDragGlobal: false, type: "setDragGlobal"});
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const onDocumentDragEnd = () => {
|
|
288
|
+
globalDragTargetsRef.current = [];
|
|
289
|
+
dispatch({isDragGlobal: false, type: "setDragGlobal"});
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const onDocumentDropGlobal = () => {
|
|
293
|
+
globalDragTargetsRef.current = [];
|
|
294
|
+
dispatch({isDragGlobal: false, type: "setDragGlobal"});
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
document.addEventListener("dragenter", onDocumentDragEnter, false);
|
|
298
|
+
document.addEventListener("dragleave", onDocumentDragLeave, false);
|
|
299
|
+
document.addEventListener("dragend", onDocumentDragEnd, false);
|
|
300
|
+
document.addEventListener("drop", onDocumentDropGlobal, false);
|
|
301
|
+
|
|
302
|
+
return () => {
|
|
303
|
+
document.removeEventListener("dragenter", onDocumentDragEnter);
|
|
304
|
+
document.removeEventListener("dragleave", onDocumentDragLeave);
|
|
305
|
+
document.removeEventListener("dragend", onDocumentDragEnd);
|
|
306
|
+
document.removeEventListener("drop", onDocumentDropGlobal);
|
|
307
|
+
};
|
|
308
|
+
}, [rootRef]);
|
|
309
|
+
|
|
310
|
+
// Auto focus the root when autoFocus is true
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (!disabled && autoFocus && rootRef.current) {
|
|
313
|
+
rootRef.current.focus();
|
|
314
|
+
}
|
|
315
|
+
return () => {};
|
|
316
|
+
}, [rootRef, autoFocus, disabled]);
|
|
317
|
+
|
|
318
|
+
const onErrCb = useCallback(
|
|
319
|
+
(e: Error) => {
|
|
320
|
+
if (onError) {
|
|
321
|
+
onError(e);
|
|
322
|
+
} else {
|
|
323
|
+
// Let the user know something's gone wrong if they haven't provided the onError cb.
|
|
324
|
+
console.error(e);
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
[onError]
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const onDragEnterCb = useCallback(
|
|
331
|
+
(event: any) => {
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
// Persist here because we need the event later after getFilesFromEvent() is done
|
|
334
|
+
event.persist?.();
|
|
335
|
+
stopPropagation(event);
|
|
336
|
+
|
|
337
|
+
dragTargetsRef.current = [...dragTargetsRef.current, event.target];
|
|
338
|
+
|
|
339
|
+
if (isEvtWithFiles(event)) {
|
|
340
|
+
Promise.resolve(getFilesFromEvent(event))
|
|
341
|
+
.then(files => {
|
|
342
|
+
if (isPropagationStopped(event) && !noDragEventsBubbling) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const fileCount = files.length;
|
|
347
|
+
const isDragAccept =
|
|
348
|
+
fileCount > 0 &&
|
|
349
|
+
allFilesAccepted({
|
|
350
|
+
files: files as File[],
|
|
351
|
+
accept: acceptAttr,
|
|
352
|
+
minSize,
|
|
353
|
+
maxSize,
|
|
354
|
+
multiple,
|
|
355
|
+
maxFiles,
|
|
356
|
+
validator
|
|
357
|
+
});
|
|
358
|
+
const isDragReject = fileCount > 0 && !isDragAccept;
|
|
359
|
+
|
|
360
|
+
dispatch({
|
|
361
|
+
isDragAccept,
|
|
362
|
+
isDragReject,
|
|
363
|
+
isDragActive: true,
|
|
364
|
+
type: "setDraggedFiles"
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (onDragEnter) {
|
|
368
|
+
onDragEnter(event);
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
.catch(e => onErrCb(e));
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
[
|
|
375
|
+
getFilesFromEvent,
|
|
376
|
+
onDragEnter,
|
|
377
|
+
onErrCb,
|
|
378
|
+
noDragEventsBubbling,
|
|
379
|
+
acceptAttr,
|
|
380
|
+
minSize,
|
|
381
|
+
maxSize,
|
|
382
|
+
multiple,
|
|
383
|
+
maxFiles,
|
|
384
|
+
validator
|
|
385
|
+
]
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const onDragOverCb = useCallback(
|
|
389
|
+
(event: any) => {
|
|
390
|
+
event.preventDefault();
|
|
391
|
+
event.persist?.();
|
|
392
|
+
stopPropagation(event);
|
|
393
|
+
|
|
394
|
+
const hasFiles = isEvtWithFiles(event);
|
|
395
|
+
if (hasFiles && event.dataTransfer) {
|
|
396
|
+
try {
|
|
397
|
+
event.dataTransfer.dropEffect = "copy";
|
|
398
|
+
} catch {
|
|
399
|
+
/* no-op */
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (hasFiles && onDragOver) {
|
|
404
|
+
onDragOver(event);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return false;
|
|
408
|
+
},
|
|
409
|
+
[onDragOver, noDragEventsBubbling]
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const onDragLeaveCb = useCallback(
|
|
413
|
+
(event: any) => {
|
|
414
|
+
event.preventDefault();
|
|
415
|
+
event.persist?.();
|
|
416
|
+
stopPropagation(event);
|
|
417
|
+
|
|
418
|
+
// Only deactivate once the dropzone and all children have been left
|
|
419
|
+
const targets = dragTargetsRef.current.filter(target => rootRef.current?.contains(target as Node));
|
|
420
|
+
// Make sure to remove a target present multiple times only once
|
|
421
|
+
// (Firefox may fire dragenter/dragleave multiple times on the same element)
|
|
422
|
+
const targetIdx = targets.indexOf(event.target);
|
|
423
|
+
if (targetIdx !== -1) {
|
|
424
|
+
targets.splice(targetIdx, 1);
|
|
425
|
+
}
|
|
426
|
+
dragTargetsRef.current = targets;
|
|
427
|
+
if (targets.length > 0) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
dispatch({
|
|
432
|
+
type: "setDraggedFiles",
|
|
433
|
+
isDragActive: false,
|
|
434
|
+
isDragAccept: false,
|
|
435
|
+
isDragReject: false
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
if (isEvtWithFiles(event) && onDragLeave) {
|
|
439
|
+
onDragLeave(event);
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
[rootRef, onDragLeave, noDragEventsBubbling]
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const setFiles = useCallback(
|
|
446
|
+
(files: FileWithPath[], event: any) => {
|
|
447
|
+
const acceptedFiles: FileWithPath[] = [];
|
|
448
|
+
const fileRejections: FileRejection[] = [];
|
|
449
|
+
|
|
450
|
+
files.forEach(file => {
|
|
451
|
+
const [accepted, acceptError] = fileAccepted(file, acceptAttr);
|
|
452
|
+
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize);
|
|
453
|
+
const customErrors = validator ? validator(file) : null;
|
|
454
|
+
|
|
455
|
+
if (accepted && sizeMatch && !customErrors) {
|
|
456
|
+
acceptedFiles.push(file);
|
|
457
|
+
} else {
|
|
458
|
+
let errors: Array<FileError | null> = [acceptError, sizeError];
|
|
459
|
+
|
|
460
|
+
if (customErrors) {
|
|
461
|
+
errors = errors.concat(customErrors);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
fileRejections.push({file, errors: errors.filter((e): e is FileError => e != null)});
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
if ((!multiple && acceptedFiles.length > 1) || (multiple && maxFiles >= 1 && acceptedFiles.length > maxFiles)) {
|
|
469
|
+
// Reject everything and empty accepted files
|
|
470
|
+
acceptedFiles.forEach(file => {
|
|
471
|
+
fileRejections.push({file, errors: [TOO_MANY_FILES_REJECTION]});
|
|
472
|
+
});
|
|
473
|
+
acceptedFiles.splice(0);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
dispatch({
|
|
477
|
+
acceptedFiles,
|
|
478
|
+
fileRejections,
|
|
479
|
+
type: "setFiles"
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
if (onDrop) {
|
|
483
|
+
onDrop(acceptedFiles, fileRejections, event);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (fileRejections.length > 0 && onDropRejected) {
|
|
487
|
+
onDropRejected(fileRejections, event);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (acceptedFiles.length > 0 && onDropAccepted) {
|
|
491
|
+
onDropAccepted(acceptedFiles, event);
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
[dispatch, multiple, acceptAttr, minSize, maxSize, maxFiles, onDrop, onDropAccepted, onDropRejected, validator]
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const onDropCb = useCallback(
|
|
498
|
+
(event: any) => {
|
|
499
|
+
event.preventDefault();
|
|
500
|
+
// Persist here because we need the event later after getFilesFromEvent() is done
|
|
501
|
+
event.persist?.();
|
|
502
|
+
stopPropagation(event);
|
|
503
|
+
|
|
504
|
+
dragTargetsRef.current = [];
|
|
505
|
+
|
|
506
|
+
if (isEvtWithFiles(event)) {
|
|
507
|
+
Promise.resolve(getFilesFromEvent(event))
|
|
508
|
+
.then(files => {
|
|
509
|
+
if (isPropagationStopped(event) && !noDragEventsBubbling) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
setFiles(files as FileWithPath[], event);
|
|
513
|
+
})
|
|
514
|
+
.catch(e => onErrCb(e));
|
|
515
|
+
}
|
|
516
|
+
dispatch({type: "reset"});
|
|
517
|
+
},
|
|
518
|
+
[getFilesFromEvent, setFiles, onErrCb, noDragEventsBubbling]
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// Fn for opening the file dialog programmatically
|
|
522
|
+
const openFileDialog = useCallback(() => {
|
|
523
|
+
// No point to use FS access APIs if context is not secure
|
|
524
|
+
// https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#feature_detection
|
|
525
|
+
if (fsAccessApiWorksRef.current) {
|
|
526
|
+
dispatch({type: "openDialog"});
|
|
527
|
+
onFileDialogOpenCb();
|
|
528
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker
|
|
529
|
+
const opts = {
|
|
530
|
+
multiple,
|
|
531
|
+
types: pickerTypes
|
|
532
|
+
};
|
|
533
|
+
(window as any)
|
|
534
|
+
.showOpenFilePicker(opts)
|
|
535
|
+
.then((handles: any) => getFilesFromEvent(handles))
|
|
536
|
+
.then((files: Array<File | DataTransferItem>) => {
|
|
537
|
+
setFiles(files as FileWithPath[], null);
|
|
538
|
+
dispatch({type: "closeDialog"});
|
|
539
|
+
})
|
|
540
|
+
.catch((e: any) => {
|
|
541
|
+
// AbortError means the user canceled
|
|
542
|
+
if (isAbort(e)) {
|
|
543
|
+
onFileDialogCancelCb(e);
|
|
544
|
+
dispatch({type: "closeDialog"});
|
|
545
|
+
} else if (isSecurityError(e)) {
|
|
546
|
+
fsAccessApiWorksRef.current = false;
|
|
547
|
+
// CORS, so cannot use this API
|
|
548
|
+
// Try using the input
|
|
549
|
+
if (inputRef.current) {
|
|
550
|
+
inputRef.current.value = "";
|
|
551
|
+
inputRef.current.click();
|
|
552
|
+
} else {
|
|
553
|
+
onErrCb(
|
|
554
|
+
new Error(
|
|
555
|
+
"Cannot open the file picker because the https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API is not supported and no <input> was provided."
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
onErrCb(e);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (inputRef.current) {
|
|
567
|
+
dispatch({type: "openDialog"});
|
|
568
|
+
onFileDialogOpenCb();
|
|
569
|
+
inputRef.current.value = "";
|
|
570
|
+
inputRef.current.click();
|
|
571
|
+
}
|
|
572
|
+
}, [dispatch, onFileDialogOpenCb, onFileDialogCancelCb, useFsAccessApi, setFiles, onErrCb, pickerTypes, multiple]);
|
|
573
|
+
|
|
574
|
+
// Cb to open the file dialog when SPACE/ENTER occurs on the dropzone
|
|
575
|
+
const onKeyDownCb = useCallback(
|
|
576
|
+
(event: any) => {
|
|
577
|
+
// Ignore keyboard events bubbling up the DOM tree
|
|
578
|
+
if (!rootRef.current?.isEqualNode(event.target)) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (event.key === " " || event.key === "Enter" || event.keyCode === 32 || event.keyCode === 13) {
|
|
583
|
+
event.preventDefault();
|
|
584
|
+
openFileDialog();
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
[rootRef, openFileDialog]
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
// Update focus state for the dropzone
|
|
591
|
+
const onFocusCb = useCallback(() => {
|
|
592
|
+
dispatch({type: "focus"});
|
|
593
|
+
}, []);
|
|
594
|
+
const onBlurCb = useCallback(() => {
|
|
595
|
+
dispatch({type: "blur"});
|
|
596
|
+
}, []);
|
|
597
|
+
|
|
598
|
+
// Cb to open the file dialog when click occurs on the dropzone
|
|
599
|
+
const onClickCb = useCallback(() => {
|
|
600
|
+
if (noClick) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// In IE11/Edge the file-browser dialog is blocking, therefore, use setTimeout()
|
|
605
|
+
// to ensure React can handle state changes
|
|
606
|
+
// See: https://github.com/react-dropzone/react-dropzone/issues/450
|
|
607
|
+
if (isIeOrEdge()) {
|
|
608
|
+
setTimeout(openFileDialog, 0);
|
|
609
|
+
} else {
|
|
610
|
+
openFileDialog();
|
|
611
|
+
}
|
|
612
|
+
}, [noClick, openFileDialog]);
|
|
613
|
+
|
|
614
|
+
const composeHandler = (fn: any) => {
|
|
615
|
+
return disabled ? null : fn;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const composeKeyboardHandler = (fn: any) => {
|
|
619
|
+
return noKeyboard ? null : composeHandler(fn);
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const composeDragHandler = (fn: any) => {
|
|
623
|
+
return noDrag ? null : composeHandler(fn);
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const stopPropagation = (event: any) => {
|
|
627
|
+
if (noDragEventsBubbling) {
|
|
628
|
+
event.stopPropagation();
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const getRootProps = useMemo(
|
|
633
|
+
() =>
|
|
634
|
+
({
|
|
635
|
+
refKey = "ref",
|
|
636
|
+
role,
|
|
637
|
+
onKeyDown,
|
|
638
|
+
onFocus,
|
|
639
|
+
onBlur,
|
|
640
|
+
onClick,
|
|
641
|
+
onDragEnter,
|
|
642
|
+
onDragOver,
|
|
643
|
+
onDragLeave,
|
|
644
|
+
onDrop,
|
|
645
|
+
...rest
|
|
646
|
+
}: DropzoneRootProps = {}) => ({
|
|
647
|
+
onKeyDown: composeKeyboardHandler(composeEventHandlers(onKeyDown, onKeyDownCb)),
|
|
648
|
+
onFocus: composeKeyboardHandler(composeEventHandlers(onFocus, onFocusCb)),
|
|
649
|
+
onBlur: composeKeyboardHandler(composeEventHandlers(onBlur, onBlurCb)),
|
|
650
|
+
onClick: composeHandler(composeEventHandlers(onClick, onClickCb)),
|
|
651
|
+
onDragEnter: composeDragHandler(composeEventHandlers(onDragEnter, onDragEnterCb)),
|
|
652
|
+
onDragOver: composeDragHandler(composeEventHandlers(onDragOver, onDragOverCb)),
|
|
653
|
+
onDragLeave: composeDragHandler(composeEventHandlers(onDragLeave, onDragLeaveCb)),
|
|
654
|
+
onDrop: composeDragHandler(composeEventHandlers(onDrop, onDropCb)),
|
|
655
|
+
role: typeof role === "string" && role !== "" ? role : "presentation",
|
|
656
|
+
[refKey]: rootRef,
|
|
657
|
+
...(!disabled && !noKeyboard ? {tabIndex: 0} : {}),
|
|
658
|
+
...rest
|
|
659
|
+
}),
|
|
660
|
+
[
|
|
661
|
+
rootRef,
|
|
662
|
+
onKeyDownCb,
|
|
663
|
+
onFocusCb,
|
|
664
|
+
onBlurCb,
|
|
665
|
+
onClickCb,
|
|
666
|
+
onDragEnterCb,
|
|
667
|
+
onDragOverCb,
|
|
668
|
+
onDragLeaveCb,
|
|
669
|
+
onDropCb,
|
|
670
|
+
noKeyboard,
|
|
671
|
+
noDrag,
|
|
672
|
+
disabled
|
|
673
|
+
]
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const onInputElementClick = useCallback((event: any) => {
|
|
677
|
+
event.stopPropagation();
|
|
678
|
+
}, []);
|
|
679
|
+
|
|
680
|
+
const getInputProps = useMemo(
|
|
681
|
+
() =>
|
|
682
|
+
({refKey = "ref", onChange, onClick, ...rest}: DropzoneInputProps = {}) => {
|
|
683
|
+
const inputProps = {
|
|
684
|
+
accept: acceptAttr,
|
|
685
|
+
multiple,
|
|
686
|
+
type: "file",
|
|
687
|
+
style: {
|
|
688
|
+
border: 0,
|
|
689
|
+
clip: "rect(0, 0, 0, 0)",
|
|
690
|
+
clipPath: "inset(50%)",
|
|
691
|
+
height: "1px",
|
|
692
|
+
margin: "0 -1px -1px 0",
|
|
693
|
+
overflow: "hidden",
|
|
694
|
+
padding: 0,
|
|
695
|
+
position: "absolute",
|
|
696
|
+
width: "1px",
|
|
697
|
+
whiteSpace: "nowrap"
|
|
698
|
+
},
|
|
699
|
+
onChange: composeHandler(composeEventHandlers(onChange, onDropCb)),
|
|
700
|
+
onClick: composeHandler(composeEventHandlers(onClick, onInputElementClick)),
|
|
701
|
+
tabIndex: -1,
|
|
702
|
+
[refKey]: inputRef
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
...inputProps,
|
|
707
|
+
...rest
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
[inputRef, accept, multiple, onDropCb, disabled]
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
...state,
|
|
715
|
+
isFocused: isFocused && !disabled,
|
|
716
|
+
getRootProps,
|
|
717
|
+
getInputProps,
|
|
718
|
+
rootRef,
|
|
719
|
+
inputRef,
|
|
720
|
+
open: composeHandler(openFileDialog)
|
|
721
|
+
} as unknown as DropzoneState;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function reducer(state: DropzoneInternalState, action: any): DropzoneInternalState {
|
|
725
|
+
switch (action.type) {
|
|
726
|
+
case "focus":
|
|
727
|
+
return {
|
|
728
|
+
...state,
|
|
729
|
+
isFocused: true
|
|
730
|
+
};
|
|
731
|
+
case "blur":
|
|
732
|
+
return {
|
|
733
|
+
...state,
|
|
734
|
+
isFocused: false
|
|
735
|
+
};
|
|
736
|
+
case "openDialog":
|
|
737
|
+
return {
|
|
738
|
+
...initialState,
|
|
739
|
+
isFileDialogActive: true
|
|
740
|
+
};
|
|
741
|
+
case "closeDialog":
|
|
742
|
+
return {
|
|
743
|
+
...state,
|
|
744
|
+
isFileDialogActive: false
|
|
745
|
+
};
|
|
746
|
+
case "setDraggedFiles":
|
|
747
|
+
return {
|
|
748
|
+
...state,
|
|
749
|
+
isDragActive: action.isDragActive,
|
|
750
|
+
isDragAccept: action.isDragAccept,
|
|
751
|
+
isDragReject: action.isDragReject
|
|
752
|
+
};
|
|
753
|
+
case "setFiles":
|
|
754
|
+
return {
|
|
755
|
+
...state,
|
|
756
|
+
acceptedFiles: action.acceptedFiles,
|
|
757
|
+
fileRejections: action.fileRejections,
|
|
758
|
+
isDragReject: false
|
|
759
|
+
};
|
|
760
|
+
case "setDragGlobal":
|
|
761
|
+
return {
|
|
762
|
+
...state,
|
|
763
|
+
isDragGlobal: action.isDragGlobal
|
|
764
|
+
};
|
|
765
|
+
case "reset":
|
|
766
|
+
return {
|
|
767
|
+
...initialState
|
|
768
|
+
};
|
|
769
|
+
default:
|
|
770
|
+
return state;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function noop() {}
|