uplofile 1.1.1 → 2.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 +3 -38
- package/dist/index.d.ts +58 -26
- package/dist/index.mjs +192 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Accessible, unstyled primitives for building your own upload UI — with drag‑
|
|
|
10
10
|
- React 16+ compatible
|
|
11
11
|
- Drag‑and‑drop or click‑to‑upload
|
|
12
12
|
- Upload progress, plus cancel/retry/remove actions
|
|
13
|
+
- **Custom Validation:** Use `beforeUpload` to validate files before they start uploading
|
|
13
14
|
- Hidden input for form submissions
|
|
14
15
|
- Unstyled — bring your own design
|
|
15
16
|
|
|
@@ -29,53 +30,17 @@ pnpm add uplofile
|
|
|
29
30
|
|
|
30
31
|
## Quick Start
|
|
31
32
|
|
|
32
|
-
Import the
|
|
33
|
+
Import and use the components in your React component:
|
|
33
34
|
|
|
34
|
-
```typescript
|
|
35
|
-
import * as Uplofile from "uplofile";
|
|
36
|
-
|
|
37
|
-
const UplofileRoot = Uplofile.Root;
|
|
38
|
-
|
|
39
|
-
const UplofileTrigger = Uplofile.Trigger;
|
|
40
|
-
|
|
41
|
-
const UplofileHiddenInput = Uplofile.HiddenInput;
|
|
42
|
-
|
|
43
|
-
const UplofileDropzone = Uplofile.Dropzone;
|
|
44
|
-
|
|
45
|
-
const UplofilePreview = Uplofile.Preview;
|
|
46
|
-
|
|
47
|
-
const UplofileCancel = Uplofile.Cancel;
|
|
48
|
-
|
|
49
|
-
const UplofileRetry = Uplofile.Retry;
|
|
50
|
-
|
|
51
|
-
const UplofileRemove = Uplofile.Remove;
|
|
52
|
-
|
|
53
|
-
export {
|
|
54
|
-
UplofileRoot,
|
|
55
|
-
UplofileTrigger,
|
|
56
|
-
UplofileHiddenInput,
|
|
57
|
-
UplofileDropzone,
|
|
58
|
-
UplofilePreview,
|
|
59
|
-
UplofileCancel,
|
|
60
|
-
UplofileRetry,
|
|
61
|
-
UplofileRemove,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Then use them in your React component:
|
|
67
35
|
```tsx
|
|
68
36
|
"use client";
|
|
69
37
|
|
|
70
38
|
import {
|
|
71
|
-
UplofileCancel,
|
|
72
39
|
UplofileDropzone,
|
|
73
40
|
UplofilePreview,
|
|
74
|
-
UplofileRemove,
|
|
75
|
-
UplofileRetry,
|
|
76
41
|
UplofileRoot,
|
|
77
42
|
UplofileTrigger,
|
|
78
|
-
} from "
|
|
43
|
+
} from "uplofile";
|
|
79
44
|
|
|
80
45
|
export default function Basic() {
|
|
81
46
|
return (
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/// <reference lib="es2015.iterable" />
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
-
import React, { HTMLAttributes,
|
|
3
|
+
import React, { HTMLAttributes, DragEvent, RefObject, ChangeEvent, PropsWithChildren, ButtonHTMLAttributes } from 'react';
|
|
4
4
|
|
|
5
5
|
declare const Dropzone: ({ asChild, ...rest }: {
|
|
6
6
|
asChild?: boolean;
|
|
7
7
|
} & HTMLAttributes<HTMLElement>) => react_jsx_runtime.JSX.Element;
|
|
8
8
|
|
|
9
9
|
type UploadStatus = "idle" | "uploading" | "done" | "error" | "canceled" | "removing";
|
|
10
|
-
type UploadFileItem = {
|
|
10
|
+
type UploadFileItem<TMeta = any> = {
|
|
11
11
|
uid: string;
|
|
12
12
|
id?: string;
|
|
13
13
|
name: string;
|
|
@@ -17,15 +17,30 @@ type UploadFileItem = {
|
|
|
17
17
|
status: UploadStatus;
|
|
18
18
|
progress?: number;
|
|
19
19
|
error?: string;
|
|
20
|
-
|
|
20
|
+
meta?: TMeta;
|
|
21
21
|
};
|
|
22
22
|
type UploadResult = {
|
|
23
23
|
url: string;
|
|
24
24
|
id?: string;
|
|
25
25
|
};
|
|
26
|
-
type
|
|
26
|
+
type UplofileRootRef<TMeta = any> = {
|
|
27
|
+
setItems: (items: UploadFileItem<TMeta>[] | ((prev: UploadFileItem<TMeta>[]) => UploadFileItem<TMeta>[])) => void;
|
|
28
|
+
getItems: () => UploadFileItem<TMeta>[];
|
|
29
|
+
onDrop: (e: DragEvent) => void;
|
|
30
|
+
onDragOver: (e: DragEvent) => void;
|
|
31
|
+
openFileDialog: () => void;
|
|
32
|
+
actions: ItemActions;
|
|
33
|
+
};
|
|
34
|
+
type BeforeUploadResult<TMeta = any> = boolean | Array<{
|
|
35
|
+
valid: boolean;
|
|
36
|
+
meta?: TMeta;
|
|
37
|
+
id?: string;
|
|
38
|
+
uid: string;
|
|
39
|
+
reason?: string;
|
|
40
|
+
}>;
|
|
41
|
+
type RootProps<TMeta = any> = PropsWithChildren<{
|
|
27
42
|
multiple?: boolean;
|
|
28
|
-
initial?: Array<Pick<UploadFileItem
|
|
43
|
+
initial?: Array<Pick<UploadFileItem<TMeta>, "uid" | "id" | "name" | "url" | "meta">>;
|
|
29
44
|
/**
|
|
30
45
|
* optimistic (default): remove from UI immediately, call onRemove in the background; if it fails, restore the item and show error.
|
|
31
46
|
* strict: call onRemove first; only remove from UI if it succeeds.
|
|
@@ -35,18 +50,19 @@ type RootProps = PropsWithChildren<{
|
|
|
35
50
|
maxCount?: number;
|
|
36
51
|
disabled?: boolean;
|
|
37
52
|
accept?: string;
|
|
38
|
-
|
|
53
|
+
beforeUpload?: (items: UploadFileItem<TMeta>[]) => BeforeUploadResult<TMeta> | Promise<BeforeUploadResult<TMeta>>;
|
|
54
|
+
onChange?: (items: UploadFileItem<TMeta>[]) => Promise<void> | void;
|
|
39
55
|
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult>;
|
|
40
|
-
onRemove?: (item: UploadFileItem
|
|
56
|
+
onRemove?: (item: UploadFileItem<TMeta>, signal: AbortSignal) => Promise<void | any>;
|
|
41
57
|
}>;
|
|
42
58
|
type ItemActions = {
|
|
43
59
|
cancel: (uid: string) => void;
|
|
44
60
|
remove: (uid: string) => void;
|
|
45
61
|
retry: (uid: string) => void;
|
|
46
62
|
};
|
|
47
|
-
type ImageUploaderContextValue = {
|
|
48
|
-
items: UploadFileItem[];
|
|
49
|
-
setItems: (items: UploadFileItem[]) => void;
|
|
63
|
+
type ImageUploaderContextValue<TMeta = any> = {
|
|
64
|
+
items: UploadFileItem<TMeta>[];
|
|
65
|
+
setItems: (items: UploadFileItem<TMeta>[]) => void;
|
|
50
66
|
disabled?: boolean;
|
|
51
67
|
multiple: boolean;
|
|
52
68
|
accept: string;
|
|
@@ -71,8 +87,8 @@ type ImageUploaderContextValue = {
|
|
|
71
87
|
hiddenInputValue: string;
|
|
72
88
|
name: string;
|
|
73
89
|
};
|
|
74
|
-
type TriggerRenderProps = {
|
|
75
|
-
items: UploadFileItem[];
|
|
90
|
+
type TriggerRenderProps<TMeta = any> = {
|
|
91
|
+
items: UploadFileItem<TMeta>[];
|
|
76
92
|
isUploading: boolean;
|
|
77
93
|
uploadingCount: number;
|
|
78
94
|
doneCount: number;
|
|
@@ -80,16 +96,16 @@ type TriggerRenderProps = {
|
|
|
80
96
|
totalProgress?: number;
|
|
81
97
|
open: () => void;
|
|
82
98
|
};
|
|
83
|
-
type PreviewRenderProps = {
|
|
84
|
-
items: UploadFileItem[];
|
|
85
|
-
setItems: (items: UploadFileItem[]) => void;
|
|
99
|
+
type PreviewRenderProps<TMeta = any> = {
|
|
100
|
+
items: UploadFileItem<TMeta>[];
|
|
101
|
+
setItems: (items: UploadFileItem<TMeta>[]) => void;
|
|
86
102
|
actions: ItemActions;
|
|
87
103
|
};
|
|
88
104
|
|
|
89
|
-
type Props = {
|
|
90
|
-
render?: (api: PreviewRenderProps) => React.ReactNode;
|
|
105
|
+
type Props<TMeta = any> = {
|
|
106
|
+
render?: (api: PreviewRenderProps<TMeta>) => React.ReactNode;
|
|
91
107
|
};
|
|
92
|
-
declare const Preview: ({ render }: Props) => string | number | boolean | Iterable<React.ReactNode> |
|
|
108
|
+
declare const Preview: <TMeta = any>({ render }: Props<TMeta>) => string | number | boolean | react_jsx_runtime.JSX.Element | Iterable<React.ReactNode> | null | undefined;
|
|
93
109
|
declare const HiddenInput: ({ name }: {
|
|
94
110
|
name?: string;
|
|
95
111
|
}) => react_jsx_runtime.JSX.Element;
|
|
@@ -102,17 +118,33 @@ declare const Cancel: ({ uid, asChild, alwaysVisible, ...rest }: ButtonProps) =>
|
|
|
102
118
|
declare const Retry: ({ uid, asChild, ...rest }: ButtonProps) => react_jsx_runtime.JSX.Element;
|
|
103
119
|
declare const Remove: ({ uid, asChild, ...rest }: ButtonProps) => react_jsx_runtime.JSX.Element;
|
|
104
120
|
|
|
105
|
-
declare const Root:
|
|
121
|
+
declare const Root: React.ForwardRefExoticComponent<{
|
|
122
|
+
multiple?: boolean;
|
|
123
|
+
initial?: Pick<UploadFileItem<unknown>, "id" | "name" | "uid" | "url" | "meta">[] | undefined;
|
|
124
|
+
removeMode?: "optimistic" | "strict";
|
|
125
|
+
name?: string;
|
|
126
|
+
maxCount?: number;
|
|
127
|
+
disabled?: boolean;
|
|
128
|
+
accept?: string;
|
|
129
|
+
beforeUpload?: ((items: UploadFileItem<unknown>[]) => BeforeUploadResult<unknown> | Promise<BeforeUploadResult<unknown>>) | undefined;
|
|
130
|
+
onChange?: ((items: UploadFileItem<unknown>[]) => Promise<void> | void) | undefined;
|
|
131
|
+
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult>;
|
|
132
|
+
onRemove?: ((item: UploadFileItem<unknown>, signal: AbortSignal) => Promise<void | any>) | undefined;
|
|
133
|
+
} & {
|
|
134
|
+
children?: React.ReactNode | undefined;
|
|
135
|
+
} & React.RefAttributes<UplofileRootRef<unknown>>>;
|
|
106
136
|
|
|
107
|
-
declare const Trigger: ({ asChild, children, render, ...rest }: PropsWithChildren<{
|
|
137
|
+
declare const Trigger: <TMeta = any>({ asChild, children, render, ...rest }: PropsWithChildren<{
|
|
108
138
|
asChild?: boolean;
|
|
109
|
-
render?: (api: TriggerRenderProps) => React.ReactNode;
|
|
110
|
-
children?: React.ReactNode | ((api: TriggerRenderProps) => React.ReactNode);
|
|
139
|
+
render?: (api: TriggerRenderProps<TMeta>) => React.ReactNode;
|
|
140
|
+
children?: React.ReactNode | ((api: TriggerRenderProps<TMeta>) => React.ReactNode);
|
|
111
141
|
} & React.HTMLAttributes<HTMLElement>>) => react_jsx_runtime.JSX.Element;
|
|
112
142
|
|
|
113
|
-
declare const useUplofile: () => ImageUploaderContextValue
|
|
143
|
+
declare const useUplofile: <TMeta = any>() => ImageUploaderContextValue<TMeta>;
|
|
114
144
|
|
|
115
|
-
declare const
|
|
145
|
+
declare const getExtension: (path: string) => string | undefined;
|
|
146
|
+
declare const isVideoFile: (item: UploadFileItem<any>, extraExtensions?: string[]) => boolean;
|
|
147
|
+
declare const isImageFile: (item: UploadFileItem<any>, extraExtensions?: string[]) => boolean;
|
|
116
148
|
|
|
117
|
-
export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, isVideoFile, useUplofile };
|
|
118
|
-
export type { ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus };
|
|
149
|
+
export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, getExtension, isImageFile, isVideoFile, useUplofile };
|
|
150
|
+
export type { ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus, UplofileRootRef };
|
package/dist/index.mjs
CHANGED
|
@@ -1,45 +1,121 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
-
import { useState, useRef, useEffect, useMemo, useCallback, createContext, useContext } from 'react';
|
|
3
|
+
import { forwardRef, useState, useRef, useEffect, useMemo, useCallback, useImperativeHandle, createContext, useContext } from 'react';
|
|
4
4
|
|
|
5
5
|
const uid = ()=>Math.random().toString(36).slice(2, 10) + Date.now().toString(36).slice(-4);
|
|
6
|
-
const
|
|
6
|
+
const VIDEO_EXTENSIONS = [
|
|
7
|
+
"mp4",
|
|
8
|
+
"webm",
|
|
9
|
+
"ogg",
|
|
10
|
+
"mov",
|
|
11
|
+
"avi",
|
|
12
|
+
"mkv",
|
|
13
|
+
"wmv",
|
|
14
|
+
"flv",
|
|
15
|
+
"3gp",
|
|
16
|
+
"m4v",
|
|
17
|
+
"mpg",
|
|
18
|
+
"mpeg"
|
|
19
|
+
];
|
|
20
|
+
const IMAGE_EXTENSIONS = [
|
|
21
|
+
"jpg",
|
|
22
|
+
"jpeg",
|
|
23
|
+
"png",
|
|
24
|
+
"gif",
|
|
25
|
+
"webp",
|
|
26
|
+
"svg",
|
|
27
|
+
"avif",
|
|
28
|
+
"bmp",
|
|
29
|
+
"ico",
|
|
30
|
+
"tiff"
|
|
31
|
+
];
|
|
32
|
+
const getExtension = (path)=>{
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(path, "http://localhost");
|
|
35
|
+
const pathname = url.pathname;
|
|
36
|
+
const parts = pathname.split(".");
|
|
37
|
+
if (parts.length > 1) return parts.pop()?.toLowerCase();
|
|
38
|
+
return undefined;
|
|
39
|
+
} catch {
|
|
40
|
+
const parts = path.split("?")[0].split("#")[0].split(".");
|
|
41
|
+
if (parts.length > 1) return parts.pop()?.toLowerCase();
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const isVideoFile = (item, extraExtensions = [])=>{
|
|
7
46
|
if (item.file) {
|
|
8
47
|
return item.file.type.startsWith("video/");
|
|
9
48
|
}
|
|
49
|
+
const allExtensions = [
|
|
50
|
+
...VIDEO_EXTENSIONS,
|
|
51
|
+
...extraExtensions
|
|
52
|
+
];
|
|
53
|
+
if (item.url) {
|
|
54
|
+
const extension = getExtension(item.url);
|
|
55
|
+
if (extension && allExtensions.includes(extension)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (item.name) {
|
|
60
|
+
const extension = getExtension(item.name);
|
|
61
|
+
if (extension && allExtensions.includes(extension)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
const isImageFile = (item, extraExtensions = [])=>{
|
|
68
|
+
if (item.file) {
|
|
69
|
+
return item.file.type.startsWith("image/");
|
|
70
|
+
}
|
|
71
|
+
const allExtensions = [
|
|
72
|
+
...IMAGE_EXTENSIONS,
|
|
73
|
+
...extraExtensions
|
|
74
|
+
];
|
|
10
75
|
if (item.url) {
|
|
11
|
-
const extension = item.url
|
|
12
|
-
|
|
13
|
-
"mp4",
|
|
14
|
-
"webm",
|
|
15
|
-
"ogg",
|
|
16
|
-
"mov",
|
|
17
|
-
"avi",
|
|
18
|
-
"mkv"
|
|
19
|
-
];
|
|
20
|
-
if (extension && videoExtensions.includes(extension)) {
|
|
76
|
+
const extension = getExtension(item.url);
|
|
77
|
+
if (extension && allExtensions.includes(extension)) {
|
|
21
78
|
return true;
|
|
22
79
|
}
|
|
23
80
|
}
|
|
24
81
|
if (item.name) {
|
|
25
|
-
const extension = item.name
|
|
26
|
-
|
|
27
|
-
"mp4",
|
|
28
|
-
"webm",
|
|
29
|
-
"ogg",
|
|
30
|
-
"mov",
|
|
31
|
-
"avi",
|
|
32
|
-
"mkv"
|
|
33
|
-
];
|
|
34
|
-
if (extension && videoExtensions.includes(extension)) {
|
|
82
|
+
const extension = getExtension(item.name);
|
|
83
|
+
if (extension && allExtensions.includes(extension)) {
|
|
35
84
|
return true;
|
|
36
85
|
}
|
|
37
86
|
}
|
|
38
87
|
return false;
|
|
39
88
|
};
|
|
89
|
+
const acceptsFile = (file, accept)=>{
|
|
90
|
+
if (!accept || accept.trim() === "") return true;
|
|
91
|
+
const type = (file.type || "").toLowerCase();
|
|
92
|
+
const name = (file.name || "").toLowerCase();
|
|
93
|
+
const tokens = accept.split(",").map((t)=>t.trim().toLowerCase()).filter(Boolean);
|
|
94
|
+
// If accept is malformed, be permissive (browser file input also ignores)
|
|
95
|
+
if (tokens.length === 0) return true;
|
|
96
|
+
// Helper: match extension token like .jpg or jpg
|
|
97
|
+
const hasExt = (token)=>{
|
|
98
|
+
const extToken = token.startsWith(".") ? token.slice(1) : token;
|
|
99
|
+
const fileExt = name.includes(".") ? name.split(".").pop() : undefined;
|
|
100
|
+
return !!fileExt && fileExt === extToken;
|
|
101
|
+
};
|
|
102
|
+
// Check any token matches
|
|
103
|
+
return tokens.some((token)=>{
|
|
104
|
+
if (token === "*/*") return true;
|
|
105
|
+
// MIME type with wildcard, e.g., image/*
|
|
106
|
+
if (token.endsWith("/*")) {
|
|
107
|
+
const prefix = token.slice(0, -1); // keep the slash
|
|
108
|
+
return type.startsWith(prefix);
|
|
109
|
+
}
|
|
110
|
+
// Full MIME type e.g., image/png
|
|
111
|
+
if (token.includes("/")) return type === token;
|
|
112
|
+
// File extensions, with or without a leading dot
|
|
113
|
+
return hasExt(token);
|
|
114
|
+
});
|
|
115
|
+
};
|
|
40
116
|
|
|
41
117
|
const UploaderCtx = /*#__PURE__*/ createContext(null);
|
|
42
|
-
const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "optimistic", onRemove, accept = "image/*", name = "
|
|
118
|
+
const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange, upload, removeMode = "optimistic", onRemove, accept = "image/*", beforeUpload, name = "image", maxCount, disabled, children }, ref)=>{
|
|
43
119
|
const [items, setItems] = useState([]);
|
|
44
120
|
const controllers = useRef(new Map());
|
|
45
121
|
const removeControllers = useRef(new Map());
|
|
@@ -56,7 +132,8 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
56
132
|
id: it.id,
|
|
57
133
|
name: it.name,
|
|
58
134
|
url: it.url,
|
|
59
|
-
status: "done"
|
|
135
|
+
status: "done",
|
|
136
|
+
meta: it.meta
|
|
60
137
|
};
|
|
61
138
|
});
|
|
62
139
|
// Only hydrate if the user hasn't already added/modified items locally
|
|
@@ -111,7 +188,7 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
111
188
|
...it,
|
|
112
189
|
status: "done",
|
|
113
190
|
url: result.url,
|
|
114
|
-
id: result.id,
|
|
191
|
+
id: result.id ?? it.id,
|
|
115
192
|
previewUrl: serverPreview,
|
|
116
193
|
progress: 100
|
|
117
194
|
};
|
|
@@ -130,12 +207,13 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
130
207
|
emitChange,
|
|
131
208
|
upload
|
|
132
209
|
]);
|
|
133
|
-
const selectFiles = useCallback((files)=>{
|
|
134
|
-
if (!files
|
|
135
|
-
const selected = Array.from(files);
|
|
210
|
+
const selectFiles = useCallback(async (files)=>{
|
|
211
|
+
if (!files) return;
|
|
212
|
+
const selected = Array.isArray(files) ? files : Array.from(files);
|
|
213
|
+
if (selected.length === 0) return;
|
|
136
214
|
const remaining = maxCount ? Math.max(0, maxCount - items.filter((i)=>i.status !== "canceled").length) : undefined;
|
|
137
215
|
const toUse = typeof remaining === "number" ? selected.slice(0, remaining) : selected;
|
|
138
|
-
|
|
216
|
+
let newItems = toUse.map((file)=>({
|
|
139
217
|
uid: uid(),
|
|
140
218
|
name: file.name,
|
|
141
219
|
file,
|
|
@@ -143,12 +221,59 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
143
221
|
status: "idle",
|
|
144
222
|
progress: 0
|
|
145
223
|
}));
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
224
|
+
if (beforeUpload) {
|
|
225
|
+
const result = await beforeUpload(newItems);
|
|
226
|
+
if (result === false) {
|
|
227
|
+
newItems.forEach((it)=>{
|
|
228
|
+
if (it.previewUrl) URL.revokeObjectURL(it.previewUrl);
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (Array.isArray(result)) {
|
|
233
|
+
const resultMap = new Map(result.map((r)=>[
|
|
234
|
+
r.uid,
|
|
235
|
+
r
|
|
236
|
+
]));
|
|
237
|
+
const processedItems = [];
|
|
238
|
+
for (const item of newItems){
|
|
239
|
+
const validation = resultMap.get(item.uid);
|
|
240
|
+
if (!validation) {
|
|
241
|
+
if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (validation.valid) {
|
|
245
|
+
processedItems.push({
|
|
246
|
+
...item,
|
|
247
|
+
meta: validation.meta !== undefined ? validation.meta : item.meta,
|
|
248
|
+
id: validation.id !== undefined ? validation.id : item.id
|
|
249
|
+
});
|
|
250
|
+
} else if (validation.reason) {
|
|
251
|
+
processedItems.push({
|
|
252
|
+
...item,
|
|
253
|
+
status: "error",
|
|
254
|
+
error: validation.reason,
|
|
255
|
+
meta: validation.meta !== undefined ? validation.meta : item.meta,
|
|
256
|
+
id: validation.id !== undefined ? validation.id : item.id
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
newItems = processedItems;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (newItems.length === 0) return;
|
|
266
|
+
emitChange((prev)=>[
|
|
267
|
+
...prev,
|
|
268
|
+
...newItems
|
|
269
|
+
]);
|
|
270
|
+
newItems.forEach((it)=>{
|
|
271
|
+
if (it.status === "idle") {
|
|
272
|
+
startUpload(it);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
151
275
|
}, [
|
|
276
|
+
beforeUpload,
|
|
152
277
|
emitChange,
|
|
153
278
|
items,
|
|
154
279
|
maxCount,
|
|
@@ -163,10 +288,15 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
163
288
|
const onDrop = useCallback((e)=>{
|
|
164
289
|
e.preventDefault();
|
|
165
290
|
if (disabled) return;
|
|
166
|
-
|
|
291
|
+
const dtFiles = e.dataTransfer?.files;
|
|
292
|
+
if (!dtFiles || dtFiles.length === 0) return;
|
|
293
|
+
const accepted = Array.from(dtFiles).filter((f)=>acceptsFile(f, accept));
|
|
294
|
+
if (accepted.length === 0) return;
|
|
295
|
+
selectFiles(accepted);
|
|
167
296
|
}, [
|
|
168
297
|
disabled,
|
|
169
|
-
selectFiles
|
|
298
|
+
selectFiles,
|
|
299
|
+
accept
|
|
170
300
|
]);
|
|
171
301
|
const onDragOver = useCallback((e)=>e.preventDefault(), []);
|
|
172
302
|
const actions = useMemo(()=>({
|
|
@@ -271,6 +401,21 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
271
401
|
hiddenInputValue,
|
|
272
402
|
name
|
|
273
403
|
};
|
|
404
|
+
// Expose imperative methods via ref
|
|
405
|
+
useImperativeHandle(ref, ()=>({
|
|
406
|
+
setItems: emitChange,
|
|
407
|
+
getItems: ()=>items,
|
|
408
|
+
onDrop,
|
|
409
|
+
onDragOver,
|
|
410
|
+
openFileDialog: ()=>inputRef.current?.click(),
|
|
411
|
+
actions
|
|
412
|
+
}), [
|
|
413
|
+
emitChange,
|
|
414
|
+
items,
|
|
415
|
+
onDrop,
|
|
416
|
+
onDragOver,
|
|
417
|
+
actions
|
|
418
|
+
]);
|
|
274
419
|
return /*#__PURE__*/ jsx(UploaderCtx.Provider, {
|
|
275
420
|
value: ctx,
|
|
276
421
|
children: /*#__PURE__*/ jsxs("div", {
|
|
@@ -285,7 +430,7 @@ const Root = ({ multiple = true, initial = [], onChange, upload, removeMode = "o
|
|
|
285
430
|
]
|
|
286
431
|
})
|
|
287
432
|
});
|
|
288
|
-
};
|
|
433
|
+
});
|
|
289
434
|
|
|
290
435
|
const useUplofile = ()=>{
|
|
291
436
|
const ctx = useContext(UploaderCtx);
|
|
@@ -582,33 +727,36 @@ const Cancel = ({ uid, asChild, alwaysVisible = false, ...rest })=>{
|
|
|
582
727
|
const Comp = asChild ? Slot : "button";
|
|
583
728
|
if (!isUploading && !alwaysVisible) return null;
|
|
584
729
|
return /*#__PURE__*/ jsx(Comp, {
|
|
730
|
+
...rest,
|
|
585
731
|
onClick: (e)=>{
|
|
586
732
|
e.stopPropagation();
|
|
587
733
|
actions.cancel(uid);
|
|
588
|
-
|
|
589
|
-
|
|
734
|
+
rest.onClick?.(e);
|
|
735
|
+
}
|
|
590
736
|
});
|
|
591
737
|
};
|
|
592
738
|
const Retry = ({ uid, asChild, ...rest })=>{
|
|
593
739
|
const { actions } = useUplofile();
|
|
594
740
|
const Comp = asChild ? Slot : "button";
|
|
595
741
|
return /*#__PURE__*/ jsx(Comp, {
|
|
742
|
+
...rest,
|
|
596
743
|
onClick: (e)=>{
|
|
597
744
|
e.stopPropagation();
|
|
598
745
|
actions.retry(uid);
|
|
599
|
-
|
|
600
|
-
|
|
746
|
+
rest.onClick?.(e);
|
|
747
|
+
}
|
|
601
748
|
});
|
|
602
749
|
};
|
|
603
750
|
const Remove = ({ uid, asChild, ...rest })=>{
|
|
604
751
|
const { actions } = useUplofile();
|
|
605
752
|
const Comp = asChild ? Slot : "button";
|
|
606
753
|
return /*#__PURE__*/ jsx(Comp, {
|
|
754
|
+
...rest,
|
|
607
755
|
onClick: (e)=>{
|
|
608
756
|
e.stopPropagation();
|
|
609
757
|
actions.remove(uid);
|
|
610
|
-
|
|
611
|
-
|
|
758
|
+
rest.onClick?.(e);
|
|
759
|
+
}
|
|
612
760
|
});
|
|
613
761
|
};
|
|
614
762
|
|
|
@@ -643,4 +791,4 @@ const Trigger = ({ asChild, children, render, ...rest })=>{
|
|
|
643
791
|
});
|
|
644
792
|
};
|
|
645
793
|
|
|
646
|
-
export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, isVideoFile, useUplofile };
|
|
794
|
+
export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, getExtension, isImageFile, isVideoFile, useUplofile };
|