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;