import React, { ReactElement, useRef, useCallback } from 'react'; import css from '../../../utils/css'; import Icon from '../../Icon'; import Typography from '../../Typography'; import { DropzoneWrapper, HiddenInput } from './StyledDragAndDrop'; import { isValidExtension } from './utils'; import { CommonProps } from '../../common'; import { fromUndefinedable, getOrElse, map } from '../../../fp/Option'; import { invoke, invokeWith, noop, pipe } from '../../../fp/function'; import { reduce } from '../../../fp/Array'; import { Either, match, right, left } from '../../../fp/Either'; export interface DragAndDropProps extends CommonProps { /** * A comma-separated list of one or more file types, or [unique file type specifiers](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers), describing which file types to allow. */ accept?: string; /** * Description for accepted files. */ description?: string | ReactElement; /** * Maximum file size (in `bytes`). */ maxSize?: number; /** * Allow to drag and drop/select multiple files. */ multiple?: boolean; /** * Event handler receiving accepted files. */ onAccept?: (files: File[]) => void; /** * Event handler receiving rejected files. */ onReject?: ( rejectedFiles: { file: File; reason: 'size-limit-exceeded' | 'format-not-allowed'; }[] ) => void; /** * Displayed text. */ text?: string | ReactElement; } const DEFAULT_MAX_SIZE = 100 * 1024 * 1024 * 1024; // 100GBs const preventDefaultEvent = (e: React.MouseEvent): void => { e.preventDefault(); }; const stopPropagationEvent = (e: React.MouseEvent): void => { e.stopPropagation(); }; const eitherValidOrInvalid = ( allowedExtensions: string[], allowedMaxSize: number ) => ( file: File ): Either => { const hasValidExtension = allowedExtensions.some( isValidExtension(file.name, file.type) ); if (allowedExtensions.length > 0 && hasValidExtension === false) { return right([file, 'format-not-allowed']); } if (file.size > allowedMaxSize) { return right([file, 'size-limit-exceeded']); } return left(file); }; const DragAndDrop = ({ text, description, onAccept, onReject, multiple = false, accept, maxSize, id, className, style, sx = {}, 'data-test-id': dataTestId, }: DragAndDropProps): ReactElement => { const inputRef = useRef(null); const onDropzoneClick = useCallback(() => { if (inputRef.current !== null) { inputRef.current.value = ''; inputRef.current.click(); } }, []); const allowedExtensions = React.useMemo( () => pipe( accept, fromUndefinedable, map(acc => acc.split(',').map(i => i.trim())), getOrElse(() => []) ), [accept] ); const allowedMaxSize = React.useMemo( () => pipe( maxSize, fromUndefinedable, getOrElse(() => DEFAULT_MAX_SIZE) ), [maxSize] ); const onChanged = React.useCallback( (files: File[]) => { const { validFiles, invalidFiles } = pipe( multiple === true ? files : files.slice(0, 1), reduce( ( acc: { invalidFiles: { file: File; reason: 'format-not-allowed' | 'size-limit-exceeded'; }[]; validFiles: File[]; }, cur: File ) => pipe( cur, eitherValidOrInvalid(allowedExtensions, allowedMaxSize), match( file => ({ ...acc, validFiles: [...acc.validFiles, file] }), ([file, reason]) => ({ ...acc, invalidFiles: [...acc.invalidFiles, { file, reason }], }) ) ), { validFiles: [], invalidFiles: [] } ) ); if (validFiles.length > 0) { pipe( onAccept, fromUndefinedable, getOrElse(() => noop), invokeWith(validFiles) ); } if (invalidFiles.length > 0) { pipe( onReject, fromUndefinedable, getOrElse(() => noop), invokeWith(invalidFiles) ); } }, [multiple, allowedMaxSize, allowedExtensions, onAccept, onReject] ); const onDrop = useCallback( e => { preventDefaultEvent(e); pipe(e.dataTransfer.files, Array.from, invoke(onChanged)); }, [onChanged] ); const onInputChange = useCallback( e => { pipe(e.target.files, Array.from, invoke(onChanged)); }, [onChanged] ); return (
{text !== undefined && (
{text}
)} {description !== undefined && ( {description} )}
); }; export default DragAndDrop;