Skip to content

feat(ImageCropper): add OnCropChangedAsync parameter#460

Merged
ArgoZhang merged 6 commits intomasterfrom
refactor-crop
Jun 7, 2025
Merged

feat(ImageCropper): add OnCropChangedAsync parameter#460
ArgoZhang merged 6 commits intomasterfrom
refactor-crop

Conversation

@ArgoZhang
Copy link
Copy Markdown
Member

@ArgoZhang ArgoZhang commented Jun 7, 2025

Link issues

fixes #459

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Add OnCropChangedAsync for live crop events and refactor JS interop storage and initialization to support callback invocations.

New Features:

  • Introduce OnCropChangedAsync parameter to receive crop box change events asynchronously.
  • Define new ImageCropperData struct to represent live crop box dimensions and transform data.
  • Extend JS init method to accept a callback identifier and invoke OnCropChangedAsync when crop region changes.

Enhancements:

  • Refactor JS Data.store to include element, invoke reference, options, and cropper instance.
  • Update all JS interop methods to extract cropper and related data from the consolidated storage object.
  • Switch to JSObjectReference mode in JSModuleAutoLoader for improved module handling.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Jun 7, 2025

Reviewer's Guide

This PR enhances the ImageCropper component with a new OnCropChangedAsync callback by extending JS interop to capture and forward crop movements, refactoring client-side state management, updating the C# component to register/invoke the callback, introducing a dedicated ImageCropperData model, and applying minor loader attribute and documentation cleanups.

Sequence Diagram: OnCropChangedAsync Event Flow

sequenceDiagram
    actor User
    participant CropperJSEvents as "Cropper.js Event Source"
    participant ImageCropperJS as "ImageCropper.razor.js"
    participant ImageCropperCS as "ImageCropper.razor.cs (C# Component)"
    participant DeveloperHandler as "Developer's OnCropChangedAsync Handler"

    User->>CropperJSEvents: Modifies crop selection
    CropperJSEvents->>ImageCropperJS: Fires 'crop' event (captures cropData)
    ImageCropperJS->>ImageCropperJS: Stores cropData

    User->>CropperJSEvents: Finishes crop modification
    CropperJSEvents->>ImageCropperJS: Fires 'cropend' event
    ImageCropperJS->>ImageCropperCS: invokeMethodAsync("TriggerOnCropChangedAsync", cropData)
    ImageCropperCS->>ImageCropperCS: Executes TriggerOnCropChangedAsync(cropData)
    ImageCropperCS->>DeveloperHandler: Invokes OnCropChangedAsync(cropData)
Loading

Sequence Diagram: ImageCropper Initialization for OnCropChangedAsync

sequenceDiagram
    participant CSharpComponent as "ImageCropper.razor.cs"
    participant BlazorInterop as "Blazor JS Interop"
    participant JSModule as "ImageCropper.razor.js"
    participant CropperLib as "Cropper.js Library"
    participant InternalData as "JS Internal Data Store"

    CSharpComponent->>BlazorInterop: InvokeVoidAsync("init", id, dotnetHelper, jsParams)
    BlazorInterop->>JSModule: init(id, dotnetHelper, jsParams containing Options & TriggerOnCropEndAsync name)
    JSModule->>JSModule: Prepare Cropper.js options based on jsParams
    JSModule->>JSModule: If TriggerOnCropEndAsync is present, set 'crop' event handler (stores data)
    JSModule->>JSModule: If TriggerOnCropEndAsync is present, set 'cropend' event handler (invokes dotnetHelper.invokeMethodAsync)
    JSModule->>CropperLib: new Cropper(imageElement, preparedOptions)
    CropperLib-->>JSModule: cropperInstance
    JSModule->>InternalData: Store { el, invoke: dotnetHelper, options: jsParams, cropper: cropperInstance }
Loading

File-Level Changes

Change Details Files
Extend JS interop to support crop change callbacks
  • Update init signature to accept invoke reference and custom options payload
  • Inject op.crop and op.cropend handlers to buffer and send crop data
  • Store { el, invoke, options, cropper } in Data.set instead of just the cropper
