react-crop-upload-pictures
Version:
upload pictures with crop and drag
280 lines • 16.8 kB
JavaScript
import React, { useCallback, useState, forwardRef, useImperativeHandle, useEffect } from "react";
import "../assets/style/index.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCropSimple, faXmark, faFileImport } from "@fortawesome/free-solid-svg-icons";
import { faFacebook, faInstagram } from '@fortawesome/free-brands-svg-icons';
import { Button, Modal } from 'react-bootstrap';
import { Draggable } from "react-drag-reorder";
import Crop from "./crop";
import SocialMediaImportPopup from "./socialMediaImportPopup";
const ERROR = {
NOT_SUPPORTED_EXTENSION: 'NOT_SUPPORTED_EXTENSION',
FILE_SIZE_TOO_LARGE: 'FILE_SIZE_TOO_LARGE',
DIMENSION_IMAGE: "DIMENSION_IMAGE"
};
const UploadPictures = forwardRef(({ title = null, imgExtension = ['.jpg', '.jpeg', '.gif', '.png'], maxFileSize = 5242880, height = "200px", width = "200px", classStyle = "", iconSize = "lg", drag = false, crop = false, multiple = true, aspect = 4 / 3, dragDescription = "You can drag pictures to rearrange their order", instructions = null, errorMessages = {
NOT_SUPPORTED_EXTENSION: 'The following files are of unsupported types: ',
FILE_SIZE_TOO_LARGE: 'The following files are too large and cannot be imported:',
DIMENSION_IMAGE: "The following images need to be cropped: "
}, handleClose = () => { }, setPhotosCallback = () => { }, openedSocialOverride = false, }, ref) => {
useImperativeHandle(ref, () => ({
getErrors() {
return {
NOT_SUPPORTED_EXTENSION: pictures.map(el => el.NOT_SUPPORTED_EXTENSION),
FILE_SIZE_TOO_LARGE: pictures.map(el => el.FILE_SIZE_TOO_LARGE),
DIMENSION_IMAGE: pictures.map(el => el.DIMENSION_IMAGE),
};
},
getPictures() {
return pictures.map(el => el.contents?.file);
},
}));
const [pictures, setPictures] = useState([]);
const [socialRawMediaPictures, setSocialMediaRawPictures] = useState([]);
const [socialMediaSelectedPictures, setSocialMediaSelectedPictures] = useState([]);
const [srcCrop, setSrcCrop] = useState(false);
const [openCrop, setOpenCrop] = useState(false);
const [indexCrop, setIndexCrop] = useState(false);
const [openedSocial, setOpenedSocial] = useState(openedSocialOverride);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (setPhotosCallback) {
setPhotosCallback(pictures);
}
}, [pictures?.length]);
useEffect(() => {
if (window.location.search.includes("?code")) {
setOpenedSocial('instagram');
}
}, []);
const hasExtension = (fileName) => {
const pattern = '(' + imgExtension.join('|').replace(/\./g, '\\.') + ')$';
return new RegExp(pattern, 'i').test(fileName);
};
const readFile = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function (e) {
let dataURL = e.target.result;
dataURL = dataURL.replace(";base64", `;name=${file.name};base64`);
resolve({ file, dataURL });
};
reader.readAsDataURL(file);
});
};
const onFileChange = useCallback(event => {
// Clear prev error files
setPictures(pictures => pictures.filter(e => !!e?.contents));
let allFilePromises = [];
if (event.target.files) {
Array.from(event.target.files).forEach((file) => {
const errors = {
NOT_SUPPORTED_EXTENSION: false,
FILE_SIZE_TOO_LARGE: false,
DIMENSION_IMAGE: false,
};
if (!hasExtension(file.name)) {
errors.NOT_SUPPORTED_EXTENSION = { type: ERROR.NOT_SUPPORTED_EXTENSION, filename: file.name };
}
if (file.size > maxFileSize) {
errors.FILE_SIZE_TOO_LARGE = { type: ERROR.FILE_SIZE_TOO_LARGE, filename: file.name };
}
const newFile = new Promise(async (resolve) => resolve({
...errors,
contents: !errors.FILE_SIZE_TOO_LARGE && !errors.NOT_SUPPORTED_EXTENSION ? await readFile(file) : null,
}));
allFilePromises.push(newFile);
});
Promise.all(allFilePromises).then(newFilesData => {
newFilesData.forEach((newFileData, index) => {
debugger;
if (!newFileData.contents || newFileData.contents === null) {
setPictures(pictures => [...pictures, newFileData]);
return;
}
newFileData.contents.file.src = newFileData.contents.dataURL;
var image = new Image();
image.src = newFileData.contents.file.src;
image.index = index;
image.addEventListener('load', () => {
const { width, height } = image;
// check aspect ratio of the image
if (aspect !== (width / height)) {
newFileData.contents.file.needsCropping = true;
newFileData.DIMENSION_IMAGE = {
index: index + pictures?.length,
type: ERROR.DIMENSION_IMAGE,
filename: newFileData.contents.file.name
};
}
setPictures(pictures => [...pictures, newFileData]);
});
});
});
}
}, [pictures?.length, aspect, hasExtension, maxFileSize]);
const remove = (index) => {
let newList = pictures.filter((_, i) => i !== index);
setPictures(newList);
};
const getChangedPos = (currentPos, newPos) => {
let newList = [...pictures];
let pic = newList.splice(currentPos, 1);
newList.splice(newPos, 0, pic[0]);
setPictures(newList);
};
const imageUrlToBase64 = async (url) => {
const data = await fetch(url);
const blob = await data.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
const base64data = reader.result;
resolve(base64data);
};
reader.onerror = reject;
});
};
const setSocialMediaPhotosCallback = (socialPics, selected) => {
setSocialMediaRawPictures(socialPics);
setSocialMediaSelectedPictures(selected);
};
const saveSocialPictures = useCallback(async () => {
const selectedPictures = socialRawMediaPictures.filter((_, index) => socialMediaSelectedPictures.includes(index));
const results = await Promise.all(selectedPictures.map(async (item, index) => {
const src = await imageUrlToBase64(item?.src);
let needsCropping = false;
let dimensionError = false;
if (aspect !== (width / height)) {
dimensionError = {
index: index + pictures?.length,
type: ERROR.DIMENSION_IMAGE,
filename: ''
};
needsCropping = true;
}
return {
contents: {
file: {
src: src,
needsCropping: needsCropping,
},
},
NOT_SUPPORTED_EXTENSION: false,
FILE_SIZE_TOO_LARGE: false,
DIMENSION_IMAGE: dimensionError,
};
}));
if (results) {
setPictures([...pictures, ...results]);
}
setOpenedSocial(false);
setSocialMediaRawPictures([]);
setSocialMediaSelectedPictures([]);
}, [pictures, socialMediaSelectedPictures, socialRawMediaPictures]);
const DraggableRender = useCallback(() => {
return (React.createElement(Draggable, { onPosChange: getChangedPos }, pictures.length > 0 && pictures.map((picture, index) => {
debugger;
return !picture.FILE_SIZE_TOO_LARGE && !picture.NOT_SUPPORTED_EXTENSION && picture.contents && picture.contents !== null && (React.createElement("div", { className: `${picture.contents?.file?.needsCropping ? "border border-warning" : ""} position-relative p-0 mx-2 drager-pictures`, key: index, style: { width: width } },
React.createElement(Actions, { index: index, iconSize: iconSize, remove: remove, cropPicture: cropPicture, crop: crop }),
React.createElement(ImageDisplay, { picture: picture.contents?.file, height: height, width: width, className: "mb-4" })));
})));
}, [pictures]);
const ImagesRender = useCallback(() => {
return (React.createElement("div", { className: "row d-flex justify-content-center" }, pictures && pictures.map((picture, index) => (React.createElement("div", { className: "position-relative p-0 mx-2", key: index, style: { width: width, height: height } },
React.createElement(Actions, { index: index, iconSize: iconSize, remove: remove, cropPicture: cropPicture, crop: crop }),
React.createElement(ImageDisplay, { picture: picture.contents?.file, height: height, width: width, className: "mb-4" }))))));
}, [pictures]);
const cropPicture = (index) => {
let picture = pictures.find((_, i) => i === index);
setSrcCrop(picture);
setOpenCrop(true);
setIndexCrop(index);
};
const saveCroppedPicture = (picture) => {
let newPicture = { ...picture };
newPicture.contents.file.needsCropping = false;
newPicture.DIMENSION_IMAGE = false;
setPictures(items => {
const res = items.map((item, i) => i === indexCrop
? newPicture
: item);
setPhotosCallback(res);
return res;
});
setSrcCrop(false);
setOpenCrop(false);
setIndexCrop(false);
};
const tooLargeErrors = pictures.map(el => (el.FILE_SIZE_TOO_LARGE)).filter(e => !!e);
const notSupportedErrors = pictures.map(el => (el.NOT_SUPPORTED_EXTENSION)).filter(e => !!e);
const dimensionErrors = pictures.map((el, index) => (el.DIMENSION_IMAGE ? { ...el.DIMENSION_IMAGE, index: index } : false)).filter(e => !!e);
const shownPictures = pictures.filter(pic => !!pic?.contents);
return (React.createElement(React.Fragment, null,
crop && React.createElement(Crop, { picture: srcCrop, isOpen: openCrop, setOpenCrop: setOpenCrop, saveCroppedPicture: saveCroppedPicture, iconSize: iconSize, aspect: aspect }),
React.createElement("div", { ref: ref }, React.createElement("div", { className: classStyle },
React.createElement("div", { className: "upload-content" },
title !== null ?
React.createElement("div", { className: "upload-header" },
React.createElement("h1", { className: "upload-title fs-5" }, title))
: "",
React.createElement("div", { className: "mb-5 upload-body" },
tooLargeErrors?.length ? (React.createElement("div", { className: "alert alert-danger", role: "alert" },
React.createElement("strong", null, errorMessages[ERROR.FILE_SIZE_TOO_LARGE]),
tooLargeErrors.map((error) => error.filename).join(', '))) : "",
notSupportedErrors?.length ? (React.createElement("div", { className: "alert alert-danger", role: "alert" },
React.createElement("strong", null, errorMessages[ERROR.NOT_SUPPORTED_EXTENSION]),
notSupportedErrors.map((error) => error.filename).join(', '))) : "",
dimensionErrors?.length ? (React.createElement("div", { className: "alert alert-warning", role: "alert" },
React.createElement("strong", null, errorMessages[ERROR.DIMENSION_IMAGE]),
dimensionErrors.map((el) => '[' + el.index + ']').join(', '))) : "",
drag && shownPictures.length === 0 && instructions &&
React.createElement("div", { className: "mb-2" },
React.createElement("div", { className: "alert alert-info", role: "alert", dangerouslySetInnerHTML: { __html: instructions } })),
React.createElement("div", { className: "row mb-5" },
React.createElement("div", { className: "col" },
React.createElement("input", { onChange: onFileChange, className: "form-control", type: "file", id: "formFile", multiple: multiple })),
React.createElement("div", { className: "col-auto ml-auto social-buttons" },
React.createElement("button", { onClick: () => setOpenedSocial('facebook'), className: "facebook-import-button" },
React.createElement(FontAwesomeIcon, { icon: faFacebook, size: iconSize, className: "facebook-import-button-icon" })),
React.createElement("button", { onClick: () => setOpenedSocial('instagram'), className: "instagram-import-button" },
React.createElement(FontAwesomeIcon, { icon: faInstagram, size: iconSize, className: "instagram-import-button-icon" })))),
loading && (React.createElement("div", { className: "d-flex justify-content-center mt-3" },
React.createElement("div", { className: "spinner-border", role: "status" },
React.createElement("span", { className: "sr-only" }, "Loading...")))),
drag && shownPictures.length > 1 &&
React.createElement("div", { className: "mb-2" },
React.createElement("div", { className: "alert alert-info", role: "alert" }, dragDescription)),
React.createElement("div", { className: "row d-flex justify-content-center space-photos" },
drag && shownPictures.length > 0 && (React.createElement(DraggableRender, null)),
!drag && shownPictures.length > 0 && (React.createElement(ImagesRender, null))))),
crop && React.createElement("div", { className: (openCrop ? "modal-backdrop fade show" : "") }))),
!!openedSocial && (React.createElement(Modal, { show: !!openedSocial, onHide: () => setOpenedSocial(false), size: "xl", keyboard: true },
React.createElement(Modal.Header, { closeButton: true },
React.createElement(Modal.Title, null, multiple ? "Select Images" : "Select an Image")),
React.createElement(Modal.Body, null,
React.createElement(SocialMediaImportPopup, { ref: ref, modalSource: openedSocial, height: "100px", width: "100px", iconSize: "lg", handleClose: () => setOpenedSocial(false), setPhotosCallback: setSocialMediaPhotosCallback, multiple: multiple })),
React.createElement(Modal.Footer, null,
React.createElement(Button, { variant: "secondary", onClick: () => setOpenedSocial(false) },
React.createElement(FontAwesomeIcon, { icon: faXmark, size: iconSize })),
React.createElement(Button, { variant: "primary", className: "text-white", onClick: saveSocialPictures, disabled: !socialMediaSelectedPictures?.length, "data-bs-toggle": "tooltip", "data-bs-placement": "top", title: "Tooltip on top" },
React.createElement(FontAwesomeIcon, { icon: faFileImport, size: iconSize })))))));
});
function ImageDisplay({ picture, width, height }) {
return React.createElement("img", { src: picture?.src, style: {
height: height,
width: width,
}, className: "mb-4", alt: "" });
}
function Actions({ index, iconSize, remove, cropPicture, crop }) {
return (React.createElement("div", { className: "d-flex w-100 justify-content-between p-0 pb-3" },
crop && React.createElement(FontAwesomeIcon, { role: "button", onClick: () => cropPicture(index), icon: faCropSimple, size: iconSize, className: "my-auto" }),
React.createElement("b", { className: "fs-5 my-auto" },
"[",
index,
"]"),
React.createElement(FontAwesomeIcon, { role: "button", onClick: () => remove(index), icon: faXmark, size: iconSize, className: "my-auto" })));
}
export default UploadPictures;
//# sourceMappingURL=index.js.map