previewInner.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. /* eslint-disable jsx-a11y/no-static-element-interactions */
  2. import React, { CSSProperties } from "react";
  3. import BaseComponent from "../_base/baseComponent";
  4. import { PreviewProps as PreviewInnerProps, PreviewInnerStates, RatioType } from "./interface";
  5. import PropTypes from "prop-types";
  6. import { cssClasses } from "@douyinfe/semi-foundation/image/constants";
  7. import cls from "classnames";
  8. import { isEqual, isFunction } from "lodash";
  9. import Portal from "../_portal";
  10. import { IconArrowLeft, IconArrowRight } from "@douyinfe/semi-icons";
  11. import Header from "./previewHeader";
  12. import Footer from "./previewFooter";
  13. import PreviewImage from "./previewImage";
  14. import PreviewInnerFoundation, { PreviewInnerAdapter } from "@douyinfe/semi-foundation/image/previewInnerFoundation";
  15. import { PreviewContext, PreviewContextProps } from "./previewContext";
  16. const prefixCls = cssClasses.PREFIX;
  17. let startMouseDown = { x: 0, y: 0 };
  18. let mouseActiveTime: number = null;
  19. let stopTiming = false;
  20. let timer = null;
  21. // let bodyOverflowValue = document.body.style.overflow;
  22. export default class PreviewInner extends BaseComponent<PreviewInnerProps, PreviewInnerStates> {
  23. static contextType = PreviewContext;
  24. static propTypes = {
  25. style: PropTypes.object,
  26. className: PropTypes.string,
  27. visible: PropTypes.bool,
  28. src: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
  29. currentIndex: PropTypes.number,
  30. defaultIndex: PropTypes.number,
  31. defaultVisible: PropTypes.bool,
  32. maskClosable: PropTypes.bool,
  33. closable: PropTypes.bool,
  34. zoomStep: PropTypes.number,
  35. infinite: PropTypes.bool,
  36. showTooltip: PropTypes.bool,
  37. closeOnEsc: PropTypes.bool,
  38. prevTip: PropTypes.string,
  39. nextTip: PropTypes.string,
  40. zoomInTip:PropTypes.string,
  41. zoomOutTip: PropTypes.string,
  42. downloadTip: PropTypes.string,
  43. adaptiveTip:PropTypes.string,
  44. originTip: PropTypes.string,
  45. lazyLoad: PropTypes.bool,
  46. preLoad: PropTypes.bool,
  47. preLoadGap: PropTypes.number,
  48. disableDownload: PropTypes.bool,
  49. viewerVisibleDelay: PropTypes.number,
  50. zIndex: PropTypes.number,
  51. renderHeader: PropTypes.func,
  52. renderPreviewMenu: PropTypes.func,
  53. getPopupContainer: PropTypes.func,
  54. onVisibleChange: PropTypes.func,
  55. onChange: PropTypes.func,
  56. onClose: PropTypes.func,
  57. onZoomIn: PropTypes.func,
  58. onZoomOut: PropTypes.func,
  59. onPrev: PropTypes.func,
  60. onNext: PropTypes.func,
  61. onDownload: PropTypes.func,
  62. onRatioChange: PropTypes.func,
  63. onRotateChange: PropTypes.func,
  64. }
  65. static defaultProps = {
  66. showTooltip: false,
  67. zoomStep: 0.1,
  68. infinite: false,
  69. closeOnEsc: true,
  70. lazyLoad: false,
  71. preLoad: true,
  72. preLoadGap: 2,
  73. zIndex: 1000,
  74. maskClosable: true,
  75. viewerVisibleDelay: 10000,
  76. };
  77. get adapter(): PreviewInnerAdapter<PreviewInnerProps, PreviewInnerStates> {
  78. return {
  79. ...super.adapter,
  80. getIsInGroup: () => this.isInGroup(),
  81. notifyChange: (index: number) => {
  82. const { onChange } = this.props;
  83. isFunction(onChange) && onChange(index);
  84. },
  85. notifyZoom: (zoom: number, increase: boolean) => {
  86. const { onZoomIn, onZoomOut } = this.props;
  87. if (increase) {
  88. isFunction(onZoomIn) && onZoomIn(zoom);
  89. } else {
  90. isFunction(onZoomOut) && onZoomOut(zoom);
  91. }
  92. },
  93. notifyClose: () => {
  94. const { onClose } = this.props;
  95. isFunction(onClose) && onClose();
  96. },
  97. notifyVisibleChange: (visible: boolean) => {
  98. const { onVisibleChange } = this.props;
  99. isFunction(onVisibleChange) && onVisibleChange(visible);
  100. },
  101. notifyRatioChange: (type: string) => {
  102. const { onRatioChange } = this.props;
  103. isFunction(onRatioChange) && onRatioChange(type);
  104. },
  105. notifyRotateChange: (angle: number) => {
  106. const { onRotateChange } = this.props;
  107. isFunction(onRotateChange) && onRotateChange(angle);
  108. },
  109. notifyDownload: (src: string, index: number) => {
  110. const { onDownload } = this.props;
  111. isFunction(onDownload) && onDownload(src, index);
  112. },
  113. registerKeyDownListener: () => {
  114. window && window.addEventListener("keydown", this.handleKeyDown);
  115. },
  116. unregisterKeyDownListener: () => {
  117. window && window.removeEventListener("keydown", this.handleKeyDown);
  118. },
  119. getMouseActiveTime: () => {
  120. return mouseActiveTime;
  121. },
  122. getStopTiming: () => {
  123. return stopTiming;
  124. },
  125. setStopTiming: (value) => {
  126. stopTiming = value;
  127. },
  128. getStartMouseDown: () => {
  129. return startMouseDown;
  130. },
  131. setStartMouseDown: (x: number, y: number) => {
  132. startMouseDown = { x, y };
  133. },
  134. setMouseActiveTime: (time: number) => {
  135. mouseActiveTime = time;
  136. },
  137. };
  138. }
  139. timer;
  140. context: PreviewContextProps;
  141. foundation: PreviewInnerFoundation;
  142. constructor(props: PreviewInnerProps) {
  143. super(props);
  144. this.state = {
  145. imgSrc: [],
  146. imgLoadStatus: new Map(),
  147. zoom: 0.1,
  148. currentIndex: 0,
  149. ratio: "adaptation",
  150. rotation: 0,
  151. viewerVisible: true,
  152. visible: false,
  153. preloadAfterVisibleChange: true,
  154. direction: "",
  155. };
  156. this.foundation = new PreviewInnerFoundation(this.adapter);
  157. }
  158. static getDerivedStateFromProps(props: PreviewInnerProps, state: PreviewInnerStates) {
  159. const willUpdateStates: Partial<PreviewInnerStates> = {};
  160. let src = [];
  161. if (props.visible) {
  162. // if src in props
  163. src = Array.isArray(props.src) ? props.src : [props.src];
  164. }
  165. if (!isEqual(src, state.imgSrc)) {
  166. willUpdateStates.imgSrc = src;
  167. }
  168. if (props.visible !== state.visible) {
  169. willUpdateStates.visible = props.visible;
  170. if (props.visible) {
  171. willUpdateStates.preloadAfterVisibleChange = true;
  172. }
  173. }
  174. if ("currentIndex" in props && props.currentIndex !== state.currentIndex) {
  175. willUpdateStates.currentIndex = props.currentIndex;
  176. }
  177. return willUpdateStates;
  178. }
  179. componentDidUpdate(prevProps: PreviewInnerProps, prevState: PreviewInnerStates) {
  180. if (prevState.visible !== this.props.visible && this.props.visible) {
  181. mouseActiveTime = new Date().getTime();
  182. timer && clearInterval(timer);
  183. timer = setInterval(this.viewVisibleChange, 1000);
  184. }
  185. // hide => show
  186. if (!prevProps.visible && this.props.visible) {
  187. this.foundation.beforeShow();
  188. }
  189. // show => hide
  190. if (prevProps.visible && !this.props.visible) {
  191. this.foundation.afterHide();
  192. }
  193. }
  194. componentWillUnmount() {
  195. timer && clearInterval(timer);
  196. }
  197. isInGroup() {
  198. return Boolean(this.context && this.context.isGroup);
  199. }
  200. viewVisibleChange = () => {
  201. this.foundation.handleViewVisibleChange();
  202. }
  203. handleSwitchImage = (direction: string) => {
  204. this.foundation.handleSwitchImage(direction);
  205. }
  206. handleDownload = () => {
  207. this.foundation.handleDownload();
  208. }
  209. handlePreviewClose = () => {
  210. this.foundation.handlePreviewClose();
  211. }
  212. handleAdjustRatio = (type: string) => {
  213. this.foundation.handleAdjustRatio(type);
  214. }
  215. handleRotateImage = (direction) => {
  216. this.foundation.handleRotateImage(direction);
  217. }
  218. handleZoomImage = (newZoom: number) => {
  219. this.foundation.handleZoomImage(newZoom);
  220. }
  221. handleMouseUp = (e): void => {
  222. this.foundation.handleMouseUp(e);
  223. }
  224. handleMouseMove = (e): void => {
  225. this.foundation.handleMouseMove(e);
  226. }
  227. handleMouseEvent = (e, event: string) => {
  228. this.foundation.handleMouseMoveEvent(e, event);
  229. }
  230. handleKeyDown = (e: KeyboardEvent) => {
  231. this.foundation.handleKeyDown(e);
  232. };
  233. onImageError = () => {
  234. this.foundation.preloadSingleImage();
  235. }
  236. onImageLoad = (src) => {
  237. this.foundation.onImageLoad(src);
  238. }
  239. handleMouseDown = (e): void => {
  240. this.foundation.handleMouseDown(e);
  241. }
  242. handleRatio = (type: RatioType): void => {
  243. this.foundation.handleRatio(type);
  244. }
  245. render() {
  246. const {
  247. getPopupContainer,
  248. zIndex,
  249. visible,
  250. className,
  251. style,
  252. infinite,
  253. zoomStep,
  254. prevTip,
  255. nextTip,
  256. zoomInTip,
  257. zoomOutTip,
  258. rotateTip,
  259. downloadTip,
  260. adaptiveTip,
  261. originTip,
  262. showTooltip,
  263. disableDownload,
  264. renderPreviewMenu,
  265. renderHeader,
  266. } = this.props;
  267. const { currentIndex, imgSrc, zoom, ratio, rotation, viewerVisible } = this.state;
  268. let wrapperStyle: {
  269. zIndex?: CSSProperties["zIndex"];
  270. position?: CSSProperties["position"];
  271. } = {
  272. zIndex,
  273. };
  274. if (getPopupContainer) {
  275. wrapperStyle = {
  276. zIndex,
  277. position: "static",
  278. };
  279. }
  280. const previewPrefixCls = `${prefixCls}-preview`;
  281. const previewWrapperCls = cls(previewPrefixCls,
  282. {
  283. [`${prefixCls}-hide`]: !visible,
  284. [`${previewPrefixCls}-popup`]: getPopupContainer,
  285. },
  286. className,
  287. );
  288. const hideViewerCls = !viewerVisible ? `${previewPrefixCls}-hide` : "";
  289. const total = imgSrc.length;
  290. const showPrev = total !== 1 && (infinite || currentIndex !== 0);
  291. const showNext = total !== 1 && (infinite || currentIndex !== total - 1);
  292. return (
  293. <Portal
  294. getPopupContainer={getPopupContainer}
  295. style={wrapperStyle}
  296. >
  297. {visible &&
  298. // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events,jsx-a11y/no-static-element-interactions
  299. <div
  300. className={previewWrapperCls}
  301. style={style}
  302. onMouseDown={this.handleMouseDown}
  303. onMouseUp={this.handleMouseUp}
  304. onMouseMove={this.handleMouseMove}
  305. onMouseOver={(e): void => this.handleMouseEvent(e, "over")}
  306. onMouseOut={(e): void => this.handleMouseEvent(e, "out")}
  307. >
  308. <Header className={cls(hideViewerCls)} onClose={this.handlePreviewClose} renderHeader={renderHeader}/>
  309. <PreviewImage
  310. src={imgSrc[currentIndex]}
  311. onZoom={this.handleZoomImage}
  312. disableDownload={disableDownload}
  313. setRatio={this.handleRatio}
  314. zoom={zoom}
  315. ratio={ratio}
  316. zoomStep={zoomStep}
  317. rotation={rotation}
  318. onError={this.onImageError}
  319. onLoad={this.onImageLoad}
  320. />
  321. {showPrev && (
  322. // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
  323. <div
  324. className={cls(`${previewPrefixCls}-icon`, `${previewPrefixCls}-prev`, hideViewerCls)}
  325. onClick={(): void => this.handleSwitchImage("prev")}
  326. >
  327. <IconArrowLeft size="large" />
  328. </div>
  329. )}
  330. {showNext && (
  331. // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
  332. <div
  333. className={cls(`${previewPrefixCls}-icon`, `${previewPrefixCls}-next`, hideViewerCls)}
  334. onClick={(): void => this.handleSwitchImage("next")}
  335. >
  336. <IconArrowRight size="large" />
  337. </div>
  338. )}
  339. <Footer
  340. className={hideViewerCls}
  341. totalNum={total}
  342. curPage={currentIndex + 1}
  343. disabledPrev={!showPrev}
  344. disabledNext={!showNext}
  345. zoom={zoom * 100}
  346. step={zoomStep * 100}
  347. showTooltip={showTooltip}
  348. ratio={ratio}
  349. prevTip={prevTip}
  350. nextTip={nextTip}
  351. zoomInTip={zoomInTip}
  352. zoomOutTip={zoomOutTip}
  353. rotateTip={rotateTip}
  354. downloadTip={downloadTip}
  355. disableDownload={disableDownload}
  356. adaptiveTip={adaptiveTip}
  357. originTip={originTip}
  358. onPrev={(): void => this.handleSwitchImage("prev")}
  359. onNext={(): void => this.handleSwitchImage("next")}
  360. onZoomIn={this.handleZoomImage}
  361. onZoomOut={this.handleZoomImage}
  362. onDownload={this.handleDownload}
  363. onRotate={this.handleRotateImage}
  364. onAdjustRatio={this.handleAdjustRatio}
  365. renderPreviewMenu={renderPreviewMenu}
  366. />
  367. </div>}
  368. </Portal>
  369. );
  370. }
  371. }