preview.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import React, { ReactNode, isValidElement } from "react";
  2. import { PreviewContext } from "./previewContext";
  3. import BaseComponent from "../_base/baseComponent";
  4. import PropTypes, { array } from "prop-types";
  5. import { PreviewProps, PreviewState } from "./interface";
  6. import PreviewInner from "./previewInner";
  7. import PreviewFoundation from "@douyinfe/semi-foundation/image/previewFoundation";
  8. import { getUuidShort } from "@douyinfe/semi-foundation/utils/uuid";
  9. import { cssClasses } from "@douyinfe/semi-foundation/image/constants";
  10. import { isObject, isEqual } from "lodash";
  11. import "@douyinfe/semi-foundation/image/image.scss";
  12. import cls from "classnames";
  13. import { omit } from "lodash";
  14. const prefixCls = cssClasses.PREFIX;
  15. export default class Preview extends BaseComponent<PreviewProps, PreviewState> {
  16. static propTypes = {
  17. style: PropTypes.object,
  18. className: PropTypes.string,
  19. visible: PropTypes.bool,
  20. src: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
  21. currentIndex: PropTypes.number,
  22. defaultCurrentIndex: PropTypes.number,
  23. defaultVisible: PropTypes.bool,
  24. maskClosable: PropTypes.bool,
  25. closable: PropTypes.bool,
  26. zoomStep: PropTypes.number,
  27. infinite: PropTypes.bool,
  28. showTooltip: PropTypes.bool,
  29. closeOnEsc: PropTypes.bool,
  30. prevTip: PropTypes.string,
  31. nextTip: PropTypes.string,
  32. zoomInTip: PropTypes.string,
  33. zoomOutTip: PropTypes.string,
  34. downloadTip: PropTypes.string,
  35. adaptiveTip: PropTypes.string,
  36. originTip: PropTypes.string,
  37. lazyLoad: PropTypes.bool,
  38. lazyLoadMargin: PropTypes.string,
  39. preLoad: PropTypes.bool,
  40. preLoadGap: PropTypes.number,
  41. previewCls: PropTypes.string,
  42. previewStyle: PropTypes.object,
  43. disableDownload: PropTypes.bool,
  44. zIndex: PropTypes.number,
  45. renderHeader: PropTypes.func,
  46. renderPreviewMenu: PropTypes.func,
  47. getPopupContainer: PropTypes.func,
  48. onVisibleChange: PropTypes.func,
  49. onChange: PropTypes.func,
  50. onClose: PropTypes.func,
  51. onZoomIn: PropTypes.func,
  52. onZoomOut: PropTypes.func,
  53. onPrev: PropTypes.func,
  54. onNext: PropTypes.func,
  55. onDownload: PropTypes.func,
  56. onRotateLeft: PropTypes.func,
  57. onRatioChange: PropTypes.func,
  58. }
  59. static defaultProps = {
  60. src: [],
  61. lazyLoad: true,
  62. lazyLoadMargin: "0px 100px 100px 0px",
  63. closable: true
  64. };
  65. get adapter() {
  66. return {
  67. ...super.adapter,
  68. };
  69. }
  70. foundation: PreviewFoundation;
  71. previewGroupId: string;
  72. previewRef: React.RefObject<PreviewInner>;
  73. previewObserver: IntersectionObserver;
  74. constructor(props) {
  75. super(props);
  76. this.state = {
  77. currentIndex: props.currentIndex || props.defaultCurrentIndex || 0,
  78. visible: props.visible || props.currentDefaultVisible || false,
  79. };
  80. this.foundation = new PreviewFoundation(this.adapter);
  81. this.previewGroupId = getUuidShort({ prefix: "semi-image-preview-group", length: 4 });
  82. this.previewRef = React.createRef<PreviewInner>();
  83. }
  84. componentDidMount() {
  85. this.props.lazyLoad && this.observerImages();
  86. }
  87. componentDidUpdate(prevProps) {
  88. if (this.props.lazyLoad) {
  89. const prevChildrenKeys = React.Children.toArray(prevProps.children).map((child) =>
  90. isValidElement(child) ? child.key : null
  91. );
  92. const currChildrenKeys = React.Children.toArray(this.props.children).map((child) =>
  93. isValidElement(child) ? child.key : null
  94. );
  95. if (!isEqual(prevChildrenKeys, currChildrenKeys)) {
  96. this.observerImages();
  97. }
  98. }
  99. }
  100. observerImages = () => {
  101. if (this.previewObserver) {
  102. // cancel the observation of all elements of the previous observer
  103. this.previewObserver.disconnect();
  104. } else {
  105. this.previewObserver = new IntersectionObserver(entries => {
  106. entries.forEach(item => {
  107. const src = (item.target as any).dataset?.src;
  108. if (item.isIntersecting && src) {
  109. (item.target as any).src = src;
  110. (item.target as any).removeAttribute("data-src");
  111. this.previewObserver.unobserve(item.target);
  112. }
  113. });
  114. },
  115. {
  116. root: document.querySelector(`#${this.previewGroupId}`),
  117. rootMargin: this.props.lazyLoadMargin,
  118. }
  119. );
  120. }
  121. const allImgElement = document.querySelectorAll(`.${prefixCls}-img`);
  122. allImgElement.forEach(item => this.previewObserver.observe(item));
  123. }
  124. static getDerivedStateFromProps(props: PreviewProps, state: PreviewState) {
  125. const willUpdateStates: Partial<PreviewState> = {};
  126. if (("currentIndex" in props) && (props.currentIndex !== state.currentIndex)) {
  127. willUpdateStates.currentIndex = props.currentIndex;
  128. }
  129. if (("visible" in props) && (props.visible !== state.visible)) {
  130. willUpdateStates.visible = props.visible;
  131. }
  132. return willUpdateStates;
  133. }
  134. componentWillUnmount(): void {
  135. if (this.previewObserver) {
  136. this.previewObserver.disconnect();
  137. this.previewObserver = null;
  138. }
  139. }
  140. handleVisibleChange = (newVisible: boolean) => {
  141. this.foundation.handleVisibleChange(newVisible);
  142. };
  143. handleCurrentIndexChange = (index: number) => {
  144. this.foundation.handleCurrentIndexChange(index);
  145. };
  146. loopImageIndex = () => {
  147. const { children } = this.props;
  148. let index = 0;
  149. const srcListInChildren = [];
  150. const titles: ReactNode [] = [];
  151. const loop = (children) => {
  152. return React.Children.map(children, (child) => {
  153. if (child && child.props && child.type) {
  154. if (child.type.isSemiImage) {
  155. const { src, preview, alt } = child.props;
  156. if (preview) {
  157. const previewSrc = isObject(preview) ? ((preview as any).src ?? src) : src;
  158. srcListInChildren.push(previewSrc);
  159. titles.push(preview?.previewTitle);
  160. return React.cloneElement(child, { imageID: index++ });
  161. }
  162. return child;
  163. }
  164. }
  165. if (child && child.props && child.props.children) {
  166. return React.cloneElement(child, {
  167. children: loop(child.props.children),
  168. });
  169. }
  170. return child;
  171. });
  172. };
  173. return {
  174. srcListInChildren,
  175. newChildren: loop(children),
  176. titles,
  177. };
  178. };
  179. render() {
  180. const { src, className, style, lazyLoad, setDownloadName, ...restProps } = this.props;
  181. const previewInnerProps = {
  182. ...omit(restProps, ['previewCls', 'previewStyle']),
  183. className: restProps?.previewCls,
  184. style: restProps?.previewStyle
  185. };
  186. const { currentIndex, visible } = this.state;
  187. const { srcListInChildren, newChildren, titles } = this.loopImageIndex();
  188. const srcArr = Array.isArray(src) ? src : (typeof src === "string" ? [src] : []);
  189. const finalSrcList = [...srcArr, ...srcListInChildren];
  190. return (
  191. <PreviewContext.Provider
  192. value={{
  193. isGroup: true,
  194. previewSrc: finalSrcList,
  195. titles: titles,
  196. currentIndex,
  197. visible,
  198. lazyLoad,
  199. previewObserver: this.previewObserver,
  200. setCurrentIndex: this.handleCurrentIndexChange,
  201. handleVisibleChange: this.handleVisibleChange,
  202. setDownloadName: setDownloadName,
  203. }}
  204. >
  205. <div id={this.previewGroupId} style={style} className={cls(`${prefixCls}-preview-group`, className)}>
  206. {newChildren}
  207. </div>
  208. <PreviewInner
  209. {...previewInnerProps}
  210. ref={this.previewRef}
  211. src={finalSrcList}
  212. currentIndex={currentIndex}
  213. visible={visible}
  214. onVisibleChange={this.handleVisibleChange}
  215. />
  216. </PreviewContext.Provider>
  217. );
  218. }
  219. }