react-dnd-crop
Version:
React custom Drag and drop with Cropper
287 lines (281 loc) • 10.5 kB
JSX
import React, { Component } from "react";
import { swap, filterWithIndex, validateImage, base64StringtoFile, replaceWithIndex, validateImageMimeType } from "./helper";
import { style } from "./style";
import Modal from "../Modal/Modal";
import CancelIcon from "../icons/cancel.svg";
import CropIcon from "../icons/crop.svg";
import ImageIcon from "../icons/image.svg";
import "./Dnd.css";
class Dnd extends Component {
constructor(props) {
super(props);
let cropRatio = props.cropRatio
? props.cropRatio
: {
unit: "%",
width: 30,
aspect: 4 / 3
};
this.state = {
files: [],
fixedDataUrl: [],
imageUrls: [],
finalUrl: [],
draggingElmIndex: null,
cropImgSrc: null,
currentCropImageIndex: null,
open: false,
url: [],
cropRatio: cropRatio,
mimeType: props.mimeType || "png/jpg/jpeg/gif",
errorMessage: props.errorMessages || {}
};
this.INPUT_REF = React.createRef();
this.onImageBrowse = this.onImageBrowse.bind(this);
this.openModal = this.openModal.bind(this);
this.onImageDrop = this.onImageDrop.bind(this);
this.onDrop = this.onDrop.bind(this);
this.callback = this.callback.bind(this);
this.getImageDataUrl = this.getImageDataUrl.bind(this);
this.onInputChange = this.onInputChange.bind(this);
this.removeImage = this.removeImage.bind(this);
this.checkValidImages = this.checkValidImages.bind(this);
this.convertImageForupload = this.convertImageForupload.bind(this);
this.getcropImage = this.getcropImage.bind(this);
this.cancelCrop = this.cancelCrop.bind(this);
this.handleClearToDefault = this.handleClearToDefault.bind(this);
this.onDragStart = this.onDragStart.bind(this);
}
preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
openModal(index) {
let { fixedDataUrl } = this.state;
let cropImgSrc = fixedDataUrl.find((_, i) => i === index);
this.setState({
cropImgSrc: cropImgSrc,
open: true,
currentCropImageIndex: index
});
}
handleClearToDefault() {
this.setState({ open: false });
}
async onDrop(e, index) {
e.stopPropagation();
let { draggingElmIndex, files, imageUrls, fixedDataUrl } = this.state;
files = swap(files, draggingElmIndex, index);
imageUrls = swap(imageUrls, draggingElmIndex, index);
fixedDataUrl = swap(fixedDataUrl, draggingElmIndex, index);
await this.setState({
files: files,
imageUrls,
fixedDataUrl,
draggingElmIndex: null
});
}
onDragStart(_, index) {
this.setState({ draggingElmIndex: index });
}
// image Events
onImageBrowse() {
let DRAG_AND_DROP_AREA_INPUT = this.INPUT_REF.current;
if (DRAG_AND_DROP_AREA_INPUT && document.createEvent) {
var evt = document.createEvent("MouseEvents");
evt.initEvent("click", true, false);
DRAG_AND_DROP_AREA_INPUT.dispatchEvent(evt);
}
}
callback(payload) {
if (this.props.callback) return this.props.callback(payload);
console.log("Provide a callback");
}
onImageDrop(e) {
this.preventDefaults(e);
let { files, imageUrls, mimeType, errorMessage, cropRatio, freeCrop } = this.state,
validUpload = true;
let newFiles = e.dataTransfer.files;
if (imageUrls.length + newFiles.length > (this.props.maxImageUpload || 12))
return this.callback({
error: errorMessage.maxImageUpload || `You Cant Upload More than ${this.props.maxImageUpload || 12} Files`
});
[...newFiles].forEach(file => {
if (!validateImageMimeType(file, mimeType)) {
validUpload = false;
this.callback({
error: errorMessage.imageFormat || "Only Jpeg/Jpg/Png/Gif Allowed"
});
return false;
}
});
if (validUpload) {
this.setState({ files: [...files, ...newFiles] });
[...newFiles].forEach(file => validateImage(file, this.getImageDataUrl, cropRatio, freeCrop));
}
}
getImageDataUrl(isValid, url) {
if (!this.props.crop) isValid = true;
let { imageUrls, fixedDataUrl, errorMessage } = this.state;
if (imageUrls.length + 1 > (this.props.maxImageUpload || 12))
return this.callback({
error: errorMessage.maxImageUpload || `You Cant Upload More than ${this.props.maxImageUpload || 12} Files`
});
this.setState({
fixedDataUrl: [...fixedDataUrl, url],
imageUrls: [...imageUrls, { url: url, isValid }]
});
}
async onInputChange(e) {
let { files, imageUrls, mimeType, errorMessage, cropRatio } = this.state;
let file = e.target.files[0];
e.target.value = null;
if (!file) return;
files = [...files, file];
if (!validateImageMimeType(file, mimeType))
return this.callback({
error: errorMessage.imageFormat || "Only Jpeg/Jpg/Png/Gif Allowed"
});
if (imageUrls.length + 1 > this.props.maxImageUpload)
return this.callback({
error: errorMessage.maxImageUpload || `You Cant Upload More than ${this.props.maxImageUpload} Files`
});
this.setState({ files }, () => {
validateImage(file, this.getImageDataUrl, cropRatio, this.props.freeCrop);
});
this.convertImageForupload();
}
async removeImage(index) {
let { files, imageUrls, fixedDataUrl } = this.state;
files = filterWithIndex(files, index);
fixedDataUrl = filterWithIndex(fixedDataUrl, index);
imageUrls = filterWithIndex(imageUrls, index);
await this.setState({ files, imageUrls, fixedDataUrl });
}
checkValidImages() {
let { imageUrls } = this.state;
for (let i = 0; i < imageUrls.length; i++) if (!imageUrls[i].isValid) return false;
return true;
}
convertImageForupload() {
let { imageUrls, files, errorMessage } = this.state;
let fileObject = [];
imageUrls.forEach((url, index) => {
let file = base64StringtoFile(url.url, files[index].name);
fileObject.push(file);
});
let isUploadValid = this.checkValidImages();
if (!isUploadValid)
return this.callback({
error: errorMessage.aspectRatio || "Image is not in the proper Aspect Ratio"
});
return this.callback({ fileObject, imageUrls });
}
getcropImage(url) {
let { currentCropImageIndex, imageUrls, files } = this.state;
let file = base64StringtoFile(url, files[currentCropImageIndex].name);
imageUrls = replaceWithIndex(imageUrls, currentCropImageIndex, {
url: url,
isValid: true
});
files = replaceWithIndex(files, currentCropImageIndex, file);
this.setState({
files,
imageUrls,
currentCropImageIndex: null,
open: false
});
this.convertImageForupload();
}
cancelCrop() {
this.setState({ open: false });
}
render() {
let { imageUrls, open, cropImgSrc } = this.state;
let styles = style(this.props);
return (
<div onMouseOut={this.convertImageForupload}>
<input ref={this.INPUT_REF} type="file" id="DRAG_AND_DROP_AREA_INPUT" onChange={this.onInputChange} />
<div
id="DRAG_AND_DROP_AREA"
style={styles.DRAG_AND_DROP_AREA}
onDragOver={e => this.preventDefaults(e)}
onDrop={this.onImageDrop}
onClick={this.onImageBrowse}
>
{imageUrls.length === 0 ? null : (
<span id="DRAG_AND_DROP_MESSAGE" style={styles.DRAG_AND_DROP_MESSAGE}>
{this.props.message || "Hold and Drag to rearrange the order"}
</span>
)}
{imageUrls.length === 0 ? (
<div id="DRAG_AND_DROP_PLACEHOLDER">
{this.props.icons && this.props.icons.labelIcon ? this.props.icons.labelIcon : <img src={ImageIcon} width="30" height="30" />}
<h1>{this.props.label || "Drag and drop images here"}</h1>
<p>{this.props.subLabel || "or click to upload"}</p>
</div>
) : (
<div>
<div id="DRAG_AND_DROP_ZONE">
{imageUrls.map((item, index) => (
<div
id="DRAG_AND_DROP_AREA_ITEM"
key={index}
draggable
onClick={e => this.preventDefaults(e)}
onDragStart={e => this.onDragStart(e, index)}
onDragOver={e => this.preventDefaults(e)}
onDrop={e => this.onDrop(e, index)}
style={item.isValid ? styles.DRAG_AND_DROP_AREA_ITEM : styles.DRAG_AND_DROP_AREA_ITEM_ERROR}
>
<div
id="DRAG_AND_DROP_AREA_ITEM_IMAGE_CONTAINER"
onDragOver={e => this.preventDefaults(e)}
style={{ backgroundImage: `url(${item.url})` }}
>
<span
id="DRAG_AND_DROP_AREA_ITEM_CANCEL_ICON"
style={styles.DRAG_AND_DROP_AREA_ITEM_CANCEL_ICON}
onClick={() => this.removeImage(index)}
>
{this.props.icons && this.props.icons.cancelIcon ? (
this.props.icons.cancelIcon
) : (
<img src={CancelIcon} width="15" height="15" />
)}
</span>
{this.props.crop ? (
<span
id="DRAG_AND_DROP_AREA_ITEM_CROP_ICON"
style={styles.DRAG_AND_DROP_AREA_ITEM_CROP_ICON}
onClick={() => this.openModal(index)}
>
{this.props.icons && this.props.icons.cropIcon ? (
this.props.icons.cropIcon
) : (
<img src={CropIcon} width="15" height="15" />
)}
</span>
) : (
""
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
<Modal
isOpen={open}
{...this.props}
onRequestClose={this.handleClearToDefault}
getcropImage={this.getcropImage}
cancelCrop={this.cancelCrop}
imageSrc={cropImgSrc}
/>
</div>
);
}
}
export default Dnd;