UNPKG

@kopexa/sight

Version:

Kopexa Sight Design System — React component library for GRC applications

302 lines (299 loc) 8.29 kB
"use client"; import { messages } from "./chunk-FBTSAODG.mjs"; // src/hooks/use-file-upload/index.ts import { useSafeIntl } from "@kopexa/i18n"; import { formatBytes } from "@kopexa/shared-utils"; import { useCallback, useRef, useState } from "react"; var useFileUpload = (options = {}) => { const { maxFiles = Number.POSITIVE_INFINITY, maxSize = Number.POSITIVE_INFINITY, accept = "*", multiple = false, initialFiles = [], onFilesChange, onFilesAdded, onError } = options; const t = useSafeIntl(); const [state, setState] = useState({ files: initialFiles.map((file) => ({ file, id: file.id, preview: file.url })), isDragging: false, errors: [] }); const inputRef = useRef(null); const validateFile = useCallback( (file) => { if (file instanceof File) { if (file.size > maxSize) { return t.formatMessage(messages.file_exceeds_max, { name: file.name, max: formatBytes(maxSize) }); } } else { if (file.size > maxSize) { return t.formatMessage(messages.file_exceeds_max, { name: file.name, max: formatBytes(maxSize) }); } } if (accept !== "*") { const acceptedTypes = accept.split(",").map((type) => type.trim()); const fileType = file instanceof File ? file.type || "" : file.type; const fileExtension = `.${file instanceof File ? file.name.split(".").pop() : file.name.split(".").pop()}`; const isAccepted = acceptedTypes.some((type) => { if (type.startsWith(".")) { return fileExtension.toLowerCase() === type.toLowerCase(); } if (type.endsWith("/*")) { const baseType = type.split("/")[0]; return fileType.startsWith(`${baseType}/`); } return fileType === type; }); if (!isAccepted) { return t.formatMessage(messages.file_not_accepted, { name: file instanceof File ? file.name : file.name }); } } return null; }, [accept, maxSize, t.formatMessage] ); const createPreview = useCallback( (file) => { if (file instanceof File) { return URL.createObjectURL(file); } return file.url; }, [] ); const generateUniqueId = useCallback((file) => { if (file instanceof File) { return `${file.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; } return file.id; }, []); const clearFiles = useCallback(() => { setState((prev) => { for (const file of prev.files) { if (file.preview && file.file instanceof File && file.file.type.startsWith("image/")) { URL.revokeObjectURL(file.preview); } } if (inputRef.current) { inputRef.current.value = ""; } const newState = { ...prev, files: [], errors: [] }; onFilesChange == null ? void 0 : onFilesChange(newState.files); return newState; }); }, [onFilesChange]); const addFiles = useCallback( (newFiles) => { if (!newFiles || newFiles.length === 0) return; const newFilesArray = Array.from(newFiles); const errors = []; setState((prev) => ({ ...prev, errors: [] })); if (!multiple) { clearFiles(); } if (multiple && maxFiles !== Number.POSITIVE_INFINITY && state.files.length + newFilesArray.length > maxFiles) { errors.push( t.formatMessage(messages.too_many_files, { max: maxFiles }) ); onError == null ? void 0 : onError(errors); setState((prev) => ({ ...prev, errors })); return; } const validFiles = []; for (const file of newFilesArray) { if (multiple) { const isDuplicate = state.files.some( (existingFile) => existingFile.file.name === file.name && existingFile.file.size === file.size ); if (isDuplicate) { return; } } if (file.size > maxSize) { errors.push( multiple ? `Some files exceed the maximum size of ${formatBytes(maxSize)}.` : `File exceeds the maximum size of ${formatBytes(maxSize)}.` ); continue; } const error = validateFile(file); if (error) { errors.push(error); } else { validFiles.push({ file, id: generateUniqueId(file), preview: createPreview(file) }); } } if (validFiles.length > 0) { onFilesAdded == null ? void 0 : onFilesAdded(validFiles); setState((prev) => { const newFiles2 = !multiple ? validFiles : [...prev.files, ...validFiles]; onFilesChange == null ? void 0 : onFilesChange(newFiles2); return { ...prev, files: newFiles2, errors }; }); } else if (errors.length > 0) { onError == null ? void 0 : onError(errors); setState((prev) => ({ ...prev, errors })); } if (inputRef.current) { inputRef.current.value = ""; } }, [ onError, state.files, maxFiles, multiple, maxSize, validateFile, createPreview, generateUniqueId, clearFiles, onFilesChange, onFilesAdded, t.formatMessage ] ); const removeFile = useCallback( (id) => { setState((prev) => { const fileToRemove = prev.files.find((file) => file.id === id); if ((fileToRemove == null ? void 0 : fileToRemove.preview) && fileToRemove.file instanceof File && fileToRemove.file.type.startsWith("image/")) { URL.revokeObjectURL(fileToRemove.preview); } const newFiles = prev.files.filter((file) => file.id !== id); onFilesChange == null ? void 0 : onFilesChange(newFiles); return { ...prev, files: newFiles, errors: [] }; }); }, [onFilesChange] ); const clearErrors = useCallback(() => { setState((prev) => ({ ...prev, errors: [] })); }, []); const handleDragEnter = useCallback((e) => { e.preventDefault(); e.stopPropagation(); setState((prev) => ({ ...prev, isDragging: true })); }, []); const handleDragLeave = useCallback((e) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget.contains(e.relatedTarget)) { return; } setState((prev) => ({ ...prev, isDragging: false })); }, []); const handleDragOver = useCallback((e) => { e.preventDefault(); e.stopPropagation(); }, []); const handleDrop = useCallback( (e) => { var _a; e.preventDefault(); e.stopPropagation(); setState((prev) => ({ ...prev, isDragging: false })); if ((_a = inputRef.current) == null ? void 0 : _a.disabled) { return; } if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { if (!multiple) { const file = e.dataTransfer.files[0]; addFiles([file]); } else { addFiles(e.dataTransfer.files); } } }, [addFiles, multiple] ); const handleFileChange = useCallback( (e) => { if (e.target.files && e.target.files.length > 0) { addFiles(e.target.files); } }, [addFiles] ); const openFileDialog = useCallback(() => { if (inputRef.current) { inputRef.current.click(); } }, []); const getInputProps = useCallback( (props = {}) => { return { ...props, type: "file", onChange: handleFileChange, accept: props.accept || accept, multiple: props.multiple !== void 0 ? props.multiple : multiple, ref: inputRef }; }, [accept, multiple, handleFileChange] ); return [ state, { addFiles, removeFile, clearFiles, clearErrors, handleDragEnter, handleDragLeave, handleDragOver, handleDrop, handleFileChange, openFileDialog, getInputProps } ]; }; export { useFileUpload };