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.

164 lines
4.2 KiB

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