UNPKG

@plone/volto

Version:
281 lines (266 loc) 7.75 kB
/** * 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);