elements_FileUpload_index.js

import { React, PropTypes, FileDrop, classnames } from '@gravityforms/libraries';
import { useStateWithDep } from '@gravityforms/react-utils';
import { trigger } from '@gravityforms/utils';
import Icon from '../Icon';
import Text from '../Text';
import Button from '../Button';

const { forwardRef, useState, useEffect, useRef } = React;

/**
 * @module FileUpload
 * @description A file upload component.
 *
 * @since 1.1.15
 *
 * @param {object}              props                   Component props.
 * @param {Array}               props.allowedFileTypes  The allowed file types.
 * @param {object}              props.customAttributes  Custom attributes for the component.
 * @param {string|Array|object} props.customClasses     Custom classes for the component.
 * @param {boolean}             props.disabled          Whether this component should be disabled.
 * @param {string}              props.fileURL           The url for an already uploaded file.
 * @param {object}              props.i18n              Translated strings for the UI.
 * @param {string}              props.id                ID of the file input.
 * @param {string|number}       props.maxHeight         The maximum height for the image.
 * @param {string|number}       props.maxWidth          The maximum width for the image.
 * @param {string}              props.name              The name attribute for the file input.
 * @param {object}              props.previewAttributes Attributes for the preview.
 * @param {Array}               props.previewClasses    Classes for the preview.
 * @param {string}              props.theme             The theme for the component.
 * @param {string}              props.uploadIcon        The icon to show for the upload button.
 * @param {string}              props.uploadIconPrefix  The prefix to use for the upload button icon.
 * @param {object}              props.wrapperAttributes Custom attributes for the wrapper element.
 * @param {string|Array|object} props.wrapperClasses    Custom classes for the wrapper element.
 * @param {object|null}         ref                     Ref to the component.
 *
 * @return {JSX.Element} The file upload component.
 *
 * @example
 * import FileUpload from '@gravityforms/components/react/admin/elements/FileUpload';
 *
 * return <FileUpload name="file-upload" />;
 *
 */
