previewInnerFoundation.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import BaseFoundation, { DefaultAdapter } from "../base/foundation";
  2. import KeyCode from "../utils/keyCode";
  3. import { getPreloadImagArr, downloadImage, isTargetEmit } from "./utils";
  4. export interface PreviewInnerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  5. getIsInGroup: () => boolean;
  6. notifyChange: (index: number, direction: string) => void;
  7. notifyZoom: (zoom: number, increase: boolean) => void;
  8. notifyClose: () => void;
  9. notifyVisibleChange: (visible: boolean) => void;
  10. notifyRatioChange: (type: string) => void;
  11. notifyRotateChange: (angle: number) => void;
  12. notifyDownload: (src: string, index: number) => void;
  13. registerKeyDownListener: () => void;
  14. unregisterKeyDownListener: () => void;
  15. getMouseActiveTime: () => number;
  16. getStopTiming: () => boolean;
  17. setStopTiming: (value: boolean) => void;
  18. getStartMouseDown: () => {x: number; y: number};
  19. setStartMouseDown: (x: number, y: number) => void;
  20. setMouseActiveTime: (time: number) => void;
  21. disabledBodyScroll: () => void;
  22. enabledBodyScroll: () => void
  23. }
  24. const NOT_CLOSE_TARGETS = ["icon", "footer"];
  25. const STOP_CLOSE_TARGET = ["icon", "footer", "header"];
  26. export default class PreviewInnerFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<PreviewInnerAdapter<P, S>, P, S> {
  27. constructor(adapter: PreviewInnerAdapter<P, S>) {
  28. super({ ...adapter });
  29. }
  30. beforeShow() {
  31. this._adapter.registerKeyDownListener();
  32. this._adapter.disabledBodyScroll();
  33. }
  34. afterHide() {
  35. this._adapter.unregisterKeyDownListener();
  36. this._adapter.enabledBodyScroll();
  37. }
  38. handleViewVisibleChange = () => {
  39. const nowTime = new Date().getTime();
  40. const mouseActiveTime = this._adapter.getMouseActiveTime();
  41. const stopTiming = this._adapter.getStopTiming();
  42. const { viewerVisibleDelay } = this.getProps();
  43. const { viewerVisible } = this.getStates();
  44. if (nowTime - mouseActiveTime > viewerVisibleDelay && !stopTiming) {
  45. viewerVisible && this.setState({
  46. viewerVisible: false,
  47. } as any);
  48. }
  49. }
  50. handleMouseMoveEvent = (e: any, event: string) => {
  51. const isTarget = isTargetEmit(e, STOP_CLOSE_TARGET);
  52. if (isTarget && event === "over") {
  53. this._adapter.setStopTiming(true);
  54. } else if (isTarget && event === "out") {
  55. this._adapter.setStopTiming(false);
  56. }
  57. }
  58. handleMouseMove = (e: any) => {
  59. this._adapter.setMouseActiveTime(new Date().getTime());
  60. this.setState({
  61. viewerVisible: true,
  62. } as any);
  63. }
  64. handleMouseUp = (e: any) => {
  65. const { maskClosable } = this.getProps();
  66. let couldClose = !isTargetEmit(e, NOT_CLOSE_TARGETS);
  67. const { clientX, clientY } = e;
  68. const { x, y } = this._adapter.getStartMouseDown();
  69. // 对鼠标移动做容错处理,当 x 和 y 方向在 mouseUp 的时候移动距离都小于等于 5px 时候就可以关闭预览
  70. // Error-tolerant processing of mouse movement, when the movement distance in the x and y directions is less than or equal to 5px in mouseUp, the preview can be closed
  71. // 不做容错处理的话,直接用 clientX !== x || y !== clientY 做判断,鼠标在用户点击时候无意识的轻微移动无法关闭预览,不符合用户预期
  72. // If you do not do fault-tolerant processing, but directly use clientX !== x || y !== clientY to make judgments, the slight movement of the mouse when the user clicks will not be able to close the preview, which does not meet the user's expectations.
  73. if (Math.abs(clientX - x) > 5 || Math.abs(y - clientY) > 5) {
  74. couldClose = false;
  75. }
  76. if (couldClose && maskClosable) {
  77. this.handlePreviewClose();
  78. }
  79. }
  80. handleMouseDown = (e: any) => {
  81. const { clientX, clientY } = e;
  82. this._adapter.setStartMouseDown(clientX, clientY);
  83. }
  84. handleKeyDown = (e: any) => {
  85. const { closeOnEsc } = this.getProps();
  86. if (closeOnEsc && e.keyCode === KeyCode.ESC) {
  87. e.stopPropagation();
  88. this._adapter.notifyVisibleChange(false);
  89. this._adapter.notifyClose();
  90. return;
  91. }
  92. }
  93. handleSwitchImage = (direction: string) => {
  94. const step = direction === "prev" ? -1 : 1;
  95. const { imgSrc, currentIndex: currentIndexInState } = this.getStates();
  96. const srcLength = imgSrc.length;
  97. const newIndex = (currentIndexInState + step + srcLength) % srcLength;
  98. if ("currentIndex" in this.getProps()) {
  99. if (this._adapter.getIsInGroup()) {
  100. const setCurrentIndex = this._adapter.getContext("setCurrentIndex");
  101. setCurrentIndex(newIndex);
  102. }
  103. } else {
  104. this.setState({
  105. currentIndex: newIndex,
  106. } as any);
  107. }
  108. this._adapter.notifyChange(newIndex, direction);
  109. this.setState({
  110. direction,
  111. rotation: 0,
  112. } as any);
  113. this._adapter.notifyRotateChange(0);
  114. }
  115. handleDownload = () => {
  116. const { currentIndex, imgSrc } = this.getStates();
  117. const downloadSrc = imgSrc[currentIndex];
  118. const downloadName = downloadSrc.slice(downloadSrc.lastIndexOf("/") + 1);
  119. downloadImage(downloadSrc, downloadName);
  120. this._adapter.notifyDownload(downloadSrc, currentIndex);
  121. }
  122. handlePreviewClose = () => {
  123. this._adapter.notifyVisibleChange(false);
  124. this._adapter.notifyClose();
  125. }
  126. handleAdjustRatio = (type: string) => {
  127. this.setState({
  128. ratio: type,
  129. } as any);
  130. this._adapter.notifyRatioChange(type);
  131. }
  132. handleRotateImage = (direction: string) => {
  133. const { rotation } = this.getStates();
  134. const newRotation = rotation + (direction === "left" ? 90 : (-90));
  135. this.setState({
  136. rotation: newRotation,
  137. } as any);
  138. this._adapter.notifyRotateChange(newRotation);
  139. }
  140. handleZoomImage = (newZoom: number) => {
  141. const { zoom } = this.getStates();
  142. this._adapter.notifyZoom(newZoom, newZoom > zoom);
  143. this.setState({
  144. zoom: newZoom,
  145. } as any);
  146. }
  147. // 当 visible 改变之后,预览组件完成首张图片加载后,启动预加载
  148. // 如: 1,2,3,4,5,6,7,8张图片, 点击第 4 张图片,preLoadGap 为 2
  149. // 当 visible 从 false 变为 true ,首先加载第 4 张图片,当第 4 张图片加载完成后,
  150. // 再按照 5,3,6,2的顺序预先加载这几张图片
  151. // When visible changes, the preview component finishes loading the first image and starts preloading
  152. // Such as: 1, 2, 3, 4, 5, 6, 7, 8 pictures, click the 4th picture, preLoadGap is 2
  153. // When visible changes from false to true , load the 4th image first, when the 4th image is loaded,
  154. // Preload these pictures in the order of 5, 3, 6, 2
  155. preloadGapImage = () => {
  156. const { preLoad, preLoadGap, infinite, currentIndex } = this.getProps();
  157. const { imgSrc }= this.getStates();
  158. if (!preLoad || typeof preLoadGap !== "number" || preLoadGap < 1){
  159. return;
  160. }
  161. const preloadImages = getPreloadImagArr(imgSrc, currentIndex, preLoadGap, infinite);
  162. const Img = new Image();
  163. let index = 0;
  164. function callback(e: any){
  165. index++;
  166. if (index < preloadImages.length) {
  167. Img.src = preloadImages[index];
  168. }
  169. }
  170. Img.onload = (e) => {
  171. this.setLoadSuccessStatus(Img.src);
  172. callback(e);
  173. };
  174. Img.onerror = callback;
  175. Img.src = preloadImages[0];
  176. }
  177. // 在切换左右图片时,当被切换图片完成加载后,根据方向决定下一个预加载的图片
  178. // 如: 1,2,3,4,5,6,7,8张图片
  179. // 当 preLoadGap 为 2, 从第 5 张图片进行切换
  180. // - 如果向 右 切换到第 6 张,则第 6 张图片加载动作结束后(无论加载成功 or 失败),会预先加载第 8 张;
  181. // - 如果向 左 切换到第 4 张,则第 4 张图片加载动作结束后(无论加载成功 or 失败),会预先加载第 2 张;
  182. // When switching the left and right pictures, when the switched picture is loaded, the next preloaded picture is determined according to the direction
  183. // Such as: 1, 2, 3, 4, 5, 6, 7, 8 pictures
  184. // When preLoadGap is 2, switch from the 5th picture
  185. // - If you switch to the 6th image(direction is next), the 8th image will be preloaded after the 6th image is loaded (whether it succeeds or fails to load);
  186. // - If you switch to the 4th image(direction is prev), the second image will be preloaded after the 4th image is loaded (whether it succeeds or fails to load);
  187. preloadSingleImage = () => {
  188. const { preLoad, preLoadGap, infinite } = this.getProps();
  189. const { imgSrc, currentIndex, direction, imgLoadStatus } = this.getStates();
  190. if (!preLoad || typeof preLoadGap !== "number" || preLoadGap < 1){
  191. return;
  192. }
  193. // 根据方向决定preload那个index
  194. // Determine the index of preload according to the direction
  195. let preloadIndex = currentIndex + (direction === "prev" ? -1 : 1) * preLoadGap;
  196. if (preloadIndex < 0 || preloadIndex >= imgSrc.length) {
  197. if (infinite) {
  198. preloadIndex = (preloadIndex + imgSrc.length) % imgSrc.length;
  199. } else {
  200. return;
  201. }
  202. }
  203. // 如果图片没有加载成功过,则进行预加载
  204. // If the image has not been loaded successfully, preload it
  205. if (!imgLoadStatus[preloadIndex]) {
  206. const Img = new Image();
  207. Img.onload = (e) => {
  208. this.setLoadSuccessStatus(imgSrc[preloadIndex]);
  209. };
  210. Img.src = imgSrc[preloadIndex];
  211. }
  212. }
  213. setLoadSuccessStatus = (src: string) => {
  214. const { imgLoadStatus } = this.getStates();
  215. const status = { ...imgLoadStatus };
  216. status[src] = true;
  217. this.setState({
  218. imgLoadStatus: status,
  219. } as any);
  220. }
  221. onImageLoad = (src: string) => {
  222. const { preloadAfterVisibleChange } = this.getStates();
  223. this.setLoadSuccessStatus(src);
  224. // 当 preview 中当前加载的图片加载完成后,
  225. // 如果是在visible change之后的第一次加载,则启动加载该currentIndex左右preloadGap范围的图片
  226. // 如果是非第一次加载,是在左右切换图片,则根据方向预先加载单张图片
  227. // When the currently loaded image in Preview is loaded,
  228. // - It is the first load after visible change, start loading the images in the preloadGap range around the currentIndex
  229. // - It is not the first load, the image is switched left and right, and a single image is preloaded according to the direction
  230. if (preloadAfterVisibleChange) {
  231. this.preloadGapImage();
  232. this.setState({
  233. preloadAfterVisibleChange: false,
  234. } as any);
  235. } else {
  236. this.preloadSingleImage();
  237. }
  238. }
  239. }