hoangnv
hoangnv

Reputation: 31

How can I block the bubble event when clicking on a child button inside a dialog?

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

Answers (2)

LaiFQZzr
LaiFQZzr

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

Rien de jong
Rien de jong

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

Related Questions