/** * A majority of this file was pulled from https://github.com/DZuz14/react-use-file-upload * with modifications to remove unnecessary data from the output, and to add * file type restriction/validation and integration with react-dnd. */ import { useState, useCallback, useEffect, useMemo } from "react"; import { useDrop, DropTargetMonitor, ConnectDropTarget } from "react-dnd"; import { NativeTypes } from "react-dnd-html5-backend"; import mime from "mime-types"; export type UseFileUploadType = { files: File[]; error: string; setError: React.Dispatch>; clearAllFiles: () => void; createFormData: () => FormData; removeFile: (file: number | string) => void; setFiles: (e: any) => void; isActive: boolean; dropTarget: ConnectDropTarget; }; export const useFileUpload = (props?: { accept?: string; mode?: "append" | "rewrite"; onDrop?(currentFiles: File[], allFiles: File[]): void; onSetFiles?(currentFiles: File[], allFiles: File[]): void; }): UseFileUploadType => { const accept = props && props.accept ? props.accept : ""; const mode = props && props.mode ? props.mode : "rewrite"; const onDrop = props && props.onDrop ? props.onDrop : undefined; const onSetFiles = props && props.onSetFiles ? props.onSetFiles : undefined; const [files, setFilesState] = useState([]); const [error, setError] = useState(""); const ERROR_TIMEOUT = 5000; useEffect(() => { const errorTimer = setTimeout(() => { if (error) { setError(""); } }, ERROR_TIMEOUT); return () => clearTimeout(errorTimer); }, [error]); const acceptedFileTypes = useMemo(() => (accept ? accept.split(",") : ""), [ accept, ]); const validateFiles = useCallback( (filesToValidate: File[]) => { const disallowedTypes = new Set(); if (acceptedFileTypes) { filesToValidate.forEach((f) => { let fileType = f.type; if (fileType === "") { const split_name = f.name.split("."); const type = split_name[split_name.length - 1]; fileType = mime.lookup(type) || "unknown"; } const allowed = acceptedFileTypes.includes(fileType); if (!allowed) { disallowedTypes.add(fileType); } }); if (disallowedTypes.size > 0) { setError( `File type${disallowedTypes.size > 1 ? "s" : ""} not allowed: ${[ ...disallowedTypes, ].join(", ")}`, ); return false; } setError(""); } return true; }, [acceptedFileTypes], ); const setFiles = useCallback( (e: any): void => { let filesArr: File[] = []; if (e && e.currentTarget && e.currentTarget.files) { filesArr = Array.from(e.currentTarget.files); } else if (e && e.dataTransfer && e.dataTransfer.files) { filesArr = Array.from(e.dataTransfer.files); } else { console.error( "Argument not recognized. Are you sure your passing setFiles an event object?", ); } const isValid = validateFiles(filesArr); if (isValid) { if (mode === "rewrite") { setFilesState(filesArr); } else if (mode === "append") { setFilesState([...files, ...filesArr]); } if (onSetFiles) { onSetFiles(filesArr, [...files, ...filesArr]); } } }, [files, validateFiles, mode, onSetFiles], ); const removeFile = useCallback( (fileToRemove: number | string): void => { if ( typeof fileToRemove !== "number" && typeof fileToRemove !== "string" ) { console.error( "argument supplied to removeFile must be of type number or string.", ); return; } if (typeof fileToRemove === "string") { setFilesState(files.filter((f: File) => f.name !== fileToRemove)); } else { setFilesState(files.filter((f: File, i) => i !== fileToRemove)); } }, [files], ); const clearAllFiles = useCallback((): void => { setFilesState([]); }, []); const createFormData = useCallback((): FormData => { const formData = new FormData(); // eslint-disable-next-line no-restricted-syntax for (const file of files) { formData.append(file.name, file); } return formData; }, [files]); const handleDragDrop = useCallback( (item: { files: File[] }) => { if (item) { const isValid = validateFiles(item.files); if (!isValid) { return; } if (mode === "rewrite") { setFilesState(item.files); } else if (mode === "append") { setFilesState([...files, ...item.files]); } if (onDrop) { onDrop(item.files, [...files, ...item.files]); } } }, [validateFiles, files, mode, onDrop], ); const [{ canDrop, isOver }, drop] = useDrop( () => ({ accept: [NativeTypes.FILE], drop(item: { files: File[] }) { handleDragDrop(item); }, collect: (monitor: DropTargetMonitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), }), }), [handleDragDrop], ); const isActive = canDrop && isOver; return { files, error, setError, clearAllFiles, createFormData, removeFile, setFiles, isActive, dropTarget: drop, }; };