b6925e16be9f8ab13543e7f7954682a2fd4a14730f71a06580311a4a35c2511b2f2ed29fe298c4faad770913a59aeae126506657469be3c11fd4e10b9d34e1 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. import { defineComponent, markRaw, ref, effectScope, computed, shallowRef, watch, nextTick, onMounted, openBlock, createBlock, unref, withCtx, createVNode, Transition, createElementVNode, normalizeClass, normalizeStyle, withModifiers, createCommentVNode, createElementBlock, Fragment, renderSlot, createTextVNode, toDisplayString, resolveDynamicComponent } from 'vue';
  2. import { clamp, useEventListener } from '@vueuse/core';
  3. import { throttle } from 'lodash-unified';
  4. import ElFocusTrap from '../../focus-trap/src/focus-trap.mjs';
  5. import { ElTeleport } from '../../teleport/index.mjs';
  6. import { ElIcon } from '../../icon/index.mjs';
  7. import { FullScreen, ScaleToOriginal, Close, ArrowLeft, ArrowRight, ZoomOut, ZoomIn, RefreshLeft, RefreshRight } from '@element-plus/icons-vue';
  8. import { imageViewerProps, imageViewerEmits } from './image-viewer2.mjs';
  9. import _export_sfc from '../../../_virtual/plugin-vue_export-helper.mjs';
  10. import { useLocale } from '../../../hooks/use-locale/index.mjs';
  11. import { useNamespace } from '../../../hooks/use-namespace/index.mjs';
  12. import { useZIndex } from '../../../hooks/use-z-index/index.mjs';
  13. import { EVENT_CODE } from '../../../constants/aria.mjs';
  14. import { keysOf } from '../../../utils/objects.mjs';
  15. const __default__ = defineComponent({
  16. name: "ElImageViewer"
  17. });
  18. const _sfc_main = /* @__PURE__ */ defineComponent({
  19. ...__default__,
  20. props: imageViewerProps,
  21. emits: imageViewerEmits,
  22. setup(__props, { expose, emit }) {
  23. var _a;
  24. const props = __props;
  25. const modes = {
  26. CONTAIN: {
  27. name: "contain",
  28. icon: markRaw(FullScreen)
  29. },
  30. ORIGINAL: {
  31. name: "original",
  32. icon: markRaw(ScaleToOriginal)
  33. }
  34. };
  35. let stopWheelListener;
  36. let prevOverflow = "";
  37. const { t } = useLocale();
  38. const ns = useNamespace("image-viewer");
  39. const { nextZIndex } = useZIndex();
  40. const wrapper = ref();
  41. const imgRef = ref();
  42. const scopeEventListener = effectScope();
  43. const scaleClamped = computed(() => {
  44. const { scale, minScale, maxScale } = props;
  45. return clamp(scale, minScale, maxScale);
  46. });
  47. const loading = ref(true);
  48. const loadError = ref(false);
  49. const activeIndex = ref(props.initialIndex);
  50. const mode = shallowRef(modes.CONTAIN);
  51. const transform = ref({
  52. scale: scaleClamped.value,
  53. deg: 0,
  54. offsetX: 0,
  55. offsetY: 0,
  56. enableTransition: false
  57. });
  58. const zIndex = ref((_a = props.zIndex) != null ? _a : nextZIndex());
  59. const isSingle = computed(() => {
  60. const { urlList } = props;
  61. return urlList.length <= 1;
  62. });
  63. const isFirst = computed(() => activeIndex.value === 0);
  64. const isLast = computed(() => activeIndex.value === props.urlList.length - 1);
  65. const currentImg = computed(() => props.urlList[activeIndex.value]);
  66. const arrowPrevKls = computed(() => [
  67. ns.e("btn"),
  68. ns.e("prev"),
  69. ns.is("disabled", !props.infinite && isFirst.value)
  70. ]);
  71. const arrowNextKls = computed(() => [
  72. ns.e("btn"),
  73. ns.e("next"),
  74. ns.is("disabled", !props.infinite && isLast.value)
  75. ]);
  76. const imgStyle = computed(() => {
  77. const { scale, deg, offsetX, offsetY, enableTransition } = transform.value;
  78. let translateX = offsetX / scale;
  79. let translateY = offsetY / scale;
  80. const radian = deg * Math.PI / 180;
  81. const cosRadian = Math.cos(radian);
  82. const sinRadian = Math.sin(radian);
  83. translateX = translateX * cosRadian + translateY * sinRadian;
  84. translateY = translateY * cosRadian - offsetX / scale * sinRadian;
  85. const style = {
  86. transform: `scale(${scale}) rotate(${deg}deg) translate(${translateX}px, ${translateY}px)`,
  87. transition: enableTransition ? "transform .3s" : ""
  88. };
  89. if (mode.value.name === modes.CONTAIN.name) {
  90. style.maxWidth = style.maxHeight = "100%";
  91. }
  92. return style;
  93. });
  94. const progress = computed(() => `${activeIndex.value + 1} / ${props.urlList.length}`);
  95. function hide() {
  96. unregisterEventListener();
  97. stopWheelListener == null ? void 0 : stopWheelListener();
  98. document.body.style.overflow = prevOverflow;
  99. emit("close");
  100. }
  101. function registerEventListener() {
  102. const keydownHandler = throttle((e) => {
  103. switch (e.code) {
  104. case EVENT_CODE.esc:
  105. props.closeOnPressEscape && hide();
  106. break;
  107. case EVENT_CODE.space:
  108. toggleMode();
  109. break;
  110. case EVENT_CODE.left:
  111. prev();
  112. break;
  113. case EVENT_CODE.up:
  114. handleActions("zoomIn");
  115. break;
  116. case EVENT_CODE.right:
  117. next();
  118. break;
  119. case EVENT_CODE.down:
  120. handleActions("zoomOut");
  121. break;
  122. }
  123. });
  124. const mousewheelHandler = throttle((e) => {
  125. const delta = e.deltaY || e.deltaX;
  126. handleActions(delta < 0 ? "zoomIn" : "zoomOut", {
  127. zoomRate: props.zoomRate,
  128. enableTransition: false
  129. });
  130. });
  131. scopeEventListener.run(() => {
  132. useEventListener(document, "keydown", keydownHandler);
  133. useEventListener(document, "wheel", mousewheelHandler);
  134. });
  135. }
  136. function unregisterEventListener() {
  137. scopeEventListener.stop();
  138. }
  139. function handleImgLoad() {
  140. loading.value = false;
  141. }
  142. function handleImgError(e) {
  143. loadError.value = true;
  144. loading.value = false;
  145. emit("error", e);
  146. e.target.alt = t("el.image.error");
  147. }
  148. function handleMouseDown(e) {
  149. if (loading.value || e.button !== 0 || !wrapper.value)
  150. return;
  151. transform.value.enableTransition = false;
  152. const { offsetX, offsetY } = transform.value;
  153. const startX = e.pageX;
  154. const startY = e.pageY;
  155. const dragHandler = throttle((ev) => {
  156. transform.value = {
  157. ...transform.value,
  158. offsetX: offsetX + ev.pageX - startX,
  159. offsetY: offsetY + ev.pageY - startY
  160. };
  161. });
  162. const removeMousemove = useEventListener(document, "mousemove", dragHandler);
  163. useEventListener(document, "mouseup", () => {
  164. removeMousemove();
  165. });
  166. e.preventDefault();
  167. }
  168. function reset() {
  169. transform.value = {
  170. scale: scaleClamped.value,
  171. deg: 0,
  172. offsetX: 0,
  173. offsetY: 0,
  174. enableTransition: false
  175. };
  176. }
  177. function toggleMode() {
  178. if (loading.value || loadError.value)
  179. return;
  180. const modeNames = keysOf(modes);
  181. const modeValues = Object.values(modes);
  182. const currentMode = mode.value.name;
  183. const index = modeValues.findIndex((i) => i.name === currentMode);
  184. const nextIndex = (index + 1) % modeNames.length;
  185. mode.value = modes[modeNames[nextIndex]];
  186. reset();
  187. }
  188. function setActiveItem(index) {
  189. loadError.value = false;
  190. const len = props.urlList.length;
  191. activeIndex.value = (index + len) % len;
  192. }
  193. function prev() {
  194. if (isFirst.value && !props.infinite)
  195. return;
  196. setActiveItem(activeIndex.value - 1);
  197. }
  198. function next() {
  199. if (isLast.value && !props.infinite)
  200. return;
  201. setActiveItem(activeIndex.value + 1);
  202. }
  203. function handleActions(action, options = {}) {
  204. if (loading.value || loadError.value)
  205. return;
  206. const { minScale, maxScale } = props;
  207. const { zoomRate, rotateDeg, enableTransition } = {
  208. zoomRate: props.zoomRate,
  209. rotateDeg: 90,
  210. enableTransition: true,
  211. ...options
  212. };
  213. switch (action) {
  214. case "zoomOut":
  215. if (transform.value.scale > minScale) {
  216. transform.value.scale = Number.parseFloat((transform.value.scale / zoomRate).toFixed(3));
  217. }
  218. break;
  219. case "zoomIn":
  220. if (transform.value.scale < maxScale) {
  221. transform.value.scale = Number.parseFloat((transform.value.scale * zoomRate).toFixed(3));
  222. }
  223. break;
  224. case "clockwise":
  225. transform.value.deg += rotateDeg;
  226. emit("rotate", transform.value.deg);
  227. break;
  228. case "anticlockwise":
  229. transform.value.deg -= rotateDeg;
  230. emit("rotate", transform.value.deg);
  231. break;
  232. }
  233. transform.value.enableTransition = enableTransition;
  234. }
  235. function onFocusoutPrevented(event) {
  236. var _a2;
  237. if (((_a2 = event.detail) == null ? void 0 : _a2.focusReason) === "pointer") {
  238. event.preventDefault();
  239. }
  240. }
  241. function onCloseRequested() {
  242. if (props.closeOnPressEscape) {
  243. hide();
  244. }
  245. }
  246. function wheelHandler(e) {
  247. if (!e.ctrlKey)
  248. return;
  249. if (e.deltaY < 0) {
  250. e.preventDefault();
  251. return false;
  252. } else if (e.deltaY > 0) {
  253. e.preventDefault();
  254. return false;
  255. }
  256. }
  257. watch(() => scaleClamped.value, (val) => {
  258. transform.value.scale = val;
  259. });
  260. watch(currentImg, () => {
  261. nextTick(() => {
  262. const $img = imgRef.value;
  263. if (!($img == null ? void 0 : $img.complete)) {
  264. loading.value = true;
  265. }
  266. });
  267. });
  268. watch(activeIndex, (val) => {
  269. reset();
  270. emit("switch", val);
  271. });
  272. onMounted(() => {
  273. registerEventListener();
  274. stopWheelListener = useEventListener("wheel", wheelHandler, {
  275. passive: false
  276. });
  277. prevOverflow = document.body.style.overflow;
  278. document.body.style.overflow = "hidden";
  279. });
  280. expose({
  281. setActiveItem
  282. });
  283. return (_ctx, _cache) => {
  284. return openBlock(), createBlock(unref(ElTeleport), {
  285. to: "body",
  286. disabled: !_ctx.teleported
  287. }, {
  288. default: withCtx(() => [
  289. createVNode(Transition, {
  290. name: "viewer-fade",
  291. appear: ""
  292. }, {
  293. default: withCtx(() => [
  294. createElementVNode("div", {
  295. ref_key: "wrapper",
  296. ref: wrapper,
  297. tabindex: -1,
  298. class: normalizeClass(unref(ns).e("wrapper")),
  299. style: normalizeStyle({ zIndex: zIndex.value })
  300. }, [
  301. createVNode(unref(ElFocusTrap), {
  302. loop: "",
  303. trapped: "",
  304. "focus-trap-el": wrapper.value,
  305. "focus-start-el": "container",
  306. onFocusoutPrevented,
  307. onReleaseRequested: onCloseRequested
  308. }, {
  309. default: withCtx(() => [
  310. createElementVNode("div", {
  311. class: normalizeClass(unref(ns).e("mask")),
  312. onClick: withModifiers(($event) => _ctx.hideOnClickModal && hide(), ["self"])
  313. }, null, 10, ["onClick"]),
  314. createCommentVNode(" CLOSE "),
  315. createElementVNode("span", {
  316. class: normalizeClass([unref(ns).e("btn"), unref(ns).e("close")]),
  317. onClick: hide
  318. }, [
  319. createVNode(unref(ElIcon), null, {
  320. default: withCtx(() => [
  321. createVNode(unref(Close))
  322. ]),
  323. _: 1
  324. })
  325. ], 2),
  326. createCommentVNode(" ARROW "),
  327. !unref(isSingle) ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
  328. createElementVNode("span", {
  329. class: normalizeClass(unref(arrowPrevKls)),
  330. onClick: prev
  331. }, [
  332. createVNode(unref(ElIcon), null, {
  333. default: withCtx(() => [
  334. createVNode(unref(ArrowLeft))
  335. ]),
  336. _: 1
  337. })
  338. ], 2),
  339. createElementVNode("span", {
  340. class: normalizeClass(unref(arrowNextKls)),
  341. onClick: next
  342. }, [
  343. createVNode(unref(ElIcon), null, {
  344. default: withCtx(() => [
  345. createVNode(unref(ArrowRight))
  346. ]),
  347. _: 1
  348. })
  349. ], 2)
  350. ], 64)) : createCommentVNode("v-if", true),
  351. _ctx.$slots.progress || _ctx.showProgress ? (openBlock(), createElementBlock("div", {
  352. key: 1,
  353. class: normalizeClass([unref(ns).e("btn"), unref(ns).e("progress")])
  354. }, [
  355. renderSlot(_ctx.$slots, "progress", {
  356. activeIndex: activeIndex.value,
  357. total: _ctx.urlList.length
  358. }, () => [
  359. createTextVNode(toDisplayString(unref(progress)), 1)
  360. ])
  361. ], 2)) : createCommentVNode("v-if", true),
  362. createCommentVNode(" ACTIONS "),
  363. createElementVNode("div", {
  364. class: normalizeClass([unref(ns).e("btn"), unref(ns).e("actions")])
  365. }, [
  366. createElementVNode("div", {
  367. class: normalizeClass(unref(ns).e("actions__inner"))
  368. }, [
  369. renderSlot(_ctx.$slots, "toolbar", {
  370. actions: handleActions,
  371. prev,
  372. next,
  373. reset: toggleMode,
  374. activeIndex: activeIndex.value,
  375. setActiveItem
  376. }, () => [
  377. createVNode(unref(ElIcon), {
  378. onClick: ($event) => handleActions("zoomOut")
  379. }, {
  380. default: withCtx(() => [
  381. createVNode(unref(ZoomOut))
  382. ]),
  383. _: 1
  384. }, 8, ["onClick"]),
  385. createVNode(unref(ElIcon), {
  386. onClick: ($event) => handleActions("zoomIn")
  387. }, {
  388. default: withCtx(() => [
  389. createVNode(unref(ZoomIn))
  390. ]),
  391. _: 1
  392. }, 8, ["onClick"]),
  393. createElementVNode("i", {
  394. class: normalizeClass(unref(ns).e("actions__divider"))
  395. }, null, 2),
  396. createVNode(unref(ElIcon), { onClick: toggleMode }, {
  397. default: withCtx(() => [
  398. (openBlock(), createBlock(resolveDynamicComponent(unref(mode).icon)))
  399. ]),
  400. _: 1
  401. }),
  402. createElementVNode("i", {
  403. class: normalizeClass(unref(ns).e("actions__divider"))
  404. }, null, 2),
  405. createVNode(unref(ElIcon), {
  406. onClick: ($event) => handleActions("anticlockwise")
  407. }, {
  408. default: withCtx(() => [
  409. createVNode(unref(RefreshLeft))
  410. ]),
  411. _: 1
  412. }, 8, ["onClick"]),
  413. createVNode(unref(ElIcon), {
  414. onClick: ($event) => handleActions("clockwise")
  415. }, {
  416. default: withCtx(() => [
  417. createVNode(unref(RefreshRight))
  418. ]),
  419. _: 1
  420. }, 8, ["onClick"])
  421. ])
  422. ], 2)
  423. ], 2),
  424. createCommentVNode(" CANVAS "),
  425. createElementVNode("div", {
  426. class: normalizeClass(unref(ns).e("canvas"))
  427. }, [
  428. loadError.value && _ctx.$slots["viewer-error"] ? renderSlot(_ctx.$slots, "viewer-error", {
  429. key: 0,
  430. activeIndex: activeIndex.value,
  431. src: unref(currentImg)
  432. }) : (openBlock(), createElementBlock("img", {
  433. ref_key: "imgRef",
  434. ref: imgRef,
  435. key: unref(currentImg),
  436. src: unref(currentImg),
  437. style: normalizeStyle(unref(imgStyle)),
  438. class: normalizeClass(unref(ns).e("img")),
  439. crossorigin: _ctx.crossorigin,
  440. onLoad: handleImgLoad,
  441. onError: handleImgError,
  442. onMousedown: handleMouseDown
  443. }, null, 46, ["src", "crossorigin"]))
  444. ], 2),
  445. renderSlot(_ctx.$slots, "default")
  446. ]),
  447. _: 3
  448. }, 8, ["focus-trap-el"])
  449. ], 6)
  450. ]),
  451. _: 3
  452. })
  453. ]),
  454. _: 3
  455. }, 8, ["disabled"]);
  456. };
  457. }
  458. });
  459. var ImageViewer = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "image-viewer.vue"]]);
  460. export { ImageViewer as default };
  461. //# sourceMappingURL=image-viewer.mjs.map