react-dropzone 19.0.1 → 19.1.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 +39 -0
- package/dist/index.cjs +124 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +124 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +210 -51
- package/src/utils/index.ts +26 -4
package/dist/index.d.cts
CHANGED
|
@@ -14,6 +14,12 @@ interface FileError {
|
|
|
14
14
|
message: string;
|
|
15
15
|
code: ErrorCode | string;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* What a custom `validator` returns: a single error, a list of errors, or `null` when the file
|
|
19
|
+
* passes. A validator may return the result directly (synchronous) or wrapped in a `Promise`
|
|
20
|
+
* (asynchronous, e.g. reading image dimensions or calling an external service).
|
|
21
|
+
*/
|
|
22
|
+
type ValidatorResult = FileError | readonly FileError[] | null;
|
|
17
23
|
declare enum ErrorCode {
|
|
18
24
|
FileInvalidType = "file-invalid-type",
|
|
19
25
|
FileTooLarge = "file-too-large",
|
|
@@ -48,7 +54,18 @@ type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> & {
|
|
|
48
54
|
onFileDialogCancel?: () => void;
|
|
49
55
|
onFileDialogOpen?: () => void;
|
|
50
56
|
onError?: (err: Error) => void;
|
|
51
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Custom validation, run once per file on drop/selection. Return `null` to accept the file, or a
|
|
59
|
+
* {@link FileError} (or array of them) to reject it. May be `async` (return a `Promise`) to support
|
|
60
|
+
* checks that can't run synchronously - e.g. reading image dimensions, inspecting file contents,
|
|
61
|
+
* or calling an external service. While an async validator is pending, {@link DropzoneState.isProcessing}
|
|
62
|
+
* is `true`, and `onDrop`/`onDropAccepted`/`onDropRejected` fire only once it settles. If the
|
|
63
|
+
* validator throws or rejects, `onError` is called and the drop is discarded.
|
|
64
|
+
*
|
|
65
|
+
* Note: the validator never runs during a drag (a `DataTransferItem` has no name/size), so a
|
|
66
|
+
* validator-configured dropzone is `isDragUnknown` until drop.
|
|
67
|
+
*/
|
|
68
|
+
validator?: <T extends File>(file: T) => ValidatorResult | Promise<ValidatorResult>;
|
|
52
69
|
/**
|
|
53
70
|
* Override the message of any rejection error (built-in or custom). Called once per error;
|
|
54
71
|
* receives the error and the file it belongs to and returns the message to use. Return
|
|
@@ -70,6 +87,14 @@ type DropzoneState = DropzoneRef & {
|
|
|
70
87
|
isDragUnknown: boolean;
|
|
71
88
|
isDragGlobal: boolean;
|
|
72
89
|
isFileDialogActive: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* `true` while a drop/selection is being processed asynchronously - i.e. while `getFilesFromEvent`
|
|
92
|
+
* reads the files and/or an async {@link DropzoneOptions.validator} runs. Spans the whole pipeline,
|
|
93
|
+
* from when files start being read until validation settles. When both are synchronous (the default
|
|
94
|
+
* `getFilesFromEvent` with no/async-free validator) the work resolves within a microtask, so it's
|
|
95
|
+
* only observable for genuinely async work. Use it to show a spinner or disable UI while processing.
|
|
96
|
+
*/
|
|
97
|
+
isProcessing: boolean;
|
|
73
98
|
acceptedFiles: readonly FileWithPath[];
|
|
74
99
|
fileRejections: readonly FileRejection[];
|
|
75
100
|
rootRef: React.RefObject<HTMLElement>;
|
|
@@ -120,5 +145,5 @@ declare const Dropzone: React.ForwardRefExoticComponent<DropzoneProps & React.Re
|
|
|
120
145
|
*/
|
|
121
146
|
declare function useDropzone(props?: DropzoneOptions): DropzoneState;
|
|
122
147
|
//#endregion
|
|
123
|
-
export { type Accept, DropEvent, DropzoneInputProps, DropzoneOptions, DropzoneProps, DropzoneRef, DropzoneRootProps, DropzoneState, ErrorCode, type FileError, FileRejection, type FileWithPath, Dropzone as default, useDropzone };
|
|
148
|
+
export { type Accept, DropEvent, DropzoneInputProps, DropzoneOptions, DropzoneProps, DropzoneRef, DropzoneRootProps, DropzoneState, ErrorCode, type FileError, FileRejection, type FileWithPath, type ValidatorResult, Dropzone as default, useDropzone };
|
|
124
149
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ interface FileError {
|
|
|
14
14
|
message: string;
|
|
15
15
|
code: ErrorCode | string;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* What a custom `validator` returns: a single error, a list of errors, or `null` when the file
|
|
19
|
+
* passes. A validator may return the result directly (synchronous) or wrapped in a `Promise`
|
|
20
|
+
* (asynchronous, e.g. reading image dimensions or calling an external service).
|
|
21
|
+
*/
|
|
22
|
+
type ValidatorResult = FileError | readonly FileError[] | null;
|
|
17
23
|
declare enum ErrorCode {
|
|
18
24
|
FileInvalidType = "file-invalid-type",
|
|
19
25
|
FileTooLarge = "file-too-large",
|
|
@@ -48,7 +54,18 @@ type DropzoneOptions = Pick<React.HTMLProps<HTMLElement>, SharedProps> & {
|
|
|
48
54
|
onFileDialogCancel?: () => void;
|
|
49
55
|
onFileDialogOpen?: () => void;
|
|
50
56
|
onError?: (err: Error) => void;
|
|
51
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Custom validation, run once per file on drop/selection. Return `null` to accept the file, or a
|
|
59
|
+
* {@link FileError} (or array of them) to reject it. May be `async` (return a `Promise`) to support
|
|
60
|
+
* checks that can't run synchronously - e.g. reading image dimensions, inspecting file contents,
|
|
61
|
+
* or calling an external service. While an async validator is pending, {@link DropzoneState.isProcessing}
|
|
62
|
+
* is `true`, and `onDrop`/`onDropAccepted`/`onDropRejected` fire only once it settles. If the
|
|
63
|
+
* validator throws or rejects, `onError` is called and the drop is discarded.
|
|
64
|
+
*
|
|
65
|
+
* Note: the validator never runs during a drag (a `DataTransferItem` has no name/size), so a
|
|
66
|
+
* validator-configured dropzone is `isDragUnknown` until drop.
|
|
67
|
+
*/
|
|
68
|
+
validator?: <T extends File>(file: T) => ValidatorResult | Promise<ValidatorResult>;
|
|
52
69
|
/**
|
|
53
70
|
* Override the message of any rejection error (built-in or custom). Called once per error;
|
|
54
71
|
* receives the error and the file it belongs to and returns the message to use. Return
|
|
@@ -70,6 +87,14 @@ type DropzoneState = DropzoneRef & {
|
|
|
70
87
|
isDragUnknown: boolean;
|
|
71
88
|
isDragGlobal: boolean;
|
|
72
89
|
isFileDialogActive: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* `true` while a drop/selection is being processed asynchronously - i.e. while `getFilesFromEvent`
|
|
92
|
+
* reads the files and/or an async {@link DropzoneOptions.validator} runs. Spans the whole pipeline,
|
|
93
|
+
* from when files start being read until validation settles. When both are synchronous (the default
|
|
94
|
+
* `getFilesFromEvent` with no/async-free validator) the work resolves within a microtask, so it's
|
|
95
|
+
* only observable for genuinely async work. Use it to show a spinner or disable UI while processing.
|
|
96
|
+
*/
|
|
97
|
+
isProcessing: boolean;
|
|
73
98
|
acceptedFiles: readonly FileWithPath[];
|
|
74
99
|
fileRejections: readonly FileRejection[];
|
|
75
100
|
rootRef: React.RefObject<HTMLElement>;
|
|
@@ -120,5 +145,5 @@ declare const Dropzone: React.ForwardRefExoticComponent<DropzoneProps & React.Re
|
|
|
120
145
|
*/
|
|
121
146
|
declare function useDropzone(props?: DropzoneOptions): DropzoneState;
|
|
122
147
|
//#endregion
|
|
123
|
-
export { type Accept, DropEvent, DropzoneInputProps, DropzoneOptions, DropzoneProps, DropzoneRef, DropzoneRootProps, DropzoneState, ErrorCode, type FileError, FileRejection, type FileWithPath, Dropzone as default, useDropzone };
|
|
148
|
+
export { type Accept, DropEvent, DropzoneInputProps, DropzoneOptions, DropzoneProps, DropzoneRef, DropzoneRootProps, DropzoneState, ErrorCode, type FileError, FileRejection, type FileWithPath, type ValidatorResult, Dropzone as default, useDropzone };
|
|
124
149
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -97,6 +97,13 @@ function isDefined(value) {
|
|
|
97
97
|
return value !== void 0 && value !== null;
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
|
+
* Check if a value is thenable (Promise-like), used to tell a synchronous validator result from an
|
|
101
|
+
* asynchronous one.
|
|
102
|
+
*/
|
|
103
|
+
function isThenable(value) {
|
|
104
|
+
return value != null && typeof value.then === "function";
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
100
107
|
* Classify a set of dragged files for the `isDragAccept`/`isDragReject`/`isDragUnknown` states.
|
|
101
108
|
*
|
|
102
109
|
* During `dragenter`/`dragover` the browser only exposes `DataTransferItem`s, which carry a MIME
|
|
@@ -129,7 +136,10 @@ function isPropagationStopped(event) {
|
|
|
129
136
|
}
|
|
130
137
|
function isEvtWithFiles(event) {
|
|
131
138
|
if (!event.dataTransfer) return !!event.target && !!event.target.files;
|
|
132
|
-
return Array.prototype.some.call(event.dataTransfer.types, (type) => type === "Files" || type === "application/x-moz-file");
|
|
139
|
+
return Array.prototype.some.call(event.dataTransfer.types, (type) => type === "Files" || type === "application/x-moz-file") || Array.prototype.some.call(event.dataTransfer.items ?? [], isKindFile);
|
|
140
|
+
}
|
|
141
|
+
function isKindFile(item) {
|
|
142
|
+
return typeof item === "object" && item !== null && item.kind === "file";
|
|
133
143
|
}
|
|
134
144
|
function onDocumentDragOver(event) {
|
|
135
145
|
event.preventDefault();
|
|
@@ -278,6 +288,7 @@ const initialState = {
|
|
|
278
288
|
isDragReject: false,
|
|
279
289
|
isDragUnknown: false,
|
|
280
290
|
isDragGlobal: false,
|
|
291
|
+
isProcessing: false,
|
|
281
292
|
acceptedFiles: [],
|
|
282
293
|
fileRejections: []
|
|
283
294
|
};
|
|
@@ -311,6 +322,23 @@ function useDropzone(props = {}) {
|
|
|
311
322
|
const inputRef = useRef(null);
|
|
312
323
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
313
324
|
const { isFocused, isFileDialogActive } = state;
|
|
325
|
+
const processingAbortRef = useRef(null);
|
|
326
|
+
const beginProcessing = useCallback(() => {
|
|
327
|
+
processingAbortRef.current?.abort();
|
|
328
|
+
const controller = new AbortController();
|
|
329
|
+
processingAbortRef.current = controller;
|
|
330
|
+
dispatch({
|
|
331
|
+
type: "setProcessing",
|
|
332
|
+
isProcessing: true
|
|
333
|
+
});
|
|
334
|
+
return controller.signal;
|
|
335
|
+
}, []);
|
|
336
|
+
const endProcessing = useCallback((signal) => {
|
|
337
|
+
if (!signal.aborted) dispatch({
|
|
338
|
+
type: "setProcessing",
|
|
339
|
+
isProcessing: false
|
|
340
|
+
});
|
|
341
|
+
}, []);
|
|
314
342
|
const fsAccessApiWorksRef = useRef(typeof window !== "undefined" && window.isSecureContext && useFsAccessApi && canUseFileSystemAccessAPI());
|
|
315
343
|
const onWindowFocus = () => {
|
|
316
344
|
if (!fsAccessApiWorksRef.current && isFileDialogActive) setTimeout(() => {
|
|
@@ -476,42 +504,72 @@ function useDropzone(props = {}) {
|
|
|
476
504
|
onDragLeave,
|
|
477
505
|
noDragEventsBubbling
|
|
478
506
|
]);
|
|
479
|
-
const setFiles = useCallback((files, event) => {
|
|
480
|
-
const acceptedFiles = [];
|
|
481
|
-
const fileRejections = [];
|
|
507
|
+
const setFiles = useCallback(async (files, event, signal) => {
|
|
482
508
|
const localizeError = (error, file) => getErrorMessage ? {
|
|
483
509
|
...error,
|
|
484
510
|
message: getErrorMessage(error, file)
|
|
485
511
|
} : error;
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
512
|
+
const commit = (results) => {
|
|
513
|
+
const acceptedFiles = [];
|
|
514
|
+
const fileRejections = [];
|
|
515
|
+
results.forEach(({ file, accepted, acceptError, sizeMatch, sizeError, customErrors }) => {
|
|
516
|
+
if (accepted && sizeMatch && !customErrors) acceptedFiles.push(file);
|
|
517
|
+
else {
|
|
518
|
+
let errors = [acceptError, sizeError];
|
|
519
|
+
if (customErrors) errors = errors.concat(customErrors);
|
|
520
|
+
fileRejections.push({
|
|
521
|
+
file,
|
|
522
|
+
errors: errors.filter((e) => e != null).map((error) => localizeError(error, file))
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
const acceptedFilesLimit = multiple ? maxFiles >= 1 ? maxFiles : Number.POSITIVE_INFINITY : 1;
|
|
527
|
+
if (acceptedFiles.length > acceptedFilesLimit) acceptedFiles.splice(acceptedFilesLimit).forEach((file) => {
|
|
494
528
|
fileRejections.push({
|
|
495
529
|
file,
|
|
496
|
-
errors:
|
|
530
|
+
errors: [localizeError(TOO_MANY_FILES_REJECTION, file)]
|
|
497
531
|
});
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
const acceptedFilesLimit = multiple ? maxFiles >= 1 ? maxFiles : Number.POSITIVE_INFINITY : 1;
|
|
501
|
-
if (acceptedFiles.length > acceptedFilesLimit) acceptedFiles.splice(acceptedFilesLimit).forEach((file) => {
|
|
502
|
-
fileRejections.push({
|
|
503
|
-
file,
|
|
504
|
-
errors: [localizeError(TOO_MANY_FILES_REJECTION, file)]
|
|
505
532
|
});
|
|
533
|
+
dispatch({
|
|
534
|
+
acceptedFiles,
|
|
535
|
+
fileRejections,
|
|
536
|
+
type: "setFiles"
|
|
537
|
+
});
|
|
538
|
+
if (onDrop) onDrop(acceptedFiles, fileRejections, event);
|
|
539
|
+
if (fileRejections.length > 0 && onDropRejected) onDropRejected(fileRejections, event);
|
|
540
|
+
if (acceptedFiles.length > 0 && onDropAccepted) onDropAccepted(acceptedFiles, event);
|
|
541
|
+
};
|
|
542
|
+
const pending = files.map((file) => {
|
|
543
|
+
const [accepted, acceptError] = fileAccepted(file, inputAcceptAttr);
|
|
544
|
+
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize);
|
|
545
|
+
return {
|
|
546
|
+
file,
|
|
547
|
+
accepted,
|
|
548
|
+
acceptError,
|
|
549
|
+
sizeMatch,
|
|
550
|
+
sizeError,
|
|
551
|
+
customErrors: validator ? validator(file) : null
|
|
552
|
+
};
|
|
506
553
|
});
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
554
|
+
if (!pending.some(({ customErrors }) => isThenable(customErrors))) {
|
|
555
|
+
commit(pending);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
let results;
|
|
559
|
+
try {
|
|
560
|
+
results = await Promise.all(pending.map(async ({ customErrors, ...rest }) => ({
|
|
561
|
+
...rest,
|
|
562
|
+
customErrors: await customErrors
|
|
563
|
+
})));
|
|
564
|
+
} catch (e) {
|
|
565
|
+
if (!signal.aborted) {
|
|
566
|
+
endProcessing(signal);
|
|
567
|
+
onErrCb(e);
|
|
568
|
+
}
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (signal.aborted) return;
|
|
572
|
+
commit(results);
|
|
515
573
|
}, [
|
|
516
574
|
dispatch,
|
|
517
575
|
multiple,
|
|
@@ -523,23 +581,39 @@ function useDropzone(props = {}) {
|
|
|
523
581
|
onDropAccepted,
|
|
524
582
|
onDropRejected,
|
|
525
583
|
validator,
|
|
526
|
-
getErrorMessage
|
|
584
|
+
getErrorMessage,
|
|
585
|
+
onErrCb,
|
|
586
|
+
endProcessing
|
|
527
587
|
]);
|
|
528
588
|
const onDropCb = useCallback((event) => {
|
|
529
589
|
event.preventDefault();
|
|
530
590
|
event.persist?.();
|
|
531
591
|
stopPropagation(event);
|
|
532
592
|
dragTargetsRef.current = [];
|
|
533
|
-
if (isEvtWithFiles(event)) Promise.resolve(getFilesFromEvent(event)).then((files) => {
|
|
534
|
-
if (isPropagationStopped(event) && !noDragEventsBubbling) return;
|
|
535
|
-
setFiles(files, event);
|
|
536
|
-
}).catch((e) => onErrCb(e));
|
|
537
593
|
dispatch({ type: "reset" });
|
|
594
|
+
if (isEvtWithFiles(event)) {
|
|
595
|
+
const signal = beginProcessing();
|
|
596
|
+
Promise.resolve(getFilesFromEvent(event)).then((files) => {
|
|
597
|
+
if (signal.aborted) return;
|
|
598
|
+
if (isPropagationStopped(event) && !noDragEventsBubbling) {
|
|
599
|
+
endProcessing(signal);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
return setFiles(files, event, signal);
|
|
603
|
+
}).catch((e) => {
|
|
604
|
+
if (!signal.aborted) {
|
|
605
|
+
endProcessing(signal);
|
|
606
|
+
onErrCb(e);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
538
610
|
}, [
|
|
539
611
|
getFilesFromEvent,
|
|
540
612
|
setFiles,
|
|
541
613
|
onErrCb,
|
|
542
|
-
noDragEventsBubbling
|
|
614
|
+
noDragEventsBubbling,
|
|
615
|
+
beginProcessing,
|
|
616
|
+
endProcessing
|
|
543
617
|
]);
|
|
544
618
|
const openFileDialog = useCallback(() => {
|
|
545
619
|
if (fsAccessApiWorksRef.current) {
|
|
@@ -549,10 +623,16 @@ function useDropzone(props = {}) {
|
|
|
549
623
|
multiple,
|
|
550
624
|
types: pickerTypes
|
|
551
625
|
};
|
|
552
|
-
|
|
553
|
-
|
|
626
|
+
let signal;
|
|
627
|
+
window.showOpenFilePicker(opts).then((handles) => {
|
|
628
|
+
signal = beginProcessing();
|
|
629
|
+
return getFilesFromEvent(handles);
|
|
630
|
+
}).then((files) => {
|
|
554
631
|
dispatch({ type: "closeDialog" });
|
|
632
|
+
if (signal.aborted) return;
|
|
633
|
+
return setFiles(files, null, signal);
|
|
555
634
|
}).catch((e) => {
|
|
635
|
+
if (signal) endProcessing(signal);
|
|
556
636
|
if (isAbort(e)) {
|
|
557
637
|
onFileDialogCancelCb(e);
|
|
558
638
|
dispatch({ type: "closeDialog" });
|
|
@@ -580,7 +660,9 @@ function useDropzone(props = {}) {
|
|
|
580
660
|
setFiles,
|
|
581
661
|
onErrCb,
|
|
582
662
|
pickerTypes,
|
|
583
|
-
multiple
|
|
663
|
+
multiple,
|
|
664
|
+
beginProcessing,
|
|
665
|
+
endProcessing
|
|
584
666
|
]);
|
|
585
667
|
const onKeyDownCb = useCallback((event) => {
|
|
586
668
|
if (!rootRef.current?.isEqualNode(event.target)) return;
|
|
@@ -707,10 +789,15 @@ function reducer(state, action) {
|
|
|
707
789
|
isDragReject: action.isDragReject,
|
|
708
790
|
isDragUnknown: action.isDragUnknown
|
|
709
791
|
};
|
|
792
|
+
case "setProcessing": return {
|
|
793
|
+
...state,
|
|
794
|
+
isProcessing: action.isProcessing
|
|
795
|
+
};
|
|
710
796
|
case "setFiles": return {
|
|
711
797
|
...state,
|
|
712
798
|
acceptedFiles: action.acceptedFiles,
|
|
713
799
|
fileRejections: action.fileRejections,
|
|
800
|
+
isProcessing: false,
|
|
714
801
|
isDragReject: false,
|
|
715
802
|
isDragUnknown: false
|
|
716
803
|
};
|