feat(ui): click outside the image in the viewer modal closes it (parity with IR)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
ref="canvasEl"
|
||||||
class="fc-canvas"
|
class="fc-canvas"
|
||||||
:class="{ 'fc-canvas--zoomed': panZoom.state.scale > 1 }"
|
:class="{ 'fc-canvas--zoomed': panZoom.state.scale > 1 }"
|
||||||
@wheel="panZoom.handlers.onWheel"
|
@wheel="panZoom.handlers.onWheel"
|
||||||
@pointerdown="panZoom.handlers.onPointerDown"
|
@pointerdown="panZoom.handlers.onPointerDown"
|
||||||
@pointermove="panZoom.handlers.onPointerMove"
|
@pointermove="panZoom.handlers.onPointerMove"
|
||||||
@pointerup="panZoom.handlers.onPointerUp"
|
@pointerup="panZoom.handlers.onPointerUp"
|
||||||
@click="panZoom.handlers.onClick"
|
@click="onCanvasClick"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
ref="imgEl"
|
||||||
:src="src" :alt="alt"
|
:src="src" :alt="alt"
|
||||||
:style="{
|
:style="{
|
||||||
transform: `translate(${panZoom.state.x}px, ${panZoom.state.y}px) scale(${panZoom.state.scale})`
|
transform: `translate(${panZoom.state.x}px, ${panZoom.state.y}px) scale(${panZoom.state.scale})`
|
||||||
@@ -19,14 +21,42 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { usePanZoom } from '../../composables/usePanZoom.js'
|
import { usePanZoom } from '../../composables/usePanZoom.js'
|
||||||
|
|
||||||
const props = defineProps({ src: String, alt: String })
|
const props = defineProps({ src: String, alt: String })
|
||||||
|
const emit = defineEmits(['close-request'])
|
||||||
const panZoom = usePanZoom()
|
const panZoom = usePanZoom()
|
||||||
|
|
||||||
|
const canvasEl = ref(null)
|
||||||
|
const imgEl = ref(null)
|
||||||
|
|
||||||
// Reset zoom when the src changes (prev/next nav).
|
// Reset zoom when the src changes (prev/next nav).
|
||||||
watch(() => props.src, () => panZoom.reset())
|
watch(() => props.src, () => panZoom.reset())
|
||||||
|
|
||||||
|
// Click on the image → toggle zoom (IR/expected behavior). Click on
|
||||||
|
// the haze area around the image → request close. We use the image's
|
||||||
|
// own bounding rect rather than @click.self because the <img> sets
|
||||||
|
// pointer-events: none for the pan-drag pathway, so the click always
|
||||||
|
// targets the canvas div regardless of where the cursor was.
|
||||||
|
function onCanvasClick(ev) {
|
||||||
|
if (panZoom.state.scale > 1) {
|
||||||
|
// When zoomed, any click resets zoom — preserve old behavior.
|
||||||
|
panZoom.handlers.onClick(ev)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const img = imgEl.value
|
||||||
|
if (!img) return
|
||||||
|
const r = img.getBoundingClientRect()
|
||||||
|
const inside =
|
||||||
|
ev.clientX >= r.left && ev.clientX <= r.right &&
|
||||||
|
ev.clientY >= r.top && ev.clientY <= r.bottom
|
||||||
|
if (inside) {
|
||||||
|
panZoom.handlers.onClick(ev)
|
||||||
|
} else {
|
||||||
|
emit('close-request')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -39,9 +39,11 @@
|
|||||||
<template v-else-if="modal.current">
|
<template v-else-if="modal.current">
|
||||||
<ImageCanvas
|
<ImageCanvas
|
||||||
v-if="!isVideo" :src="modal.current.image_url" :alt="`Image ${modal.current.id}`"
|
v-if="!isVideo" :src="modal.current.image_url" :alt="`Image ${modal.current.id}`"
|
||||||
|
@close-request="$emit('close')"
|
||||||
/>
|
/>
|
||||||
<VideoCanvas
|
<VideoCanvas
|
||||||
v-else :src="modal.current.image_url" :mime="modal.current.mime"
|
v-else :src="modal.current.image_url" :mime="modal.current.mime"
|
||||||
|
@close-request="$emit('close')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<v-alert v-else-if="modal.error" type="error" variant="tonal">
|
<v-alert v-else-if="modal.error" type="error" variant="tonal">
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fc-canvas fc-canvas--video">
|
<div class="fc-canvas fc-canvas--video" @click.self="$emit('close-request')">
|
||||||
<video
|
<video
|
||||||
v-if="playable" :src="src" controls playsinline preload="metadata"
|
v-if="playable" :src="src" controls playsinline preload="metadata"
|
||||||
class="fc-canvas__video"
|
class="fc-canvas__video"
|
||||||
|
@click.stop
|
||||||
/>
|
/>
|
||||||
<div v-else class="fc-canvas__unsupported">
|
<div v-else class="fc-canvas__unsupported" @click.stop>
|
||||||
<v-icon size="56" icon="mdi-video-off-outline" />
|
<v-icon size="56" icon="mdi-video-off-outline" />
|
||||||
<h3 class="fc-canvas__unsupported-title">Format not browser-playable</h3>
|
<h3 class="fc-canvas__unsupported-title">Format not browser-playable</h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
defineEmits(['close-request'])
|
||||||
const props = defineProps({ src: String, mime: String })
|
const props = defineProps({ src: String, mime: String })
|
||||||
|
|
||||||
const BROWSER_PLAYABLE = new Set([
|
const BROWSER_PLAYABLE = new Set([
|
||||||
|
|||||||
Reference in New Issue
Block a user