ImageCropModal.jsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import loggerFactory from '@alias/logger';
  4. import canvasToBlob from 'async-canvas-to-blob';
  5. import {
  6. Modal,
  7. ModalHeader,
  8. ModalBody,
  9. ModalFooter,
  10. } from 'reactstrap';
  11. import { withTranslation } from 'react-i18next';
  12. import ReactCrop from 'react-image-crop';
  13. import AppContainer from '../../services/AppContainer';
  14. import { createSubscribedElement } from '../UnstatedUtils';
  15. import 'react-image-crop/dist/ReactCrop.css';
  16. import { toastError } from '../../util/apiNotification';
  17. const logger = loggerFactory('growi:ImageCropModal');
  18. class ImageCropModal extends React.Component {
  19. // demo: https://codesandbox.io/s/72py4jlll6
  20. constructor(props) {
  21. super();
  22. this.state = {
  23. crop: null,
  24. imageRef: null,
  25. };
  26. this.onImageLoaded = this.onImageLoaded.bind(this);
  27. this.onCropChange = this.onCropChange.bind(this);
  28. this.getCroppedImg = this.getCroppedImg.bind(this);
  29. this.crop = this.crop.bind(this);
  30. this.reset = this.reset.bind(this);
  31. this.imageRef = null;
  32. }
  33. onImageLoaded(image) {
  34. this.setState({ imageRef: image }, () => this.reset());
  35. return false; // Return false when setting crop state in here.
  36. }
  37. onCropChange(crop) {
  38. this.setState({ crop });
  39. }
  40. async getCroppedImg(image, crop, fileName) {
  41. const canvas = document.createElement('canvas');
  42. const scaleX = image.naturalWidth / image.width;
  43. const scaleY = image.naturalHeight / image.height;
  44. canvas.width = crop.width;
  45. canvas.height = crop.height;
  46. const ctx = canvas.getContext('2d');
  47. ctx.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, crop.width, crop.height);
  48. try {
  49. const blob = await canvasToBlob(canvas);
  50. return blob;
  51. }
  52. catch (err) {
  53. logger.error(err);
  54. toastError(new Error('Failed to draw image'));
  55. }
  56. }
  57. async crop() {
  58. // crop immages
  59. if (this.state.imageRef && this.state.crop.width && this.state.crop.height) {
  60. const croppedImage = await this.getCroppedImg(this.state.imageRef, this.state.crop, '/images/icons/user');
  61. this.props.onCropCompleted(croppedImage);
  62. }
  63. }
  64. reset() {
  65. const size = Math.min(this.state.imageRef.width, this.state.imageRef.height);
  66. this.setState({
  67. crop: {
  68. aspect: 1,
  69. unit: 'px',
  70. x: this.state.imageRef.width / 2 - size / 2,
  71. y: this.state.imageRef.height / 2 - size / 2,
  72. width: size,
  73. height: size,
  74. },
  75. });
  76. }
  77. render() {
  78. return (
  79. <Modal isOpen={this.props.show} toggle={this.props.onModalClose}>
  80. <ModalHeader tag="h4" toggle={this.props.onModalClose} className="bg-info text-light">
  81. Image Crop
  82. </ModalHeader>
  83. <ModalBody className="my-4">
  84. <ReactCrop circularCrop src={this.props.src} crop={this.state.crop} onImageLoaded={this.onImageLoaded} onChange={this.onCropChange} />
  85. </ModalBody>
  86. <ModalFooter>
  87. <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={this.reset}>
  88. Reset
  89. </button>
  90. <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={this.props.onModalClose}>
  91. Cancel
  92. </button>
  93. <button type="button" className="btn btn-outline-primary rounded-pill" onClick={this.crop}>
  94. Crop
  95. </button>
  96. </ModalFooter>
  97. </Modal>
  98. );
  99. }
  100. }
  101. /**
  102. * Wrapper component for using unstated
  103. */
  104. const ProfileImageFormWrapper = (props) => {
  105. return createSubscribedElement(ImageCropModal, props, [AppContainer]);
  106. };
  107. ImageCropModal.propTypes = {
  108. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  109. show: PropTypes.bool.isRequired,
  110. src: PropTypes.string,
  111. onModalClose: PropTypes.func.isRequired,
  112. onCropCompleted: PropTypes.func.isRequired,
  113. };
  114. export default withTranslation()(ProfileImageFormWrapper);