mirror of https://github.com/nocodb/nocodb
Browse Source
* fix: reload row if the attachments are expired * fix: handle attachment url expiry * fix:build feat: image zoom * fix:attchment comments * fix: async issuepull/9762/head
Anbarasu
4 weeks ago
committed by
GitHub
6 changed files with 208 additions and 29 deletions
@ -1,33 +1,146 @@ |
|||||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||||
|
import { useEventListener } from '@vueuse/core' |
||||||
|
|
||||||
interface Props { |
interface Props { |
||||||
srcs: string[] |
srcs: string[] |
||||||
alt?: string |
alt?: string |
||||||
objectFit?: string |
objectFit?: string |
||||||
|
controls?: boolean |
||||||
} |
} |
||||||
|
|
||||||
const props = defineProps<Props>() |
const props = defineProps<Props>() |
||||||
|
const emit = defineEmits(['error']) |
||||||
|
|
||||||
const index = ref(0) |
const index = ref(0) |
||||||
|
const scale = ref(1) |
||||||
|
const isDragging = ref(false) |
||||||
|
const startPos = ref({ x: 0, y: 0 }) |
||||||
|
const position = ref({ x: 0, y: 0 }) |
||||||
|
const containerRef = ref<HTMLDivElement | null>(null) |
||||||
|
const imageRef = ref<HTMLImageElement | null>(null) |
||||||
|
|
||||||
|
const MIN_SCALE = 1 |
||||||
|
const MAX_SCALE = 4 |
||||||
|
const ZOOM_STEP = 0.5 |
||||||
|
|
||||||
|
const transformStyle = computed(() => ({ |
||||||
|
transform: `translate(${position.value.x}px, ${position.value.y}px) scale(${scale.value})`, |
||||||
|
transition: isDragging.value ? 'none' : 'transform 0.2s ease-out', |
||||||
|
cursor: scale.value > 1 ? 'grab' : 'default', |
||||||
|
})) |
||||||
|
|
||||||
|
const limitDrag = (x: number, y: number) => { |
||||||
|
if (!containerRef.value || !imageRef.value) return { x, y } |
||||||
|
|
||||||
|
const containerRect = containerRef.value.getBoundingClientRect() |
||||||
|
const imageRect = imageRef.value.getBoundingClientRect() |
||||||
|
|
||||||
|
const maxX = (imageRect.width * scale.value - containerRect.width) / 8 |
||||||
|
const maxY = (imageRect.height * scale.value - containerRect.height) / 8 |
||||||
|
|
||||||
|
return { |
||||||
|
x: Math.max(Math.min(x, maxX), -maxX), |
||||||
|
y: Math.max(Math.min(y, maxY), -maxY), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const onError = async () => { |
||||||
|
index.value++ |
||||||
|
if (index.value >= props.srcs.length) { |
||||||
|
const isURLExp = await isURLExpired(props.srcs[0]) |
||||||
|
if (isURLExp.isExpired) { |
||||||
|
emit('error') |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const zoom = (direction: 'in' | 'out') => { |
||||||
|
const newScale = |
||||||
|
direction === 'in' ? Math.min(scale.value + ZOOM_STEP, MAX_SCALE) : Math.max(scale.value - ZOOM_STEP, MIN_SCALE) |
||||||
|
|
||||||
|
scale.value = newScale |
||||||
|
if (newScale === MIN_SCALE) { |
||||||
|
position.value = { x: 0, y: 0 } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
const onError = () => index.value++ |
const startDrag = (clientX: number, clientY: number) => { |
||||||
|
if (scale.value <= 1) return |
||||||
|
isDragging.value = true |
||||||
|
startPos.value = { |
||||||
|
x: clientX - position.value.x, |
||||||
|
y: clientY - position.value.y, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const drag = (clientX: number, clientY: number) => { |
||||||
|
if (!isDragging.value) return |
||||||
|
const newPosition = { |
||||||
|
x: clientX - startPos.value.x, |
||||||
|
y: clientY - startPos.value.y, |
||||||
|
} |
||||||
|
position.value = limitDrag(newPosition.x, newPosition.y) |
||||||
|
} |
||||||
|
|
||||||
|
const stopDrag = () => { |
||||||
|
isDragging.value = false |
||||||
|
} |
||||||
|
|
||||||
|
const stopPropagationIfScaled = (e: MouseEvent | TouchEvent) => { |
||||||
|
if (scale.value <= 1) return |
||||||
|
e.preventDefault() |
||||||
|
e.stopPropagation() |
||||||
|
} |
||||||
|
|
||||||
|
useEventListener(window, 'mousemove', (e: MouseEvent) => drag(e.clientX, e.clientY)) |
||||||
|
useEventListener(window, 'mouseup', stopDrag) |
||||||
|
useEventListener(window, 'touchmove', (e: TouchEvent) => drag(e.touches[0].clientX, e.touches[0].clientY)) |
||||||
|
useEventListener(window, 'touchend', stopDrag) |
||||||
|
|
||||||
|
const onMouseDown = (e: MouseEvent) => { |
||||||
|
stopPropagationIfScaled(e) |
||||||
|
startDrag(e.clientX, e.clientY) |
||||||
|
} |
||||||
|
const onTouchStart = (e: TouchEvent) => { |
||||||
|
stopPropagationIfScaled(e) |
||||||
|
startDrag(e.touches[0].clientX, e.touches[0].clientY) |
||||||
|
} |
||||||
</script> |
</script> |
||||||
|
|
||||||
<template> |
<template> |
||||||
<!-- Replacing with Image component as nuxt-image is not triggering @error when the image doesn't load. Will fix later |
<div class="relative h-full w-full"> |
||||||
TODO: @DarkPhoenix2704 Fix this later |
<div ref="containerRef" class="h-full w-full overflow-hidden" @mousedown="onMouseDown" @touchstart.prevent="onTouchStart"> |
||||||
--> |
|
||||||
<img |
<img |
||||||
v-if="index < props.srcs?.length" |
v-if="index < props.srcs?.length" |
||||||
|
ref="imageRef" |
||||||
:src="props.srcs[index]" |
:src="props.srcs[index]" |
||||||
quality="75" |
|
||||||
:placeholder="props.alt" |
|
||||||
:class="{ |
|
||||||
'!object-contain': props.objectFit === 'contain', |
|
||||||
}" |
|
||||||
loading="lazy" |
|
||||||
:alt="props?.alt || ''" |
:alt="props?.alt || ''" |
||||||
class="m-auto h-full max-h-full w-auto nc-attachment-image object-cover" |
:style="transformStyle" |
||||||
|
:class="{ '!object-contain': props.objectFit === 'contain' }" |
||||||
|
class="m-auto h-full max-h-full w-auto nc-attachment-image object-cover origin-center" |
||||||
|
loading="lazy" |
||||||
@error="onError" |
@error="onError" |
||||||
/> |
/> |
||||||
<component :is="iconMap.imagePlaceholder" v-else /> |
<component :is="iconMap.imagePlaceholder" v-else /> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div v-if="controls" class="absolute mx-auto w-full bottom-4 flex items-center justify-center gap-2"> |
||||||
|
<button |
||||||
|
class="rounded-full bg-gray-800/70 p-2 text-white hover:bg-gray-700/70 disabled:opacity-50" |
||||||
|
:disabled="scale >= MAX_SCALE" |
||||||
|
title="Zoom in" |
||||||
|
@click="zoom('in')" |
||||||
|
> |
||||||
|
<GeneralIcon icon="ncZoomIn" class="h-5 w-5" /> |
||||||
|
</button> |
||||||
|
<button |
||||||
|
class="rounded-full bg-gray-800/70 p-2 text-white hover:bg-gray-700/70 disabled:opacity-50" |
||||||
|
:disabled="scale <= MIN_SCALE" |
||||||
|
title="Zoom out" |
||||||
|
@click="zoom('out')" |
||||||
|
> |
||||||
|
<GeneralIcon icon="ncZoomOut" class="h-5 w-5" /> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
</template> |
</template> |
||||||
|
Loading…
Reference in new issue