advanced-cropper / react-advanced-cropper

The react cropper library that embraces power of the advanced cropper core to give the possibility to create croppers that exactly suited for your website design
https://advanced-cropper.github.io/react-advanced-cropper/
Other
619 stars 26 forks source link

Potential bug when cropping in chrome/safari #66

Open garganmol111 opened 2 months ago

garganmol111 commented 2 months ago

I have created the following ImageCropper component for my project:

'use client';
import {
  Button,
  Modal,
  ModalContent,
  ModalHeader,
  ModalBody,
  ModalFooter,
  useDisclosure,
} from '@nextui-org/react';
import { useDropzone } from 'react-dropzone';
import React, { useEffect, useState } from 'react';
import { Cropper, ImageRestriction } from 'react-advanced-cropper';
import 'react-advanced-cropper/dist/style.css';
import { ImagePlus } from 'lucide-react';

const ImageCropper = ({
  croppedImage,
  setCroppedImage,
  label,
  className,
  imageType,
  aspect,
}) => {
  const [cropper, setCropper] = useState(null);
  const [isCropping, setIsCropping] = useState(false);
  const [image, setImage] = useState(null);
  const [croppedImageURL, setCroppedImageURL] = useState(null);

  const { isOpen, onOpen, onClose } = useDisclosure();

  const { getRootProps, getInputProps, open } = useDropzone({
    accept: {
      'image/png': ['.png'],
      'image/jpeg': ['.jpg', '.jpeg'],
    },
    noKeyboard: true,
    noClick: true,
    onDrop: acceptedFiles => {
      setIsCropping(true); // Set isCropping to true when a new image is dropped
      //   setCroppedImage(acceptedFiles[0]);
      onOpen();
    },
  });

  const onChange = cropper => {
    setCropper(cropper);
  };

  const handleDropzoneClick = e => {
    // If the modal is open, do nothing
    if (isOpen) {
      e.stopPropagation();
      // Prevent click from reaching dropzone
    } else {
      open();
    }
  };

  const handleFileChange = e => {
    const file = e.target.files[0];
    if (file) {
      setIsCropping(true); // Set isCropping to true when a new image is selected
      const reader = new FileReader();
      reader.onload = () => {
        setImage(reader.result);
        onOpen();
      };
      reader.readAsDataURL(file);
    }
  };

  const applyCrop = () => {
    if (cropper) {
      const canvas = cropper.getCanvas();
      if (canvas) {
        canvas.toBlob(blob => {
          setCroppedImage(blob);
        });

        setCropper(null);
        setIsCropping(false);
        onClose();
      }
    }
  };

  const cancelCrop = e => {
    setCroppedImage(null);
    setImage(null);
    setCropper(null);
    setIsCropping(false); // Reset isCropping when cropping is cancelled
    onClose();
  };

  useEffect(() => {
    if (croppedImage instanceof Blob) {
      setCroppedImageURL(URL.createObjectURL(croppedImage));
    } else if (croppedImage?.url) {
      setCroppedImageURL(croppedImage?.url);
    } else if (croppedImage === null) setCroppedImageURL(null);
  }, [croppedImage]);

  return (
    <div
      {...getRootProps({ className: 'dropzone' })}
      onClick={handleDropzoneClick}
      className={`${className}flex justify-center items-center flex-col text-center `}
    >
      <div>
        <input
          {...getInputProps()}
          id={imageType}
          onChange={handleFileChange}
        />
      </div>
      <div>
        <Modal
          isOpen={isOpen}
          onClose={cancelCrop}
          size="3xl"
          isDismissable={false}
        >
          <ModalContent>
            <ModalHeader className="flex flex-col gap-1">{label}</ModalHeader>
            <ModalBody>
              {image && isCropping && (
                <div>
                  <Cropper
                    src={image}
                    onChange={onChange}
                    className={'cropper'}
                    aspectRatio={aspect}
                    initialCrop={{ x: 0, y: 0, width: 100, height: 100 }}
                    stencilProps={{
                      handlers: true,
                      lines: true,
                      movable: true,
                      resizable: true,
                    }}
                    canvas={true}
                    stencilSize={{ width: 300, height: 300 }}
                    imageRestriction={ImageRestriction.fillArea}
                  />
                  <div className="flex gap-5 justify-center pt-4">
                    <Button color="primary" onClick={applyCrop}>
                      Apply Crop
                    </Button>
                    <Button color="danger" onClick={cancelCrop}>
                      Cancel Crop
                    </Button>
                  </div>
                </div>
              )}
            </ModalBody>
          </ModalContent>
        </Modal>
      </div>
      {croppedImageURL ? (
        <img
          src={croppedImageURL}
          alt="Cropped Image"
          className="
        object-cover"
        />
      ) : (
        <div>
          {label ? (
            <p className=" flex flex-col justify-center items-center w-full text-primary text-[8px]">
              <ImagePlus /> {label}
            </p>
          ) : (
            <ImagePlus className="text-primary" size={32} />
          )}
        </div>
      )}
    </div>
  );
};

