Skip to content

Commit cc80d8c

Browse files
author
ricwilson
committed
Refactor code structure for improved readability and maintainability
1 parent 7331252 commit cc80d8c

File tree

6 files changed

+110
-28
lines changed

6 files changed

+110
-28
lines changed

ImageCrop/ImageCrop/ControlManifest.Input.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest>
3-
<control namespace="RAW" constructor="ImageCrop" version="0.0.6" display-name-key="ImageCrop_DisplayName" description-key="ImageCrop_Description" control-type="standard">
3+
<control namespace="RAW" constructor="ImageCrop" version="0.0.12" display-name-key="ImageCrop_DisplayName" description-key="ImageCrop_Description" control-type="standard">
44
<!--external-service-usage node declares whether this 3rd party PCF control is using external service or not, if yes, this control will be considered as premium and please also add the external domain it is using.
55
If it is not using any external service, please set the enabled="false" and DO NOT add any domain below. The "enabled" will be false by default.
66
Example1:
@@ -40,7 +40,7 @@
4040
<property name="DefaultY" display-name-key="ImageCrop_DefaultY_DisplayName" description-key="ImageCrop_DefaultY_Description" of-type="Decimal" usage="input" required="false" default-value="-1" />
4141
<property name="DefaultWidth" display-name-key="ImageCrop_DefaultWidth_DisplayName" description-key="ImageCrop_DefaultWidth_Description" of-type="Decimal" usage="input" required="false" default-value="-1" />
4242
<property name="DefaultHeight" display-name-key="ImageCrop_DefaultHeight_DisplayName" description-key="ImageCrop_DefaultHeight_Description" of-type="Decimal" usage="input" required="false" default-value="-1" />
43-
<property name="actionSchema" display-name-key="ImageCrop_ActionSchema_DisplayName" description-key="ImageCrop_ActionSchema_Description" of-type="SingleLine.Text" usage="input" hidden="true" />
43+
<property name="actionSchema" display-name-key="ImageCrop_ActionSchema_DisplayName" description-key="ImageCrop_ActionSchema_Description" of-type="SingleLine.Text" usage="bound" hidden="true" />
4444
<property name="actionOutput" display-name-key="ImageCrop_ActionOutput_DisplayName" description-key="ImageCrop_ActionOutput_Description" of-type="Object" usage="output" default-value="" />
4545
<property-dependencies>
4646
<property-dependency input="actionSchema" output="actionOutput" required-for="schema" />

ImageCrop/ImageCrop/index.ts

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class ImageCrop implements ComponentFramework.StandardControl<IInputs, IO
1919
private _actionDragEnd: boolean;
2020
private _actionCropComplete: boolean;
2121
private _cropResults: string | undefined;
22+
private _actionOutput: { action: string, x: number, y: number } | null;
2223

