You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

143 lines
3.4 KiB

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import classNames from 'classnames';
  4. export default class ImageLoader extends React.PureComponent {
  5. static propTypes = {
  6. alt: PropTypes.string,
  7. src: PropTypes.string.isRequired,
  8. previewSrc: PropTypes.string.isRequired,
  9. width: PropTypes.number.isRequired,
  10. height: PropTypes.number.isRequired,
  11. }
  12. static defaultProps = {
  13. alt: '',
  14. };
  15. state = {
  16. loading: true,
  17. error: false,
  18. }
  19. removers = [];
  20. get canvasContext() {
  21. if (!this.canvas) {
  22. return null;
  23. }
  24. this._canvasContext = this._canvasContext || this.canvas.getContext('2d');
  25. return this._canvasContext;
  26. }
  27. componentDidMount () {
  28. this.loadImage(this.props);
  29. }
  30. componentWillReceiveProps (nextProps) {
  31. if (this.props.src !== nextProps.src) {
  32. this.loadImage(nextProps);
  33. }
  34. }
  35. loadImage (props) {
  36. this.removeEventListeners();
  37. this.setState({ loading: true, error: false });
  38. Promise.all([
  39. this.loadPreviewCanvas(props),
  40. this.loadOriginalImage(props),
  41. ])
  42. .then(() => {
  43. this.setState({ loading: false, error: false });
  44. this.clearPreviewCanvas();
  45. })
  46. .catch(() => this.setState({ loading: false, error: true }));
  47. }
  48. loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => {
  49. const image = new Image();
  50. const removeEventListeners = () => {
  51. image.removeEventListener('error', handleError);
  52. image.removeEventListener('load', handleLoad);
  53. };
  54. const handleError = () => {
  55. removeEventListeners();
  56. reject();
  57. };
  58. const handleLoad = () => {
  59. removeEventListeners();
  60. this.canvasContext.drawImage(image, 0, 0, width, height);
  61. resolve();
  62. };
  63. image.addEventListener('error', handleError);
  64. image.addEventListener('load', handleLoad);
  65. image.src = previewSrc;
  66. this.removers.push(removeEventListeners);
  67. })
  68. clearPreviewCanvas () {
  69. const { width, height } = this.canvas;
  70. this.canvasContext.clearRect(0, 0, width, height);
  71. }
  72. loadOriginalImage = ({ src }) => new Promise((resolve, reject) => {
  73. const image = new Image();
  74. const removeEventListeners = () => {
  75. image.removeEventListener('error', handleError);
  76. image.removeEventListener('load', handleLoad);
  77. };
  78. const handleError = () => {
  79. removeEventListeners();
  80. reject();
  81. };
  82. const handleLoad = () => {
  83. removeEventListeners();
  84. resolve();
  85. };
  86. image.addEventListener('error', handleError);
  87. image.addEventListener('load', handleLoad);
  88. image.src = src;
  89. this.removers.push(removeEventListeners);
  90. });
  91. removeEventListeners () {
  92. this.removers.forEach(listeners => listeners());
  93. this.removers = [];
  94. }
  95. setCanvasRef = c => {
  96. this.canvas = c;
  97. }
  98. render () {
  99. const { alt, src, width, height } = this.props;
  100. const { loading } = this.state;
  101. const className = classNames('image-loader', {
  102. 'image-loader--loading': loading,
  103. });
  104. return (
  105. <div className={className}>
  106. <canvas
  107. className='image-loader__preview-canvas'
  108. width={width}
  109. height={height}
  110. ref={this.setCanvasRef}
  111. />
  112. {!loading && (
  113. <img
  114. alt={alt}
  115. className='image-loader__img'
  116. src={src}
  117. width={width}
  118. height={height}
  119. />
  120. )}
  121. </div>
  122. );
  123. }
  124. }