uploaderkit 1.0.0 → 2.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/LICENSE +21 -0
- package/NOTICE +16 -0
- package/README.es.md +1140 -0
- package/README.md +1135 -0
- package/dist/FileViewer-ChTf7-uY.d.cts +49 -0
- package/dist/FileViewer-nuD-GEdJ.d.ts +49 -0
- package/dist/adapters/gcs.cjs +83 -0
- package/dist/adapters/gcs.cjs.map +1 -0
- package/dist/adapters/gcs.d.cts +31 -0
- package/dist/adapters/gcs.d.ts +31 -0
- package/dist/adapters/gcs.js +81 -0
- package/dist/adapters/gcs.js.map +1 -0
- package/dist/adapters/memory.cjs +27 -0
- package/dist/adapters/memory.cjs.map +1 -0
- package/dist/adapters/memory.d.cts +18 -0
- package/dist/adapters/memory.d.ts +18 -0
- package/dist/adapters/memory.js +25 -0
- package/dist/adapters/memory.js.map +1 -0
- package/dist/adapters/s3.cjs +75 -0
- package/dist/adapters/s3.cjs.map +1 -0
- package/dist/adapters/s3.d.cts +27 -0
- package/dist/adapters/s3.d.ts +27 -0
- package/dist/adapters/s3.js +73 -0
- package/dist/adapters/s3.js.map +1 -0
- package/dist/chunk-3FI44IOW.js +150 -0
- package/dist/chunk-3FI44IOW.js.map +1 -0
- package/dist/chunk-H7BRW5IY.js +308 -0
- package/dist/chunk-H7BRW5IY.js.map +1 -0
- package/dist/chunk-PDKAF4GX.js +707 -0
- package/dist/chunk-PDKAF4GX.js.map +1 -0
- package/dist/chunk-PTEX7F4R.js +309 -0
- package/dist/chunk-PTEX7F4R.js.map +1 -0
- package/dist/chunk-T5YZG6JW.js +511 -0
- package/dist/chunk-T5YZG6JW.js.map +1 -0
- package/dist/index.cjs +220 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -170
- package/dist/index.d.ts +57 -170
- package/dist/index.js +1 -324
- package/dist/index.js.map +1 -1
- package/dist/presets.cjs +1665 -0
- package/dist/presets.cjs.map +1 -0
- package/dist/presets.d.cts +100 -0
- package/dist/presets.d.ts +100 -0
- package/dist/presets.js +405 -0
- package/dist/presets.js.map +1 -0
- package/dist/react.cjs +981 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +106 -0
- package/dist/react.d.ts +106 -0
- package/dist/react.js +5 -0
- package/dist/react.js.map +1 -0
- package/dist/server/express.cjs +179 -0
- package/dist/server/express.cjs.map +1 -0
- package/dist/server/express.d.cts +64 -0
- package/dist/server/express.d.ts +64 -0
- package/dist/server/express.js +111 -0
- package/dist/server/express.js.map +1 -0
- package/dist/server/next.cjs +165 -0
- package/dist/server/next.cjs.map +1 -0
- package/dist/server/next.d.cts +38 -0
- package/dist/server/next.d.ts +38 -0
- package/dist/server/next.js +107 -0
- package/dist/server/next.js.map +1 -0
- package/dist/server.cjs +550 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +19 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.js +80 -0
- package/dist/server.js.map +1 -0
- package/dist/storage-CYkSHWZX.d.cts +133 -0
- package/dist/storage-Qc9epG0G.d.ts +133 -0
- package/dist/types-BSlJJwti.d.cts +341 -0
- package/dist/types-BSlJJwti.d.ts +341 -0
- package/dist/ui.cjs +2325 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +331 -0
- package/dist/ui.d.ts +331 -0
- package/dist/ui.js +749 -0
- package/dist/ui.js.map +1 -0
- package/dist/useSlottedUploader-BzT5jV8c.d.cts +139 -0
- package/dist/useSlottedUploader-IkG5RhEO.d.ts +139 -0
- package/dist/useUploader-BiBdS-7y.d.cts +117 -0
- package/dist/useUploader-CQHpj_oI.d.ts +117 -0
- package/package.json +189 -3
- package/tailwind.css +80 -0
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/react/compressImage.ts
|
|
7
|
+
var COMPRESSIBLE = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
8
|
+
var compressImage = async (file, { maxWidth, maxHeight, quality = 0.8 }) => {
|
|
9
|
+
if (!COMPRESSIBLE.has(file.type)) return file;
|
|
10
|
+
if (typeof createImageBitmap !== "function") return file;
|
|
11
|
+
try {
|
|
12
|
+
const bitmap = await createImageBitmap(file);
|
|
13
|
+
const scale = Math.min(
|
|
14
|
+
1,
|
|
15
|
+
(maxWidth ?? Infinity) / bitmap.width,
|
|
16
|
+
(maxHeight ?? Infinity) / bitmap.height
|
|
17
|
+
);
|
|
18
|
+
const canvas = document.createElement("canvas");
|
|
19
|
+
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
|
20
|
+
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
|
21
|
+
const context = canvas.getContext("2d");
|
|
22
|
+
if (!context) return file;
|
|
23
|
+
context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
|
24
|
+
bitmap.close();
|
|
25
|
+
const blob = await new Promise(
|
|
26
|
+
(resolve) => canvas.toBlob(resolve, file.type, quality)
|
|
27
|
+
);
|
|
28
|
+
if (!blob || blob.size >= file.size) return file;
|
|
29
|
+
return new File([blob], file.name, { type: file.type });
|
|
30
|
+
} catch {
|
|
31
|
+
return file;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/constants.ts
|
|
36
|
+
var KB = 1024;
|
|
37
|
+
var MB = 1024 * KB;
|
|
38
|
+
var MAGIC_NUMBERS = {
|
|
39
|
+
png: [137, 80, 78, 71, 13, 10, 26, 10],
|
|
40
|
+
jpg: [255, 216, 255],
|
|
41
|
+
jpeg: [255, 216, 255],
|
|
42
|
+
gif: [71, 73, 70, 56],
|
|
43
|
+
webp: [82, 73, 70, 70],
|
|
44
|
+
// RIFF; bytes 8-11 spell WEBP
|
|
45
|
+
ico: [0, 0, 1, 0],
|
|
46
|
+
pdf: [37, 80, 68, 70],
|
|
47
|
+
// %PDF
|
|
48
|
+
// OOXML files are ZIP containers, so docx/xlsx share the PK signature.
|
|
49
|
+
docx: [80, 75, 3, 4],
|
|
50
|
+
xlsx: [80, 75, 3, 4],
|
|
51
|
+
doc: [208, 207, 17, 224],
|
|
52
|
+
xls: [208, 207, 17, 224],
|
|
53
|
+
// DER/ASN.1 sequence header.
|
|
54
|
+
cer: [48, 130],
|
|
55
|
+
crt: [48, 130],
|
|
56
|
+
key: [48, 130],
|
|
57
|
+
pem: [45, 45, 45, 45, 45],
|
|
58
|
+
// -----
|
|
59
|
+
webm: [26, 69, 223, 163],
|
|
60
|
+
avi: [82, 73, 70, 70],
|
|
61
|
+
mp3: [73, 68, 51],
|
|
62
|
+
// ID3
|
|
63
|
+
wav: [82, 73, 70, 70],
|
|
64
|
+
ogg: [79, 103, 103, 83],
|
|
65
|
+
aac: [255, 241]
|
|
66
|
+
};
|
|
67
|
+
var MIME_TYPES = {
|
|
68
|
+
png: "image/png",
|
|
69
|
+
jpg: "image/jpeg",
|
|
70
|
+
jpeg: "image/jpeg",
|
|
71
|
+
svg: "image/svg+xml",
|
|
72
|
+
webp: "image/webp",
|
|
73
|
+
gif: "image/gif",
|
|
74
|
+
ico: "image/x-icon",
|
|
75
|
+
heic: "image/heic",
|
|
76
|
+
cer: "application/x-x509-ca-cert",
|
|
77
|
+
crt: "application/x-x509-ca-cert",
|
|
78
|
+
pem: "application/x-pem-file",
|
|
79
|
+
key: "application/x-pem-file",
|
|
80
|
+
pdf: "application/pdf",
|
|
81
|
+
doc: "application/msword",
|
|
82
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
83
|
+
xls: "application/vnd.ms-excel",
|
|
84
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
85
|
+
csv: "text/csv",
|
|
86
|
+
txt: "text/plain",
|
|
87
|
+
xml: "application/xml",
|
|
88
|
+
json: "application/json",
|
|
89
|
+
mp4: "video/mp4",
|
|
90
|
+
mov: "video/quicktime",
|
|
91
|
+
avi: "video/x-msvideo",
|
|
92
|
+
webm: "video/webm",
|
|
93
|
+
mp3: "audio/mpeg",
|
|
94
|
+
wav: "audio/wav",
|
|
95
|
+
ogg: "audio/ogg",
|
|
96
|
+
aac: "audio/aac"
|
|
97
|
+
};
|
|
98
|
+
var FILE_CATEGORY_CONFIG = {
|
|
99
|
+
image: {
|
|
100
|
+
extensions: ["png", "jpg", "jpeg", "svg", "webp", "gif"],
|
|
101
|
+
maxBytes: 5 * MB,
|
|
102
|
+
validateMagicNumbers: true,
|
|
103
|
+
label: "image",
|
|
104
|
+
accept: "image/*"
|
|
105
|
+
},
|
|
106
|
+
certificate: {
|
|
107
|
+
extensions: ["cer", "crt", "pem"],
|
|
108
|
+
maxBytes: 50 * KB,
|
|
109
|
+
validateMagicNumbers: true,
|
|
110
|
+
label: "certificate",
|
|
111
|
+
accept: ".cer,.crt,.pem"
|
|
112
|
+
},
|
|
113
|
+
key: {
|
|
114
|
+
extensions: ["key"],
|
|
115
|
+
maxBytes: 50 * KB,
|
|
116
|
+
validateMagicNumbers: true,
|
|
117
|
+
label: "private key",
|
|
118
|
+
accept: ".key"
|
|
119
|
+
},
|
|
120
|
+
pdf: {
|
|
121
|
+
extensions: ["pdf"],
|
|
122
|
+
maxBytes: 20 * MB,
|
|
123
|
+
validateMagicNumbers: true,
|
|
124
|
+
label: "PDF",
|
|
125
|
+
accept: ".pdf"
|
|
126
|
+
},
|
|
127
|
+
document: {
|
|
128
|
+
extensions: ["pdf", "doc", "docx", "xls", "xlsx", "csv", "txt"],
|
|
129
|
+
maxBytes: 20 * MB,
|
|
130
|
+
validateMagicNumbers: false,
|
|
131
|
+
label: "document",
|
|
132
|
+
accept: ".pdf,.doc,.docx,.xls,.xlsx,.csv,.txt"
|
|
133
|
+
},
|
|
134
|
+
data: {
|
|
135
|
+
extensions: ["xml", "json"],
|
|
136
|
+
maxBytes: 5 * MB,
|
|
137
|
+
validateMagicNumbers: false,
|
|
138
|
+
label: "data file",
|
|
139
|
+
accept: ".xml,.json"
|
|
140
|
+
},
|
|
141
|
+
video: {
|
|
142
|
+
extensions: ["mp4", "mov", "avi", "webm"],
|
|
143
|
+
maxBytes: 100 * MB,
|
|
144
|
+
validateMagicNumbers: false,
|
|
145
|
+
label: "video",
|
|
146
|
+
accept: "video/*"
|
|
147
|
+
},
|
|
148
|
+
audio: {
|
|
149
|
+
extensions: ["mp3", "wav", "ogg", "aac"],
|
|
150
|
+
maxBytes: 20 * MB,
|
|
151
|
+
validateMagicNumbers: false,
|
|
152
|
+
label: "audio",
|
|
153
|
+
accept: "audio/*"
|
|
154
|
+
},
|
|
155
|
+
any: {
|
|
156
|
+
extensions: [],
|
|
157
|
+
maxBytes: 20 * MB,
|
|
158
|
+
validateMagicNumbers: false,
|
|
159
|
+
label: "file",
|
|
160
|
+
accept: "*/*"
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/file.ts
|
|
165
|
+
var getFileExtension = (fileName) => {
|
|
166
|
+
const dot = fileName.lastIndexOf(".");
|
|
167
|
+
if (dot < 1 || dot === fileName.length - 1) return "";
|
|
168
|
+
return fileName.slice(dot + 1).toLowerCase();
|
|
169
|
+
};
|
|
170
|
+
var isKnownExtension = (value) => value in MIME_TYPES;
|
|
171
|
+
var formatFileSize = (bytes) => {
|
|
172
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
173
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
174
|
+
let size = bytes / 1024;
|
|
175
|
+
let unit = 0;
|
|
176
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
177
|
+
size /= 1024;
|
|
178
|
+
unit++;
|
|
179
|
+
}
|
|
180
|
+
return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unit]}`;
|
|
181
|
+
};
|
|
182
|
+
var toAcceptAttribute = (extensions) => extensions.length === 0 ? "*/*" : extensions.map((extension) => `.${extension}`).join(",");
|
|
183
|
+
|
|
184
|
+
// src/react/slots.ts
|
|
185
|
+
var matchSlotByExtension = (filled) => (slots, fileName) => {
|
|
186
|
+
const extension = getFileExtension(fileName);
|
|
187
|
+
const candidates = slots.filter((slot) => slot.extensions.includes(extension));
|
|
188
|
+
if (candidates.length === 0) return null;
|
|
189
|
+
return (candidates.find((slot) => !filled.has(slot.id)) ?? candidates[0]).id;
|
|
190
|
+
};
|
|
191
|
+
var matchSlotByName = (slots, fileName) => {
|
|
192
|
+
const base = fileName.toLowerCase();
|
|
193
|
+
return slots.find((slot) => base.startsWith(slot.id.toLowerCase()))?.id ?? null;
|
|
194
|
+
};
|
|
195
|
+
var slotFileName = (slotId, originalName) => `${slotId}.${getFileExtension(originalName)}`;
|
|
196
|
+
var slotOfStored = (slots, stored) => slots.find((slot) => stored.fileName.startsWith(`${slot.id}.`))?.id ?? null;
|
|
197
|
+
|
|
198
|
+
// src/labels.ts
|
|
199
|
+
var ES_LABELS = {
|
|
200
|
+
dropPrompt: "Selecciona un archivo o arr\xE1stralo aqu\xED",
|
|
201
|
+
tapPrompt: "Toca para elegir un archivo",
|
|
202
|
+
bulkDropPrompt: "Arrastra varios archivos \u2014 cada uno cae en su documento",
|
|
203
|
+
bulkTapPrompt: "Toca para agregar varios archivos",
|
|
204
|
+
uploadAll: "Subir archivos",
|
|
205
|
+
view: "Ver",
|
|
206
|
+
remove: "Quitar",
|
|
207
|
+
cancel: "Cancelar",
|
|
208
|
+
ready: "Listo",
|
|
209
|
+
staged: "Listo para subir",
|
|
210
|
+
upload: "Subir",
|
|
211
|
+
replace: "Reemplazar",
|
|
212
|
+
dropToReplace: "Suelta para reemplazar",
|
|
213
|
+
previous: "Anterior",
|
|
214
|
+
next: "Siguiente",
|
|
215
|
+
uploadFailed: "No se pudo subir el archivo",
|
|
216
|
+
uploadNetworkFailed: "No se pudo subir el archivo. Revisa tu conexi\xF3n",
|
|
217
|
+
removeFailed: "No se pudo borrar el archivo del almacenamiento",
|
|
218
|
+
readFailed: "No se pudo cargar el archivo",
|
|
219
|
+
maxFilesReached: (max) => `M\xE1ximo ${max} archivo(s)`,
|
|
220
|
+
slotFormatNotAllowed: (slotLabel, extensions) => `${slotLabel}: formato no permitido. Se aceptan: ${extensions.join(", ")}`,
|
|
221
|
+
fileWithoutSlot: (fileName) => `"${fileName}" no corresponde a ning\xFAn documento`,
|
|
222
|
+
viewerLoading: "Cargando\u2026",
|
|
223
|
+
viewerError: "No se pudo cargar la vista previa",
|
|
224
|
+
viewerErrorHint: "El archivo puede haberse movido o el enlace expir\xF3. Puedes reintentar o descargarlo.",
|
|
225
|
+
viewerRetry: "Reintentar",
|
|
226
|
+
openInTab: "Abrir en pesta\xF1a",
|
|
227
|
+
downloadShort: "Descargar",
|
|
228
|
+
close: "Cerrar",
|
|
229
|
+
noPreview: "Este formato no tiene vista previa",
|
|
230
|
+
download: (fileName) => `Descargar ${fileName}`,
|
|
231
|
+
filePreview: "Archivo",
|
|
232
|
+
confirm: "Confirmar",
|
|
233
|
+
confirmRemoveTitle: "Quitar archivo",
|
|
234
|
+
confirmRemoveMessage: (fileName) => `\xBFQuitar "${fileName}"? Podr\xE1s subir otro despu\xE9s.`,
|
|
235
|
+
confirmReplaceTitle: "Reemplazar archivo",
|
|
236
|
+
confirmReplaceMessage: (current, incoming) => `\xBFReemplazar "${current}" por "${incoming}"?`,
|
|
237
|
+
dropAnywhereTitle: "Suelta los archivos aqu\xED",
|
|
238
|
+
dropAnywhereHint: "Cualquier lugar de la pantalla cuenta",
|
|
239
|
+
avatarChange: "Cambiar foto",
|
|
240
|
+
avatarDropHere: "Suelta aqu\xED",
|
|
241
|
+
avatarRemove: "Quitar foto",
|
|
242
|
+
galleryAdd: "Agregar",
|
|
243
|
+
fileHasNoExtension: "El archivo no tiene extensi\xF3n",
|
|
244
|
+
formatNotAllowed: (extensions) => `Formato no permitido. Se aceptan: ${extensions.join(", ")}`,
|
|
245
|
+
fileIsEmpty: "El archivo est\xE1 vac\xEDo",
|
|
246
|
+
fileTooLarge: (size, max) => `El archivo pesa ${size} y el m\xE1ximo es ${max}`,
|
|
247
|
+
fileIncomplete: "El archivo est\xE1 incompleto",
|
|
248
|
+
contentDoesNotMatchExtension: "El contenido del archivo no corresponde a su extensi\xF3n",
|
|
249
|
+
requestIncomplete: "Solicitud incompleta",
|
|
250
|
+
unauthorized: "No autorizado",
|
|
251
|
+
fileRequired: "Archivo requerido",
|
|
252
|
+
unknownScope: "Destino de archivo no v\xE1lido",
|
|
253
|
+
processingFailed: "No se pudo procesar el archivo",
|
|
254
|
+
unnamedFile: "archivo"
|
|
255
|
+
};
|
|
256
|
+
var DEFAULT_LABELS = {
|
|
257
|
+
dropPrompt: "Choose a file or drag it here",
|
|
258
|
+
tapPrompt: "Tap to choose a file",
|
|
259
|
+
bulkDropPrompt: "Drop several files \u2014 each lands on its document",
|
|
260
|
+
bulkTapPrompt: "Tap to add several files",
|
|
261
|
+
uploadAll: "Upload files",
|
|
262
|
+
view: "View",
|
|
263
|
+
remove: "Remove",
|
|
264
|
+
cancel: "Cancel",
|
|
265
|
+
ready: "Done",
|
|
266
|
+
staged: "Ready to upload",
|
|
267
|
+
upload: "Upload",
|
|
268
|
+
replace: "Replace",
|
|
269
|
+
dropToReplace: "Drop to replace",
|
|
270
|
+
previous: "Previous",
|
|
271
|
+
next: "Next",
|
|
272
|
+
uploadFailed: "The file could not be uploaded",
|
|
273
|
+
uploadNetworkFailed: "The file could not be uploaded. Check your connection",
|
|
274
|
+
removeFailed: "The file could not be removed from storage",
|
|
275
|
+
readFailed: "The file could not be loaded",
|
|
276
|
+
maxFilesReached: (max) => `At most ${max} file(s)`,
|
|
277
|
+
slotFormatNotAllowed: (slotLabel, extensions) => `${slotLabel}: format not allowed. Accepted: ${extensions.join(", ")}`,
|
|
278
|
+
fileWithoutSlot: (fileName) => `"${fileName}" does not match any document`,
|
|
279
|
+
viewerLoading: "Loading\u2026",
|
|
280
|
+
viewerError: "The preview could not be loaded",
|
|
281
|
+
viewerErrorHint: "The file may have moved or the link expired. You can retry or download it.",
|
|
282
|
+
viewerRetry: "Retry",
|
|
283
|
+
openInTab: "Open in tab",
|
|
284
|
+
downloadShort: "Download",
|
|
285
|
+
close: "Close",
|
|
286
|
+
noPreview: "This format has no preview",
|
|
287
|
+
download: (fileName) => `Download ${fileName}`,
|
|
288
|
+
filePreview: "File",
|
|
289
|
+
confirm: "Confirm",
|
|
290
|
+
confirmRemoveTitle: "Remove file",
|
|
291
|
+
confirmRemoveMessage: (fileName) => `Remove "${fileName}"? You can upload another one later.`,
|
|
292
|
+
confirmReplaceTitle: "Replace file",
|
|
293
|
+
confirmReplaceMessage: (current, incoming) => `Replace "${current}" with "${incoming}"?`,
|
|
294
|
+
dropAnywhereTitle: "Drop the files here",
|
|
295
|
+
dropAnywhereHint: "Anywhere on the screen counts",
|
|
296
|
+
avatarChange: "Change photo",
|
|
297
|
+
avatarDropHere: "Drop here",
|
|
298
|
+
avatarRemove: "Remove photo",
|
|
299
|
+
galleryAdd: "Add",
|
|
300
|
+
fileHasNoExtension: "The file has no extension",
|
|
301
|
+
formatNotAllowed: (extensions) => `Format not allowed. Accepted: ${extensions.join(", ")}`,
|
|
302
|
+
fileIsEmpty: "The file is empty",
|
|
303
|
+
fileTooLarge: (size, max) => `The file weighs ${size} and the maximum is ${max}`,
|
|
304
|
+
fileIncomplete: "The file is incomplete",
|
|
305
|
+
contentDoesNotMatchExtension: "The file contents do not match its extension",
|
|
306
|
+
requestIncomplete: "Incomplete request",
|
|
307
|
+
unauthorized: "Unauthorized",
|
|
308
|
+
fileRequired: "A file is required",
|
|
309
|
+
unknownScope: "Unknown file destination",
|
|
310
|
+
processingFailed: "The file could not be processed",
|
|
311
|
+
unnamedFile: "file"
|
|
312
|
+
};
|
|
313
|
+
var resolveLabels = (labels, base = DEFAULT_LABELS) => labels ? { ...base, ...labels } : base;
|
|
314
|
+
({ ...DEFAULT_LABELS });
|
|
315
|
+
|
|
316
|
+
// src/react/strategy.ts
|
|
317
|
+
var createXhrUploadStrategy = ({
|
|
318
|
+
endpoint,
|
|
319
|
+
headers,
|
|
320
|
+
fieldName = "file",
|
|
321
|
+
credentials,
|
|
322
|
+
labels
|
|
323
|
+
}) => (file, scope, entityId, { onProgress, signal }) => new Promise((resolve, reject) => {
|
|
324
|
+
const copy = resolveLabels(labels);
|
|
325
|
+
const xhr = new XMLHttpRequest();
|
|
326
|
+
const url = `${endpoint.replace(/\/$/, "")}/${encodeURIComponent(scope)}/${encodeURIComponent(entityId)}/upload`;
|
|
327
|
+
xhr.open("POST", url);
|
|
328
|
+
xhr.responseType = "json";
|
|
329
|
+
xhr.withCredentials = credentials === "include";
|
|
330
|
+
for (const [name, value] of Object.entries(headers?.() ?? {})) {
|
|
331
|
+
xhr.setRequestHeader(name, value);
|
|
332
|
+
}
|
|
333
|
+
const abort = () => xhr.abort();
|
|
334
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
335
|
+
xhr.upload.onprogress = (event) => {
|
|
336
|
+
if (event.lengthComputable) {
|
|
337
|
+
onProgress(Math.round(event.loaded / event.total * 100));
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
xhr.onload = () => {
|
|
341
|
+
signal.removeEventListener("abort", abort);
|
|
342
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
343
|
+
resolve(xhr.response);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const message = xhr.response?.message ?? copy.uploadFailed;
|
|
347
|
+
reject(new Error(message));
|
|
348
|
+
};
|
|
349
|
+
xhr.onerror = () => {
|
|
350
|
+
signal.removeEventListener("abort", abort);
|
|
351
|
+
reject(new Error(copy.uploadNetworkFailed));
|
|
352
|
+
};
|
|
353
|
+
xhr.onabort = () => {
|
|
354
|
+
signal.removeEventListener("abort", abort);
|
|
355
|
+
const error = new Error("Upload cancelled");
|
|
356
|
+
error.name = "AbortError";
|
|
357
|
+
reject(error);
|
|
358
|
+
};
|
|
359
|
+
const form = new FormData();
|
|
360
|
+
form.append(fieldName, file);
|
|
361
|
+
xhr.send(form);
|
|
362
|
+
});
|
|
363
|
+
var createRemoveStrategy = ({ endpoint, headers, credentials }) => async (stored, scope, entityId) => {
|
|
364
|
+
const url = `${endpoint.replace(/\/$/, "")}/${encodeURIComponent(scope)}/${encodeURIComponent(entityId)}`;
|
|
365
|
+
const response = await fetch(url, {
|
|
366
|
+
method: "DELETE",
|
|
367
|
+
credentials,
|
|
368
|
+
headers: { "Content-Type": "application/json", ...headers?.() },
|
|
369
|
+
body: JSON.stringify({ key: stored.key })
|
|
370
|
+
});
|
|
371
|
+
if (!response.ok) return false;
|
|
372
|
+
const body = await response.json().catch(() => null);
|
|
373
|
+
return body?.deleted === true;
|
|
374
|
+
};
|
|
375
|
+
var LabelsContext = react.createContext(DEFAULT_LABELS);
|
|
376
|
+
var UploaderProvider = ({
|
|
377
|
+
children,
|
|
378
|
+
language = "en",
|
|
379
|
+
labels
|
|
380
|
+
}) => {
|
|
381
|
+
const value = react.useMemo(
|
|
382
|
+
() => resolveLabels(labels, language === "es" ? ES_LABELS : DEFAULT_LABELS),
|
|
383
|
+
[language, labels]
|
|
384
|
+
);
|
|
385
|
+
return /* @__PURE__ */ jsxRuntime.jsx(LabelsContext.Provider, { value, children });
|
|
386
|
+
};
|
|
387
|
+
var useUploaderLabels = (overrides) => {
|
|
388
|
+
const base = react.useContext(LabelsContext);
|
|
389
|
+
return react.useMemo(
|
|
390
|
+
() => overrides ? { ...base, ...overrides } : base,
|
|
391
|
+
[base, overrides]
|
|
392
|
+
);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/validation.ts
|
|
396
|
+
var ok = { valid: true };
|
|
397
|
+
var fail = (code, message) => ({ valid: false, code, message });
|
|
398
|
+
var validateExtension = (fileName, allowed, labels) => {
|
|
399
|
+
if (allowed.length === 0) return ok;
|
|
400
|
+
const copy = resolveLabels(labels);
|
|
401
|
+
const extension = getFileExtension(fileName);
|
|
402
|
+
if (!extension) {
|
|
403
|
+
return fail("extension-not-allowed", copy.fileHasNoExtension);
|
|
404
|
+
}
|
|
405
|
+
if (!allowed.includes(extension)) {
|
|
406
|
+
return fail("extension-not-allowed", copy.formatNotAllowed(allowed));
|
|
407
|
+
}
|
|
408
|
+
return ok;
|
|
409
|
+
};
|
|
410
|
+
var validateSize = (size, maxBytes, labels) => {
|
|
411
|
+
const copy = resolveLabels(labels);
|
|
412
|
+
if (size === 0) return fail("empty-file", copy.fileIsEmpty);
|
|
413
|
+
if (size > maxBytes) {
|
|
414
|
+
return fail(
|
|
415
|
+
"too-large",
|
|
416
|
+
copy.fileTooLarge(formatFileSize(size), formatFileSize(maxBytes))
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return ok;
|
|
420
|
+
};
|
|
421
|
+
var validateMagicNumbers = async (file, labels) => {
|
|
422
|
+
const copy = resolveLabels(labels);
|
|
423
|
+
const extension = getFileExtension(file.name);
|
|
424
|
+
if (!isKnownExtension(extension)) return ok;
|
|
425
|
+
const expected = MAGIC_NUMBERS[extension];
|
|
426
|
+
if (!expected) return ok;
|
|
427
|
+
const head = new Uint8Array(await file.arrayBuffer()).slice(
|
|
428
|
+
0,
|
|
429
|
+
expected.length
|
|
430
|
+
);
|
|
431
|
+
if (head.length < expected.length) {
|
|
432
|
+
return fail("magic-number-mismatch", copy.fileIncomplete);
|
|
433
|
+
}
|
|
434
|
+
const matches = expected.every((byte, index) => head[index] === byte);
|
|
435
|
+
return matches ? ok : fail("magic-number-mismatch", copy.contentDoesNotMatchExtension);
|
|
436
|
+
};
|
|
437
|
+
var validateFile = async (file, options = {}) => {
|
|
438
|
+
const {
|
|
439
|
+
maxBytes = Infinity,
|
|
440
|
+
allowedExtensions = [],
|
|
441
|
+
validateMagicNumbers: checkMagic = false,
|
|
442
|
+
labels,
|
|
443
|
+
customValidation
|
|
444
|
+
} = options;
|
|
445
|
+
const size = validateSize(file.size, maxBytes, labels);
|
|
446
|
+
if (!size.valid) return size;
|
|
447
|
+
const extension = validateExtension(file.name, allowedExtensions, labels);
|
|
448
|
+
if (!extension.valid) return extension;
|
|
449
|
+
if (checkMagic) {
|
|
450
|
+
const magic = await validateMagicNumbers(file, labels);
|
|
451
|
+
if (!magic.valid) return magic;
|
|
452
|
+
}
|
|
453
|
+
if (customValidation) return customValidation(file);
|
|
454
|
+
return ok;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// src/scopes.ts
|
|
458
|
+
var ScopeError = class extends Error {
|
|
459
|
+
name = "ScopeError";
|
|
460
|
+
};
|
|
461
|
+
var PROBE_ENTITY = "__entity__";
|
|
462
|
+
var probeFile = (name) => ({
|
|
463
|
+
name,
|
|
464
|
+
size: 0,
|
|
465
|
+
type: "",
|
|
466
|
+
arrayBuffer: async () => new ArrayBuffer(0)
|
|
467
|
+
});
|
|
468
|
+
var hasStableKey = (scope) => scope.path(PROBE_ENTITY, probeFile("a.png")) === scope.path(PROBE_ENTITY, probeFile("b.png"));
|
|
469
|
+
var resolveReplaceMode = (scope) => {
|
|
470
|
+
if (scope.replace !== void 0) return scope.replace;
|
|
471
|
+
if ((scope.maxFiles ?? 1) > 1) return "key";
|
|
472
|
+
return hasStableKey(scope) ? "key" : "entity";
|
|
473
|
+
};
|
|
474
|
+
var validateForScope = async (registry, name, file, labels) => {
|
|
475
|
+
const scope = registry.get(name);
|
|
476
|
+
const category = scope.category ? FILE_CATEGORY_CONFIG[scope.category] : void 0;
|
|
477
|
+
return validateFile(file, {
|
|
478
|
+
maxBytes: scope.maxBytes,
|
|
479
|
+
allowedExtensions: scope.accept,
|
|
480
|
+
validateMagicNumbers: category?.validateMagicNumbers ?? true,
|
|
481
|
+
labels
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// src/warn.ts
|
|
486
|
+
var seen = /* @__PURE__ */ new Set();
|
|
487
|
+
var warnDev = (key, message) => {
|
|
488
|
+
if (process.env.NODE_ENV === "production") return;
|
|
489
|
+
if (seen.has(key)) return;
|
|
490
|
+
seen.add(key);
|
|
491
|
+
console.warn(`[uploaderkit] ${message}`);
|
|
492
|
+
};
|
|
493
|
+
var createId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : Math.random().toString(36).slice(2);
|
|
494
|
+
var previewOf = (file) => file.type.startsWith("image/") && typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : void 0;
|
|
495
|
+
var revokePreview = (file) => {
|
|
496
|
+
if (file.preview) URL.revokeObjectURL(file.preview);
|
|
497
|
+
};
|
|
498
|
+
var normalizeRetry = (retry) => {
|
|
499
|
+
const options = typeof retry === "number" ? { attempts: retry } : retry ?? {};
|
|
500
|
+
return {
|
|
501
|
+
attempts: Math.max(1, options.attempts ?? 1),
|
|
502
|
+
backoffMs: options.backoffMs ?? 500
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
var wait = (ms, signal) => new Promise((resolve, reject) => {
|
|
506
|
+
const abort = () => {
|
|
507
|
+
clearTimeout(timer);
|
|
508
|
+
const error = new Error("Upload cancelled");
|
|
509
|
+
error.name = "AbortError";
|
|
510
|
+
reject(error);
|
|
511
|
+
};
|
|
512
|
+
const timer = setTimeout(() => {
|
|
513
|
+
signal.removeEventListener("abort", abort);
|
|
514
|
+
resolve();
|
|
515
|
+
}, ms);
|
|
516
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
517
|
+
});
|
|
518
|
+
var runPool = async (items, limit, run) => {
|
|
519
|
+
const results = new Array(items.length);
|
|
520
|
+
let cursor = 0;
|
|
521
|
+
const workers = Array.from(
|
|
522
|
+
{ length: Math.min(Math.max(1, limit), items.length) },
|
|
523
|
+
async () => {
|
|
524
|
+
while (cursor < items.length) {
|
|
525
|
+
const index = cursor++;
|
|
526
|
+
results[index] = await run(items[index]);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
await Promise.all(workers);
|
|
531
|
+
return results;
|
|
532
|
+
};
|
|
533
|
+
var useUploader = ({
|
|
534
|
+
scopes,
|
|
535
|
+
scope,
|
|
536
|
+
entityId,
|
|
537
|
+
strategy,
|
|
538
|
+
multiple: multipleOption,
|
|
539
|
+
maxFiles: maxFilesOption,
|
|
540
|
+
uploadOn = "manual",
|
|
541
|
+
onUploadStart,
|
|
542
|
+
onUploaded,
|
|
543
|
+
onError,
|
|
544
|
+
retry,
|
|
545
|
+
concurrency = Infinity,
|
|
546
|
+
rename,
|
|
547
|
+
labels
|
|
548
|
+
}) => {
|
|
549
|
+
const scopeMaxFiles = scopes.get(scope).maxFiles;
|
|
550
|
+
const maxFiles = maxFilesOption ?? scopeMaxFiles;
|
|
551
|
+
const multiple = multipleOption ?? (maxFiles !== void 0 && maxFiles > 1);
|
|
552
|
+
const [files, setFiles] = react.useState([]);
|
|
553
|
+
const controllers = react.useRef(/* @__PURE__ */ new Map());
|
|
554
|
+
const filesRef = react.useRef([]);
|
|
555
|
+
filesRef.current = files;
|
|
556
|
+
const copy = useUploaderLabels(labels);
|
|
557
|
+
const { attempts, backoffMs } = normalizeRetry(retry);
|
|
558
|
+
const patch = react.useCallback((id, changes) => {
|
|
559
|
+
setFiles(
|
|
560
|
+
(previous) => previous.map((file) => file.id === id ? { ...file, ...changes } : file)
|
|
561
|
+
);
|
|
562
|
+
}, []);
|
|
563
|
+
const uploadOne = react.useCallback(
|
|
564
|
+
async (target) => {
|
|
565
|
+
if (!strategy) {
|
|
566
|
+
warnDev(
|
|
567
|
+
"upload-without-strategy",
|
|
568
|
+
"upload() ran without a `strategy` \u2014 files stay local. Pass `strategy` (e.g. createXhrUploadStrategy) to useUploader."
|
|
569
|
+
);
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
const config = scopes.get(scope);
|
|
573
|
+
const controller = new AbortController();
|
|
574
|
+
controllers.current.set(target.id, controller);
|
|
575
|
+
patch(target.id, { status: "uploading", progress: 0 });
|
|
576
|
+
try {
|
|
577
|
+
const payload = config.compress ? await compressImage(target.file, config.compress) : target.file;
|
|
578
|
+
for (let attempt = 1; ; attempt++) {
|
|
579
|
+
try {
|
|
580
|
+
const stored = await strategy(payload, scope, entityId, {
|
|
581
|
+
onProgress: (percent) => patch(target.id, { progress: percent }),
|
|
582
|
+
signal: controller.signal
|
|
583
|
+
});
|
|
584
|
+
patch(target.id, { status: "success", progress: 100, stored });
|
|
585
|
+
return stored;
|
|
586
|
+
} catch (error) {
|
|
587
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
588
|
+
if (aborted || attempt >= attempts) throw error;
|
|
589
|
+
patch(target.id, { progress: 0 });
|
|
590
|
+
await wait(backoffMs * 2 ** (attempt - 1), controller.signal);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
} catch (error) {
|
|
594
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
595
|
+
const message = error instanceof Error && error.message ? error.message : copy.uploadFailed;
|
|
596
|
+
patch(target.id, {
|
|
597
|
+
status: aborted ? "idle" : "error",
|
|
598
|
+
progress: 0,
|
|
599
|
+
...aborted ? {} : { error: message }
|
|
600
|
+
});
|
|
601
|
+
if (!aborted) onError?.(message);
|
|
602
|
+
return null;
|
|
603
|
+
} finally {
|
|
604
|
+
controllers.current.delete(target.id);
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
[
|
|
608
|
+
strategy,
|
|
609
|
+
scopes,
|
|
610
|
+
scope,
|
|
611
|
+
entityId,
|
|
612
|
+
patch,
|
|
613
|
+
onError,
|
|
614
|
+
attempts,
|
|
615
|
+
backoffMs,
|
|
616
|
+
copy
|
|
617
|
+
]
|
|
618
|
+
);
|
|
619
|
+
const uploadBatch = react.useCallback(
|
|
620
|
+
async (targets) => {
|
|
621
|
+
if (targets.length === 0) return [];
|
|
622
|
+
onUploadStart?.(targets);
|
|
623
|
+
const results = await runPool(targets, concurrency, uploadOne);
|
|
624
|
+
const stored = results.filter((r) => r !== null);
|
|
625
|
+
if (stored.length > 0) onUploaded?.(stored);
|
|
626
|
+
return stored;
|
|
627
|
+
},
|
|
628
|
+
[uploadOne, onUploadStart, onUploaded, concurrency]
|
|
629
|
+
);
|
|
630
|
+
const addFiles = react.useCallback(
|
|
631
|
+
async (incoming) => {
|
|
632
|
+
let batch = Array.from(incoming);
|
|
633
|
+
if (!multiple && batch.length > 1) {
|
|
634
|
+
warnDev(
|
|
635
|
+
"single-mode-multi-drop",
|
|
636
|
+
`addFiles received ${batch.length} files but \`multiple\` is false \u2014 only the first is kept. Pass multiple: true if that is not what you meant.`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (!multiple) batch = batch.slice(0, 1);
|
|
640
|
+
if (maxFiles !== void 0) {
|
|
641
|
+
const room = maxFiles - (multiple ? filesRef.current.length : 0);
|
|
642
|
+
if (batch.length > room) {
|
|
643
|
+
batch = batch.slice(0, Math.max(0, room));
|
|
644
|
+
onError?.(copy.maxFilesReached(maxFiles));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (batch.length === 0) return;
|
|
648
|
+
if (rename) {
|
|
649
|
+
batch = batch.map(
|
|
650
|
+
(file) => new File([file], rename(file), { type: file.type })
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
const next = [];
|
|
654
|
+
for (const file of batch) {
|
|
655
|
+
const result = await validateForScope(scopes, scope, file, copy);
|
|
656
|
+
next.push({
|
|
657
|
+
id: createId(),
|
|
658
|
+
file,
|
|
659
|
+
status: result.valid ? "idle" : "error",
|
|
660
|
+
progress: 0,
|
|
661
|
+
...result.valid ? { preview: previewOf(file) } : { error: result.message }
|
|
662
|
+
});
|
|
663
|
+
if (!result.valid) onError?.(next[next.length - 1].error);
|
|
664
|
+
}
|
|
665
|
+
setFiles((previous) => {
|
|
666
|
+
if (multiple) return [...previous, ...next];
|
|
667
|
+
previous.forEach(revokePreview);
|
|
668
|
+
return next;
|
|
669
|
+
});
|
|
670
|
+
if (uploadOn === "select") {
|
|
671
|
+
await uploadBatch(next.filter((file) => file.status === "idle"));
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
[
|
|
675
|
+
multiple,
|
|
676
|
+
maxFiles,
|
|
677
|
+
scopes,
|
|
678
|
+
scope,
|
|
679
|
+
uploadOn,
|
|
680
|
+
uploadBatch,
|
|
681
|
+
onError,
|
|
682
|
+
rename,
|
|
683
|
+
copy
|
|
684
|
+
]
|
|
685
|
+
);
|
|
686
|
+
const upload = react.useCallback(
|
|
687
|
+
() => uploadBatch(filesRef.current.filter((f) => f.status === "idle")),
|
|
688
|
+
[uploadBatch]
|
|
689
|
+
);
|
|
690
|
+
const abort = react.useCallback((id) => {
|
|
691
|
+
if (id) {
|
|
692
|
+
controllers.current.get(id)?.abort();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
controllers.current.forEach((controller) => controller.abort());
|
|
696
|
+
}, []);
|
|
697
|
+
const removeFile = react.useCallback(
|
|
698
|
+
(id) => {
|
|
699
|
+
abort(id);
|
|
700
|
+
setFiles((previous) => {
|
|
701
|
+
const target = previous.find((file) => file.id === id);
|
|
702
|
+
if (target) revokePreview(target);
|
|
703
|
+
return previous.filter((file) => file.id !== id);
|
|
704
|
+
});
|
|
705
|
+
},
|
|
706
|
+
[abort]
|
|
707
|
+
);
|
|
708
|
+
const clear = react.useCallback(() => {
|
|
709
|
+
abort();
|
|
710
|
+
setFiles((previous) => {
|
|
711
|
+
previous.forEach(revokePreview);
|
|
712
|
+
return [];
|
|
713
|
+
});
|
|
714
|
+
}, [abort]);
|
|
715
|
+
react.useEffect(
|
|
716
|
+
() => () => {
|
|
717
|
+
filesRef.current.forEach(revokePreview);
|
|
718
|
+
controllers.current.forEach((controller) => controller.abort());
|
|
719
|
+
},
|
|
720
|
+
[]
|
|
721
|
+
);
|
|
722
|
+
const accept = react.useMemo(() => scopes.accept(scope), [scopes, scope]);
|
|
723
|
+
return {
|
|
724
|
+
files,
|
|
725
|
+
accept,
|
|
726
|
+
isUploading: files.some((file) => file.status === "uploading"),
|
|
727
|
+
hasPending: files.some((file) => file.status === "idle"),
|
|
728
|
+
addFiles,
|
|
729
|
+
upload,
|
|
730
|
+
abort,
|
|
731
|
+
removeFile,
|
|
732
|
+
clear
|
|
733
|
+
};
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
// src/react/useSlottedUploader.ts
|
|
737
|
+
var useSlottedUploader = ({
|
|
738
|
+
scopes,
|
|
739
|
+
scope,
|
|
740
|
+
entityId,
|
|
741
|
+
strategy,
|
|
742
|
+
slots,
|
|
743
|
+
value,
|
|
744
|
+
onChange,
|
|
745
|
+
matchBy = "extension",
|
|
746
|
+
match,
|
|
747
|
+
uploadOn = "select",
|
|
748
|
+
onUploadStart,
|
|
749
|
+
removeStrategy,
|
|
750
|
+
onRemoveStored,
|
|
751
|
+
onError,
|
|
752
|
+
retry,
|
|
753
|
+
concurrency,
|
|
754
|
+
labels
|
|
755
|
+
}) => {
|
|
756
|
+
const copy = useUploaderLabels(labels);
|
|
757
|
+
react.useMemo(() => {
|
|
758
|
+
const ids = /* @__PURE__ */ new Set();
|
|
759
|
+
for (const slot of slots) {
|
|
760
|
+
if (ids.has(slot.id)) {
|
|
761
|
+
throw new ScopeError(
|
|
762
|
+
`useSlottedUploader: duplicate slot id "${slot.id}" \u2014 slot ids are storage keys and must be unique`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
ids.add(slot.id);
|
|
766
|
+
}
|
|
767
|
+
if (slots.length > 1 && resolveReplaceMode(scopes.get(scope)) === "entity") {
|
|
768
|
+
throw new ScopeError(
|
|
769
|
+
`useSlottedUploader: scope "${scope}" replaces per entity, so filling one of its ${slots.length} slots would delete the others. Declare "maxFiles" on the scope (one per slot, at least), or set replace: 'key'.`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
const accepted = scopes.get(scope).accept;
|
|
773
|
+
for (const slot of slots) {
|
|
774
|
+
const outside = slot.extensions.filter(
|
|
775
|
+
(extension) => !accepted.includes(extension)
|
|
776
|
+
);
|
|
777
|
+
if (outside.length > 0) {
|
|
778
|
+
warnDev(
|
|
779
|
+
`slot-outside-scope:${slot.id}`,
|
|
780
|
+
`slot "${slot.id}" accepts ${outside.join(", ")} but scope "${scope}" does not \u2014 those uploads will be rejected by validation. Align the slot's extensions with the scope's accept list.`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}, [slots, scopes, scope]);
|
|
785
|
+
const valueRef = react.useRef(value);
|
|
786
|
+
valueRef.current = value;
|
|
787
|
+
const onChangeRef = react.useRef(onChange);
|
|
788
|
+
onChangeRef.current = onChange;
|
|
789
|
+
const landedRef = react.useRef(/* @__PURE__ */ new Map());
|
|
790
|
+
for (const [slot, item] of landedRef.current) {
|
|
791
|
+
const absorbed = value.some(
|
|
792
|
+
(entry) => entry.slot === slot && entry.stored.key === item.stored.key && entry.stored.uploadedAt === item.stored.uploadedAt
|
|
793
|
+
);
|
|
794
|
+
if (absorbed) landedRef.current.delete(slot);
|
|
795
|
+
}
|
|
796
|
+
const uploader = useUploader({
|
|
797
|
+
scopes,
|
|
798
|
+
scope,
|
|
799
|
+
entityId,
|
|
800
|
+
strategy,
|
|
801
|
+
multiple: true,
|
|
802
|
+
uploadOn,
|
|
803
|
+
onUploadStart,
|
|
804
|
+
onError,
|
|
805
|
+
...retry !== void 0 ? { retry } : {},
|
|
806
|
+
...concurrency !== void 0 ? { concurrency } : {},
|
|
807
|
+
...labels ? { labels } : {},
|
|
808
|
+
onUploaded: (stored) => {
|
|
809
|
+
for (const item of stored) {
|
|
810
|
+
const slot = slots.find((s) => item.fileName.startsWith(`${s.id}.`));
|
|
811
|
+
if (slot)
|
|
812
|
+
landedRef.current.set(slot.id, { slot: slot.id, stored: item });
|
|
813
|
+
}
|
|
814
|
+
const merged = new Map(valueRef.current.map((entry) => [entry.slot, entry]));
|
|
815
|
+
landedRef.current.forEach((entry, slot) => merged.set(slot, entry));
|
|
816
|
+
void onChangeRef.current([...merged.values()]);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
const resolveSlot = react.useMemo(() => {
|
|
820
|
+
if (match) return match;
|
|
821
|
+
if (matchBy === "name") return matchSlotByName;
|
|
822
|
+
return (all, fileName) => matchSlotByExtension(new Set(valueRef.current.map((item) => item.slot)))(
|
|
823
|
+
all,
|
|
824
|
+
fileName
|
|
825
|
+
);
|
|
826
|
+
}, [match, matchBy]);
|
|
827
|
+
const assign = react.useCallback(
|
|
828
|
+
(slotId, file) => {
|
|
829
|
+
const slot = slots.find((s) => s.id === slotId);
|
|
830
|
+
if (!slot) return null;
|
|
831
|
+
const extension = getFileExtension(file.name);
|
|
832
|
+
if (!slot.extensions.includes(extension)) {
|
|
833
|
+
onError?.(copy.slotFormatNotAllowed(slot.label, slot.extensions));
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
return new File([file], slotFileName(slotId, file.name), {
|
|
837
|
+
type: file.type
|
|
838
|
+
});
|
|
839
|
+
},
|
|
840
|
+
[slots, onError, copy]
|
|
841
|
+
);
|
|
842
|
+
const addToSlot = react.useCallback(
|
|
843
|
+
async (slotId, file) => {
|
|
844
|
+
const renamed = assign(slotId, file);
|
|
845
|
+
if (renamed) await uploader.addFiles([renamed]);
|
|
846
|
+
},
|
|
847
|
+
[assign, uploader]
|
|
848
|
+
);
|
|
849
|
+
const addFiles = react.useCallback(
|
|
850
|
+
async (incoming) => {
|
|
851
|
+
const batch = [];
|
|
852
|
+
for (const file of Array.from(incoming)) {
|
|
853
|
+
const slotId = resolveSlot(slots, file.name);
|
|
854
|
+
if (!slotId) {
|
|
855
|
+
onError?.(copy.fileWithoutSlot(file.name));
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
const renamed = assign(slotId, file);
|
|
859
|
+
if (renamed) batch.push(renamed);
|
|
860
|
+
}
|
|
861
|
+
if (batch.length > 0) await uploader.addFiles(batch);
|
|
862
|
+
},
|
|
863
|
+
[slots, resolveSlot, assign, uploader, onError, copy]
|
|
864
|
+
);
|
|
865
|
+
const removeSlot = react.useCallback(
|
|
866
|
+
(slotId) => {
|
|
867
|
+
uploader.files.filter((file) => file.file.name.startsWith(`${slotId}.`)).forEach((file) => uploader.removeFile(file.id));
|
|
868
|
+
landedRef.current.delete(slotId);
|
|
869
|
+
const forgotten = valueRef.current.find((item) => item.slot === slotId);
|
|
870
|
+
if (forgotten) {
|
|
871
|
+
if (removeStrategy && !scopes.get(scope).keepOnRemove) {
|
|
872
|
+
removeStrategy(forgotten.stored, scope, entityId).then((deleted) => {
|
|
873
|
+
if (!deleted) onError?.(copy.removeFailed);
|
|
874
|
+
}).catch(() => onError?.(copy.removeFailed));
|
|
875
|
+
}
|
|
876
|
+
onRemoveStored?.(forgotten);
|
|
877
|
+
}
|
|
878
|
+
void onChangeRef.current(
|
|
879
|
+
valueRef.current.filter((item) => item.slot !== slotId)
|
|
880
|
+
);
|
|
881
|
+
},
|
|
882
|
+
[
|
|
883
|
+
uploader,
|
|
884
|
+
removeStrategy,
|
|
885
|
+
scopes,
|
|
886
|
+
scope,
|
|
887
|
+
entityId,
|
|
888
|
+
onRemoveStored,
|
|
889
|
+
onError,
|
|
890
|
+
copy
|
|
891
|
+
]
|
|
892
|
+
);
|
|
893
|
+
const abort = react.useCallback(
|
|
894
|
+
(slotId) => {
|
|
895
|
+
if (!slotId) {
|
|
896
|
+
uploader.abort();
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
uploader.files.filter((file) => file.file.name.startsWith(`${slotId}.`)).forEach((file) => uploader.abort(file.id));
|
|
900
|
+
},
|
|
901
|
+
[uploader]
|
|
902
|
+
);
|
|
903
|
+
const slotStates = react.useMemo(
|
|
904
|
+
() => slots.map((definition) => ({
|
|
905
|
+
definition,
|
|
906
|
+
filled: value.find((item) => item.slot === definition.id),
|
|
907
|
+
pending: uploader.files.find(
|
|
908
|
+
(file) => file.file.name.startsWith(`${definition.id}.`) && file.status !== "success"
|
|
909
|
+
)
|
|
910
|
+
})),
|
|
911
|
+
[slots, value, uploader.files]
|
|
912
|
+
);
|
|
913
|
+
const accept = react.useMemo(
|
|
914
|
+
() => toAcceptAttribute([...new Set(slots.flatMap((s) => s.extensions))]),
|
|
915
|
+
[slots]
|
|
916
|
+
);
|
|
917
|
+
const upload = react.useCallback(async () => {
|
|
918
|
+
await uploader.upload();
|
|
919
|
+
}, [uploader]);
|
|
920
|
+
return {
|
|
921
|
+
slots: slotStates,
|
|
922
|
+
accept,
|
|
923
|
+
isUploading: uploader.isUploading,
|
|
924
|
+
hasPending: uploader.hasPending,
|
|
925
|
+
addFiles,
|
|
926
|
+
addToSlot,
|
|
927
|
+
removeSlot,
|
|
928
|
+
abort,
|
|
929
|
+
upload
|
|
930
|
+
};
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// src/react/viewResolver.ts
|
|
934
|
+
var isOwnUrl = (url, baseUrl) => {
|
|
935
|
+
if (url.startsWith("/")) return true;
|
|
936
|
+
try {
|
|
937
|
+
return new URL(url).origin === new URL(baseUrl).origin;
|
|
938
|
+
} catch {
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
var fetchStored = async (url, { baseUrl, headers, credentials, labels }) => {
|
|
943
|
+
const own = isOwnUrl(url, baseUrl);
|
|
944
|
+
const response = await fetch(
|
|
945
|
+
url.startsWith("/") ? `${baseUrl.replace(/\/$/, "")}${url}` : url,
|
|
946
|
+
own ? { headers: headers?.(), credentials } : void 0
|
|
947
|
+
);
|
|
948
|
+
if (!response.ok) {
|
|
949
|
+
throw new Error(resolveLabels(labels).readFailed);
|
|
950
|
+
}
|
|
951
|
+
return response;
|
|
952
|
+
};
|
|
953
|
+
var retyped = (blob, mimeType) => mimeType && (!blob.type || blob.type === "application/octet-stream") ? new Blob([blob], { type: mimeType }) : blob;
|
|
954
|
+
var createBlobUrlResolver = (options) => async (file) => {
|
|
955
|
+
if (!isOwnUrl(file.url, options.baseUrl)) return file.url;
|
|
956
|
+
const blob = await (await fetchStored(file.url, options)).blob();
|
|
957
|
+
return URL.createObjectURL(retyped(blob, file.mimeType));
|
|
958
|
+
};
|
|
959
|
+
var createBytesResolver = (options) => async (url) => new Uint8Array(await (await fetchStored(url, options)).arrayBuffer());
|
|
960
|
+
var viewUrlFileName = (url) => {
|
|
961
|
+
const query = url.split("?")[1];
|
|
962
|
+
const key = query ? new URLSearchParams(query).get("key") : null;
|
|
963
|
+
return (key ?? url.split("?")[0]).split("/").pop() || void 0;
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
exports.UploaderProvider = UploaderProvider;
|
|
967
|
+
exports.compressImage = compressImage;
|
|
968
|
+
exports.createBlobUrlResolver = createBlobUrlResolver;
|
|
969
|
+
exports.createBytesResolver = createBytesResolver;
|
|
970
|
+
exports.createRemoveStrategy = createRemoveStrategy;
|
|
971
|
+
exports.createXhrUploadStrategy = createXhrUploadStrategy;
|
|
972
|
+
exports.matchSlotByExtension = matchSlotByExtension;
|
|
973
|
+
exports.matchSlotByName = matchSlotByName;
|
|
974
|
+
exports.slotFileName = slotFileName;
|
|
975
|
+
exports.slotOfStored = slotOfStored;
|
|
976
|
+
exports.useSlottedUploader = useSlottedUploader;
|
|
977
|
+
exports.useUploader = useUploader;
|
|
978
|
+
exports.useUploaderLabels = useUploaderLabels;
|
|
979
|
+
exports.viewUrlFileName = viewUrlFileName;
|
|
980
|
+
//# sourceMappingURL=react.cjs.map
|
|
981
|
+
//# sourceMappingURL=react.cjs.map
|