const FileUpload = forwardRef( ( {
	allowedFileTypes = [],
	customAttributes = {},
	customClasses = [],
	disabled = false,
	externalManager = false,
	fileURL = '',
	fileId = 0,
	i18n = {},
	id = '',
	maxHeight = '',
	maxWidth = '',
	name = '',
	previewAttributes = {},
	previewClasses = [],
	theme = 'cosmos',
	uploadIcon = 'upload-file',
	uploadIconPrefix = 'gform-common-icon',
	wrapperAttributes = {},
	wrapperClasses = [],
}, ref ) => {
	const [ selectedFile, setSelectedFile ] = useState( '' );
	const [ selectedExternalManagerFile, setSelectedExternalManagerFile ] = useStateWithDep( fileURL );
	const [ selectedExternalManagerId, setSelectedExternalManagerId ] = useStateWithDep( fileId );
	const [ preview, setPreview ] = useStateWithDep( fileURL );
	const fileInputRef = useRef( null );

	useEffect( () => {
		if ( ! externalManager ) {
			return;
		}
		document.addEventListener( 'gform/file_upload/external_manager/file_selected', handleExternalManager );

		// Clean up the event listener when component unmounts
		return () => {
			document.removeEventListener( 'gform/file_upload/external_manager/file_selected', handleExternalManager );
		};
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [] );

	useEffect( () => {
		if ( ! selectedFile ) {
			fileInputRef.current.value = null;
			return;
		}

		const objectUrl = URL.createObjectURL( selectedFile );
		// eslint-disable-next-line react-hooks/exhaustive-deps
		setPreview( objectUrl );

		// This frees up memory once we're done with the URL.
		return () => URL.revokeObjectURL( objectUrl );
	}, [ selectedFile ] );

	const handleExternalManager = ( event ) => {
		// exit if the id of the event doesn't match this instance id
		if ( event.detail?.fileUploadId !== id ) {
			return;
		}

		setSelectedExternalManagerFile( event.detail.url );
		setSelectedExternalManagerId( event.detail.id );
		setPreview( event.detail.url );
	};

	/**
	 * @function onSelectFile
	 * @description Handler for selecting a file.
	 *
	 * @since 1.1.15
	 *
	 * @param {Array} files Array of files.
	 *
	 * @return {Function} Function to clear timeout.
	 */
	const onSelectFile = ( files ) => {
		if ( externalManager ) {
			trigger( { event: 'gform/file_upload/external_manager/save', data: { id, event, file: files[ 0 ] }, native: false } );
		}

		const delayed = setTimeout( () => {
			if ( ! files || files.length === 0 ) {
				setSelectedFile( '' );
				return;
			}

			setSelectedFile( files[ 0 ] );
			fileInputRef.current.files = files;
		}, 0 );

		return () => clearTimeout( delayed );
	};

	/**
	 * @function onFileInputChange
	 * @description Handler for change event on file input.
	 *
	 * @since 1.1.15
	 *
	 * @param {object} event Event object.
	 *
	 * @return {void}
	 */
	const onFileInputChange = ( event ) => {
		const { files } = event.target;
		onSelectFile( files );
	};

	/**
	 * @function onFileInputKeyDown
	 * @description Handler for keydown event on file input.
	 *
	 * @since 2.0.1
	 *
	 * @param {object} event Event object.
	 *
	 * @return {void}
	 */
	const onFileInputKeyDown = ( event ) => {
		if ( ( event.key === ' ' || event.key === 'Enter' ) && externalManager ) {
			event.preventDefault();
			trigger( { event: 'gform/file_upload/external_manager/open', data: { id, event }, native: false } );
		}
	};

	/**
	 * @function onTargetClick
	 * @description Handler for click event on file drop target.
	 *
	 * @since 1.1.15
	 *
	 * @param {object} event Event object.
	 *
	 * @return {void}
	 */
	const onTargetClick = ( event ) => {
		event.preventDefault();
		if ( disabled ) {
			return;
		}
		if ( externalManager ) {
			trigger( { event: 'gform/file_upload/external_manager/open', data: { id, event }, native: false } );
		} else {
			fileInputRef.current.click();
		}
	};

	/**
	 * @function handleRemove
	 * @description Handler for removing file from file upload field.
	 *
	 * @since 1.1.15
	 *
	 * @return {void}
	 */
	const handleRemove = () => {
		trigger( { event: 'gform/file_upload/external_manager/file_remove', data: { id, event }, native: false } );
		setPreview( '' );
		setSelectedFile( '' );
		setSelectedExternalManagerFile( '' );
		setSelectedExternalManagerId( 0 );
	};

	/**
	 * @function renderFileOptions
	 * @description Renders allowed file type options.
	 *
	 * @since 1.1.15
	 *
	 * @return {string} String list of allowed file types.
	 */
	const renderFileOptions = () => {
		const fileOptions = allowedFileTypes;

		return fileOptions.join( ', ' );
	};

	const removeButtonProps = {
		onClick: handleRemove,
		className: 'gform-file-upload__remove',
		label: i18n.delete || '',
		type: 'secondary',
	};

	const replaceButtonProps = {
		onClick: onTargetClick,
		className: 'gform-file-upload__replace',
		label: i18n.replace || '',
	};

	const wrapperProps = {
		...wrapperAttributes,
		className: classnames( {
			'gform-file-upload__wrapper': true,
			[ `gform-file-upload__wrapper--theme-${ theme }` ]: true,
			'gform-file-upload__wrapper--has-preview': preview,
			'gform-file-upload__wrapper--disabled': disabled,
		}, wrapperClasses ),
		ref,
	};

	const uploaderProps = {
		...customAttributes,
		className: classnames( {
			'gform-file-upload': true,
			[ `gform-file-upload--theme-${ theme }` ]: true,
		}, customClasses ),
	};

	const previewProps = {
		...previewAttributes,
		className: classnames( {
			'gform-file-upload__preview': true,
			[ `gform-file-upload__preview--theme-${ theme }` ]: true,
		}, previewClasses ),
	};

	const previewImgProps = {
		src: preview,
		alt: 'Image Preview',
	};

	const fileDropProps = {
		onTargetClick,
		onDrop: ( files ) => {
			if ( disabled ) {
				return;
			}
			onSelectFile( files );
		},
	};

	const fileInputProps = {
		onChange: onFileInputChange,
		onKeyDown: onFileInputKeyDown,
		ref: fileInputRef,
		type: 'file',
		id,
		name,
	};

	const buttonsWrapperProps = {
		className: classnames( {
			'gform-file-upload__buttons-wrapper': true,
		} ),
	};

	const fileUrlInputProps = {
		name: `${ name }[file_url]`,
		type: 'hidden',
		value: externalManager ? selectedExternalManagerFile : fileURL,
	};

	const fileIdInputProps = {
		name: `${ name }[attachment_id]`,
		type: 'hidden',
		value: externalManager ? selectedExternalManagerId : fileId,
	};

	return (
		<div { ...wrapperProps }>
			{ preview &&
			<div { ...previewProps }>
				{ /* eslint-disable-next-line jsx-a11y/alt-text */ }
				<img { ...previewImgProps } />
			</div>
			}
			<div { ...uploaderProps }>
				<FileDrop { ...fileDropProps }>
					<Icon
						customClasses={ [ 'gform-file-upload__icon' ] }
						icon={ uploadIcon }
						iconPrefix={ uploadIconPrefix }
						spacing={ 2 }
					/>
					<Text customClasses={ [ 'gform-file-upload__message' ] }>
						<span className="gform-file-upload__bold-text">{ i18n.click_to_upload }</span> { i18n.drag_n_drop }
					</Text>
					<Text customClasses={ [ 'gform-file-upload__filetypes' ] }>
						{ renderFileOptions() } ({ i18n.max } { maxWidth }x{ maxHeight }px)
					</Text>
				</FileDrop>
				<input className="gform-file-upload__input" { ...fileInputProps } />
				{ fileUrlInputProps.value !== '' && <input { ...fileUrlInputProps } /> }
				{ Number( fileIdInputProps.value ) !== 0 && <input { ...fileIdInputProps } /> }
			</div>
			{ ( selectedFile || preview ) &&
			<div { ...buttonsWrapperProps }>
				<Button { ...replaceButtonProps } />
				<Button { ...removeButtonProps } />
			</div>
			}
		</div>
	);
} );

FileUpload.propTypes = {
	allowedFileTypes: PropTypes.array,
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	disabled: PropTypes.bool,
	fileURL: PropTypes.string,
	i18n: PropTypes.object,
	id: PropTypes.string,
	maxHeight: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
	] ),
	maxWidth: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
	] ),
	name: PropTypes.string,
	previewAttributes: PropTypes.object,
	previewClasses: PropTypes.array,
	theme: PropTypes.string,
	uploadIcon: PropTypes.string,
	uploadIconPrefix: PropTypes.string,
	wrapperAttributes: PropTypes.object,
	wrapperClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
};

FileUpload.displayName = 'FileUpload';

export default FileUpload;