1- import { ReactEventHandler , useRef , useState } from 'react' ;
2- import useEmitterFactory , { Emits } from 'react-emitter-factory' ;
1+ import {
2+ forwardRef ,
3+ ReactEventHandler ,
4+ useImperativeHandle ,
5+ useRef ,
6+ useState ,
7+ } from 'react' ;
38import ReactCrop , { PercentCrop , PixelCrop } from 'react-image-crop' ;
49import { RotateRight } from '@mui/icons-material' ;
510import { Slider } from '@mui/material' ;
@@ -15,7 +20,7 @@ const DEFAULT_CROP: PercentCrop = {
1520 height : 50 ,
1621} ;
1722
18- export interface ImageCropperEmitter {
23+ export interface ImageCropperRef {
1924 /**
2025 * Resets the cropper to allow initial crop to be generated on new `src` load.
2126 */
@@ -27,7 +32,7 @@ export interface ImageCropperEmitter {
2732 getImage ?: ( ) => Promise < Blob | undefined > ;
2833}
2934
30- interface ImageCropperProps extends Emits < ImageCropperEmitter > {
35+ interface ImageCropperProps {
3136 src ?: string ;
3237 alt ?: string ;
3338 circular ?: boolean ;
@@ -37,78 +42,82 @@ interface ImageCropperProps extends Emits<ImageCropperEmitter> {
3742 type ?: string ;
3843}
3944
40- const ImageCropper = ( props : ImageCropperProps ) : JSX . Element => {
41- const [ crop , setCrop ] = useState < PercentCrop > ( ) ;
42- const [ completedCrop , setCompletedCrop ] = useState < PixelCrop > ( ) ;
43- const [ rotation , setRotation ] = useState ( 0 ) ;
44- const imgRef = useRef < HTMLImageElement > ( null ) ;
45+ const ImageCropper = forwardRef < ImageCropperRef , ImageCropperProps > (
46+ ( props , ref ) => {
47+ const [ crop , setCrop ] = useState < PercentCrop > ( ) ;
48+ const [ completedCrop , setCompletedCrop ] = useState < PixelCrop > ( ) ;
49+ const [ rotation , setRotation ] = useState ( 0 ) ;
50+ const imgRef = useRef < HTMLImageElement > ( null ) ;
4551
46- const generateImage = async ( ) : Promise < Blob | undefined > => {
47- if ( ! imgRef . current || ! completedCrop ) return undefined ;
48- return getImage (
49- imgRef . current ! ,
50- completedCrop ,
51- rotation ,
52- props . type ?? 'image/jpeg' ,
53- ) ;
54- } ;
52+ const generateImage = async ( ) : Promise < Blob | undefined > => {
53+ if ( ! imgRef . current || ! completedCrop ) return undefined ;
54+ return getImage (
55+ imgRef . current ! ,
56+ completedCrop ,
57+ rotation ,
58+ props . type ?? 'image/jpeg' ,
59+ ) ;
60+ } ;
5561
56- const handleImageLoad : ReactEventHandler < HTMLImageElement > = ( e ) => {
57- if ( props . aspect ) {
58- const { width, height } = e . currentTarget ;
59- setCrop ( centerAspectCrop ( width , height , props . aspect ) ) ;
60- } else {
61- setCrop ( DEFAULT_CROP ) ;
62- }
63- } ;
62+ const handleImageLoad : ReactEventHandler < HTMLImageElement > = ( e ) => {
63+ if ( props . aspect ) {
64+ const { width, height } = e . currentTarget ;
65+ setCrop ( centerAspectCrop ( width , height , props . aspect ) ) ;
66+ } else {
67+ setCrop ( DEFAULT_CROP ) ;
68+ }
69+ } ;
6470
65- // Must set completedCrop and rotation as dependencies because generateImage
66- // captures them as the state changes, except imgRef which persists throughout.
67- useEmitterFactory (
68- props ,
69- {
70- resetImage : ( ) => setCrop ( undefined ) ,
71- getImage : ( ) => generateImage ( ) ,
72- } ,
73- [ completedCrop , rotation ] ,
74- ) ;
71+ // Must set completedCrop and rotation as dependencies because generateImage
72+ // captures them as the state changes, except imgRef which persists throughout.
73+ useImperativeHandle (
74+ ref ,
75+ ( ) => ( {
76+ resetImage : ( ) : void => setCrop ( undefined ) ,
77+ getImage : ( ) : Promise < Blob | undefined > => generateImage ( ) ,
78+ } ) ,
79+ [ completedCrop , rotation ] ,
80+ ) ;
7581
76- return (
77- < div >
78- < ReactCrop
79- aspect = { props . aspect }
80- circularCrop = { props . circular }
81- crop = { crop }
82- keepSelection
83- onChange = { ( _ , percentCrop ) : void => setCrop ( percentCrop ) }
84- onComplete = { setCompletedCrop }
85- ruleOfThirds = { props . grids ?? true }
86- >
87- < img
88- ref = { imgRef }
89- alt = { props . alt }
90- className = "pointer-events-none select-none"
91- onError = { props . onLoadError }
92- onLoad = { handleImageLoad }
93- src = { props . src }
94- style = { { transform : `rotate(${ rotation } deg)` } }
95- />
96- </ ReactCrop >
82+ return (
83+ < div >
84+ < ReactCrop
85+ aspect = { props . aspect }
86+ circularCrop = { props . circular }
87+ crop = { crop }
88+ keepSelection
89+ onChange = { ( _ , percentCrop ) : void => setCrop ( percentCrop ) }
90+ onComplete = { setCompletedCrop }
91+ ruleOfThirds = { props . grids ?? true }
92+ >
93+ < img
94+ ref = { imgRef }
95+ alt = { props . alt }
96+ className = "pointer-events-none select-none"
97+ onError = { props . onLoadError }
98+ onLoad = { handleImageLoad }
99+ src = { props . src }
100+ style = { { transform : `rotate(${ rotation } deg)` } }
101+ />
102+ </ ReactCrop >
97103
98- < div className = "flex items-center" >
99- < RotateRight className = "mr-8" />
104+ < div className = "flex items-center" >
105+ < RotateRight className = "mr-8" />
100106
101- < Slider
102- max = { 180 }
103- min = { 0 }
104- onChange = { ( _ , angle ) : void => setRotation ( angle as number ) }
105- value = { rotation }
106- valueLabelDisplay = "auto"
107- valueLabelFormat = { ( degree ) : string => `${ degree } \u00B0` }
108- />
107+ < Slider
108+ max = { 180 }
109+ min = { 0 }
110+ onChange = { ( _ , angle ) : void => setRotation ( angle as number ) }
111+ value = { rotation }
112+ valueLabelDisplay = "auto"
113+ valueLabelFormat = { ( degree ) : string => `${ degree } \u00B0` }
114+ />
115+ </ div >
109116 </ div >
110- </ div >
111- ) ;
112- } ;
117+ ) ;
118+ } ,
119+ ) ;
120+
121+ ImageCropper . displayName = 'ImageCropper' ;
113122
114123export default ImageCropper ;
0 commit comments