import {UnexpectedObjectError} from './error'; import {type FileWithPath, toFileWithPath, withMimeType} from './file'; import {DEFAULT_MIME_TYPES} from './mime-default'; const FILES_TO_IGNORE = [ // Thumbnail cache files for macOS and Windows '.DS_Store', // macOs 'Thumbs.db' // Windows ]; export interface FromEventOptions { /** * Extension-to-MIME lookup used to set `type` on files the browser left typeless. * Defaults to a small built-in set of common types ({@link DEFAULT_MIME_TYPES}). * * The full extension-to-MIME table (~1,200 entries) is not bundled into the main entry; * import it from the `file-selector/mime` subpath when broader coverage is needed: * * ```ts * import {fromEvent} from 'file-selector'; * import {COMMON_MIME_TYPES} from 'file-selector/mime'; * await fromEvent(evt, {mimeTypes: COMMON_MIME_TYPES}); * ``` * * See https://github.com/react-dropzone/file-selector/issues/127 */ mimeTypes?: Map; } /** * Convert a DragEvent's DataTrasfer object to a list of File objects * NOTE: If some of the items are folders, * everything will be flattened and placed in the same list but the paths will be kept as a {path} property. * * EXPERIMENTAL: A list of https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle objects can also be passed as an arg * and a list of File objects will be returned. * * @param evt * @param options */ export async function fromEvent( evt: Event | any, {mimeTypes = DEFAULT_MIME_TYPES}: FromEventOptions = {} ): Promise<(FileWithPath | DataTransferItem)[]> { const items = await getFilesOrItems(evt); // Guess a MIME type from the file extension once, over the final flat list, for any file the // browser left typeless. DataTransferItems (returned for non-'drop' drag events) pass through as-is. return items.map(item => (item instanceof File ? withMimeType(item, mimeTypes) : item)); } async function getFilesOrItems(evt: Event | any): Promise<(FileWithPath | DataTransferItem)[]> { if (isObject(evt) && isDataTransfer(evt.dataTransfer)) { return getDataTransferFiles(evt.dataTransfer, evt.type); } else if (isChangeEvt(evt)) { return getInputFiles(evt); } else if (Array.isArray(evt) && evt.every(item => 'getFile' in item && typeof item.getFile === 'function')) { return getFsHandleFiles(evt); } return []; } function isDataTransfer(value: any): value is DataTransfer { return isObject(value); } function isChangeEvt(value: any): value is Event { return isObject(value) && isObject(value.target); } function isObject(v: any): v is T { return typeof v === 'object' && v !== null; } function getInputFiles(evt: Event) { return fromList((evt.target as HTMLInputElement).files).map(file => toFileWithPath(file)); } // Ee expect each handle to be https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle async function getFsHandleFiles(handles: any[]) { const files = await Promise.all(handles.map(h => h.getFile())); return files.map(file => toFileWithPath(file)); } async function getDataTransferFiles(dt: DataTransfer, type: string) { const items = fromList(dt.items).filter(item => item.kind === 'file'); // According to https://html.spec.whatwg.org/multipage/dnd.html#dndevents, // only 'dragstart' and 'drop' has access to the data (source node) if (type !== 'drop') { return items; } const files = await Promise.all(items.map(toFilePromises)); return noIgnoredFiles(flatten(files)); } function noIgnoredFiles(files: FileWithPath[]) { return files.filter(file => FILES_TO_IGNORE.indexOf(file.name) === -1); } // https://developer.mozilla.org/en-US/docs/Web/API/FileList // https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItemList function fromList(items: DataTransferItemList | FileList | null): T[] { // {items} can be null, e.g. an with no selection if (items === null) { return []; } return Array.from(items as unknown as ArrayLike); } // https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem async function toFilePromises(item: DataTransferItem) { if (typeof item.webkitGetAsEntry !== 'function') { return fromDataTransferItem(item); } const entry = item.webkitGetAsEntry(); // Safari supports dropping an image node from a different window and can be retrieved using // the DataTransferItem.getAsFile() API // NOTE: FileSystemEntry.file() throws if trying to get the file if (entry?.isDirectory) { // Prefer the File System Access API for directory traversal where it's available. // The legacy Entry API (FileSystemDirectoryReader.readEntries) throws on some platforms, // e.g. dragging a folder from Windows Explorer into a Chromium browser, which would // otherwise reject the entire drop and lose every file. // See https://github.com/react-dropzone/file-selector/issues/130 const handle = await getFsHandle(item); if (handle?.kind === 'directory') { return fromDirHandle(handle, `/${handle.name}`); } return fromDirEntry(entry) as any; } return fromDataTransferItem(item, entry); } function flatten(items: any[]): T[] { const result: T[] = []; for (const item of items) { if (Array.isArray(item)) { result.push(...flatten(item)); } else { result.push(item); } } return result; } async function fromDataTransferItem(item: DataTransferItem, entry?: FileSystemEntry | null) { const h = await getFsHandle(item); if (h === null) { throw new UnexpectedObjectError(item); } // It seems that the handle can be `undefined` (see https://github.com/react-dropzone/file-selector/issues/120), // so we check if it isn't; if it is, the code path continues to the next API (`getAsFile`). // The handle is also `undefined` when the File System Access API isn't available or we're not // in a secure context, in which case we likewise fall back to `getAsFile`. if (h !== undefined) { const file = await h.getFile(); file.handle = h; return toFileWithPath(file); } const file = item.getAsFile(); if (!file) { throw new UnexpectedObjectError(item); } const fwp = toFileWithPath(file, entry?.fullPath ?? undefined); return fwp; } // Resolve the https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle for a dropped item. // Returns `undefined` when the File System Access API isn't available or we're not in a secure context. // // We check for a secure context because, due to a bug in Chrome (as far as we know), // the browser crashes when calling this API (yet to be confirmed as a consistent behaviour). // See: // - https://issues.chromium.org/issues/40186242 // - https://github.com/react-dropzone/react-dropzone/issues/1397 async function getFsHandle(item: DataTransferItem) { if (globalThis.isSecureContext && typeof (item as any).getAsFileSystemHandle === 'function') { return (item as any).getAsFileSystemHandle(); } return undefined; } // Traverse a directory recursively using the File System Access API, returning a flat list of files. // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle async function fromDirHandle(handle: any, path: string): Promise { const files: FileWithPath[] = []; for await (const child of handle.values()) { const childPath = `${path}/${child.name}`; if (child.kind === 'directory') { files.push(...(await fromDirHandle(child, childPath))); } else { const file = await child.getFile(); files.push(toFileWithPath(file, childPath, child)); } } return files; } // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemEntry async function fromEntry(entry: any) { return entry.isDirectory ? fromDirEntry(entry) : fromFileEntry(entry); } // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryEntry function fromDirEntry(entry: any) { const reader = entry.createReader(); return new Promise((resolve, reject) => { const entries: Promise[] = []; function readEntries() { // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryEntry/createReader // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader/readEntries reader.readEntries( async (batch: any[]) => { if (!batch.length) { // Done reading directory try { const files = await Promise.all(entries); resolve(files); } catch (err) { reject(err); } } else { const items = Promise.all(batch.map(fromEntry)); entries.push(items); // Continue reading readEntries(); } }, (err: any) => { reject(err); } ); } readEntries(); }); } // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileEntry async function fromFileEntry(entry: any) { return new Promise((resolve, reject) => { entry.file( (file: FileWithPath) => { const fwp = toFileWithPath(file, entry.fullPath); resolve(fwp); }, (err: any) => { reject(err); } ); }); } // Infinite type recursion // https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540 interface FileArray extends Array {} type FileValue = FileWithPath | FileArray[];