2324
/**
2425
* Empty constructor.
@@ -54,19 +55,15 @@ export class ImageCrop implements ComponentFramework.StandardControl<IInputs, IO
5455

5556
public async getOutputSchema(context: ComponentFramework.Context<IInputs>): Promise<Record<string, unknown>> {
5657
return Promise.resolve({
57-
Data: ActionOutputSchema
58+
actionOutput: ActionOutputSchema
5859
});
5960
}
6061

6162
/**
6263
* Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
6364
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
6465
*/
65-
public updateView(context: ComponentFramework.Context<IInputs>): void {
66-
// if (this._updateFromOutput) {
67-
// this._updateFromOutput = false;
68-
// return;
69-
// }
66+
public updateView(context: ComponentFramework.Context<IInputs>): void {
7067

7168
this._reactRoot.render(
7269
React.createElement(ImageCropApp, {
@@ -90,12 +87,22 @@ export class ImageCrop implements ComponentFramework.StandardControl<IInputs, IO
9087
// Callback for drag start
9188
public onDragStart = (e: PointerEvent) => {
9289
this._actionDragStart = true;
90+
this._actionOutput = {
91+
action: "dragStart",
92+
x: e.clientX,
93+
y: e.clientY
94+
};
9395
this._notifyOutputChanged();
9496
};
9597

9698
// Callback for drag end
9799
public onDragEnd = (e: PointerEvent) => {
98100
this._actionDragEnd = true;
101+
this._actionOutput = {
102+
action: "dragEnd",
103+
x: e.clientX,
104+
y: e.clientY
105+
};
99106
this._notifyOutputChanged();
100107
};
101108

@@ -105,34 +112,22 @@ export class ImageCrop implements ComponentFramework.StandardControl<IInputs, IO
105112
*/
106113
public getOutputs(): IOutputs {
107114
this._updateFromOutput = true;
108-
let notifyAgain = false;
109115

110116
const output: IOutputs = {
111-
actionOutput: {
112-
action: "TEST"
113-
}
117+
actionOutput: null
114118
};
115119

116120
if (this._actionCropComplete) {
117-
notifyAgain = true;
121+
//notifyAgain = true;
118122
output.imageOutput = this._cropResults ? this._cropResults : undefined;
119123
this._actionCropComplete = false;
120124
}
121125

122-
if (this._actionDragStart) {
123-
notifyAgain = true;
124-
this._actionDragStart = false;
125-
}
126-
127-
if (this._actionDragEnd) {
128-
notifyAgain = true;
129-
this._actionDragEnd = false;
126+
if (this._actionDragStart || this._actionDragEnd) {
127+
output.actionOutput = this._actionOutput;
128+
this._actionOutput = null;
130129
}
131-
132-
if (notifyAgain) {
133-
this._notifyOutputChanged();
134-
}
135-
130+
136131
return output;
137132
}
138133

ImageCrop/ImageCrop/types/actionOutputSchema.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ export const ActionOutputSchema = {
44
properties: {
55
action:{
66
type: "string"
7-
}
7+
},
8+
x:{
9+
type: "number"
10+
},
11+
y:{
12+
type: "number"
13+
},
814
}
915
};

ImageCrop/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Image Cropper PCF Control
2+
3+
A Power Apps Component Framework (PCF) control for cropping, rotating, and transforming images, built with React and designed for robust, modular, and Power Apps-compliant use.
4+
5+
![Image Cropper Demo](./ImageCrop/images/image-cropper-demo.gif)
6+
7+
## Overview
8+
9+
The Image Cropper control provides a modern, accessible, and highly configurable image cropping experience for both Model-driven and Canvas Power Apps. It supports aspect ratio locking, scaling, rotation, circular/elliptical cropping, and advanced browser scaling handling. The control is built with React functional components and custom hooks for maintainability and extensibility.
10+
11+
## Features
12+
13+
- Crop images with drag-and-resize UI
14+
- Lock aspect ratio or allow freeform cropping
15+
- Rotate and scale images
16+
- Circular/elliptical crop support
17+
- Handles browser and container scaling
18+
- Default crop values from manifest
19+
- Robust image load and crop state management
20+
- Outputs cropped image as base64 PNG
21+
- Fully modular React hooks architecture
22+
23+
## Installation
24+
25+
[Download Latest](https://github.com/rwilson504/PCFControls/releases/latest/download/ImageCropperControl_managed.zip)
26+
27+
Import the managed solution into your environment.
28+
Ensure PCF controls are enabled. [Enable PCF](https://docs.microsoft.com/en-us/powerapps/developer/component-framework/component-framework-for-canvas-apps)
29+
30+
## Sample Application
31+
32+
A sample solution is available for testing and demonstration:
33+
34+
[Download Sample App](./Sample/RAW!%20ImageCropper%20Sample.msapp)
35+
36+
## Configuration
37+
38+
Add the Image Cropper control to your form or app and configure the required properties.
39+
**Any field referenced in the properties must be present in your view or data source.**
40+
41+
### Control Properties
42+
43+
| Name | Usage | Type | Required | Default | Description |
44+
|---------------------|----------|------------------|----------|---------|------------------------------------------------------------------|
45+
| imageInput | input | SingleLine.Text | Yes | | Image source (base64 or URL) |
46+
| aspect | input | Decimal Number | No | 0 | Aspect ratio (width/height), blank for freeform |
47+
| minWidth | input | Whole.Number | No | -1 | Minimum crop width |
48+
| maxWidth | input | Whole.Number | No | -1 | Maximum crop width |
49+
| minHeight | input | Whole.Number | No | -1 | Minimum crop height |
50+
| maxHeight | input | Whole.Number | No | -1 | Maximum crop height |
51+
| rotation | input | Whole.Number | No | 0 | Image rotation in degrees |
52+
| scaling | input | Decimal Number | No | 1 | Image scaling factor |
53+
| circularCrop | input | TwoOptions | No | false | Enable circular/elliptical crop |
54+
| keepSelection | input | TwoOptions | No | false | Keep crop selection after crop |
55+
| locked | input | TwoOptions | No | false | Lock crop area (disable editing) |
56+
| disabled | input | TwoOptions | No | false | Disable the control |
57+
| ruleOfThirds | input | TwoOptions | No | false | Show rule-of-thirds grid |
58+
| DefaultUnit | input | OptionSet | No | % | Default crop unit (px or %) |
59+
| DefaultX | input | Whole.Number | No | -1 | Default crop X position |
60+
| DefaultY | input | Whole.Number | No | -1 | Default crop Y position |
61+
| DefaultWidth | input | Whole.Number | No | -1 | Default crop width |
62+
| DefaultHeight | input | Whole.Number | No | -1 | Default crop height |
63+
64+
### Output Properties
65+
66+
| Name | Type | Description |
67+
|--------------|----------------|---------------------------------------------|
68+
| croppedImage | SingleLine.Text| Cropped image as base64 PNG (data URL) |
69+
70+
## Advanced Usage
71+
72+
- All crop, aspect, and transform logic is modularized in custom React hooks for maintainability.
73+
- The control automatically handles browser scaling, image load timing, and crop validity.
74+
- Circular/elliptical cropping uses canvas ellipse masking for true round crops.
75+
- Default crop values are only applied after the image is loaded.
76+
77+
## Resources
78+
79+
- [PCF Documentation](https://docs.microsoft.com/en-us/powerapps/developer/component-framework/overview)
80+
- [react-image-crop](https://github.com/dominictarr/react-image-crop)
81+
- [PCF Controls Repo](https://github.com/rwilson504/PCFControls)

ImageCrop/Sample/RAW Image Crop.msapp

2.08 MB
Binary file not shown.

ImageCrop/Solution/RAW!ImageCropControl/src/Other/Solution.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<LocalizedName description="RAW!ImageCropControl" languagecode="1033" />
99
</LocalizedNames>
1010
<Descriptions />
11-
<Version>1.0.6</Version>
11+
<Version>1.0.12</Version>
1212
<!-- Solution Package Type: Unmanaged(0)/Managed(1)/Both(2)-->
1313
<Managed>2</Managed>
1414
<Publisher>

0 commit comments

Comments
 (0)