ImageCropper.razor.js
Refactor JS methods to use guarded state and nested options
  • Switch Data.get usage to destructure stored object (ic) and null-check before operations
  • Adjust crop, replace, reset, rotate, clear, enable/disable to use ic.cropper and ic.el
  • Use nested options.options for isRound flag when generating cropped output
ImageCropper.razor.js
Implement OnCropChangedAsync on the C# component
  • Add Func<ImageCropperData, Task>? OnCropChangedAsync parameter
  • Pass callback identifier in InvokeInitAsync payload
  • Add [JSInvokable] TriggerOnCropChangedAsync method to forward events
ImageCropper.razor.cs
Introduce ImageCropperData struct for crop event payload
  • Create new ImageCropperData with properties X, Y, Width, Height, Rotate, ScaleX, ScaleY
ImageCropperData.cs
Apply minor loader and documentation cleanups
  • Enable JSObjectReference in JSModuleAutoLoader attribute
  • Remove redundant block from ImageCropperResult
ImageCropper.razor
ImageCropperResult.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#459 Add an OnCropChangedAsync parameter to the ImageCropper component.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@bb-auto bb-auto Bot added the enhancement New feature or request label Jun 7, 2025
@bb-auto bb-auto Bot added this to the v9.2.0 milestone Jun 7, 2025
@ArgoZhang ArgoZhang merged commit 8741d5e into master Jun 7, 2025
1 check passed
@ArgoZhang ArgoZhang deleted the refactor-crop branch June 7, 2025 12:01
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @ArgoZhang - I've reviewed your changes - here's some feedback:

  • In your dispose method you destroy the cropper but don’t dispose the stored DotNetObjectReference (invoke), so consider calling invoke.dispose() after removing it from Data to avoid a memory leak.
  • The JS destructuring expects a lower-camelCase triggerOnCropEndAsync property, but your C# anonymous object uses PascalCase TriggerOnCropEndAsync—either add a JsonPropertyName attribute or switch to camelCase to ensure the JS sees the flag correctly.
  • Wrap the invoke.invokeMethodAsync call inside a try/catch in JS to prevent any .NET callback errors from bubbling up and breaking the cropper.
Here's what I looked at during the review
  • 🟡 General issues: 1 issue found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


const image = el.querySelector(".bb-cropper-image");
const cropper = new Cropper(image, getOptions(options));
const { options: op, triggerOnCropEndAsync } = options;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Avoid shadowing the options identifier

Consider renaming either the parameter or the destructured variable to avoid confusion and improve code clarity.

}
}

export function reset(id) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider introducing a utility function to simplify repeated instance retrieval and null-checks in your API methods.

Suggested change
export function reset(id) {
// at top of module, add a small utility to collapse the Data.get + null-checks
function getInstance(id) {
return Data.get(id) || {};
}
// -------------------- refactor your APIs --------------------
// before:
// export function reset(id) {
// const ic = Data.get(id);
// if (ic != null) {
// const { cropper } = ic;
// if (cropper) {
// cropper.reset();
// }
// }
// }
// after:
export function reset(id) {
getInstance(id).cropper?.reset();
}
// before:
// export async function enable(id) {
// const ic = Data.get(id);
// if (ic != null) {
// const { el, cropper } = ic;
// if (cropper) {
// cropper.enable();
// }
// if (el) {
// el.classList.remove("disabled");
// }
// }
// }
// after:
export async function enable(id) {
const { el, cropper } = getInstance(id);
cropper?.enable();
el?.classList.remove("disabled");
}
// before (crop has more logic and nested destructuring):
// export function crop(id) {
// let ret = null;
// const ic = Data.get(id);
// if (ic != null) {
// const { cropper, options } = ic;
// if (cropper !== null) {
// cropper.crop();
// let resultData = cropper.getCroppedCanvas();
// const { isRound } = options.options;
// if (isRound) {
// resultData = getRoundCanvas(resultData);
// }
// ret = resultData.toDataURL();
// }
// }
// return ret;
// }
// after:
export function crop(id) {
const { cropper, options: { options: opts } = {} } = getInstance(id);
if (!cropper) return null;
cropper.crop();
let canvas = cropper.getCroppedCanvas();
if (opts?.isRound) canvas = getRoundCanvas(canvas);
return canvas.toDataURL();
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ImageCropper): add OnCropChangedAsync parameter

1 participant