ImagePreviewItem.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import { ref, watch, computed, reactive, defineComponent, createVNode as _createVNode } from "vue";
  2. import { clamp, numericProp, preventDefault, createNamespace, makeRequiredProp, LONG_PRESS_START_TIME } from "../utils/index.mjs";
  3. import { useExpose } from "../composables/use-expose.mjs";
  4. import { useTouch } from "../composables/use-touch.mjs";
  5. import { raf, useEventListener, useRect } from "@vant/use";
  6. import { Image } from "../image/index.mjs";
  7. import { Loading } from "../loading/index.mjs";
  8. import { SwipeItem } from "../swipe-item/index.mjs";
  9. const getDistance = (touches) => Math.sqrt((touches[0].clientX - touches[1].clientX) ** 2 + (touches[0].clientY - touches[1].clientY) ** 2);
  10. const getCenter = (touches) => ({
  11. x: (touches[0].clientX + touches[1].clientX) / 2,
  12. y: (touches[0].clientY + touches[1].clientY) / 2
  13. });
  14. const bem = createNamespace("image-preview")[1];
  15. const longImageRatio = 2.6;
  16. const imagePreviewItemProps = {
  17. src: String,
  18. show: Boolean,
  19. active: Number,
  20. minZoom: makeRequiredProp(numericProp),
  21. maxZoom: makeRequiredProp(numericProp),
  22. rootWidth: makeRequiredProp(Number),
  23. rootHeight: makeRequiredProp(Number),
  24. disableZoom: Boolean,
  25. doubleScale: Boolean,
  26. closeOnClickImage: Boolean,
  27. closeOnClickOverlay: Boolean,
  28. vertical: Boolean
  29. };
  30. var stdin_default = defineComponent({
  31. props: imagePreviewItemProps,
  32. emits: ["scale", "close", "longPress"],
  33. setup(props, {
  34. emit,
  35. slots
  36. }) {
  37. const state = reactive({
  38. scale: 1,
  39. moveX: 0,
  40. moveY: 0,
  41. moving: false,
  42. zooming: false,
  43. initializing: false,
  44. imageRatio: 0
  45. });
  46. const touch = useTouch();
  47. const imageRef = ref();
  48. const swipeItem = ref();
  49. const vertical = ref(false);
  50. const isLongImage = ref(false);
  51. let initialMoveY = 0;
  52. const imageStyle = computed(() => {
  53. const {
  54. scale,
  55. moveX,
  56. moveY,
  57. moving,
  58. zooming,
  59. initializing
  60. } = state;
  61. const style = {
  62. transitionDuration: zooming || moving || initializing ? "0s" : ".3s"
  63. };
  64. if (scale !== 1 || isLongImage.value) {
  65. style.transform = `matrix(${scale}, 0, 0, ${scale}, ${moveX}, ${moveY})`;
  66. }
  67. return style;
  68. });
  69. const maxMoveX = computed(() => {
  70. if (state.imageRatio) {
  71. const {
  72. rootWidth,
  73. rootHeight
  74. } = props;
  75. const displayWidth = vertical.value ? rootHeight / state.imageRatio : rootWidth;
  76. return Math.max(0, (state.scale * displayWidth - rootWidth) / 2);
  77. }
  78. return 0;
  79. });
  80. const maxMoveY = computed(() => {
  81. if (state.imageRatio) {
  82. const {
  83. rootWidth,
  84. rootHeight
  85. } = props;
  86. const displayHeight = vertical.value ? rootHeight : rootWidth * state.imageRatio;
  87. return Math.max(0, (state.scale * displayHeight - rootHeight) / 2);
  88. }
  89. return 0;
  90. });
  91. const setScale = (scale, center) => {
  92. var _a;
  93. scale = clamp(scale, +props.minZoom, +props.maxZoom + 1);
  94. if (scale !== state.scale) {
  95. const ratio = scale / state.scale;
  96. state.scale = scale;
  97. if (center) {
  98. const imageRect = useRect((_a = imageRef.value) == null ? void 0 : _a.$el);
  99. const origin = {
  100. x: imageRect.width * 0.5,
  101. y: imageRect.height * 0.5
  102. };
  103. const moveX = state.moveX - (center.x - imageRect.left - origin.x) * (ratio - 1);
  104. const moveY = state.moveY - (center.y - imageRect.top - origin.y) * (ratio - 1);
  105. state.moveX = clamp(moveX, -maxMoveX.value, maxMoveX.value);
  106. state.moveY = clamp(moveY, -maxMoveY.value, maxMoveY.value);
  107. } else {
  108. state.moveX = 0;
  109. state.moveY = isLongImage.value ? initialMoveY : 0;
  110. }
  111. emit("scale", {
  112. scale,
  113. index: props.active
  114. });
  115. }
  116. };
  117. const resetScale = () => {
  118. setScale(1);
  119. };
  120. const toggleScale = () => {
  121. const scale = state.scale > 1 ? 1 : 2;
  122. setScale(scale, scale === 2 || isLongImage.value ? {
  123. x: touch.startX.value,
  124. y: touch.startY.value
  125. } : void 0);
  126. };
  127. let fingerNum;
  128. let startMoveX;
  129. let startMoveY;
  130. let startScale;
  131. let startDistance;
  132. let lastCenter;
  133. let doubleTapTimer;
  134. let touchStartTime;
  135. let isImageMoved = false;
  136. const onTouchStart = (event) => {
  137. const {
  138. touches
  139. } = event;
  140. fingerNum = touches.length;
  141. if (fingerNum === 2 && props.disableZoom) {
  142. return;
  143. }
  144. const {
  145. offsetX
  146. } = touch;
  147. touch.start(event);
  148. startMoveX = state.moveX;
  149. startMoveY = state.moveY;
  150. touchStartTime = Date.now();
  151. isImageMoved = false;
  152. state.moving = fingerNum === 1 && (state.scale !== 1 || isLongImage.value);
  153. state.zooming = fingerNum === 2 && !offsetX.value;
  154. if (state.zooming) {
  155. startScale = state.scale;
  156. startDistance = getDistance(touches);
  157. }
  158. };
  159. const onTouchMove = (event) => {
  160. const {
  161. touches
  162. } = event;
  163. touch.move(event);
  164. if (state.moving) {
  165. const {
  166. deltaX,
  167. deltaY
  168. } = touch;
  169. const moveX = deltaX.value + startMoveX;
  170. const moveY = deltaY.value + startMoveY;
  171. if ((props.vertical ? touch.isVertical() && Math.abs(moveY) > maxMoveY.value : touch.isHorizontal() && Math.abs(moveX) > maxMoveX.value) && !isImageMoved) {
  172. state.moving = false;
  173. return;
  174. }
  175. isImageMoved = true;
  176. preventDefault(event, true);
  177. state.moveX = clamp(moveX, -maxMoveX.value, maxMoveX.value);
  178. state.moveY = clamp(moveY, -maxMoveY.value, maxMoveY.value);
  179. }
  180. if (state.zooming) {
  181. preventDefault(event, true);
  182. if (touches.length === 2) {
  183. const distance = getDistance(touches);
  184. const scale = startScale * distance / startDistance;
  185. lastCenter = getCenter(touches);
  186. setScale(scale, lastCenter);
  187. }
  188. }
  189. };
  190. const checkClose = (event) => {
  191. var _a;
  192. const swipeItemEl = (_a = swipeItem.value) == null ? void 0 : _a.$el;
  193. if (!swipeItemEl) return;
  194. const imageEl = swipeItemEl.firstElementChild;
  195. const isClickOverlay = event.target === swipeItemEl;
  196. const isClickImage = imageEl == null ? void 0 : imageEl.contains(event.target);
  197. if (!props.closeOnClickImage && isClickImage) return;
  198. if (!props.closeOnClickOverlay && isClickOverlay) return;
  199. emit("close");
  200. };
  201. const checkTap = (event) => {
  202. if (fingerNum > 1) {
  203. return;
  204. }
  205. const deltaTime = Date.now() - touchStartTime;
  206. const TAP_TIME = 250;
  207. if (touch.isTap.value) {
  208. if (deltaTime < TAP_TIME) {
  209. if (props.doubleScale) {
  210. if (doubleTapTimer) {
  211. clearTimeout(doubleTapTimer);
  212. doubleTapTimer = null;
  213. toggleScale();
  214. } else {
  215. doubleTapTimer = setTimeout(() => {
  216. checkClose(event);
  217. doubleTapTimer = null;
  218. }, TAP_TIME);
  219. }
  220. } else {
  221. checkClose(event);
  222. }
  223. } else if (deltaTime > LONG_PRESS_START_TIME) {
  224. emit("longPress");
  225. }
  226. }
  227. };
  228. const onTouchEnd = (event) => {
  229. let stopPropagation = false;
  230. if (state.moving || state.zooming) {
  231. stopPropagation = true;
  232. if (state.moving && startMoveX === state.moveX && startMoveY === state.moveY) {
  233. stopPropagation = false;
  234. }
  235. if (!event.touches.length) {
  236. if (state.zooming) {
  237. state.moveX = clamp(state.moveX, -maxMoveX.value, maxMoveX.value);
  238. state.moveY = clamp(state.moveY, -maxMoveY.value, maxMoveY.value);
  239. state.zooming = false;
  240. }
  241. state.moving = false;
  242. startMoveX = 0;
  243. startMoveY = 0;
  244. startScale = 1;
  245. if (state.scale < 1) {
  246. resetScale();
  247. }
  248. const maxZoom = +props.maxZoom;
  249. if (state.scale > maxZoom) {
  250. setScale(maxZoom, lastCenter);
  251. }
  252. }
  253. }
  254. preventDefault(event, stopPropagation);
  255. checkTap(event);
  256. touch.reset();
  257. };
  258. const resize = () => {
  259. const {
  260. rootWidth,
  261. rootHeight
  262. } = props;
  263. const rootRatio = rootHeight / rootWidth;
  264. const {
  265. imageRatio
  266. } = state;
  267. vertical.value = state.imageRatio > rootRatio && imageRatio < longImageRatio;
  268. isLongImage.value = state.imageRatio > rootRatio && imageRatio >= longImageRatio;
  269. if (isLongImage.value) {
  270. initialMoveY = (imageRatio * rootWidth - rootHeight) / 2;
  271. state.moveY = initialMoveY;
  272. state.initializing = true;
  273. raf(() => {
  274. state.initializing = false;
  275. });
  276. }
  277. resetScale();
  278. };
  279. const onLoad = (event) => {
  280. const {
  281. naturalWidth,
  282. naturalHeight
  283. } = event.target;
  284. state.imageRatio = naturalHeight / naturalWidth;
  285. resize();
  286. };
  287. watch(() => props.active, resetScale);
  288. watch(() => props.show, (value) => {
  289. if (!value) {
  290. resetScale();
  291. }
  292. });
  293. watch(() => [props.rootWidth, props.rootHeight], resize);
  294. useEventListener("touchmove", onTouchMove, {
  295. target: computed(() => {
  296. var _a;
  297. return (_a = swipeItem.value) == null ? void 0 : _a.$el;
  298. })
  299. });
  300. useExpose({
  301. resetScale
  302. });
  303. return () => {
  304. const imageSlots = {
  305. loading: () => _createVNode(Loading, {
  306. "type": "spinner"
  307. }, null)
  308. };
  309. return _createVNode(SwipeItem, {
  310. "ref": swipeItem,
  311. "class": bem("swipe-item"),
  312. "onTouchstartPassive": onTouchStart,
  313. "onTouchend": onTouchEnd,
  314. "onTouchcancel": onTouchEnd
  315. }, {
  316. default: () => [slots.image ? _createVNode("div", {
  317. "class": bem("image-wrap")
  318. }, [slots.image({
  319. src: props.src,
  320. onLoad,
  321. style: imageStyle.value
  322. })]) : _createVNode(Image, {
  323. "ref": imageRef,
  324. "src": props.src,
  325. "fit": "contain",
  326. "class": bem("image", {
  327. vertical: vertical.value
  328. }),
  329. "style": imageStyle.value,
  330. "onLoad": onLoad
  331. }, imageSlots)]
  332. });
  333. };
  334. }
  335. });
  336. export {
  337. stdin_default as default
  338. };