{"version":3,"file":"image-viewer2.mjs","names":[],"sources":["../../../../../../packages/components/image-viewer/src/image-viewer.vue"],"sourcesContent":["<template>\n  <el-teleport to=\"body\" :disabled=\"!teleported\">\n    <transition name=\"viewer-fade\" appear>\n      <div\n        ref=\"wrapper\"\n        :tabindex=\"-1\"\n        :class=\"ns.e('wrapper')\"\n        :style=\"{ zIndex }\"\n      >\n        <el-focus-trap\n          loop\n          trapped\n          :focus-trap-el=\"wrapper\"\n          focus-start-el=\"container\"\n          @focusout-prevented=\"onFocusoutPrevented\"\n          @release-requested=\"onCloseRequested\"\n        >\n          <div :class=\"ns.e('mask')\" @click.self=\"hideOnClickModal && hide()\" />\n\n          <!-- CLOSE -->\n          <span :class=\"[ns.e('btn'), ns.e('close')]\" @click=\"hide\">\n            <el-icon>\n              <Close />\n            </el-icon>\n          </span>\n\n          <!-- ARROW -->\n          <template v-if=\"!isSingle\">\n            <span :class=\"arrowPrevKls\" @click=\"prev\">\n              <el-icon>\n                <ArrowLeft />\n              </el-icon>\n            </span>\n            <span :class=\"arrowNextKls\" @click=\"next\">\n              <el-icon>\n                <ArrowRight />\n              </el-icon>\n            </span>\n          </template>\n          <div\n            v-if=\"$slots.progress || showProgress\"\n            :class=\"[ns.e('btn'), ns.e('progress')]\"\n          >\n            <slot\n              name=\"progress\"\n              :active-index=\"activeIndex\"\n              :total=\"urlList.length\"\n            >\n              {{ progress }}\n            </slot>\n          </div>\n          <!-- ACTIONS -->\n          <div :class=\"[ns.e('btn'), ns.e('actions')]\">\n            <div :class=\"ns.e('actions__inner')\">\n              <slot\n                name=\"toolbar\"\n                :actions=\"handleActions\"\n                :prev=\"prev\"\n                :next=\"next\"\n                :reset=\"toggleMode\"\n                :active-index=\"activeIndex\"\n                :set-active-item=\"setActiveItem\"\n              >\n                <el-icon @click=\"handleActions('zoomOut')\">\n                  <ZoomOut />\n                </el-icon>\n                <el-icon @click=\"handleActions('zoomIn')\">\n                  <ZoomIn />\n                </el-icon>\n                <i :class=\"ns.e('actions__divider')\" />\n                <el-icon @click=\"toggleMode\">\n                  <component :is=\"mode.icon\" />\n                </el-icon>\n                <i :class=\"ns.e('actions__divider')\" />\n                <el-icon @click=\"handleActions('anticlockwise')\">\n                  <RefreshLeft />\n                </el-icon>\n                <el-icon @click=\"handleActions('clockwise')\">\n                  <RefreshRight />\n                </el-icon>\n              </slot>\n            </div>\n          </div>\n          <!-- CANVAS -->\n          <div :class=\"ns.e('canvas')\">\n            <slot\n              v-if=\"loadError && $slots['viewer-error']\"\n              name=\"viewer-error\"\n              :active-index=\"activeIndex\"\n              :src=\"currentImg\"\n            />\n            <img\n              v-else\n              ref=\"imgRef\"\n              :key=\"currentImg\"\n              :src=\"currentImg\"\n              :style=\"imgStyle\"\n              :class=\"ns.e('img')\"\n              :crossorigin=\"crossorigin\"\n              @load=\"handleImgLoad\"\n              @error=\"handleImgError\"\n              @mousedown=\"handleMouseDown\"\n              @touchstart=\"handleTouchStart\"\n            />\n          </div>\n          <slot />\n        </el-focus-trap>\n      </div>\n    </transition>\n  </el-teleport>\n</template>\n\n<script lang=\"ts\" setup>\nimport {\n  computed,\n  effectScope,\n  markRaw,\n  nextTick,\n  onMounted,\n  ref,\n  shallowRef,\n  watch,\n} from 'vue'\nimport { clamp, useEventListener } from '@vueuse/core'\nimport { throttle } from 'lodash-unified'\nimport {\n  useLocale,\n  useLockscreen,\n  useNamespace,\n  useZIndex,\n} from '@element-plus/hooks'\nimport { EVENT_CODE } from '@element-plus/constants'\nimport { getEventCode, keysOf } from '@element-plus/utils'\nimport ElFocusTrap from '@element-plus/components/focus-trap'\nimport ElTeleport from '@element-plus/components/teleport'\nimport ElIcon from '@element-plus/components/icon'\nimport {\n  ArrowLeft,\n  ArrowRight,\n  Close,\n  FullScreen,\n  RefreshLeft,\n  RefreshRight,\n  ScaleToOriginal,\n  ZoomIn,\n  ZoomOut,\n} from '@element-plus/icons-vue'\nimport { imageViewerEmits } from './image-viewer'\n\nimport type { CSSProperties } from 'vue'\nimport type {\n  ImageViewerAction,\n  ImageViewerMode,\n  ImageViewerProps,\n} from './image-viewer'\n\nconst modes: Record<'CONTAIN' | 'ORIGINAL', ImageViewerMode> = {\n  CONTAIN: {\n    name: 'contain',\n    icon: markRaw(FullScreen),\n  },\n  ORIGINAL: {\n    name: 'original',\n    icon: markRaw(ScaleToOriginal),\n  },\n}\n\ndefineOptions({\n  name: 'ElImageViewer',\n})\n\nconst props = withDefaults(defineProps<ImageViewerProps>(), {\n  urlList: () => [],\n  initialIndex: 0,\n  infinite: true,\n  closeOnPressEscape: true,\n  zoomRate: 1.2,\n  scale: 1,\n  minScale: 0.2,\n  maxScale: 7,\n})\nconst emit = defineEmits(imageViewerEmits)\n\nlet stopWheelListener: (() => void) | undefined\n\nconst { t } = useLocale()\nconst ns = useNamespace('image-viewer')\nconst { nextZIndex } = useZIndex()\nconst wrapper = ref<HTMLDivElement>()\nconst imgRef = ref<HTMLImageElement>()\n\nconst scopeEventListener = effectScope()\n\nconst scaleClamped = computed(() => {\n  const { scale, minScale, maxScale } = props\n  return clamp(scale, minScale, maxScale)\n})\n\nconst loading = ref(true)\nconst loadError = ref(false)\nconst visible = ref(false)\nconst activeIndex = ref(props.initialIndex)\nconst mode = shallowRef<ImageViewerMode>(modes.CONTAIN)\nconst transform = ref({\n  scale: scaleClamped.value,\n  deg: 0,\n  offsetX: 0,\n  offsetY: 0,\n  enableTransition: false,\n})\nconst zIndex = ref(props.zIndex ?? nextZIndex())\n\nuseLockscreen(visible, { ns })\n\nconst isSingle = computed(() => {\n  const { urlList } = props\n  return urlList.length <= 1\n})\n\nconst isFirst = computed(() => activeIndex.value === 0)\n\nconst isLast = computed(() => activeIndex.value === props.urlList.length - 1)\n\nconst currentImg = computed(() => props.urlList[activeIndex.value])\n\nconst arrowPrevKls = computed(() => [\n  ns.e('btn'),\n  ns.e('prev'),\n  ns.is('disabled', !props.infinite && isFirst.value),\n])\n\nconst arrowNextKls = computed(() => [\n  ns.e('btn'),\n  ns.e('next'),\n  ns.is('disabled', !props.infinite && isLast.value),\n])\n\nconst imgStyle = computed(() => {\n  const { scale, deg, offsetX, offsetY, enableTransition } = transform.value\n  let translateX = offsetX / scale\n  let translateY = offsetY / scale\n\n  const radian = (deg * Math.PI) / 180\n  const cosRadian = Math.cos(radian)\n  const sinRadian = Math.sin(radian)\n  translateX = translateX * cosRadian + translateY * sinRadian\n  translateY = translateY * cosRadian - (offsetX / scale) * sinRadian\n\n  const style: CSSProperties = {\n    transform: `scale(${scale}) rotate(${deg}deg) translate(${translateX}px, ${translateY}px)`,\n    transition: enableTransition ? 'transform .3s' : '',\n  }\n  if (mode.value.name === modes.CONTAIN.name) {\n    style.maxWidth = style.maxHeight = '100%'\n  }\n  return style\n})\n\nconst progress = computed(\n  () => `${activeIndex.value + 1} / ${props.urlList.length}`\n)\n\nfunction hide() {\n  unregisterEventListener()\n  stopWheelListener?.()\n  visible.value = false\n  emit('close')\n}\n\nfunction registerEventListener() {\n  const keydownHandler = throttle((e: KeyboardEvent) => {\n    const code = getEventCode(e)\n\n    switch (code) {\n      // ESC\n      case EVENT_CODE.esc:\n        props.closeOnPressEscape && hide()\n        break\n      // SPACE\n      case EVENT_CODE.space:\n        toggleMode()\n        break\n      // LEFT_ARROW\n      case EVENT_CODE.left:\n        prev()\n        break\n      // UP_ARROW\n      case EVENT_CODE.up:\n        handleActions('zoomIn')\n        break\n      // RIGHT_ARROW\n      case EVENT_CODE.right:\n        next()\n        break\n      // DOWN_ARROW\n      case EVENT_CODE.down:\n        handleActions('zoomOut')\n        break\n    }\n  })\n  const mousewheelHandler = throttle((e: WheelEvent) => {\n    const delta = e.deltaY || e.deltaX\n    handleActions(delta < 0 ? 'zoomIn' : 'zoomOut', {\n      zoomRate: props.zoomRate,\n      enableTransition: false,\n    })\n  })\n\n  scopeEventListener.run(() => {\n    useEventListener(document, 'keydown', keydownHandler)\n    useEventListener(wrapper, 'wheel', mousewheelHandler)\n  })\n}\n\nfunction unregisterEventListener() {\n  scopeEventListener.stop()\n}\n\nfunction handleImgLoad() {\n  loading.value = false\n}\n\nfunction handleImgError(e: Event) {\n  loadError.value = true\n  loading.value = false\n  emit('error', e)\n  ;(e.target as HTMLImageElement).alt = t('el.image.error')\n}\n\nfunction handleMouseDown(e: MouseEvent) {\n  if (loading.value || e.button !== 0 || !wrapper.value) return\n  transform.value.enableTransition = false\n\n  const { offsetX, offsetY } = transform.value\n  const startX = e.pageX\n  const startY = e.pageY\n\n  const dragHandler = throttle((ev: MouseEvent) => {\n    transform.value = {\n      ...transform.value,\n      offsetX: offsetX + ev.pageX - startX,\n      offsetY: offsetY + ev.pageY - startY,\n    }\n  })\n  const removeMousemove = useEventListener(document, 'mousemove', dragHandler)\n  const removeMouseup = useEventListener(document, 'mouseup', () => {\n    removeMousemove()\n    removeMouseup()\n  })\n\n  e.preventDefault()\n}\n\nfunction handleTouchStart(e: TouchEvent) {\n  if (loading.value || !wrapper.value || e.touches.length !== 1) return\n  transform.value.enableTransition = false\n\n  const { offsetX, offsetY } = transform.value\n  const { pageX: startX, pageY: startY } = e.touches[0]\n\n  const dragHandler = throttle((ev: TouchEvent) => {\n    const targetTouch = ev.touches[0]\n    transform.value = {\n      ...transform.value,\n      offsetX: offsetX + targetTouch.pageX - startX,\n      offsetY: offsetY + targetTouch.pageY - startY,\n    }\n  })\n  const removeTouchmove = useEventListener(document, 'touchmove', dragHandler)\n  const removeTouchend = useEventListener(document, 'touchend', () => {\n    removeTouchmove()\n    removeTouchend()\n  })\n\n  e.preventDefault()\n}\n\nfunction reset() {\n  transform.value = {\n    scale: scaleClamped.value,\n    deg: 0,\n    offsetX: 0,\n    offsetY: 0,\n    enableTransition: false,\n  }\n}\n\nfunction toggleMode() {\n  if (loading.value || loadError.value) return\n\n  const modeNames = keysOf(modes)\n  const modeValues = Object.values(modes)\n  const currentMode = mode.value.name\n  const index = modeValues.findIndex((i) => i.name === currentMode)\n  const nextIndex = (index + 1) % modeNames.length\n  mode.value = modes[modeNames[nextIndex]]\n  reset()\n}\n\nfunction setActiveItem(index: number) {\n  loadError.value = false\n  const len = props.urlList.length\n  activeIndex.value = (index + len) % len\n}\n\nfunction prev() {\n  if (isFirst.value && !props.infinite) return\n  setActiveItem(activeIndex.value - 1)\n}\n\nfunction next() {\n  if (isLast.value && !props.infinite) return\n  setActiveItem(activeIndex.value + 1)\n}\n\nfunction handleActions(action: ImageViewerAction, options = {}) {\n  if (loading.value || loadError.value) return\n  const { minScale, maxScale } = props\n  const { zoomRate, rotateDeg, enableTransition } = {\n    zoomRate: props.zoomRate,\n    rotateDeg: 90,\n    enableTransition: true,\n    ...options,\n  }\n  switch (action) {\n    case 'zoomOut':\n      if (transform.value.scale > minScale) {\n        transform.value.scale = Number.parseFloat(\n          (transform.value.scale / zoomRate).toFixed(3)\n        )\n      }\n      break\n    case 'zoomIn':\n      if (transform.value.scale < maxScale) {\n        transform.value.scale = Number.parseFloat(\n          (transform.value.scale * zoomRate).toFixed(3)\n        )\n      }\n      break\n    case 'clockwise':\n      transform.value.deg += rotateDeg\n      emit('rotate', transform.value.deg)\n      break\n    case 'anticlockwise':\n      transform.value.deg -= rotateDeg\n      emit('rotate', transform.value.deg)\n      break\n  }\n  transform.value.enableTransition = enableTransition\n}\n\nfunction onFocusoutPrevented(event: CustomEvent) {\n  if (event.detail?.focusReason === 'pointer') {\n    event.preventDefault()\n  }\n}\n\nfunction onCloseRequested() {\n  if (props.closeOnPressEscape) {\n    hide()\n  }\n}\n\nfunction wheelHandler(e: WheelEvent) {\n  if (!e.ctrlKey) return\n\n  if (e.deltaY < 0) {\n    e.preventDefault()\n    return false\n  } else if (e.deltaY > 0) {\n    e.preventDefault()\n    return false\n  }\n}\n\nwatch(\n  () => scaleClamped.value,\n  (val) => {\n    transform.value.scale = val\n  }\n)\n\nwatch(currentImg, () => {\n  nextTick(() => {\n    const $img = imgRef.value\n    if (!$img?.complete) {\n      loading.value = true\n    }\n  })\n})\n\nwatch(activeIndex, (val) => {\n  reset()\n  emit('switch', val)\n})\n\nonMounted(() => {\n  visible.value = true\n  registerEventListener()\n\n  stopWheelListener = useEventListener('wheel', wheelHandler, {\n    passive: false,\n  })\n})\n\ndefineExpose({\n  /**\n   * @description manually switch image\n   */\n  setActiveItem,\n})\n</script>\n"],"mappings":""}