symfony / ux

Symfony UX initiative: a JavaScript ecosystem for Symfony
https://ux.symfony.com/
MIT License
856 stars 315 forks source link

[CropperJS] Edit the cropper image #2380

Open RehArk opened 1 week ago

RehArk commented 1 week ago

Hi everyone, I need to crop my image based on my file input, but the cropper is not allowing me to edit the image. I want to know if it is possible to do something like this to update the cropper:

import { Controller } from '@hotwired/stimulus';
import Cropper from 'cropperjs';
import CropEvent = Cropper.CropEvent;

export default class CropperController extends Controller {
    declare readonly publicUrlValue: string;
    declare readonly optionsValue: object;
    declare img: HTMLImageElement;
    declare cropper: Cropper;

    static values = {
        publicUrl: String,
        options: Object,
        img: HTMLImageElement,
        cropper: Cropper
    };

    connect() {
        this.initializeCropper();
    }

    initializeCropper() {
        // Remove any existing cropper image and instance before initializing
        if (this.img) {
            this.img.remove();
        }
        if (this.cropper) {
            this.cropper.destroy();
        }

        // Create image view
        this.img = document.createElement('img');
        this.img.classList.add('cropperjs-image');
        this.img.src = this.publicUrlValue; // The current image URL value

        const parent = this.element.parentNode;
        if (!parent) {
            throw new Error('Missing parent node for Cropperjs');
        }

        parent.appendChild(this.img);

        // Options for the cropper instance
        const options = this.optionsValue;

        // Dispatch pre-connect event
        this.dispatchEvent('pre-connect', { options, img: this.img });

        // Build the cropper with the given image and options
        this.cropper = new Cropper(this.img, options);

        // Event listener to update the input value on crop
        this.img.addEventListener('crop', (event) => {
            (this.element as HTMLInputElement).value = JSON.stringify((event as CropEvent).detail);
        });

        // Dispatch connect event
        this.dispatchEvent('connect', { cropper: this.cropper, options, img: this.img });
    }

    // Method to update the image URL and reinitialize the cropper
    updateImage(newImageUrl) {
        this.img.src = newImageUrl; // Update the public URL value
        this.initializeCropper(); // Reinitialize the cropper with the new image
    }

    disconnect() {
        if (this.cropper) {
            this.cropper.destroy();
        }
        if (this.img) {
            this.img.remove();
        }
    }

    // Helper method to dispatch events
    private dispatchEvent(name: string, payload: any) {
        this.dispatch(name, { detail: payload, prefix: 'cropperjs' });
    }
}
smnandre commented 3 days ago

I’m not entirely sure about the expected behavior you’re aiming for.

The current CropperJS implementation is designed to crop an existing image, and adapting it to work with non-uploaded images would require quite a few changes to its code. :/

    // Method to update the image URL and reinitialize the cropper
    updateImage(newImageUrl) {
        this.img.src = newImageUrl; // Update the public URL value
        this.initializeCropper(); // Reinitialize the cropper with the new image
    }

This snippet wouldn’t work as expected because this.img gets reset right after the line // Create image view. I suppose you meant to reference this.publicUrl instead?

If you need this kind of behavior, one possible solution could be to set the initial HTML inside a <template> tag and dynamically create the <div> whenever you have a new imageUrl.

Alternatively, maybe consider a custom controller? You could use the various events dispatched by the UX controller?