Reputation: 31
I have a problem when I click on the zoomOut, zoomIn or downloadImage function, it seems like the closeModal function is called first and then the clicked function is called, how can I prevent that from happening?
my code:
<template>
<TransitionRoot appear :show="isOpen" as="template" :initialFocus="divRef">
<Dialog as="div" @close="closeModal" class="relative z-50">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-black/45" />
</TransitionChild>
<div class="fixed inset-0 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-2.5">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<DialogPanel
class="w-auto transform rounded-xl text-left align-middle shadow-xl transition-all"
>
<div
ref="divRef"
class="flex h-full w-full items-center justify-center rounded-xl"
:class="isLoading ? 'h-48' : ''"
>
<div
v-if="isLoading"
class="absolute inset-0 z-10 flex items-center justify-center bg-gray-100"
>
<LoadingBase size="large" />
</div>
<img
v-if="isAssetFile"
:src="props.url"
@load="onIframeLoad"
class="h-[80vh] w-full object-cover"
alt="Preview"
:style="{ transform: `scale(${zoomLevel})` }"
/>
<embed
v-else-if="isPDFFile(props.url as string)"
:src="getStorageUrl(props.url) + '#toolbar=0'"
width="100%"
class="h-[80vh] max-h-[80vh]"
@load="onIframeLoad"
/>
<img
v-else-if="isImageFile(props.url as string)"
:src="getStorageUrl(props.url)"
@load="onIframeLoad"
class="h-[80vh] object-cover"
alt="Preview"
:style="{ transform: `scale(${zoomLevel})` }"
/>
</div>
</DialogPanel>
</TransitionChild>
</div>
<div
class="fixed bottom-8 left-1/2 flex -translate-x-1/2 items-center justify-center gap-5 rounded-[100px] bg-black/10 px-5 py-3"
>
<button
v-if="isImageFile(props.url as string)"
@click.stop="zoomOut"
:disabled="isMinZoom"
>
<IconZoomOutPre
:class="
isMinZoom ? 'cursor-not-allowed text-white/25' : 'text-white/85 hover:text-white'
"
/>
</button>
<button
v-if="isImageFile(props.url as string)"
@click.stop="zoomIn"
:disabled="isMaxZoom"
>
<IconZoomInPre
:class="
isMaxZoom ? 'cursor-not-allowed text-white/25' : 'text-white/85 hover:text-white'
"
/>
</button>
<button @click.stop="downloadImage">
<IconDownloadPre class="text-white/85 hover:text-white" />
</button>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<script setup lang="ts">
import { getStorageUrl, isImageFile, isPDFFile } from '@/utils/common'
import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
import { computed, ref, watch } from 'vue'
import LoadingBase from '../loading/LoadingBase.vue'
import IconZoomOutPre from '@/components/icon/IconZoomOutPre.vue'
import IconZoomInPre from '@/components/icon/IconZoomInPre.vue'
import IconDownloadPre from '@/components/icon/IconDownloadPre.vue'
const props = defineProps<{
url?: string
isAssetFile?: boolean
}>()
const isOpen = defineModel<boolean>('open', {
default: false
})
const emits = defineEmits(['onOk', 'onCancel'])
const isLoading = ref(true)
const divRef = ref(null)
const zoomLevel = ref(1) // default is 100%
const minZoom = 1
const maxZoom = 3
const isMinZoom = computed(() => zoomLevel.value <= minZoom)
const isMaxZoom = computed(() => zoomLevel.value >= maxZoom)
const onIframeLoad = () => {
isLoading.value = false
}
const closeModal = () => {
isOpen.value = false
emits('onCancel')
}
const zoomIn = () => {
if (!isMaxZoom.value) zoomLevel.value = Math.min(zoomLevel.value + 0.1, maxZoom) // maximum zoom in is 300%
}
const zoomOut = () => {
if (!isMinZoom.value) zoomLevel.value = Math.max(zoomLevel.value - 0.1, minZoom) // maximum zoom out is 50%
}
const downloadImage = () => {
if (!props.url) return
const link = document.createElement('a')
link.href = props.url
link.download = 'image.jpg'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
watch(isOpen, (newValue) => {
if (newValue && props.url) {
isLoading.value = true
zoomLevel.value = 1
}
})
</script>
UPDATE I tried adding a flag to check if I'm zooming or not, it works, but there's a problem: when I zoom in or zoom out too quickly, the modal will close?
<template>
<TransitionRoot appear :show="isOpen" as="template" :initialFocus="divRef">
<Dialog as="div" @close="closeModal" class="relative z-50">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-black/45" />
</TransitionChild>
<div class="fixed inset-0 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-2.5">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<DialogPanel
class="w-auto transform rounded-xl text-left align-middle shadow-xl transition-all"
>
<div
ref="divRef"
class="flex h-full w-full items-center justify-center rounded-xl"
:class="isLoading ? 'h-48' : ''"
>
<div
v-if="isLoading"
class="absolute inset-0 z-10 flex items-center justify-center bg-gray-100"
>
<LoadingBase size="large" />
</div>
<img
v-if="isAssetFile"
:src="props.url"
@load="onIframeLoad"
class="h-[80vh] w-full object-cover"
alt="Preview"
:style="{ transform: `scale(${zoomLevel})` }"
/>
<embed
v-else-if="isPDFFile(props.url as string)"
:src="getStorageUrl(props.url) + '#toolbar=0'"
width="100%"
class="h-[80vh] max-h-[80vh]"
@load="onIframeLoad"
/>
<img
v-else-if="isImageFile(props.url as string)"
:src="getStorageUrl(props.url)"
@load="onIframeLoad"
class="h-[80vh] object-cover"
alt="Preview"
:style="{ transform: `scale(${zoomLevel})` }"
/>
</div>
</DialogPanel>
</TransitionChild>
</div>
<div
class="fixed bottom-8 left-1/2 flex -translate-x-1/2 items-center justify-center gap-5 rounded-[100px] bg-black/10 px-5 py-3"
>
<button
v-if="isImageFile(props.url as string)"
@click.stop.prevent="zoomOut"
@mousedown="handleZoomStart"
@mouseup="handleZoomEnd"
:disabled="isMinZoom"
>
<IconZoomOutPre
:class="
isMinZoom ? 'cursor-not-allowed text-white/25' : 'text-white/85 hover:text-white'
"
/>
</button>
<button
v-if="isImageFile(props.url as string)"
@click.stop.prevent="zoomIn"
@mousedown="handleZoomStart"
@mouseup="handleZoomEnd"
:disabled="isMaxZoom"
>
<IconZoomInPre
:class="
isMaxZoom ? 'cursor-not-allowed text-white/25' : 'text-white/85 hover:text-white'
"
/>
</button>
<button @click.stop.prevent="downloadImage">
<IconDownloadPre class="text-white/85 hover:text-white" />
</button>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<script setup lang="ts">
import { getStorageUrl, isImageFile, isPDFFile } from '@/utils/common'
import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
import { computed, ref, watch } from 'vue'
import LoadingBase from '../loading/LoadingBase.vue'
import IconZoomOutPre from '@/components/icon/IconZoomOutPre.vue'
import IconZoomInPre from '@/components/icon/IconZoomInPre.vue'
import IconDownloadPre from '@/components/icon/IconDownloadPre.vue'
const props = defineProps<{
url?: string
isAssetFile?: boolean
}>()
const isOpen = defineModel<boolean>('open', {
default: false
})
const emits = defineEmits(['onOk', 'onCancel'])
const isLoading = ref(true)
const divRef = ref(null)
const zoomLevel = ref(1) // default is 100%
const isZooming = ref(false)
const minZoom = 1
const maxZoom = 3
const isMinZoom = computed(() => zoomLevel.value <= minZoom)
const isMaxZoom = computed(() => zoomLevel.value >= maxZoom)
const onIframeLoad = () => {
isLoading.value = false
}
const closeModal = () => {
if (isZooming.value) return
isOpen.value = false
emits('onCancel')
}
const handleZoomStart = () => {
isZooming.value = true
}
const handleZoomEnd = () => {
setTimeout(() => {
isZooming.value = false
}, 300)
}
const zoomIn = () => {
if (!isMaxZoom.value) zoomLevel.value = Math.min(zoomLevel.value + 0.1, maxZoom) // maximum zoom in is 300%
}
const zoomOut = () => {
if (!isMinZoom.value) zoomLevel.value = Math.max(zoomLevel.value - 0.1, minZoom) // maximum zoom out is 50%
}
const downloadImage = () => {
if (!props.url) return
const link = document.createElement('a')
link.href = props.url
link.download = 'image.jpg'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
watch(isOpen, (newValue) => {
if (newValue && props.url) {
isLoading.value = true
zoomLevel.value = 1
}
})
</script>
i tried @click.stop, @click.capture [email protected] but it doesn't work. I would appreciate it if you could give me some suggestions.
Upvotes: 0
Views: 38
Reputation: 101
I think the bug is due to the fact that you put <button>
outside the <DialogPanel>
causing @close
to be triggered, can you put the button inside the <DialogPanel>
, try it if you must put it outside:
Remove the @close
from the <Dialog>
and let closeModal()
decide whether to close or not, but this will cause the Dialog to not close when you click on the mask layer.
Upvotes: 0
Reputation: 11
Consider turning the @click.stop
into @click.prevent.stop
and handle the closeModal within the zoomOut, zoomIn and downloadImage methods
the .prevent ignores the @click which you've added to the main component while still keeping the closeModal functionality outside of your buttons
Information on Event Modifiers: https://vuejs.org/guide/essentials/event-handling.html#event-modifiers
Upvotes: 0