image.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  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. constructor(props: ImageProps) {
  49. super(props);
  50. this.state = {
  51. src: "",
  52. loadStatus: "loading",
  53. previewVisible: false,
  54. };
  55. this.foundation = new ImageFoundation(this.adapter);
  56. }
  57. static getDerivedStateFromProps(props: ImageProps, state: ImageStates) {
  58. const willUpdateStates: Partial<ImageStates> = {};
  59. if (props.src !== state.src) {
  60. willUpdateStates.src = props.src;
  61. willUpdateStates.loadStatus = "loading";
  62. }
  63. if (isObject(props.preview)) {
  64. const { visible } = props.preview;
  65. if (isBoolean(visible)) {
  66. willUpdateStates.previewVisible = visible;
  67. }
  68. }
  69. return willUpdateStates;
  70. }
  71. isInGroup() {
  72. return Boolean(this.context && this.context.isGroup);
  73. }
  74. isLazyLoad() {
  75. if (this.context) {
  76. return this.context.lazyLoad;
  77. }
  78. return false;
  79. }
  80. handleClick = (e) => {
  81. this.foundation.handleClick(e);
  82. };
  83. handleLoaded = (e) => {
  84. this.foundation.handleLoaded(e);
  85. }
  86. handleError = (e) => {
  87. this.foundation.handleError(e);
  88. }
  89. handlePreviewVisibleChange = (visible: boolean) => {
  90. this.foundation.handlePreviewVisibleChange(visible);
  91. }
  92. renderDefaultLoading = () => {
  93. const { width, height } = this.props;
  94. return (
  95. <Skeleton.Image style={{ width, height }} />
  96. );
  97. };
  98. renderDefaultError = () => {
  99. const prefixClsName = `${prefixCls}-status`;
  100. return (
  101. <div className={prefixClsName}>
  102. <IconUploadError size={"extra-large"} />
  103. </div>
  104. );
  105. };
  106. renderLoad = () => {
  107. const prefixClsName = `${prefixCls}-status`;
  108. const { placeholder } = this.props;
  109. return (
  110. placeholder ? (
  111. <div className={prefixClsName}>
  112. {placeholder}
  113. </div>
  114. ) : this.renderDefaultLoading()
  115. );
  116. }
  117. renderError = () => {
  118. const { fallback } = this.props;
  119. const prefixClsName = `${prefixCls}-status`;
  120. const fallbackNode = typeof fallback === "string" ? (<img style={{ width: "100%", height: "100%" }}src={fallback} alt="fallback"/>) : fallback;
  121. return (
  122. fallback ? (
  123. <div className={prefixClsName}>
  124. {fallbackNode}
  125. </div>
  126. ) :this.renderDefaultError()
  127. );
  128. }
  129. renderExtra = () => {
  130. const { loadStatus } = this.state;
  131. return (
  132. <div className={`${prefixCls}-overlay`}>
  133. {loadStatus === "error" && this.renderError()}
  134. {loadStatus === "loading" && this.renderLoad()}
  135. </div>
  136. );
  137. }
  138. getLocalTextByKey = (key: string) => (
  139. <LocaleConsumer<Locale["Image"]> componentName="Image" >
  140. {(locale: Locale["Image"]) => locale[key]}
  141. </LocaleConsumer>
  142. );
  143. renderMask = () => (<div className={`${prefixCls}-mask`}>
  144. <div className={`${prefixCls}-mask-info`}>
  145. <IconEyeOpened size="extra-large"/>
  146. <span className={`${prefixCls}-mask-info-text`}>{this.getLocalTextByKey("preview")}</span>
  147. </div>
  148. </div>)
  149. render() {
  150. const { src, loadStatus, previewVisible } = this.state;
  151. const { src: picSrc, width, height, alt, style, className, crossOrigin, preview, fallback, placeholder, imageID, ...restProps } = this.props;
  152. const outerStyle = Object.assign({ width, height }, style);
  153. const outerCls = cls(prefixCls, className);
  154. const canPreview = loadStatus === "success" && preview && !this.isInGroup();
  155. const showPreviewCursor = preview && loadStatus === "success";
  156. const previewSrc = isObject(preview) ? ((preview as any).src ?? src) : src;
  157. const previewProps = isObject(preview) ? preview : {};
  158. return (
  159. // eslint-disable jsx-a11y/no-static-element-interactions
  160. // eslint-disable jsx-a11y/click-events-have-key-events
  161. <div
  162. style={outerStyle}
  163. className={outerCls}
  164. onClick={this.handleClick}
  165. >
  166. <img
  167. {...restProps}
  168. src={this.isInGroup() && this.isLazyLoad() ? undefined : src}
  169. data-src={src}
  170. alt={alt}
  171. className={cls(`${prefixCls}-img`, {
  172. [`${prefixCls}-img-preview`]: showPreviewCursor,
  173. [`${prefixCls}-img-error`]: loadStatus === "error",
  174. })}
  175. width={width}
  176. height={height}
  177. crossOrigin={crossOrigin}
  178. onError={this.handleError}
  179. onLoad={this.handleLoaded}
  180. />
  181. {loadStatus !== "success" && this.renderExtra()}
  182. {canPreview &&
  183. <PreviewInner
  184. {...previewProps}
  185. src={previewSrc}
  186. visible={previewVisible}
  187. onVisibleChange={this.handlePreviewVisibleChange}
  188. crossOrigin={!isUndefined(crossOrigin) ? crossOrigin : previewProps?.crossOrigin}
  189. />
  190. }
  191. </div>
  192. );
  193. }
  194. }