image.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. /* eslint-disable jsx-a11y/click-events-have-key-events */
  2. /* eslint-disable jsx-a11y/no-static-element-interactions */
  3. import React from "react";
  4. import BaseComponent from "../_base/baseComponent";
  5. import { ImageProps, ImageStates } from "./interface";
  6. import PropTypes from "prop-types";
  7. import { cssClasses } from "@douyinfe/semi-foundation/image/constants";
  8. import cls from "classnames";
  9. import { IconUploadError, IconEyeOpened } from "@douyinfe/semi-icons";
  10. import PreviewInner from "./previewInner";
  11. import { PreviewContext, PreviewContextProps } from "./previewContext";
  12. import ImageFoundation, { ImageAdapter } from "@douyinfe/semi-foundation/image/imageFoundation";
  13. import LocaleConsumer from "../locale/localeConsumer";
  14. import { Locale } from "../locale/interface";
  15. import { isBoolean, isObject, isUndefined } from "lodash";
  16. import Skeleton from "../skeleton";
  17. import "@douyinfe/semi-foundation/image/image.scss";
  18. const prefixCls = cssClasses.PREFIX;
  19. export default class Image extends BaseComponent<ImageProps, ImageStates> {
  20. static isSemiImage = true;
  21. static contextType = PreviewContext;
  22. static propTypes = {
  23. style: PropTypes.object,
  24. className: PropTypes.string,
  25. src: PropTypes.string,
  26. width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  27. height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  28. alt: PropTypes.string,
  29. placeholder: PropTypes.node,
  30. fallback: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  31. preview: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
  32. onLoad: PropTypes.func,
  33. onError: PropTypes.func,
  34. crossOrigin: PropTypes.string,
  35. imageID: PropTypes.number,
  36. }
  37. static defaultProps = {
  38. preview: true,
  39. };
  40. get adapter(): ImageAdapter<ImageProps, ImageStates> {
  41. return {
  42. ...super.adapter,
  43. getIsInGroup: () => this.isInGroup(),
  44. };
  45. }
  46. context: PreviewContextProps;
  47. foundation: ImageFoundation;
  48. imgRef: React.RefObject<HTMLImageElement>;
  49. constructor(props: ImageProps) {
  50. super(props);
  51. this.state = {
  52. src: "",
  53. loadStatus: "loading",
  54. previewVisible: false,
  55. };
  56. this.foundation = new ImageFoundation(this.adapter);
  57. this.imgRef = React.createRef<HTMLImageElement>();
  58. }
  59. static getDerivedStateFromProps(props: ImageProps, state: ImageStates) {
  60. const willUpdateStates: Partial<ImageStates> = {};
  61. if (props.src !== state.src) {
  62. willUpdateStates.src = props.src;
  63. willUpdateStates.loadStatus = "loading";
  64. }
  65. if (isObject(props.preview)) {
  66. const { visible } = props.preview;
  67. if (isBoolean(visible)) {
  68. willUpdateStates.previewVisible = visible;
  69. }
  70. }
  71. return willUpdateStates;
  72. }
  73. componentDidMount() {
  74. this.observeImage();
  75. }
  76. componentDidUpdate(prevProps: ImageProps, prevState: ImageStates) {
  77. prevProps.src !== this.props.src && this.observeImage();
  78. }
  79. observeImage() {
  80. if (!this.isLazyLoad()) {
  81. return;
  82. }
  83. const { previewObserver } = this.context;
  84. previewObserver.observe(this.imgRef.current);
  85. }
  86. isInGroup() {
  87. return Boolean(this.context && this.context.isGroup);
  88. }
  89. isLazyLoad() {
  90. if (this.context) {
  91. return this.context.lazyLoad;
  92. }
  93. return false;
  94. }
  95. handleClick = (e) => {
  96. this.foundation.handleClick(e);
  97. };
  98. handleLoaded = (e) => {
  99. this.foundation.handleLoaded(e);
  100. }
  101. handleError = (e) => {
  102. this.foundation.handleError(e);
  103. }
  104. handlePreviewVisibleChange = (visible: boolean) => {
  105. this.foundation.handlePreviewVisibleChange(visible);
  106. }
  107. renderDefaultLoading = () => {
  108. const { width, height } = this.props;
  109. return (
  110. <Skeleton.Image style={{ width, height }} />
  111. );
  112. };
  113. renderDefaultError = () => {
  114. const prefixClsName = `${prefixCls}-status`;
  115. return (
  116. <div className={prefixClsName}>
  117. <IconUploadError size={"extra-large"} />
  118. </div>
  119. );
  120. };
  121. renderLoad = () => {
  122. const prefixClsName = `${prefixCls}-status`;
  123. const { placeholder } = this.props;
  124. return (
  125. placeholder ? (
  126. <div className={prefixClsName}>
  127. {placeholder}
  128. </div>
  129. ) : this.renderDefaultLoading()
  130. );
  131. }
  132. renderError = () => {
  133. const { fallback } = this.props;
  134. const prefixClsName = `${prefixCls}-status`;
  135. const fallbackNode = typeof fallback === "string" ? (<img style={{ width: "100%", height: "100%" }}src={fallback} alt="fallback"/>) : fallback;
  136. return (
  137. fallback ? (
  138. <div className={prefixClsName}>
  139. {fallbackNode}
  140. </div>
  141. ) :this.renderDefaultError()
  142. );
  143. }
  144. renderExtra = () => {
  145. const { loadStatus } = this.state;
  146. return (
  147. <div className={`${prefixCls}-overlay`}>
  148. {loadStatus === "error" && this.renderError()}
  149. {loadStatus === "loading" && this.renderLoad()}
  150. </div>
  151. );
  152. }
  153. getLocalTextByKey = (key: string) => (
  154. <LocaleConsumer<Locale["Image"]> componentName="Image" >
  155. {(locale: Locale["Image"]) => locale[key]}
  156. </LocaleConsumer>
  157. );
  158. renderMask = () => (<div className={`${prefixCls}-mask`}>
  159. <div className={`${prefixCls}-mask-info`}>
  160. <IconEyeOpened size="extra-large"/>
  161. <span className={`${prefixCls}-mask-info-text`}>{this.getLocalTextByKey("preview")}</span>
  162. </div>
  163. </div>);
  164. render() {
  165. const { src, loadStatus, previewVisible } = this.state;
  166. const { src: picSrc, width, height, alt, style, className, crossOrigin, preview, fallback, placeholder, imageID, ...restProps } = this.props;
  167. const outerStyle = Object.assign({ width, height }, style);
  168. const outerCls = cls(prefixCls, className);
  169. const canPreview = loadStatus === "success" && preview && !this.isInGroup();
  170. const showPreviewCursor = preview && loadStatus === "success";
  171. const previewSrc = isObject(preview) ? ((preview as any).src ?? src) : src;
  172. const previewProps = isObject(preview) ? preview : {};
  173. return (
  174. // eslint-disable jsx-a11y/no-static-element-interactions
  175. // eslint-disable jsx-a11y/click-events-have-key-events
  176. <div
  177. style={outerStyle}
  178. className={outerCls}
  179. onClick={this.handleClick}
  180. >
  181. <img
  182. ref={this.imgRef}
  183. {...restProps}
  184. src={this.isInGroup() && this.isLazyLoad() ? undefined : src}
  185. data-src={src}
  186. alt={alt}
  187. className={cls(`${prefixCls}-img`, {
  188. [`${prefixCls}-img-preview`]: showPreviewCursor,
  189. [`${prefixCls}-img-error`]: loadStatus === "error",
  190. })}
  191. width={width}
  192. height={height}
  193. crossOrigin={crossOrigin}
  194. onError={this.handleError}
  195. onLoad={this.handleLoaded}
  196. />
  197. {loadStatus !== "success" && this.renderExtra()}
  198. {canPreview &&
  199. <PreviewInner
  200. {...previewProps}
  201. src={previewSrc}
  202. visible={previewVisible}
  203. onVisibleChange={this.handlePreviewVisibleChange}
  204. crossOrigin={!isUndefined(crossOrigin) ? crossOrigin : previewProps?.crossOrigin}
  205. />
  206. }
  207. </div>
  208. );
  209. }
  210. }