2017-07-25 18:40:03 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
This component supports either uploading and storing a single image, or
|
|
|
|
an array of images.
|
|
|
|
|
|
|
|
Note also that an image can be stored as a simple string, or as an array of formats
|
|
|
|
(each format being itself an object).
|
|
|
|
|
|
|
|
*/
|
2017-09-22 12:24:15 +02:00
|
|
|
import { Components, getSetting, registerSetting, registerComponent } from 'meteor/vulcan:lib';
|
2017-05-19 14:42:43 -06:00
|
|
|
import React, { PureComponent } from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
2017-03-28 12:07:01 +09:00
|
|
|
import Dropzone from 'react-dropzone';
|
2017-10-05 06:24:47 -03:00
|
|
|
import 'cross-fetch/polyfill'; // patch for browser which don't have fetch implemented
|
2017-08-05 14:46:02 +09:00
|
|
|
import { FormattedMessage } from 'meteor/vulcan:i18n';
|
2017-03-28 12:07:01 +09:00
|
|
|
|
2017-09-22 12:24:15 +02:00
|
|
|
registerSetting('cloudinary.cloudName', null, 'Cloudinary cloud name (for image uploads)');
|
|
|
|
|
2017-07-25 18:40:03 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
Get a URL from an image or an array of images
|
|
|
|
|
|
|
|
*/
|
|
|
|
const getImageUrl = imageOrImageArray => {
|
|
|
|
// if image is actually an array of formats, use first format
|
|
|
|
const image = Array.isArray(imageOrImageArray) ? imageOrImageArray[0] : imageOrImageArray;
|
|
|
|
// if image is an object, return secure_url; else return image itself
|
|
|
|
const imageUrl = typeof image === 'string' ? image : image.secure_url;
|
|
|
|
return imageUrl
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Remove the nth item from an array
|
|
|
|
|
|
|
|
*/
|
|
|
|
const removeNthItem = (array, n) => [..._.first(array, n), ..._.rest(array, n+1)];
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Display a single image
|
|
|
|
|
|
|
|
*/
|
|
|
|
class Image extends PureComponent {
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.clearImage = this.clearImage.bind(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
clearImage(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
this.props.clearImage(this.props.index);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
return (
|
2017-08-05 14:46:02 +09:00
|
|
|
<div className="upload-image">
|
|
|
|
<div className="upload-image-contents">
|
|
|
|
<img style={{width: 150}} src={getImageUrl(this.props.image)} />
|
|
|
|
{this.props.image.loading ? <div className="upload-loading"><Components.Loading /></div> : null}
|
|
|
|
</div>
|
2017-07-25 18:40:03 +09:00
|
|
|
<a href="javascript:void(0)" onClick={this.clearImage}><Components.Icon name="close"/> Remove image</a>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
Cloudinary Image Upload component
|
|
|
|
|
|
|
|
*/
|
2017-05-19 14:42:43 -06:00
|
|
|
class Upload extends PureComponent {
|
2017-03-28 12:07:01 +09:00
|
|
|
|
2017-07-25 18:40:03 +09:00
|
|
|
constructor(props, context) {
|
2017-03-28 12:07:01 +09:00
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.onDrop = this.onDrop.bind(this);
|
|
|
|
this.clearImage = this.clearImage.bind(this);
|
2017-07-25 18:40:03 +09:00
|
|
|
this.enableMultiple = this.enableMultiple.bind(this);
|
|
|
|
|
2017-08-16 15:08:06 +09:00
|
|
|
const isEmpty = !props.value || (this.enableMultiple() && props.value.length === 0);
|
2017-07-25 18:40:03 +09:00
|
|
|
const emptyValue = this.enableMultiple() ? [] : '';
|
2017-03-28 12:07:01 +09:00
|
|
|
|
|
|
|
this.state = {
|
|
|
|
preview: '',
|
|
|
|
uploading: false,
|
2017-07-25 18:40:03 +09:00
|
|
|
value: isEmpty ? emptyValue : props.value,
|
2017-03-28 12:07:01 +09:00
|
|
|
}
|
2017-07-25 18:40:03 +09:00
|
|
|
|
2017-03-28 12:07:01 +09:00
|
|
|
}
|
|
|
|
|
2017-07-25 18:40:03 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
Add to autofilled values so SmartForms doesn't think the field is empty
|
|
|
|
if the user submits the form without changing it
|
|
|
|
|
|
|
|
*/
|
2017-03-28 12:07:01 +09:00
|
|
|
componentWillMount() {
|
2017-08-16 15:08:06 +09:00
|
|
|
const isEmpty = !this.props.value || (this.enableMultiple() && this.props.value.length === 0);
|
2017-07-25 18:40:03 +09:00
|
|
|
const emptyValue = this.enableMultiple() ? [] : '';
|
|
|
|
this.context.addToAutofilledValues({[this.props.name]: isEmpty ? emptyValue : this.props.value});
|
2017-03-28 12:07:01 +09:00
|
|
|
}
|
|
|
|
|
2017-07-25 18:40:03 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
Check the field's type to decide if the component should handle
|
|
|
|
multiple image uploads or not
|
|
|
|
|
|
|
|
*/
|
|
|
|
enableMultiple() {
|
2018-03-26 17:50:03 +09:00
|
|
|
return this.props.datatype && this.props.datatype[0].type === Array;
|
2017-07-25 18:40:03 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
When an image is uploaded
|
|
|
|
|
|
|
|
*/
|
2017-03-28 12:07:01 +09:00
|
|
|
onDrop(files) {
|
|
|
|
|
2017-08-05 14:46:02 +09:00
|
|
|
const preview = {secure_url: files[0].preview, loading: true};
|
|
|
|
|
2017-03-28 12:07:01 +09:00
|
|
|
// set the component in upload mode with the preview
|
|
|
|
this.setState({
|
2017-08-05 14:46:02 +09:00
|
|
|
preview: this.enableMultiple() ? [...this.state.preview, preview] : preview,
|
2017-03-28 12:07:01 +09:00
|
|
|
uploading: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
// request url to cloudinary
|
2017-09-22 12:24:15 +02:00
|
|
|
const cloudinaryUrl = `https://api.cloudinary.com/v1_1/${getSetting('cloudinary.cloudName')}/upload`;
|
2017-03-28 12:07:01 +09:00
|
|
|
|
|
|
|
// request body
|
|
|
|
const body = new FormData();
|
2017-05-19 14:42:43 -06:00
|
|
|
body.append('file', files[0]);
|
|
|
|
body.append('upload_preset', this.props.options.preset);
|
2017-03-28 12:07:01 +09:00
|
|
|
|
|
|
|
// post request to cloudinary
|
|
|
|
fetch(cloudinaryUrl, {
|
2017-05-19 14:42:43 -06:00
|
|
|
method: 'POST',
|
2017-03-28 12:07:01 +09:00
|
|
|
body,
|
|
|
|
})
|
|
|
|
.then(res => res.json()) // json-ify the readable strem
|
|
|
|
.then(body => {
|
2017-07-25 18:40:03 +09:00
|
|
|
// use the https:// url given by cloudinary; or eager property if using transformations
|
|
|
|
const imageUrl = body.eager ? body.eager : body.secure_url;
|
|
|
|
const newValue = this.enableMultiple() ? [...this.state.value, imageUrl] : imageUrl;
|
2017-03-28 12:07:01 +09:00
|
|
|
|
|
|
|
// set the uploading status to false
|
|
|
|
this.setState({
|
|
|
|
preview: '',
|
|
|
|
uploading: false,
|
2017-07-25 18:40:03 +09:00
|
|
|
value: newValue,
|
2017-03-28 12:07:01 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
// tell vulcanForm to catch the value
|
2017-07-25 18:40:03 +09:00
|
|
|
this.context.addToAutofilledValues({[this.props.name]: newValue});
|
2017-03-28 12:07:01 +09:00
|
|
|
})
|
2018-01-25 15:03:03 -06:00
|
|
|
// eslint-disable-next-line no-console
|
2017-05-19 14:42:43 -06:00
|
|
|
.catch(err => console.log('err', err));
|
2017-03-28 12:07:01 +09:00
|
|
|
}
|
|
|
|
|
2017-07-25 18:40:03 +09:00
|
|
|
/*
|
|
|
|
|
|
|
|
Remove the image at `index` (or just remove image if no index is passed)
|
|
|
|
|
|
|
|
*/
|
|
|
|
clearImage(index) {
|
|
|
|
const newValue = this.enableMultiple() ? removeNthItem(this.state.value, index): '';
|
|
|
|
this.context.addToAutofilledValues({[this.props.name]: newValue});
|
2017-03-28 12:07:01 +09:00
|
|
|
this.setState({
|
2017-12-21 10:04:16 +09:00
|
|
|
preview: null,
|
2017-07-25 18:40:03 +09:00
|
|
|
value: newValue,
|
2017-03-28 12:07:01 +09:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const { uploading, preview, value } = this.state;
|
2017-12-21 10:04:16 +09:00
|
|
|
|
2017-03-28 12:07:01 +09:00
|
|
|
// show the actual uploaded image or the preview
|
2017-08-05 14:46:02 +09:00
|
|
|
const imageData = this.enableMultiple() ? (preview ? value.concat(preview) : value) : value || preview;
|
2017-03-28 12:07:01 +09:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="form-group row">
|
|
|
|
<label className="control-label col-sm-3">{this.props.label}</label>
|
|
|
|
<div className="col-sm-9">
|
|
|
|
<div className="upload-field">
|
|
|
|
<Dropzone ref="dropzone"
|
2017-07-25 18:40:03 +09:00
|
|
|
multiple={this.enableMultiple()}
|
2017-03-28 12:07:01 +09:00
|
|
|
onDrop={this.onDrop}
|
|
|
|
accept="image/*"
|
|
|
|
className="dropzone-base"
|
|
|
|
activeClassName="dropzone-active"
|
|
|
|
rejectClassName="dropzone-reject"
|
|
|
|
>
|
2017-08-05 14:46:02 +09:00
|
|
|
<div><FormattedMessage id="upload.prompt"/></div>
|
|
|
|
{uploading ? <div className="upload-uploading"><span><FormattedMessage id="upload.uploading"/></span></div> : null}
|
2017-03-28 12:07:01 +09:00
|
|
|
</Dropzone>
|
|
|
|
|
2018-01-27 15:06:26 -06:00
|
|
|
{imageData && (Array.isArray(imageData) ? imageData.length > 0 : true) ?
|
2017-03-28 12:07:01 +09:00
|
|
|
<div className="upload-state">
|
2017-08-05 14:46:02 +09:00
|
|
|
<div className="upload-images">
|
2017-07-25 18:40:03 +09:00
|
|
|
{this.enableMultiple() ?
|
|
|
|
imageData.map((image, index) => <Image clearImage={this.clearImage} key={index} index={index} image={image}/>) :
|
|
|
|
<Image clearImage={this.clearImage} image={imageData}/>
|
|
|
|
}
|
|
|
|
</div>
|
2017-03-28 12:07:01 +09:00
|
|
|
</div>
|
|
|
|
: null}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Upload.propTypes = {
|
2017-05-19 14:42:43 -06:00
|
|
|
name: PropTypes.string,
|
|
|
|
value: PropTypes.any,
|
|
|
|
label: PropTypes.string
|
2017-03-28 12:07:01 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
Upload.contextTypes = {
|
2017-05-19 14:42:43 -06:00
|
|
|
addToAutofilledValues: PropTypes.func,
|
|
|
|
};
|
2017-03-28 12:07:01 +09:00
|
|
|
|
|
|
|
registerComponent('Upload', Upload);
|
|
|
|
|
|
|
|
export default Upload;
|