@plone/volto
Version:
Volto
281 lines (266 loc) • 7.75 kB
JSX
/**
* FileWidget component.
* @module components/manage/Widgets/FileWidget
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Dimmer } from 'semantic-ui-react';
import { readAsDataURL } from 'promise-file-reader';
import { injectIntl } from 'react-intl';
import deleteSVG from '@plone/volto/icons/delete.svg';
import Icon from '@plone/volto/components/theme/Icon/Icon';
import Toast from '@plone/volto/components/manage/Toast/Toast';
import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
import Image from '@plone/volto/components/theme/Image/Image';
import loadable from '@loadable/component';
import { validateFileUploadSize } from '@plone/volto/helpers/FormValidation/FormValidation';
import { defineMessages, useIntl } from 'react-intl';
import { toast } from 'react-toastify';
const imageMimetypes = [
'image/png',
'image/jpeg',
'image/webp',
'image/jpg',
'image/gif',
'image/svg+xml',
];
const Dropzone = loadable(() => import('react-dropzone'));
const messages = defineMessages({
releaseDrag: {
id: 'Drop files here ...',
defaultMessage: 'Drop files here ...',
},
editFile: {
id: 'Drop file here to replace the existing file',
defaultMessage: 'Drop file here to replace the existing file',
},
fileDrag: {
id: 'Drop file here to upload a new file',
defaultMessage: 'Drop file here to upload a new file',
},
replaceFile: {
id: 'Replace existing file',
defaultMessage: 'Replace existing file',
},
addNewFile: {
id: 'Choose a file',
defaultMessage: 'Choose a file',
},
maxSizeError: {
id: 'The file you uploaded exceeded the maximum allowed size of {size} bytes',
defaultMessage:
'The file you uploaded exceeded the maximum allowed size of {size} bytes',
},
acceptError: {
id: 'File is not of the accepted type {accept}',
defaultMessage: 'File is not of the accepted type {accept}',
},
});
/**
* FileWidget component class.
* @function FileWidget
* @returns {string} Markup of the component.
*
* To use it, in schema properties, declare a field like:
*
* ```jsx
* {
* title: "File",
* widget: 'file',
* }
* ```
* or:
*
* ```jsx
* {
* title: "File",
* type: 'object',
* }
* ```
*
*/
const FileWidget = (props) => {
const { id, value, onChange, isDisabled } = props;
const [fileType, setFileType] = React.useState(false);
const intl = useIntl();
React.useEffect(() => {
if (value && imageMimetypes.includes(value['content-type'])) {
setFileType(true);
}
}, [value]);
const imgAttrs = React.useMemo(() => {
const data = {};
if (value?.download) {
data.item = {
'@id': value.download.substring(0, value.download.indexOf('/@@images')),
image: value,
};
} else if (value?.data) {
data.src = `data:${value['content-type']};${value.encoding},${value.data}`;
}
return data;
}, [value]);
/**
* Drop handler
* @method onDrop
* @param {array} files File objects
* @returns {undefined}
*/
const onDrop = (files, rejectedFiles) => {
rejectedFiles.forEach((file) => {
file.errors.forEach((err) => {
if (err.code === 'file-too-large') {
toast.error(
<Toast
error
title={intl.formatMessage(messages.maxSizeError, {
size: props.size,
})}
/>,
);
}
if (err.code === 'file-invalid-type') {
toast.error(
<Toast
error
title={intl.formatMessage(messages.acceptError, {
accept: props.accept,
})}
/>,
);
}
});
});
if (files.length < 1) return;
const file = files[0];
if (!validateFileUploadSize(file, intl.formatMessage)) return;
readAsDataURL(file).then((data) => {
const fields = data.match(/^data:(.*);(.*),(.*)$/);
onChange(id, {
data: fields[3],
encoding: fields[2],
'content-type': fields[1],
filename: file.name,
});
});
let reader = new FileReader();
reader.onload = function () {
const fields = reader.result.match(/^data:(.*);(.*),(.*)$/);
if (imageMimetypes.includes(fields[1])) {
setFileType(true);
let imagePreview = document.getElementById(`field-${id}-image`);
if (imagePreview) imagePreview.src = reader.result;
} else {
setFileType(false);
}
};
reader.readAsDataURL(files[0]);
};
return (
<FormFieldWrapper {...props}>
<Dropzone
onDrop={onDrop}
{...(props.size ? { maxSize: props.size } : {})}
{...(props.accept ? { accept: props.accept } : {})}
>
{({ getRootProps, getInputProps, isDragActive }) => (
<div className="file-widget-dropzone" {...getRootProps()}>
{isDragActive && <Dimmer active></Dimmer>}
{fileType ? (
<Image
className="image-preview small ui image"
id={`field-${id}-image`}
{...imgAttrs}
/>
) : (
<div className="dropzone-placeholder">
{isDragActive ? (
<p className="dropzone-text">
{intl.formatMessage(messages.releaseDrag)}
</p>
) : value ? (
<p className="dropzone-text">
{intl.formatMessage(messages.editFile)}
</p>
) : (
<p className="dropzone-text">
{intl.formatMessage(messages.fileDrag)}
</p>
)}
</div>
)}
<label className="label-file-widget-input">
{value
? intl.formatMessage(messages.replaceFile)
: intl.formatMessage(messages.addNewFile)}
</label>
<input
{...getInputProps({
type: 'file',
style: { display: 'none' },
})}
id={`field-${id}`}
name={id}
type="file"
disabled={isDisabled}
/>
</div>
)}
</Dropzone>
<div className="field-file-name">
{value && (
<UniversalLink href={value.download} download={true}>
{value.filename}
</UniversalLink>
)}
{value && (
<Button
type="button"
icon
basic
className="delete-button"
aria-label="delete file"
disabled={isDisabled}
onClick={() => {
onChange(id, null);
setFileType(false);
}}
>
<Icon name={deleteSVG} size="20px" />
</Button>
)}
</div>
</FormFieldWrapper>
);
};
/**
* Property types.
* @property {Object} propTypes Property types.
* @static
*/
FileWidget.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string,
required: PropTypes.bool,
error: PropTypes.arrayOf(PropTypes.string),
value: PropTypes.shape({
'@type': PropTypes.string,
title: PropTypes.string,
}),
onChange: PropTypes.func.isRequired,
wrapped: PropTypes.bool,
};
/**
* Default properties.
* @property {Object} defaultProps Default properties.
* @static
*/
FileWidget.defaultProps = {
description: null,
required: false,
error: [],
value: null,
};
export default injectIntl(FileWidget);