Reputation: 6916
I have an application that allows the user to upload photos, to edit them (crop, zoom and rotate) and to download.
I'm trying to add a preview mode, so the user can see how the file he's downloading will look like.
What I did works fine except for one thing - when the user clicks on the Clear Preview
button the photos go back to their original state (the state is being reset on Clear Preview
button click),
without keeping the changes the user made to them.
Any idea what am I missing and how to prevent the state reset?
export default function ImagesGrid() {
const classes = useStyles({})
const {
photos,
preparePhotos,
checkUnusedSuppliedTags,
usedTagsList,
suppliedTags
} = useContext(SitePhotosContext)
const taggedPhotos = photos.filter(photo => photo.tags.length)
const pages = Math.ceil(taggedPhotos.length / 6)
const [isPreviewMode, setIsPreviewMode] = useState(false)
const [preparedPhotos, setPreparedPhotos] = useState([])
const renderedPhotos = isPreviewMode ? preparedPhotos : photos
let countColor: TypographyProps['color'] = 'initial'
const tags = usedTagsList.concat(
checkUnusedSuppliedTags(usedTagsList, suppliedTags)
)
useEffect(() => {
preparePhotos(photos).then(photos => setPreparedPhotos(photos))
}, [photos])
return (
<div className={classes.root}>
<AppBar position="sticky" color="default">
<Toolbar>
{!isPreviewMode ? (
<div className={classes.toolbarText}>
<Typography
variant="h6"
color={countColor}
className={classes.pageCount}
>
{`${taggedPhotos.length} Tagged Photos / ${pages} Pages`}
</Typography>
{taggedPhotos.length === 0 && (
<Typography
variant="body2"
color={countColor}
className={classes.instructions}
>
Tag some photos to generate a report
</Typography>
)}
</div>
) : (
<DownloadPreflightModal
photos={preparedPhotos}
usedTagsList={usedTagsList}
/>
)}
<DownloadAndPreview
isPreviewMode={isPreviewMode}
setIsPreviewMode={setIsPreviewMode}
tags={tags}
/>
</Toolbar>
</AppBar>
<Grid
container
justify="flex-start"
spacing={2}
className={classes.imageGrid}
>
{renderedPhotos.map(photo => (
<Grid
item
xs={12}
sm={6}
md={isPreviewMode ? 6 : 4}
key={photo.id}
className={isPreviewMode ? classes.imageInternalGrid : ''}
>
<Image photo={photo} mode={isPreviewMode ? 'preview' : 'editor'} />
</Grid>
))}
</Grid>
</div>
)
}
DownloadAndPreview
component:
export default function DownloadAndPreview({
isPreviewMode,
setIsPreviewMode,
tags
}) {
const classes = useStyles({})
const generatorRef = useRef()
const {photos} = useContext(SitePhotosContext)
return (
<>
{!isPreviewMode ? (
<Button color="inherit" onClick={() => setIsPreviewMode(true)}>
Preview
<VisibilityIcon />
</Button>
) : (
<div>
<Button
color="inherit"
onClick={() => setIsPreviewMode(false)}
className={classes.previewModeBtn}
>
Clear Preview
<ClearIcon />
</Button>
<Button
color="inherit"
onClick={() => {
downloadDoc(generatorRef, photos, tags)
}}
>
Download
</Button>
</div>
)}
</>
)
}
<Image />
component:
export default function Image({photo, mode}: {photo: ImageItem; mode: string}) {
const classes = useStyles({})
const {
setImageEditData
} = useContext(SitePhotosContext)
return mode === 'editor' ? (
<Paper>
{!photo.src && (
<div className={classes.loading}>Loading {photo.data?.name}...</div>
)}
<div>
<ImageEditor
photo={photo}
onEditComplete={editData => {
setImageEditData(photo.id, editData)
}}
/>
</div>
</Paper>
) : (
<div>
<img src={photo.src} className={classes.img} />
<label>{photo.tags.join(' / ')}</label>
</div>
)
}
<ImageEditor />
component:
export default function ImageEditor({
photo,
onEditComplete,
showControls
}: editorProps) {
const classes = useStyles({})
const [crop, setCrop] = useState({x: 0, y: 0})
const [rotation, setRotation] = useState(0)
const [zoom, setZoom] = useState(1)
const onCropComplete = useCallback(
(croppedArea, croppedAreaPixels) => {
const editSettings: editData = {crop: croppedAreaPixels, rotate: rotation}
onEditComplete(editSettings)
},
[rotation, onEditComplete]
)
return (
<>
<Box className={classes.cropContainer}>
<Cropper
image={photo.src}
aspect={maxImageWidth / maxImageHeight}
crop={crop}
rotation={rotation}
zoom={zoom}
zoomWithScroll={false}
onCropChange={setCrop}
onRotationChange={setRotation}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
/>
</Box>
{showControls && (
<Box display="flex" className={classes.controls}>
<IconButton
color="primary"
onClick={() => setRotation(rotation => rotation - 90)}
>
<RotateLeftIcon />
</IconButton>
<Box display="inline-flex" width="100%">
<ImageIcon
fontSize="small"
color="action"
className={classes.imageIcon}
/>
<Slider
className={classes.slider}
value={zoom}
min={1}
max={3}
step={0.1}
onChange={(e, zoom) => setZoom(zoom)}
/>
<ImageIcon
fontSize="large"
color="action"
className={classes.imageIcon}
/>
</Box>
<IconButton
color="primary"
onClick={() => setRotation(rotation => rotation + 90)}
>
<RotateRightIcon />
</IconButton>
</Box>
)}
</>
)
}
context
functions:
const preparePhotos = async (photos: PhotoList) => {
const res = photos
.filter(photo => photo.tags.length)
.sort(sortByPriority)
if (res.length % 2) res.pop()
const edittedPhotos = await Promise.all(res.map(editImage))
return edittedPhotos
}
const setImageEditData = (id, editData) => {
setPhotos(photos =>
photos.map(photo => {
if (photo.id === id) {
return {...photo, editData}
}
return photo
})
)
}
const editImage = async (image: ImageItem) => {
if (!image.editData) return image
const {crop, rotate} = image.editData
const edittedImage = await cropAndRotateImage(image.src, crop, rotate)
return {
...image,
src: edittedImage,
dimensions: {width: crop.width, height: crop.height}
}
}
Expected behavior is similar to this: https://codesandbox.io/s/react-easy-crop-custom-image-demo-y09komm059?from-embed=&file=/src/index.js:3141-3151
After the user closes the preview modal the photo stay in the same position it was - before clicking on the preview button. (What by me - it doesn't)
Upvotes: 3
Views: 666
Reputation: 13853
Your question still lacks some vital information. But let me guess...
<Cropper>
calls onCropComplete
. And it rerenders every time you switch into editor
-mode.react-easy-crop
sets crop
, rotation
and zoom
to respective values passed to <Cropper>
which you hold in <ImageEditor>
component's state.function Image(...) {
...
return mode === 'editor' ? ( ... <ImageEditor ... /> ... ) : ( ... )
}
you loose <ImageEditor>
component's state on every mode switch.preview
-mode into editor
-mode <Cropper>
receives default values for crop
, rotation
and zoom
, calls onCropComplete
and your implementation of onCropComplete
sets editData
to these default values.There are several ways to fix this. But the main issue, as I see it, is that you don't have a single source of truth for photo's editData
. So I think you should get rid of <ImageEditor>
component's state and instead pass editData
to the <Cropper>
. Like so:
<Cropper
image = {photo.src}
crop = {photo.editData.crop}
rotation = {photo.editData.rotation}
zoom = {photo.editData.zoom}
...
/>
You don't have a minimal reproducible example, so I can't test it. For this to works you should probably at least add some default values for photo.editData
Upvotes: 3
Reputation: 11166
Ciao, I think you could blob your image modified, then save it to localStorage and, when user exit from preview mode, show image blobbed instead of original photo
.
I try to explain better: lets say you have this image in preview mode and user zooms it. So now original image is modified. Then, user clicks Clear Preview
button. Just before call setIsPreviewMode(false)
you could store modified image using, for example, dom-to-image. Something like:
domtoimage.toBlob(document.getElementById("ImageModifiedId"))
.then(function (blob) {
// here you have the image modified blobbed into blob item
// then localStorage.setItem("ImageKey", blob);
// and finally setIsPreviewMode(false)
});
Ok now the last step, instead of passing to <Image>
always the same photo
, you could verify if there is a modifed photo in your localStorage and, if there is, load localStorage.getItem("ImageKey")
into <Image>
.
Upvotes: 0