previewInner.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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. onRotateLeft: 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. private bodyOverflow: string;
  78. private scrollBarWidth: number;
  79. private originBodyWidth: string;
  80. get adapter(): PreviewInnerAdapter<PreviewInnerProps, PreviewInnerStates> {
  81. return {
  82. ...super.adapter,
  83. getIsInGroup: () => this.isInGroup(),
  84. disabledBodyScroll: () => {
  85. const { getPopupContainer } = this.props;
  86. this.bodyOverflow = document.body.style.overflow || '';
  87. if (!getPopupContainer && this.bodyOverflow !== 'hidden') {
  88. document.body.style.overflow = 'hidden';
  89. document.body.style.width = `calc(${this.originBodyWidth || '100%'} - ${this.scrollBarWidth}px)`;
  90. }
  91. },
  92. enabledBodyScroll: () => {
  93. const { getPopupContainer } = this.props;
  94. if (!getPopupContainer && this.bodyOverflow !== 'hidden') {
  95. document.body.style.overflow = this.bodyOverflow;
  96. document.body.style.width = this.originBodyWidth;
  97. }
  98. },
  99. notifyChange: (index: number, direction: string) => {
  100. const { onChange, onPrev, onNext } = this.props;
  101. isFunction(onChange) && onChange(index);
  102. if (direction === "prev") {
  103. onPrev && onPrev(index);
  104. } else {
  105. onNext && onNext(index);
  106. }
  107. },
  108. notifyZoom: (zoom: number, increase: boolean) => {
  109. const { onZoomIn, onZoomOut } = this.props;
  110. if (increase) {
  111. isFunction(onZoomIn) && onZoomIn(zoom);
  112. } else {
  113. isFunction(onZoomOut) && onZoomOut(zoom);
  114. }
  115. },
  116. notifyClose: () => {
  117. const { onClose } = this.props;
  118. isFunction(onClose) && onClose();
  119. },
  120. notifyVisibleChange: (visible: boolean) => {
  121. const { onVisibleChange } = this.props;
  122. isFunction(onVisibleChange) && onVisibleChange(visible);
  123. },
  124. notifyRatioChange: (type: string) => {
  125. const { onRatioChange } = this.props;
  126. isFunction(onRatioChange) && onRatioChange(type);
  127. },
  128. notifyRotateChange: (angle: number) => {
  129. const { onRotateLeft } = this.props;
  130. isFunction(onRotateLeft) && onRotateLeft(angle);
  131. },
  132. notifyDownload: (src: string, index: number) => {
  133. const { onDownload } = this.props;
  134. isFunction(onDownload) && onDownload(src, index);
  135. },
  136. registerKeyDownListener: () => {
  137. window && window.addEventListener("keydown", this.handleKeyDown);
  138. },
  139. unregisterKeyDownListener: () => {
  140. window && window.removeEventListener("keydown", this.handleKeyDown);
  141. },
  142. getMouseActiveTime: () => {
  143. return mouseActiveTime;
  144. },
  145. getStopTiming: () => {
  146. return stopTiming;
  147. },
  148. setStopTiming: (value) => {
  149. stopTiming = value;
  150. },
  151. getStartMouseDown: () => {
  152. return startMouseDown;
  153. },
  154. setStartMouseDown: (x: number, y: number) => {
  155. startMouseDown = { x, y };
  156. },
  157. setMouseActiveTime: (time: number) => {
  158. mouseActiveTime = time;
  159. },
  160. };
  161. }
  162. timer;
  163. context: PreviewContextProps;
  164. foundation: PreviewInnerFoundation;
  165. constructor(props: PreviewInnerProps) {
  166. super(props);
  167. this.state = {
  168. imgSrc: [],
  169. imgLoadStatus: new Map(),
  170. zoom: 0.1,
  171. currentIndex: 0,
  172. ratio: "adaptation",
  173. rotation: 0,
  174. viewerVisible: true,
  175. visible: false,
  176. preloadAfterVisibleChange: true,
  177. direction: "",
  178. };
  179. this.foundation = new PreviewInnerFoundation(this.adapter);
  180. this.bodyOverflow = '';
  181. this.originBodyWidth = '100%';
  182. this.scrollBarWidth = 0;
  183. }
  184. static getDerivedStateFromProps(props: PreviewInnerProps, state: PreviewInnerStates) {
  185. const willUpdateStates: Partial<PreviewInnerStates> = {};
  186. let src = [];
  187. if (props.visible) {
  188. // if src in props
  189. src = Array.isArray(props.src) ? props.src : [props.src];
  190. }
  191. if (!isEqual(src, state.imgSrc)) {
  192. willUpdateStates.imgSrc = src;
  193. }
  194. if (props.visible !== state.visible) {
  195. willUpdateStates.visible = props.visible;
  196. if (props.visible) {
  197. willUpdateStates.preloadAfterVisibleChange = true;
  198. }
  199. }
  200. if ("currentIndex" in props && props.currentIndex !== state.currentIndex) {
  201. willUpdateStates.currentIndex = props.currentIndex;
  202. // ratio will set to adaptation when change picture,
  203. // attention: If the ratio is controlled, the ratio should not change as the index changes
  204. willUpdateStates.ratio = 'adaptation';
  205. }
  206. return willUpdateStates;
  207. }
  208. static getScrollbarWidth() {
  209. if (globalThis && Object.prototype.toString.call(globalThis) === '[object Window]') {
  210. return window.innerWidth - document.documentElement.clientWidth;
  211. }
  212. return 0;
  213. }
  214. componentDidMount() {
  215. this.scrollBarWidth = PreviewInner.getScrollbarWidth();
  216. this.originBodyWidth = document.body.style.width;
  217. if (this.props.visible) {
  218. this.foundation.beforeShow();
  219. }
  220. }
  221. componentDidUpdate(prevProps: PreviewInnerProps, prevState: PreviewInnerStates) {
  222. if (prevState.visible !== this.props.visible && this.props.visible) {
  223. mouseActiveTime = new Date().getTime();
  224. timer && clearInterval(timer);
  225. timer = setInterval(this.viewVisibleChange, 1000);
  226. }
  227. // hide => show
  228. if (!prevProps.visible && this.props.visible) {
  229. this.foundation.beforeShow();
  230. }
  231. // show => hide
  232. if (prevProps.visible && !this.props.visible) {
  233. this.foundation.afterHide();
  234. }
  235. }
  236. componentWillUnmount() {
  237. timer && clearInterval(timer);
  238. }
  239. isInGroup() {
  240. return Boolean(this.context && this.context.isGroup);
  241. }
  242. viewVisibleChange = () => {
  243. this.foundation.handleViewVisibleChange();
  244. }
  245. handleSwitchImage = (direction: string) => {
  246. this.foundation.handleSwitchImage(direction);
  247. }
  248. handleDownload = () => {
  249. this.foundation.handleDownload();
  250. }
  251. handlePreviewClose = () => {
  252. this.foundation.handlePreviewClose();
  253. }
  254. handleAdjustRatio = (type: string) => {
  255. this.foundation.handleAdjustRatio(type);
  256. }
  257. handleRotateImage = (direction) => {
  258. this.foundation.handleRotateImage(direction);
  259. }
  260. handleZoomImage = (newZoom: number) => {
  261. this.foundation.handleZoomImage(newZoom);
  262. }
  263. handleMouseUp = (e): void => {
  264. this.foundation.handleMouseUp(e.nativeEvent);
  265. }
  266. handleMouseMove = (e): void => {
  267. this.foundation.handleMouseMove(e);
  268. }
  269. handleMouseEvent = (e, event: string) => {
  270. this.foundation.handleMouseMoveEvent(e, event);
  271. }
  272. handleKeyDown = (e: KeyboardEvent) => {
  273. this.foundation.handleKeyDown(e);
  274. };
  275. onImageError = () => {
  276. this.foundation.preloadSingleImage();
  277. }
  278. onImageLoad = (src) => {
  279. this.foundation.onImageLoad(src);
  280. }
  281. handleMouseDown = (e): void => {
  282. this.foundation.handleMouseDown(e);
  283. }
  284. render() {
  285. const {
  286. getPopupContainer,
  287. zIndex,
  288. visible,
  289. className,
  290. style,
  291. infinite,
  292. zoomStep,
  293. crossOrigin,
  294. prevTip,
  295. nextTip,
  296. zoomInTip,
  297. zoomOutTip,
  298. rotateTip,
  299. downloadTip,
  300. adaptiveTip,
  301. originTip,
  302. showTooltip,
  303. disableDownload,
  304. renderPreviewMenu,
  305. renderHeader,
  306. } = this.props;
  307. const { currentIndex, imgSrc, zoom, ratio, rotation, viewerVisible } = this.state;
  308. let wrapperStyle: {
  309. zIndex?: CSSProperties["zIndex"];
  310. position?: CSSProperties["position"]
  311. } = {
  312. zIndex,
  313. };
  314. if (getPopupContainer) {
  315. wrapperStyle = {
  316. zIndex,
  317. position: "static",
  318. };
  319. }
  320. const previewPrefixCls = `${prefixCls}-preview`;
  321. const previewWrapperCls = cls(previewPrefixCls,
  322. {
  323. [`${prefixCls}-hide`]: !visible,
  324. [`${previewPrefixCls}-popup`]: getPopupContainer,
  325. },
  326. className,
  327. );
  328. const hideViewerCls = !viewerVisible ? `${previewPrefixCls}-hide` : "";
  329. const total = imgSrc.length;
  330. const showPrev = total !== 1 && (infinite || currentIndex !== 0);
  331. const showNext = total !== 1 && (infinite || currentIndex !== total - 1);
  332. return (
  333. <Portal
  334. getPopupContainer={getPopupContainer}
  335. style={wrapperStyle}
  336. >
  337. {visible &&
  338. // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events,jsx-a11y/no-static-element-interactions
  339. <div
  340. className={previewWrapperCls}
  341. style={style}
  342. onMouseDown={this.handleMouseDown}
  343. onMouseUp={this.handleMouseUp}
  344. onMouseMove={this.handleMouseMove}
  345. onMouseOver={(e): void => this.handleMouseEvent(e.nativeEvent, "over")}
  346. onMouseOut={(e): void => this.handleMouseEvent(e.nativeEvent, "out")}
  347. >
  348. <Header className={cls(hideViewerCls)} onClose={this.handlePreviewClose} renderHeader={renderHeader}/>
  349. <PreviewImage
  350. src={imgSrc[currentIndex]}
  351. onZoom={this.handleZoomImage}
  352. disableDownload={disableDownload}
  353. setRatio={this.handleAdjustRatio}
  354. zoom={zoom}
  355. ratio={ratio}
  356. zoomStep={zoomStep}
  357. rotation={rotation}
  358. crossOrigin={crossOrigin}
  359. onError={this.onImageError}
  360. onLoad={this.onImageLoad}
  361. />
  362. {showPrev && (
  363. // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
  364. <div
  365. className={cls(`${previewPrefixCls}-icon`, `${previewPrefixCls}-prev`, hideViewerCls)}
  366. onClick={(): void => this.handleSwitchImage("prev")}
  367. >
  368. <IconArrowLeft size="large" />
  369. </div>
  370. )}
  371. {showNext && (
  372. // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
  373. <div
  374. className={cls(`${previewPrefixCls}-icon`, `${previewPrefixCls}-next`, hideViewerCls)}
  375. onClick={(): void => this.handleSwitchImage("next")}
  376. >
  377. <IconArrowRight size="large" />
  378. </div>
  379. )}
  380. <Footer
  381. className={hideViewerCls}
  382. totalNum={total}
  383. curPage={currentIndex + 1}
  384. disabledPrev={!showPrev}
  385. disabledNext={!showNext}
  386. zoom={zoom * 100}
  387. step={zoomStep * 100}
  388. showTooltip={showTooltip}
  389. ratio={ratio}
  390. prevTip={prevTip}
  391. nextTip={nextTip}
  392. zoomInTip={zoomInTip}
  393. zoomOutTip={zoomOutTip}
  394. rotateTip={rotateTip}
  395. downloadTip={downloadTip}
  396. disableDownload={disableDownload}
  397. adaptiveTip={adaptiveTip}
  398. originTip={originTip}
  399. onPrev={(): void => this.handleSwitchImage("prev")}
  400. onNext={(): void => this.handleSwitchImage("next")}
  401. onZoomIn={this.handleZoomImage}
  402. onZoomOut={this.handleZoomImage}
  403. onDownload={this.handleDownload}
  404. onRotate={this.handleRotateImage}
  405. onAdjustRatio={this.handleAdjustRatio}
  406. renderPreviewMenu={renderPreviewMenu}
  407. />
  408. </div>}
  409. </Portal>
  410. );
  411. }
  412. }