ImageCropModal.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import React, {
  2. FC, useCallback, useEffect, useState,
  3. } from 'react';
  4. import canvasToBlob from 'async-canvas-to-blob';
  5. import { useTranslation } from 'react-i18next';
  6. import ReactCrop from 'react-image-crop';
  7. import {
  8. Modal,
  9. ModalHeader,
  10. ModalBody,
  11. ModalFooter,
  12. } from 'reactstrap';
  13. import { toastError } from '~/client/util/apiNotification';
  14. import loggerFactory from '~/utils/logger';
  15. import 'react-image-crop/dist/ReactCrop.css';
  16. const logger = loggerFactory('growi:ImageCropModal');
  17. interface ICropOptions {
  18. aspect: number
  19. unit: string,
  20. x: number
  21. y: number
  22. width: number,
  23. height: number,
  24. }
  25. type CropOptions = ICropOptions | null
  26. type Props = {
  27. isShow: boolean,
  28. src: string | ArrayBuffer | null,
  29. onModalClose: () => void,
  30. onCropCompleted: (res: any) => void,
  31. isCircular: boolean,
  32. }
  33. const ImageCropModal: FC<Props> = (props: Props) => {
  34. const {
  35. isShow, src, onModalClose, onCropCompleted, isCircular,
  36. } = props;
  37. const [imageRef, setImageRef] = useState<HTMLImageElement>();
  38. const [cropOptions, setCropOtions] = useState<CropOptions>(null);
  39. const { t } = useTranslation();
  40. const reset = useCallback(() => {
  41. if (imageRef) {
  42. const size = Math.min(imageRef.width, imageRef.height);
  43. setCropOtions({
  44. aspect: 1,
  45. unit: 'px',
  46. x: imageRef.width / 2 - size / 2,
  47. y: imageRef.height / 2 - size / 2,
  48. width: size,
  49. height: size,
  50. });
  51. }
  52. }, [imageRef]);
  53. useEffect(() => {
  54. document.body.style.position = 'static';
  55. reset();
  56. }, [reset]);
  57. const onImageLoaded = (image) => {
  58. setImageRef(image);
  59. reset();
  60. return false;
  61. };
  62. const onCropChange = (crop) => {
  63. setCropOtions(crop);
  64. };
  65. const getCroppedImg = async(image, crop) => {
  66. const canvas = document.createElement('canvas');
  67. const scaleX = image.naturalWidth / image.width;
  68. const scaleY = image.naturalHeight / image.height;
  69. canvas.width = crop.width;
  70. canvas.height = crop.height;
  71. const ctx = canvas.getContext('2d');
  72. ctx?.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, crop.width, crop.height);
  73. try {
  74. const blob = await canvasToBlob(canvas);
  75. return blob;
  76. }
  77. catch (err) {
  78. logger.error(err);
  79. toastError(new Error('Failed to draw image'));
  80. }
  81. };
  82. const crop = async() => {
  83. // crop immages
  84. if (imageRef && cropOptions?.width && cropOptions.height) {
  85. const result = await getCroppedImg(imageRef, cropOptions);
  86. onCropCompleted(result);
  87. }
  88. };
  89. return (
  90. <Modal isOpen={isShow} toggle={onModalClose}>
  91. <ModalHeader tag="h4" toggle={onModalClose} className="bg-info text-light">
  92. {t('crop_image_modal.image_crop')}
  93. </ModalHeader>
  94. <ModalBody className="my-4">
  95. <ReactCrop src={src} crop={cropOptions} onImageLoaded={onImageLoaded} onChange={onCropChange} circularCrop={isCircular} />
  96. </ModalBody>
  97. <ModalFooter>
  98. <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={reset}>
  99. {t('crop_image_modal.reset')}
  100. </button>
  101. <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={onModalClose}>
  102. {t('crop_image_modal.cancel')}
  103. </button>
  104. <button type="button" className="btn btn-outline-primary rounded-pill" onClick={crop}>
  105. {t('crop_image_modal.crop')}
  106. </button>
  107. </ModalFooter>
  108. </Modal>
  109. );
  110. };
  111. export default ImageCropModal;