Skip to content

Commit c1e032d

Browse files
author
ricwilson
committed
feat: implement image loading state management and default crop handling in ImageCropControl
1 parent eaced9f commit c1e032d

File tree

5 files changed

+86
-37
lines changed

5 files changed

+86
-37
lines changed

ImageCrop/ImageCrop/components/imageCropControl.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from "react";
2-
import { Crop, PixelCrop } from "react-image-crop";
2+
import { convertToPixelCrop, Crop, PixelCrop } from "react-image-crop";
33
import "react-image-crop/dist/ReactCrop.css";
44
import { usePcfContext } from "../services/pcfContext";
55
import { IImageCropControlProps } from "../types/imageCropTypes";
@@ -18,52 +18,64 @@ import {
1818
useCropToBase64,
1919
useKeepSelection,
2020
useRotation,
21-
useScaling
21+
useScaling,
22+
useImageLoaded
2223
} from "../hooks";
23-
import { useDefaultCrop } from "../hooks/useDefaultCrop";
24+
import { getDefaultCrop, useDefaultCrop } from "../hooks/useDefaultCrop";
2425
import CropWrapper from "./imageCropWrapper";
2526

2627
const ImageCropControl: React.FC<IImageCropControlProps> = (props) => {
2728
const pcfContext = usePcfContext();
28-
const [crop, setCrop] = React.useState<Crop>();
29-
// Set crop from manifest defaults on first load (modular)
30-
useDefaultCrop(pcfContext.context, setCrop, crop);
3129
const [completedCrop, setCompletedCrop] = React.useState<PixelCrop>()
30+
const [crop, setCrop] = React.useState<Crop>();
31+
32+
// Image loaded state (modular)
33+
const [imageLoaded, handleImageLoad, handleImageError, handleImageSrcChange] = useImageLoaded();
34+
3235
const imgRef = React.useRef<HTMLImageElement>(null) as React.RefObject<HTMLImageElement>;
3336
const appScaling = useResponsiveAppScaling(pcfContext.context, imgRef);
3437

3538
// Get the locked property from PCF context
3639
const locked = useLocked(pcfContext.context);
37-
3840
// Get the disabled property from PCF context
3941
const disabled = useDisabled(pcfContext.context);
40-
4142
// Get the ruleOfThirds property from PCF context
4243
const ruleOfThirds = useRuleOfThirds(pcfContext.context);
43-
4444
// Get the circularCrop property from PCF context
4545
const circularCrop = useCircularCrop(pcfContext.context);
46-
4746
// Get min/max width/height from PCF context, scaled for browser
4847
const minWidth = useMinWidth(pcfContext.context);
4948
const maxWidth = useMaxWidth(pcfContext.context);
5049
const minHeight = useMinHeight(pcfContext.context);
5150
const maxHeight = useMaxHeight(pcfContext.context);
52-
5351
// Get the aspect ratio from PCF context and helper to center crop
5452
const [aspect, centerCropIfNeeded] = useAspect(pcfContext.context, imgRef, setCrop);
5553
// Get the keepSelection property from PCF context
5654
const keepSelection = useKeepSelection(pcfContext.context);
57-
5855
// Get the image from the PCF context property (should be base64)
5956
const imageSrc = useImageSrc(pcfContext.context);
60-
6157
// Get the rotation property from PCF context
6258
const rotation = useRotation(pcfContext.context);
63-
6459
// Get the scaling property from PCF context
6560
const scaling = useScaling(pcfContext.context);
6661

62+
// Get the default crop object (not a hook)
63+
const defaultCrop = useDefaultCrop(pcfContext.context);
64+
65+
// Reset imageLoaded state and crop when imageSrc changes
66+
React.useEffect(() => {
67+
handleImageSrcChange(imageSrc);
68+
setCrop(undefined);
69+
}, [imageSrc]);
70+
71+
// Set crop to default when image is loaded and crop is undefined
72+
React.useEffect(() => {
73+
if (imageLoaded && !crop && defaultCrop) {
74+
setCrop(defaultCrop);
75+
setCompletedCrop(convertToPixelCrop(defaultCrop, imgRef.current.width, imgRef.current.height));
76+
}
77+
}, [imageLoaded]);
78+
6779
// Use custom hook to handle crop-to-base64 conversion and callback
6880
useCropToBase64(imgRef, completedCrop, props.onCropComplete, rotation, scaling, circularCrop);
6981

@@ -90,6 +102,8 @@ const ImageCropControl: React.FC<IImageCropControlProps> = (props) => {
90102
ref={imgRef}
91103
alt="Crop"
92104
src={imageSrc || ''}
105+
onLoad={handleImageLoad}
106+
onError={handleImageError}
93107
style={{
94108
maxWidth: '100%',
95109
maxHeight: '100%',

ImageCrop/ImageCrop/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from "./useCropToBase64";
1313
export * from "./useKeepSelection";
1414
export * from "./useRotation";
1515
export * from "./useScaling";
16+
export * from "./useImageLoaded"

ImageCrop/ImageCrop/hooks/useCropToBase64.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, RefObject } from "react";
2-
import { PixelCrop } from "react-image-crop";
2+
import { Crop, PixelCrop } from "react-image-crop";
33

44
export function useCropToBase64(
55
imgRef: RefObject<HTMLImageElement | null>,
@@ -30,11 +30,12 @@ export function useCropToBase64(
3030
if (circularCrop) {
3131
ctx.save();
3232
ctx.beginPath();
33-
const radius = Math.min(canvas.width, canvas.height) / (2 * pixelRatio);
34-
ctx.arc(
33+
ctx.ellipse(
3534
canvas.width / (2 * pixelRatio),
3635
canvas.height / (2 * pixelRatio),
37-
radius,
36+
canvas.width / (2 * pixelRatio),
37+
canvas.height / (2 * pixelRatio),
38+
0,
3839
0,
3940
2 * Math.PI
4041
);
@@ -85,5 +86,5 @@ export function useCropToBase64(
8586
},
8687
"image/png"
8788
);
88-
}, [completedCrop, imgRef, onCropComplete, rotation, scaling, circularCrop]);
89+
}, [completedCrop, imgRef, rotation, scaling, circularCrop]);
8990
}
Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
11
import { Crop } from "react-image-crop";
22
import { IInputs } from "../generated/ManifestTypes";
3-
import { useRef, useEffect } from "react";
3+
import { useState, useEffect } from "react";
44

5-
export function useDefaultCrop(
6-
context: ComponentFramework.Context<IInputs>,
7-
setCrop: (crop: Crop) => void,
8-
crop: Crop | undefined
9-
) {
10-
const didSet = useRef(false);
5+
export function useDefaultCrop(context: ComponentFramework.Context<IInputs>) {
6+
const [defaultCrop, setDefaultCrop] = useState<Crop | undefined>(() => getDefaultCrop(context));
117

128
useEffect(() => {
13-
if (didSet.current) return;
14-
const unit = context.parameters.DefaultUnit.raw || "%";
15-
const x = context.parameters.DefaultX.raw ?? -1;
16-
const y = context.parameters.DefaultY.raw ?? -1;
17-
const width = context.parameters.DefaultWidth.raw ?? -1;
18-
const height = context.parameters.DefaultHeight.raw ?? -1;
19-
if (x !== -1 && y !== -1 && width !== -1 && height !== -1 && !crop) {
20-
setCrop({ unit, x, y, width, height });
21-
didSet.current = true;
22-
}
23-
}, [context, setCrop, crop]);
9+
setDefaultCrop(getDefaultCrop(context));
10+
}, [
11+
context.parameters.DefaultUnit.raw,
12+
context.parameters.DefaultX.raw,
13+
context.parameters.DefaultY.raw,
14+
context.parameters.DefaultWidth.raw,
15+
context.parameters.DefaultHeight.raw
16+
]);
17+
18+
return defaultCrop;
19+
}
20+
21+
export function getDefaultCrop(context: ComponentFramework.Context<IInputs>) {
22+
const unit = context.parameters.DefaultUnit.raw || "%";
23+
const x = context.parameters.DefaultX.raw ?? -1;
24+
const y = context.parameters.DefaultY.raw ?? -1;
25+
const width = context.parameters.DefaultWidth.raw ?? -1;
26+
const height = context.parameters.DefaultHeight.raw ?? -1;
27+
if (x !== -1 && y !== -1 && width !== -1 && height !== -1) {
28+
return { unit, x, y, width, height };
29+
}
30+
return undefined;
2431
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useState, useCallback } from "react";
2+
3+
/**
4+
* Hook to track image loaded state, including reset when src is cleared.
5+
* @returns [imageLoaded, handleImageLoad, handleImageError, handleImageSrcChange]
6+
*/
7+
export function useImageLoaded() {
8+
const [imageLoaded, setImageLoaded] = useState(false);
9+
10+
// Call this in <img onLoad={...} />
11+
const handleImageLoad = useCallback(() => {
12+
setImageLoaded(true);
13+
}, []);
14+
15+
// Call this in <img onError={...} />
16+
const handleImageError = useCallback(() => {
17+
setImageLoaded(false);
18+
}, []);
19+
20+
// Call this when the src changes
21+
const handleImageSrcChange = useCallback((src: string | null | undefined) => {
22+
setImageLoaded(false);
23+
}, []);
24+
25+
return [imageLoaded, handleImageLoad, handleImageError, handleImageSrcChange] as const;
26+
}

0 commit comments

Comments
 (0)