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.

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