@plone/volto
Version:
Volto
340 lines (318 loc) • 11 kB
JSX
import React, { useEffect, useRef } from 'react';
import { Button, Dimmer, Loader, Message } from 'semantic-ui-react';
import { useIntl, defineMessages } from 'react-intl';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import loadable from '@loadable/component';
import { connect } from 'react-redux';
import { compose } from 'redux';
import useLinkEditor from '@plone/volto/components/manage/AnchorPlugin/useLinkEditor';
import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
import {
flattenToAppURL,
getBaseUrl,
isInternalURL,
} from '@plone/volto/helpers/Url/Url';
import { validateFileUploadSize } from '@plone/volto/helpers/FormValidation/FormValidation';
import { usePrevious } from '@plone/volto/helpers/Utils/usePrevious';
import { createContent } from '@plone/volto/actions/content/content';
import { readAsDataURL } from 'promise-file-reader';
import FormFieldWrapper from '@plone/volto/components/manage/Widgets/FormFieldWrapper';
import Icon from '@plone/volto/components/theme/Icon/Icon';
import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
import clearSVG from '@plone/volto/icons/clear.svg';
import navTreeSVG from '@plone/volto/icons/nav.svg';
import linkSVG from '@plone/volto/icons/link.svg';
import uploadSVG from '@plone/volto/icons/upload.svg';
const Dropzone = loadable(() => import('react-dropzone'));
export const ImageToolbar = ({ className, data, id, onChange, selected }) => (
<div className="image-upload-widget-toolbar">
<Button.Group>
<Button icon basic onClick={() => onChange(id, null)}>
<Icon className="circled" name={clearSVG} size="24px" color="#e40166" />
</Button>
</Button.Group>
</div>
);
const messages = defineMessages({
addImage: {
id: 'Browse the site, drop an image, or type a URL',
defaultMessage: 'Browse the site, drop an image, or use a URL',
},
pickAnImage: {
id: 'pickAnImage',
defaultMessage: 'Pick an existing image',
},
uploadAnImage: {
id: 'uploadAnImage',
defaultMessage: 'Upload an image from your computer',
},
linkAnImage: {
id: 'linkAnImage',
defaultMessage: 'Enter a URL to an image',
},
uploadingImage: {
id: 'Uploading image',
defaultMessage: 'Uploading image',
},
});
const UnconnectedImageInput = (props) => {
const {
id,
onChange,
onFocus,
openObjectBrowser,
value,
imageSize = 'teaser',
selected = true,
hideLinkPicker = false,
hideObjectBrowserPicker = false,
restrictFileUpload = false,
objectBrowserPickerType = 'image',
description,
placeholderLinkInput = '',
onSelectItem,
} = props;
const imageValue = value?.[0]?.['@id'] || value;
const intl = useIntl();
const linkEditor = useLinkEditor();
const location = useLocation();
const dispatch = useDispatch();
const contextUrl = location.pathname;
const [uploading, setUploading] = React.useState(false);
const [dragging, setDragging] = React.useState(false);
const imageUploadInputRef = useRef(null);
const requestId = `image-upload-${id}`;
const loaded = props.request.loaded;
const { content } = props;
const imageId = content?.['@id'];
const image = content?.image;
let loading = false;
useEffect(() => {
if (uploading && loading && loaded) {
setUploading(false);
onChange(id, imageId, {
image_field: 'image',
image_scales: { image: [image] },
});
}
}, [loading, loaded, uploading, imageId, image, id, onChange]); // Explicitly list all dependencies
loading = usePrevious(props.request?.loading);
const handleUpload = React.useCallback(
(eventOrFile) => {
if (restrictFileUpload === true) return;
eventOrFile.target && eventOrFile.stopPropagation();
setUploading(true);
const file = eventOrFile.target
? eventOrFile.target.files[0]
: eventOrFile[0];
if (!validateFileUploadSize(file, intl.formatMessage)) return;
readAsDataURL(file).then((fileData) => {
const fields = fileData.match(/^data:(.*);(.*),(.*)$/);
dispatch(
createContent(
getBaseUrl(contextUrl),
{
'@type': 'Image',
title: file.name,
image: {
data: fields[3],
encoding: fields[2],
'content-type': fields[1],
filename: file.name,
},
},
props.block || requestId,
),
);
});
},
[
restrictFileUpload,
intl.formatMessage,
dispatch,
props,
contextUrl,
requestId,
],
);
const onDragEnter = React.useCallback(() => {
if (restrictFileUpload === false) setDragging(true);
}, [restrictFileUpload]);
const onDragLeave = React.useCallback(() => setDragging(false), []);
return imageValue ? (
<div
className="image-upload-widget-image"
onClick={onFocus}
onKeyDown={onFocus}
role="toolbar"
>
{selected && <ImageToolbar {...props} />}
<img
className={props.className}
src={
isInternalURL(imageValue)
? `${flattenToAppURL(imageValue)}/@@images/image/${imageSize}`
: imageValue
}
alt=""
/>
</div>
) : (
<div
className="image-upload-widget"
onClick={onFocus}
onKeyDown={onFocus}
role="toolbar"
>
<Dropzone
noClick
onDrop={handleUpload}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
className="dropzone"
>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<Message>
{dragging && <Dimmer active></Dimmer>}
{uploading && (
<Dimmer active>
<Loader indeterminate>
{intl.formatMessage(messages.uploadingImage)}
</Loader>
</Dimmer>
)}
<img src={imageBlockSVG} alt="" className="placeholder" />
<p>{description || intl.formatMessage(messages.addImage)}</p>
<div className="toolbar-wrapper">
<div className="toolbar-inner" ref={linkEditor.anchorNode}>
{hideObjectBrowserPicker === false && (
<Button.Group>
<Button
aria-label={intl.formatMessage(messages.pickAnImage)}
icon
basic
onClick={(e) => {
onFocus && onFocus();
e.preventDefault();
openObjectBrowser({
mode: objectBrowserPickerType,
onSelectItem: onSelectItem
? onSelectItem
: (url, { title, image_field, image_scales }) => {
onChange(props.id, flattenToAppURL(url), {
title,
image_field,
image_scales,
});
},
currentPath: contextUrl,
});
}}
type="button"
>
<Icon name={navTreeSVG} size="24px" />
</Button>
</Button.Group>
)}
{restrictFileUpload === false && (
<Button.Group>
<Button
aria-label={intl.formatMessage(messages.uploadAnImage)}
icon
basic
compact
onClick={() => {
imageUploadInputRef.current.click();
}}
type="button"
>
<Icon name={uploadSVG} size="24px" />
</Button>
<input
{...getInputProps({
type: 'file',
ref: imageUploadInputRef,
onChange: handleUpload,
style: { display: 'none' },
})}
/>
</Button.Group>
)}
{hideLinkPicker === false && (
<Button.Group>
<Button
icon
basic
aria-label={intl.formatMessage(messages.linkAnImage)}
onClick={(e) => {
!props.selected && onFocus && onFocus();
linkEditor.show();
}}
type="button"
>
<Icon name={linkSVG} circled size="24px" />
</Button>
</Button.Group>
)}
</div>
{linkEditor.anchorNode && (
<linkEditor.LinkEditor
value={imageValue}
placeholder={
placeholderLinkInput ||
intl.formatMessage(messages.linkAnImage)
}
objectBrowserPickerType={objectBrowserPickerType}
onChange={(_, e) =>
onChange(
props.id,
isInternalURL(e) ? flattenToAppURL(e) : e,
{},
)
}
id={id}
/>
)}
</div>
</Message>
</div>
)}
</Dropzone>
</div>
);
};
export const ImageInput = compose(
connect(
(state, ownProps) => {
const requestId = `image-upload-${ownProps.id}`;
return {
request: state.content.subrequests[ownProps.block || requestId] || {},
content: state.content.subrequests[ownProps.block || requestId]?.data,
};
},
{ createContent },
),
)(withObjectBrowser(UnconnectedImageInput));
const ImageUploadWidget = (props) => {
const { fieldSet, id, title } = props;
return (
<FormFieldWrapper
{...props}
columns={1}
className="block image-upload-widget"
>
<div className="wrapper">
<label
id={`fieldset-${fieldSet}-field-label-${id}`}
htmlFor={`field-${id}`}
>
{title}
</label>
</div>
<ImageInput {...props} />
</FormFieldWrapper>
);
};
export default ImageUploadWidget;