import type React from 'react'; import { useCallback, useMemo, useRef, useState } from 'react'; const fileEquals = (file1: FileInputItem, file2: FileInputItem) => { if (typeof file1 === 'string' || typeof file2 === 'string') { return file1 === file2; } return file1.size === file2.size && file1.name === file2.name; }; /** Represents a file that has been already uploaded to the server */ export type StoredFile = { id: string; name: string; size: number }; export type FileInputItem = File | StoredFile | string; export interface FileInputOptions { /** * Allow multiple files to be uploaded * @default false */ multiple?: boolean; /** * Maximum number of files that can be uploaded * Applies only when `multiple` is set to `true` * @default undefined */ maxFiles?: number; /** * Allowed file mime types * @default undefined */ accept?: string; /** * Represents accepted files only */ value?: FileInputItem[]; /** * Callback function that is called when files change */ onChange?: (files: FileInputItem[]) => void; /** * Disables the input * @default undefined * */ disabled?: boolean; } export const useFileInput = (options: FileInputOptions) => { const inputRef = useRef(null); const [localFiles, setLocalFiles] = useState(options.value || []); const files = options.value === undefined ? localFiles : options.value; const handleChange = useCallback( (updatedFiles: FileInputItem[]) => { setLocalFiles(updatedFiles); options.onChange?.(updatedFiles); }, [options] ); const add = useCallback(() => { if (!inputRef.current) return; const handleInputChange = (event: Event) => { if (!event.target) return; const newFiles = [...((event.target as HTMLInputElement).files || [])]; const uniqueNewFiles = newFiles.filter(newFile => !files.some(existingFile => fileEquals(newFile, existingFile))); if (uniqueNewFiles.length === 0) return; handleChange([...files, ...uniqueNewFiles]); if (inputRef.current) { inputRef.current.value = ''; } }; inputRef.current.multiple = options.multiple || false; inputRef.current.addEventListener('change', handleInputChange, { once: true }); inputRef.current.click(); }, [options.multiple, files, handleChange]); const replace = useCallback( (index: number) => { if (!inputRef.current) return; const handleInputChange = (event: Event) => { if (!event.target) return; const newFile = [...((event.target as HTMLInputElement).files || [])][0]; if (!newFile) return; const updatedFiles = [...files]; updatedFiles[index] = newFile; handleChange(updatedFiles); if (inputRef.current) { inputRef.current.value = ''; } }; inputRef.current.multiple = false; inputRef.current.addEventListener('change', handleInputChange, { once: true }); inputRef.current.click(); }, [files, handleChange] ); const remove = useCallback( (index: number) => { handleChange(files.filter((_, i) => i !== index)); }, [files, handleChange] ); const inputProps = useMemo>>(() => { return { multiple: options.multiple ?? false, type: 'file', accept: options.accept, hidden: true, disabled: options.disabled, }; }, [options.accept, options.multiple, options.disabled]); return { files, add, replace, remove, inputProps, inputRef, }; };