uplofile 2.0.0 → 2.2.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 +19 -7
- package/dist/index.mjs +76 -25
- 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
|
@@ -6,6 +6,7 @@ declare const Dropzone: ({ asChild, ...rest }: {
|
|
|
6
6
|
asChild?: boolean;
|
|
7
7
|
} & HTMLAttributes<HTMLElement>) => react_jsx_runtime.JSX.Element;
|
|
8
8
|
|
|
9
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
9
10
|
type UploadStatus = "idle" | "uploading" | "done" | "error" | "canceled" | "removing";
|
|
10
11
|
type UploadFileItem<TMeta = any> = {
|
|
11
12
|
uid: string;
|
|
@@ -19,9 +20,11 @@ type UploadFileItem<TMeta = any> = {
|
|
|
19
20
|
error?: string;
|
|
20
21
|
meta?: TMeta;
|
|
21
22
|
};
|
|
22
|
-
type UploadResult = {
|
|
23
|
+
type UploadResult<TMeta = any> = {
|
|
23
24
|
url: string;
|
|
24
25
|
id?: string;
|
|
26
|
+
meta?: TMeta;
|
|
27
|
+
previewUrl?: string;
|
|
25
28
|
};
|
|
26
29
|
type UplofileRootRef<TMeta = any> = {
|
|
27
30
|
setItems: (items: UploadFileItem<TMeta>[] | ((prev: UploadFileItem<TMeta>[]) => UploadFileItem<TMeta>[])) => void;
|
|
@@ -31,9 +34,16 @@ type UplofileRootRef<TMeta = any> = {
|
|
|
31
34
|
openFileDialog: () => void;
|
|
32
35
|
actions: ItemActions;
|
|
33
36
|
};
|
|
37
|
+
type BeforeUploadResult<TMeta = any> = boolean | Array<{
|
|
38
|
+
valid: boolean;
|
|
39
|
+
meta?: TMeta;
|
|
40
|
+
id?: string;
|
|
41
|
+
uid: string;
|
|
42
|
+
reason?: string;
|
|
43
|
+
}>;
|
|
34
44
|
type RootProps<TMeta = any> = PropsWithChildren<{
|
|
35
45
|
multiple?: boolean;
|
|
36
|
-
initial?: Array<Pick<UploadFileItem<TMeta>, "uid" | "id" | "name" | "url" | "meta"
|
|
46
|
+
initial?: MaybePromise<Array<Pick<UploadFileItem<TMeta>, "uid" | "id" | "name" | "url" | "meta">>>;
|
|
37
47
|
/**
|
|
38
48
|
* optimistic (default): remove from UI immediately, call onRemove in the background; if it fails, restore the item and show error.
|
|
39
49
|
* strict: call onRemove first; only remove from UI if it succeeds.
|
|
@@ -43,8 +53,9 @@ type RootProps<TMeta = any> = PropsWithChildren<{
|
|
|
43
53
|
maxCount?: number;
|
|
44
54
|
disabled?: boolean;
|
|
45
55
|
accept?: string;
|
|
56
|
+
beforeUpload?: (items: UploadFileItem<TMeta>[]) => BeforeUploadResult<TMeta> | Promise<BeforeUploadResult<TMeta>>;
|
|
46
57
|
onChange?: (items: UploadFileItem<TMeta>[]) => Promise<void> | void;
|
|
47
|
-
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult
|
|
58
|
+
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult<TMeta>>;
|
|
48
59
|
onRemove?: (item: UploadFileItem<TMeta>, signal: AbortSignal) => Promise<void | any>;
|
|
49
60
|
}>;
|
|
50
61
|
type ItemActions = {
|
|
@@ -97,7 +108,7 @@ type PreviewRenderProps<TMeta = any> = {
|
|
|
97
108
|
type Props<TMeta = any> = {
|
|
98
109
|
render?: (api: PreviewRenderProps<TMeta>) => React.ReactNode;
|
|
99
110
|
};
|
|
100
|
-
declare const Preview: <TMeta = any>({ render }: Props<TMeta>) => string | number | boolean | Iterable<React.ReactNode> |
|
|
111
|
+
declare const Preview: <TMeta = any>({ render }: Props<TMeta>) => string | number | boolean | react_jsx_runtime.JSX.Element | Iterable<React.ReactNode> | null | undefined;
|
|
101
112
|
declare const HiddenInput: ({ name }: {
|
|
102
113
|
name?: string;
|
|
103
114
|
}) => react_jsx_runtime.JSX.Element;
|
|
@@ -112,14 +123,15 @@ declare const Remove: ({ uid, asChild, ...rest }: ButtonProps) => react_jsx_runt
|
|
|
112
123
|
|
|
113
124
|
declare const Root: React.ForwardRefExoticComponent<{
|
|
114
125
|
multiple?: boolean;
|
|
115
|
-
initial?: Pick<UploadFileItem<unknown>, "
|
|
126
|
+
initial?: MaybePromise<Pick<UploadFileItem<unknown>, "id" | "name" | "uid" | "url" | "meta">[]> | undefined;
|
|
116
127
|
removeMode?: "optimistic" | "strict";
|
|
117
128
|
name?: string;
|
|
118
129
|
maxCount?: number;
|
|
119
130
|
disabled?: boolean;
|
|
120
131
|
accept?: string;
|
|
132
|
+
beforeUpload?: ((items: UploadFileItem<unknown>[]) => BeforeUploadResult<unknown> | Promise<BeforeUploadResult<unknown>>) | undefined;
|
|
121
133
|
onChange?: ((items: UploadFileItem<unknown>[]) => Promise<void> | void) | undefined;
|
|
122
|
-
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult
|
|
134
|
+
upload: (file: File, signal: AbortSignal, setProgress?: (pct: number) => void) => Promise<UploadResult<unknown>>;
|
|
123
135
|
onRemove?: ((item: UploadFileItem<unknown>, signal: AbortSignal) => Promise<void | any>) | undefined;
|
|
124
136
|
} & {
|
|
125
137
|
children?: React.ReactNode | undefined;
|
|
@@ -138,4 +150,4 @@ declare const isVideoFile: (item: UploadFileItem<any>, extraExtensions?: string[
|
|
|
138
150
|
declare const isImageFile: (item: UploadFileItem<any>, extraExtensions?: string[]) => boolean;
|
|
139
151
|
|
|
140
152
|
export { Cancel, Dropzone, HiddenInput, Preview, Remove, Retry, Root, Trigger, getExtension, isImageFile, isVideoFile, useUplofile };
|
|
141
|
-
export type { ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus, UplofileRootRef };
|
|
153
|
+
export type { BeforeUploadResult, ImageUploaderContextValue, ItemActions, RootProps, UploadFileItem, UploadResult, UploadStatus, UplofileRootRef };
|
package/dist/index.mjs
CHANGED
|
@@ -115,7 +115,7 @@ const acceptsFile = (file, accept)=>{
|
|
|
115
115
|
};
|
|
116
116
|
|
|
117
117
|
const UploaderCtx = /*#__PURE__*/ createContext(null);
|
|
118
|
-
const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange, upload, removeMode = "optimistic", onRemove, accept = "image/*", name = "image", maxCount, disabled, children }, ref)=>{
|
|
118
|
+
const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange, upload, removeMode = "optimistic", onRemove, accept = "image/*", beforeUpload, name = "image", maxCount, disabled, children }, ref)=>{
|
|
119
119
|
const [items, setItems] = useState([]);
|
|
120
120
|
const controllers = useRef(new Map());
|
|
121
121
|
const removeControllers = useRef(new Map());
|
|
@@ -124,21 +124,24 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
|
|
|
124
124
|
// Hydrate initial items from the server and keep them marked as done
|
|
125
125
|
useEffect(()=>{
|
|
126
126
|
if (hasHydratedInitialRef.current) return;
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
127
|
+
const hydrate = async ()=>{
|
|
128
|
+
const arr = await initial;
|
|
129
|
+
if (!Array.isArray(arr) || arr.length === 0) return;
|
|
130
|
+
const mapped = arr.map((it)=>{
|
|
131
|
+
return {
|
|
132
|
+
uid: it.uid || it.id,
|
|
133
|
+
id: it.id,
|
|
134
|
+
name: it.name,
|
|
135
|
+
url: it.url,
|
|
136
|
+
status: "done",
|
|
137
|
+
meta: it.meta
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
// Only hydrate if the user hasn't already added/modified items locally
|
|
141
|
+
setItems((prev)=>prev.length === 0 ? mapped : prev);
|
|
142
|
+
hasHydratedInitialRef.current = true;
|
|
143
|
+
};
|
|
144
|
+
void hydrate();
|
|
142
145
|
}, [
|
|
143
146
|
initial
|
|
144
147
|
]);
|
|
@@ -183,13 +186,14 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
|
|
|
183
186
|
} catch {
|
|
184
187
|
/*fail silently*/ }
|
|
185
188
|
}
|
|
186
|
-
const serverPreview = result
|
|
189
|
+
const serverPreview = result.previewUrl ?? result.preview ?? result.url;
|
|
187
190
|
return {
|
|
188
191
|
...it,
|
|
189
192
|
status: "done",
|
|
190
193
|
url: result.url,
|
|
191
|
-
id: result.id,
|
|
194
|
+
id: result.id ?? it.id,
|
|
192
195
|
previewUrl: serverPreview,
|
|
196
|
+
meta: result.meta ?? it.meta,
|
|
193
197
|
progress: 100
|
|
194
198
|
};
|
|
195
199
|
}));
|
|
@@ -207,13 +211,13 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
|
|
|
207
211
|
emitChange,
|
|
208
212
|
upload
|
|
209
213
|
]);
|
|
210
|
-
const selectFiles = useCallback((files)=>{
|
|
214
|
+
const selectFiles = useCallback(async (files)=>{
|
|
211
215
|
if (!files) return;
|
|
212
216
|
const selected = Array.isArray(files) ? files : Array.from(files);
|
|
213
217
|
if (selected.length === 0) return;
|
|
214
218
|
const remaining = maxCount ? Math.max(0, maxCount - items.filter((i)=>i.status !== "canceled").length) : undefined;
|
|
215
219
|
const toUse = typeof remaining === "number" ? selected.slice(0, remaining) : selected;
|
|
216
|
-
|
|
220
|
+
let newItems = toUse.map((file)=>({
|
|
217
221
|
uid: uid(),
|
|
218
222
|
name: file.name,
|
|
219
223
|
file,
|
|
@@ -221,12 +225,59 @@ const Root = /*#__PURE__*/ forwardRef(({ multiple = true, initial = [], onChange
|
|
|
221
225
|
status: "idle",
|
|
222
226
|
progress: 0
|
|
223
227
|
}));
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
if (beforeUpload) {
|
|
229
|
+
const result = await beforeUpload(newItems);
|
|
230
|
+
if (result === false) {
|
|
231
|
+
newItems.forEach((it)=>{
|
|
232
|
+
if (it.previewUrl) URL.revokeObjectURL(it.previewUrl);
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (Array.isArray(result)) {
|
|
237
|
+
const resultMap = new Map(result.map((r)=>[
|
|
238
|
+
r.uid,
|
|
239
|
+
r
|
|
240
|
+
]));
|
|
241
|
+
const processedItems = [];
|
|
242
|
+
for (const item of newItems){
|
|
243
|
+
const validation = resultMap.get(item.uid);
|
|
244
|
+
if (!validation) {
|
|
245
|
+
if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (validation.valid) {
|
|
249
|
+
processedItems.push({
|
|
250
|
+
...item,
|
|
251
|
+
meta: validation.meta !== undefined ? validation.meta : item.meta,
|
|
252
|
+
id: validation.id !== undefined ? validation.id : item.id
|
|
253
|
+
});
|
|
254
|
+
} else if (validation.reason) {
|
|
255
|
+
processedItems.push({
|
|
256
|
+
...item,
|
|
257
|
+
status: "error",
|
|
258
|
+
error: validation.reason,
|
|
259
|
+
meta: validation.meta !== undefined ? validation.meta : item.meta,
|
|
260
|
+
id: validation.id !== undefined ? validation.id : item.id
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
newItems = processedItems;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (newItems.length === 0) return;
|
|
270
|
+
emitChange((prev)=>[
|
|
271
|
+
...prev,
|
|
272
|
+
...newItems
|
|
273
|
+
]);
|
|
274
|
+
newItems.forEach((it)=>{
|
|
275
|
+
if (it.status === "idle") {
|
|
276
|
+
startUpload(it);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
229
279
|
}, [
|
|
280
|
+
beforeUpload,
|
|
230
281
|
emitChange,
|
|
231
282
|
items,
|
|
232
283
|
maxCount,
|