Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
Size: Mime:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Resizer from 'react-image-file-resizer';

import Button from '../../Button/Button';
import ButtonLabel from '../../Button/ButtonLabel';
import LoadingButton from '../../Button/LoadingButton';
import InputFeedback from '../../Input/InputFeedback';
import Modal from '../../../structure/Modal';

import ImageCropper from './ImageCropper';

import { getCroppedImg, readImageFile, validateFileSizeLimit, validateImageDimensions } from './imageHelper';
import { maxImageDimensions, aspectRatio, errorKeys } from './config';

class ImageUpload extends Component {
  static propTypes = {
    buttonLabel: PropTypes.string.isRequired,
    name: PropTypes.string.isRequired,
    onChange: PropTypes.func,
    isLoading: PropTypes.bool.isRequired,
    imageType: PropTypes.string,
    note: PropTypes.string,
    buttonCancelText: PropTypes.string.isRequired,
    buttonApplyText: PropTypes.string.isRequired,
    onError: PropTypes.func,
  };

  static defaultProps = {
    onChange: () => {},
    note: '',
    imageType: '',
    onError: () => {},
  };

  constructor(props) {
    super(props);

    this.state = {
      imageSrc: '',
      fileType: '',
    };

    this.handleImageUploadButtonClick = this.handleImageUploadButtonClick.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.setInputRef = this.setInputRef.bind(this);
    this.handleSaveCroppedImage = this.handleSaveCroppedImage.bind(this);
    this.compressFile = this.compressFile.bind(this);
    this.handleCancelUploadFile = this.handleCancelUploadFile.bind(this);
    this.handleChangeFile = this.handleChangeFile.bind(this);
    this.resetState = this.resetState.bind(this);
    this.handleCropper = this.handleCropper.bind(this);
  }

  setInputRef(node) {
    this.inputFile = node;
  }

  /**
   * Validates file size does not exceed limit in kilobytes
   * Call onChange with file if no imageType prop has been provided, else handle the image cropper
   */
  handleChange(event) {
    const { target: { files } } = event;
    const { imageType } = this.props;

    if (files && files.length > 0) {
      const file = files[0];
      const isBelowFileSize = validateFileSizeLimit(file.size, 1000);

      if (isBelowFileSize) {
        if (!imageType) this.props.onChange(file);
        this.handleCropper(file);
      } else {
        this.props.onError({ errorKey: errorKeys.fileSizeError });
      }

      if (this.inputFile) {
        this.inputFile.value = '';
      }
    }
  }

  /**
   * Reads the file to obtain the image source required for the ImageCropper
   * imageSrc state triggers the modal with cropper
   * @param {File} file - the image file
   */
  async handleCropper(file) {
    try {
      const { imageSrc } = await readImageFile(file);
      this.setState({ imageSrc, fileType: file.type });
    } catch (error) {
      this.props.onError({ errorKey: errorKeys.readImageError });
    }
  }

  /**
   * Returns a new blob of the cropped image from getCroppedImg helper.
   * A second file size check is performed as in some edge cases the produced cropped image can have a slightly higher size than the original
   * (typically this only happens if the file is near 1MB already and the zoom range is below 1)
   * If the image is too large (dimensions and size) use react-image-resizer to resize and compress the image
   * @param {object} croppedAreaPixels - used in the helper to draw the cropped image on a new canvas
   */

  async handleSaveCroppedImage(croppedAreaPixels) {
    try {
      const { imageSrc, fileType } = this.state;
      const { imageType } = this.props;
      const croppedImg = await getCroppedImg(imageSrc, croppedAreaPixels, fileType);
      const isBelowDimensionsLimit = validateImageDimensions(croppedImg, imageType);
      const isBelowFileSize = validateFileSizeLimit(croppedImg.size, 1000);
      if (isBelowDimensionsLimit && isBelowFileSize) {
        this.handleChangeFile(croppedImg);
      } else {
        this.compressFile(croppedImg);
      }
    } catch (error) {
      this.props.onError({ errorKey: errorKeys.cropImageError });
    }
  }

  /**
   * Calls react-image-resizer to resize and compress the image
   * handleChangeFile callback is called with the new image URI as params
   * @param {File} file - the new cropped image blob returned
   */

  compressFile(file) {
    const { imageType } = this.props;
    const { fileType } = this.state;
    const { maxWidth, maxHeight } = maxImageDimensions[imageType];
    Resizer.imageFileResizer(
      file, // e.g event.target.files[0]
      maxWidth, // is the maxWidth of the  new image
      maxHeight, // is the maxHeight of the  new image
      fileType.match(/[^/]+$/g), // is the compressFormat of the  new image
      70, // is the quality of the  new image - A number between 0 and 100. Used for the JPEG compression.(if no compress is needed, just set it to 100)
      0, // is the rotatoion of the  new image
      async uri => this.handleChangeFile(uri), // is the callBack function of the new image URI
      'blob' // is the output type of the new image
    );
  }

  resetState() {
    this.setState({ imageSrc: '', fileType: '' });
  }

  handleImageUploadButtonClick() {
    this.inputFile.click();
  }

  handleCancelUploadFile() {
    this.resetState();
  }

  handleChangeFile(file) {
    this.resetState();
    this.props.onChange(file);
  }

  render() {
    const { buttonLabel, name, isLoading, imageType, note, buttonCancelText, buttonApplyText } = this.props;
    const { imageSrc } = this.state;

    return (
      <React.Fragment>
        <Modal show={Boolean(imageSrc)} title="Modal for image cropping tool" variant="ink">
          <ImageCropper
            imageSrc={imageSrc}
            type={imageType}
            onSaveCroppedImage={this.handleSaveCroppedImage}
            onCancelCroppedImage={this.handleCancelUploadFile}
            buttonCancelText={buttonCancelText}
            buttonApplyText={buttonApplyText}
            aspect={aspectRatio[imageType]}
          />
        </Modal>
        <div className="ImageUpload">
          <input
            className="ImageUpload-input"
            name={name}
            type="file"
            accept="image/jpeg,image/png,image/gif"
            onChange={this.handleChange}
            ref={this.setInputRef}
          />
          <Button
            variant="whiteWithBorder"
            dimension="compact"
            className="ImageUpload-uploadButton"
            type="button"
            onClick={this.handleImageUploadButtonClick}
            disabled={isLoading}
            inputButtonClass={isLoading ? 'loading' : null}
          >
            <LoadingButton />
            <ButtonLabel>{buttonLabel}</ButtonLabel>
          </Button>
          {note && (
            <div className="ImageUpload-note Input--dark">
              <InputFeedback>{note}</InputFeedback>
            </div>
          )}
        </div>
      </React.Fragment>
    );
  }
}

export default ImageUpload;