export default ImageCropper;

In the above code, whenever ImageCropper is clicked on, the cropper opens in a nextui-org/react Modal. The above code works perfectly in Firefox, but doesn't seem to work in Chrome/Safari. In chrome/safari, the modal opens but the cropper is not shown. Plus there is no error/warning in the browser console when this happens as well. Below are screen recording of how it looks on both browsers.

Firefox:

https://github.com/advanced-cropper/react-advanced-cropper/assets/33829986/dd7b9a1c-25eb-4ad7-aaef-2febaa148b8d

Chrome:

https://github.com/advanced-cropper/react-advanced-cropper/assets/33829986/116fe4ea-70db-46a8-a77c-2466d0d68ef4

Norserium commented 1 month ago

@garganmol111, try to add the following callback to Modal:

<Modal
   onOpenChange={(isOpen) => {
       if (isOpen) {
          cropperRef.current?.refresh();
       }
   })
   ...
/>
garganmol111 commented 4 weeks ago

@Norserium I have added the given callback, and the code now looks like:

'use client';
import {
  Button,
  Modal,
  ModalContent,
  ModalHeader,
  ModalBody,
  useDisclosure,
} from '@nextui-org/react';
import { useDropzone } from 'react-dropzone';
import React, { useEffect, useState, useRef } from 'react';
import { Cropper, ImageRestriction } from 'react-advanced-cropper';
import 'react-advanced-cropper/dist/style.css';
import { ImagePlus } from 'lucide-react';

const ImageCropper = ({
  croppedImage,
  setCroppedImage,
  label,
  className,
  imageType,
  aspect,
}) => {
  ...
  const cropperRef = useRef(null);

  ...

  return (
    <div
      {...getRootProps({ className: 'dropzone' })}
      onClick={handleDropzoneClick}
      className={`${className}flex justify-center items-center flex-col text-center `}
    >
      <div>
        <input
          {...getInputProps()}
          id={imageType}
          onChange={handleFileChange}
        />
      </div>
      <div>
        <Modal
          isOpen={isOpen}
          onClose={cancelCrop}
          onOpenChange={isOpen => {
            if (isOpen) {
              cropperRef.current?.refresh();
            }
          }}
          size="3xl"
          isDismissable={false}
        >
          <ModalContent>
            <ModalHeader className="flex flex-col gap-1">{label}</ModalHeader>
            <ModalBody>
              {image && isCropping && (
                <div>
                  <Cropper
                    src={image}
                    onChange={onChange}
                   onReady={instance => {
                      cropperRef.current = instance;
                      console.log('Inside onReady:', cropperRef.current);
                    }}
                    className={'cropper'}
                    aspectRatio={aspect}
                    initialCrop={{ x: 0, y: 0, width: 100, height: 100 }}
                    stencilProps={{
                      handlers: true,
                      lines: true,
                      movable: true,
                      resizable: true,
                    }}
                    canvas={true}
                    stencilSize={{ width: 300, height: 300 }}
                    imageRestriction={ImageRestriction.fillArea}
                  />
                  <div className="flex gap-5 justify-center pt-4">
                    <Button color="primary" onClick={applyCrop}>
                      Apply Crop
                    </Button>
                    <Button color="danger" onClick={cancelCrop}>
                      Cancel Crop
                    </Button>
                  </div>
                </div>
              )}
            </ModalBody>
          </ModalContent>
        </Modal>
      </div>
      {croppedImageURL ? (
        <img
          src={croppedImageURL}
          alt="Cropped Image"
          className="
        object-cover"
        />
      ) : (
        <div>
          {label ? (
            <p className=" flex flex-col justify-center items-center w-full text-primary text-[8px]">
              <ImagePlus /> {label}
            </p>
          ) : (
            <ImagePlus className="text-primary" size={32} />
          )}
        </div>
      )}
    </div>
  );
};

export default ImageCropper;

in the above code, I'm still facing the same issue. in the onReady callback in Cropper component, the console.log statement happens only in firefox and not in chrome/safari.

Edit: Having the same issue in the latest version (v0.20.0) as well

Norserium commented 4 weeks ago

@garganmol111, does the cropper appear when you resize window while your modal window is open?