react-dropzone 19.2.0 → 19.3.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 +4 -0
- package/dist/index.cjs +76 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -3
- package/dist/index.d.ts +21 -3
- package/dist/index.js +76 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +11 -6
- package/src/utils/index.ts +153 -32
package/src/utils/index.ts
CHANGED
|
@@ -8,9 +8,28 @@ const accepts =
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* A map of accepted MIME types to file extensions, as passed to the `accept` prop.
|
|
11
|
+
*
|
|
12
|
+
* An extension value may be a single extension string or an array of them - both are
|
|
13
|
+
* accepted, mirroring the shape of `window.showOpenFilePicker`'s `accept`.
|
|
11
14
|
*/
|
|
12
15
|
export interface Accept {
|
|
13
|
-
[key: string]: readonly string[];
|
|
16
|
+
[key: string]: string | readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A labeled group of accepted types, mirroring one entry of `window.showOpenFilePicker`'s
|
|
21
|
+
* `types` option. Passing the `accept` prop as an array of these lets the File System Access
|
|
22
|
+
* picker present multiple named filter rows instead of a single one (see
|
|
23
|
+
* {@link pickerOptionsFromAccept}). The optional `description` labels the row; when omitted it
|
|
24
|
+
* is derived from the group's extensions.
|
|
25
|
+
*
|
|
26
|
+
* Grouping only surfaces when the FS Access picker is actually used (`useFsAccessApi` +
|
|
27
|
+
* a secure context + browser support). The native `<input>` fallback has no concept of groups
|
|
28
|
+
* or descriptions, so every group is flattened into one accept attribute there.
|
|
29
|
+
*/
|
|
30
|
+
export interface AcceptGroup {
|
|
31
|
+
description?: string;
|
|
32
|
+
accept: Accept;
|
|
14
33
|
}
|
|
15
34
|
|
|
16
35
|
/**
|
|
@@ -323,41 +342,142 @@ export function canUseFileSystemAccessAPI(): boolean {
|
|
|
323
342
|
}
|
|
324
343
|
|
|
325
344
|
/**
|
|
326
|
-
*
|
|
345
|
+
* Coerce an `accept` extension value to an array. `window.showOpenFilePicker` allows a single
|
|
346
|
+
* extension string as well as an array, so we accept both and normalize to an array. Anything
|
|
347
|
+
* else (a malformed value) collapses to an empty list.
|
|
327
348
|
*/
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
);
|
|
338
|
-
ok = false;
|
|
339
|
-
}
|
|
349
|
+
function toExtensions(ext: string | readonly string[] | undefined): readonly string[] {
|
|
350
|
+
if (Array.isArray(ext)) {
|
|
351
|
+
return ext;
|
|
352
|
+
}
|
|
353
|
+
if (typeof ext === "string") {
|
|
354
|
+
return [ext];
|
|
355
|
+
}
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
340
358
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Normalize either `accept` form to the internal array of `{description, accept}` groups.
|
|
361
|
+
*
|
|
362
|
+
* - The object form (`{mime: ext}`) becomes a single group; its `description` (which the object
|
|
363
|
+
* form can't express) is derived from the extensions like any other group.
|
|
364
|
+
* - The array form is passed through; entries missing an `accept` map are dropped.
|
|
365
|
+
*
|
|
366
|
+
* In both cases a missing `description` is filled in later from the group's extensions (see
|
|
367
|
+
* {@link describeAccept}), so the picker always gets the non-empty description it requires
|
|
368
|
+
* (https://crbug.com/1264708). Returns `undefined` when no `accept` is provided.
|
|
369
|
+
*/
|
|
370
|
+
function normalizeAcceptGroups(accept?: Accept | readonly AcceptGroup[]): AcceptGroup[] | undefined {
|
|
371
|
+
if (!isDefined(accept)) {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
if (Array.isArray(accept)) {
|
|
375
|
+
return (accept as readonly AcceptGroup[]).filter(group => isDefined(group) && isDefined(group.accept));
|
|
376
|
+
}
|
|
377
|
+
return [{accept: accept as Accept}];
|
|
378
|
+
}
|
|
345
379
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Build a human-readable label for a picker group from its (validated) accept map, used when the
|
|
382
|
+
* caller doesn't supply a `description`. Prefer the file extensions, fall back to the MIME types,
|
|
383
|
+
* then to a generic label - `showOpenFilePicker` requires a non-empty description (crbug 1264708).
|
|
384
|
+
*/
|
|
385
|
+
function describeAccept(accept: Accept): string {
|
|
386
|
+
const extensions: string[] = [];
|
|
387
|
+
const mimeTypes = Object.keys(accept);
|
|
388
|
+
for (const ext of Object.values(accept)) {
|
|
389
|
+
for (const e of toExtensions(ext)) {
|
|
390
|
+
if (!extensions.includes(e)) {
|
|
391
|
+
extensions.push(e);
|
|
357
392
|
}
|
|
358
|
-
|
|
393
|
+
}
|
|
359
394
|
}
|
|
360
|
-
|
|
395
|
+
if (extensions.length > 0) {
|
|
396
|
+
return extensions.join(", ");
|
|
397
|
+
}
|
|
398
|
+
if (mimeTypes.length > 0) {
|
|
399
|
+
return mimeTypes.join(", ");
|
|
400
|
+
}
|
|
401
|
+
return "Files";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Merge every `accept` group into a single MIME-type -> extensions map. Duplicate MIME keys union
|
|
406
|
+
* their extensions. This flattened map drives the native `<input accept>` attribute and the
|
|
407
|
+
* drag/drop validators, which have no concept of groups or descriptions.
|
|
408
|
+
*/
|
|
409
|
+
export function flattenAccept(accept?: Accept | readonly AcceptGroup[]): Accept | undefined {
|
|
410
|
+
const groups = normalizeAcceptGroups(accept);
|
|
411
|
+
if (!isDefined(groups)) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
const merged: Record<string, string[]> = {};
|
|
415
|
+
for (const group of groups) {
|
|
416
|
+
for (const [mimeType, ext] of Object.entries(group.accept)) {
|
|
417
|
+
const existing = merged[mimeType] ?? (merged[mimeType] = []);
|
|
418
|
+
for (const e of toExtensions(ext)) {
|
|
419
|
+
if (!existing.includes(e)) {
|
|
420
|
+
existing.push(e);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return merged;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Convert the `{accept}` dropzone prop to the `{types}` option for showOpenFilePicker.
|
|
430
|
+
*
|
|
431
|
+
* Each group becomes one filter row in the picker. Invalid MIME types / extensions are dropped
|
|
432
|
+
* with a warning; a group left with no valid entries is omitted entirely (showOpenFilePicker
|
|
433
|
+
* rejects an empty `accept`). Returns `undefined` when nothing valid remains.
|
|
434
|
+
*/
|
|
435
|
+
export function pickerOptionsFromAccept(
|
|
436
|
+
accept?: Accept | readonly AcceptGroup[]
|
|
437
|
+
): Array<{description: string; accept: Accept}> | undefined {
|
|
438
|
+
const groups = normalizeAcceptGroups(accept);
|
|
439
|
+
if (!isDefined(groups)) {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const options = groups
|
|
444
|
+
.map(group => {
|
|
445
|
+
const acceptForPicker = Object.entries(group.accept)
|
|
446
|
+
.filter(([mimeType, ext]) => {
|
|
447
|
+
let ok = true;
|
|
448
|
+
|
|
449
|
+
if (!isMIMEType(mimeType)) {
|
|
450
|
+
console.warn(
|
|
451
|
+
`Skipped "${mimeType}" because it is not a valid MIME type. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types for a list of valid MIME types.`
|
|
452
|
+
);
|
|
453
|
+
ok = false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const isExtInput = Array.isArray(ext) || typeof ext === "string";
|
|
457
|
+
if (!isExtInput || !toExtensions(ext).every(isExt)) {
|
|
458
|
+
console.warn(`Skipped "${mimeType}" because an invalid file extension was provided.`);
|
|
459
|
+
ok = false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return ok;
|
|
463
|
+
})
|
|
464
|
+
.reduce<Accept>((agg, [mimeType, ext]) => {
|
|
465
|
+
agg[mimeType] = toExtensions(ext);
|
|
466
|
+
return agg;
|
|
467
|
+
}, {});
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
description:
|
|
471
|
+
isDefined(group.description) && group.description !== ""
|
|
472
|
+
? group.description
|
|
473
|
+
: describeAccept(acceptForPicker),
|
|
474
|
+
accept: acceptForPicker
|
|
475
|
+
};
|
|
476
|
+
})
|
|
477
|
+
// Drop groups with no valid entries - showOpenFilePicker rejects an empty accept map.
|
|
478
|
+
.filter(option => Object.keys(option.accept).length > 0);
|
|
479
|
+
|
|
480
|
+
return options.length > 0 ? options : undefined;
|
|
361
481
|
}
|
|
362
482
|
|
|
363
483
|
/**
|
|
@@ -379,7 +499,8 @@ export function acceptPropAsAcceptAttr(
|
|
|
379
499
|
if (isDefined(accept)) {
|
|
380
500
|
return (
|
|
381
501
|
Object.entries(accept)
|
|
382
|
-
.reduce<string[]>((a, [mimeType,
|
|
502
|
+
.reduce<string[]>((a, [mimeType, extVal]) => {
|
|
503
|
+
const ext = toExtensions(extVal);
|
|
383
504
|
if (omitWildcardMimeTypesWithExtensions && isMIMETypeWildcard(mimeType) && ext.some(isExt)) {
|
|
384
505
|
a.push(...ext);
|
|
385
506
|
} else {
|