102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
type FileSystemHandleLike = FileSystemFileHandleLike | FileSystemDirectoryHandleLike;
|
|
type FileSystemFileHandleLike = { kind: "file"; getFile: () => Promise<File> };
|
|
type FileSystemDirectoryHandleLike = {
|
|
kind: "directory";
|
|
values?: () => AsyncIterable<FileSystemHandleLike>;
|
|
};
|
|
type WebKitFileEntryLike = {
|
|
isFile: true;
|
|
isDirectory: false;
|
|
file: (
|
|
success: (file: File) => void,
|
|
error?: (error: DOMException) => void
|
|
) => void;
|
|
};
|
|
type WebKitDirectoryEntryLike = { isFile: false; isDirectory: true };
|
|
type WebKitEntryLike = WebKitFileEntryLike | WebKitDirectoryEntryLike;
|
|
type DropItemWithFileHandles = DataTransferItem & {
|
|
getAsFileSystemHandle?: () => Promise<FileSystemHandleLike | null>;
|
|
webkitGetAsEntry?: () => unknown;
|
|
};
|
|
|
|
/**
|
|
* Resolve a browser file drop exactly once, preferring the standard file list
|
|
* before progressively trying item and directory-drag compatibility APIs.
|
|
*/
|
|
export async function resolveDroppedFiles(
|
|
dataTransfer: Pick<DataTransfer, "files" | "items">
|
|
): Promise<File[]> {
|
|
const directFiles = Array.from(dataTransfer.files);
|
|
if (directFiles.length > 0) return directFiles;
|
|
|
|
const items = Array.from(dataTransfer.items).filter(
|
|
(item) => item.kind === "file"
|
|
) as DropItemWithFileHandles[];
|
|
const itemFiles = items
|
|
.map((item) => item.getAsFile())
|
|
.filter((file): file is File => Boolean(file));
|
|
if (itemFiles.length > 0) return itemFiles;
|
|
|
|
const handleFiles = await filesFromDataTransferHandles(items);
|
|
if (handleFiles.length > 0) return handleFiles;
|
|
|
|
return filesFromWebKitEntries(items);
|
|
}
|
|
|
|
async function filesFromDataTransferHandles(
|
|
items: DropItemWithFileHandles[]
|
|
): Promise<File[]> {
|
|
const files: File[] = [];
|
|
for (const item of items) {
|
|
let handle: FileSystemHandleLike | null | undefined;
|
|
try {
|
|
handle = await item.getAsFileSystemHandle?.();
|
|
} catch {
|
|
handle = null;
|
|
}
|
|
if (!handle) continue;
|
|
files.push(...await filesFromFileSystemHandle(handle));
|
|
}
|
|
return files;
|
|
}
|
|
|
|
async function filesFromFileSystemHandle(
|
|
handle: FileSystemHandleLike
|
|
): Promise<File[]> {
|
|
if (handle.kind === "file") return [await handle.getFile()];
|
|
const values = handle.values?.();
|
|
if (!values) return [];
|
|
const files: File[] = [];
|
|
for await (const child of values) {
|
|
files.push(...await filesFromFileSystemHandle(child));
|
|
}
|
|
return files;
|
|
}
|
|
|
|
async function filesFromWebKitEntries(
|
|
items: DropItemWithFileHandles[]
|
|
): Promise<File[]> {
|
|
const entries = items.flatMap((item) => {
|
|
const entry = item.webkitGetAsEntry?.() as unknown;
|
|
return isWebKitEntryLike(entry) ? [entry] : [];
|
|
});
|
|
const files = await Promise.all(entries.map(fileFromWebKitEntry));
|
|
return files.filter((file): file is File => Boolean(file));
|
|
}
|
|
|
|
function isWebKitEntryLike(value: unknown): value is WebKitEntryLike {
|
|
if (!value || typeof value !== "object") return false;
|
|
const entry = value as { isFile?: unknown; isDirectory?: unknown };
|
|
return (
|
|
typeof entry.isFile === "boolean" &&
|
|
typeof entry.isDirectory === "boolean"
|
|
);
|
|
}
|
|
|
|
function fileFromWebKitEntry(entry: WebKitEntryLike): Promise<File | null> {
|
|
if (!entry.isFile) return Promise.resolve(null);
|
|
return new Promise((resolve, reject) => {
|
|
entry.file(resolve, reject);
|
|
});
|
|
}
|