mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-26 01:34:07 +00:00
feat: add batch image navigation to ImageCompare node (#9151)
## Summary Add batch image navigation to the ImageCompare node so users can browse all images in a batch instead of only seeing the first one. ## Changes The backend already returns all batch images in a_images/b_images arrays, but the frontend only used index [0]. Now all images are mapped to URLs and a navigation bar with prev/next controls appears above the comparison slider when either side has more than one image. A/B sides navigate independently. Extracted a reusable BatchNavigation component for the index selector UI. fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/9098 ## Screenshots (if applicable) https://github.com/user-attachments/assets/a801cc96-9182-4b0d-a342-4e6107290f47 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9151-feat-add-batch-image-navigation-to-ImageCompare-node-3116d73d365081498be6d401773619a3) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -26,22 +26,18 @@ useExtensionService().registerExtension({
|
||||
const { a_images: aImages, b_images: bImages } = output
|
||||
const rand = app.getRandParam()
|
||||
|
||||
const beforeUrl =
|
||||
aImages && aImages.length > 0
|
||||
? api.apiURL(`/view?${new URLSearchParams(aImages[0])}${rand}`)
|
||||
: ''
|
||||
const afterUrl =
|
||||
bImages && bImages.length > 0
|
||||
? api.apiURL(`/view?${new URLSearchParams(bImages[0])}${rand}`)
|
||||
: ''
|
||||
const toUrl = (params: Record<string, string>) =>
|
||||
api.apiURL(`/view?${new URLSearchParams(params)}${rand}`)
|
||||
|
||||
const beforeImages =
|
||||
aImages && aImages.length > 0 ? aImages.map(toUrl) : []
|
||||
const afterImages =
|
||||
bImages && bImages.length > 0 ? bImages.map(toUrl) : []
|
||||
|
||||
const widget = node.widgets?.find((w) => w.type === 'imagecompare')
|
||||
|
||||
if (widget) {
|
||||
widget.value = {
|
||||
before: beforeUrl,
|
||||
after: afterUrl
|
||||
}
|
||||
widget.value = { beforeImages, afterImages }
|
||||
widget.callback?.(widget.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1796,7 +1796,12 @@
|
||||
"errorNotSupported": "Clipboard API not supported in your browser"
|
||||
},
|
||||
"imageCompare": {
|
||||
"noImages": "No images to compare"
|
||||
"noImages": "No images to compare",
|
||||
"batchLabelA": "A:",
|
||||
"batchLabelB": "B:"
|
||||
},
|
||||
"batch": {
|
||||
"index": "{current} / {total}"
|
||||
},
|
||||
"load3d": {
|
||||
"switchCamera": "Switch Camera",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div v-if="count > 1" class="flex items-center gap-1">
|
||||
<span class="mr-1 text-muted-foreground">
|
||||
<slot name="label" />
|
||||
</span>
|
||||
<Button
|
||||
variant="muted-textonly"
|
||||
size="icon-sm"
|
||||
:disabled="index === 0"
|
||||
data-testid="batch-prev"
|
||||
@click="index--"
|
||||
>
|
||||
<i class="icon-[lucide--chevron-left]" />
|
||||
</Button>
|
||||
<span data-testid="batch-counter">
|
||||
{{ $t('batch.index', { current: index + 1, total: count }) }}
|
||||
</span>
|
||||
<Button
|
||||
variant="muted-textonly"
|
||||
size="icon-sm"
|
||||
:disabled="index === count - 1"
|
||||
data-testid="batch-next"
|
||||
@click="index++"
|
||||
>
|
||||
<i class="icon-[lucide--chevron-right]" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
const index = defineModel<number>({ required: true })
|
||||
|
||||
defineProps<{
|
||||
count: number
|
||||
}>()
|
||||
</script>
|
||||
@@ -24,7 +24,12 @@ describe('WidgetImageCompare Display', () => {
|
||||
return mount(WidgetImageCompare, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key: string) => key
|
||||
$t: (key: string, params?: Record<string, unknown>) => {
|
||||
if (key === 'batch.index' && params) {
|
||||
return `${params.current} / ${params.total}`
|
||||
}
|
||||
return key
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
@@ -37,8 +42,8 @@ describe('WidgetImageCompare Display', () => {
|
||||
describe('Component Rendering', () => {
|
||||
it('renders with proper structure and styling when images are provided', () => {
|
||||
const value: ImageCompareValue = {
|
||||
before: 'https://example.com/before.jpg',
|
||||
after: 'https://example.com/after.jpg'
|
||||
beforeImages: ['https://example.com/before.jpg'],
|
||||
afterImages: ['https://example.com/after.jpg']
|
||||
}
|
||||
const widget = createMockWidget(value)
|
||||
const wrapper = mountComponent(widget)
|
||||
@@ -46,7 +51,7 @@ describe('WidgetImageCompare Display', () => {
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images).toHaveLength(2)
|
||||
|
||||
// In the new implementation: after image is first (background), before image is second (overlay)
|
||||
// After image is first (background), before image is second (overlay)
|
||||
expect(images[0].attributes('src')).toBe('https://example.com/after.jpg')
|
||||
expect(images[1].attributes('src')).toBe('https://example.com/before.jpg')
|
||||
|
||||
@@ -60,8 +65,8 @@ describe('WidgetImageCompare Display', () => {
|
||||
it('handles alt text correctly - custom, default, and empty', () => {
|
||||
// Test custom alt text
|
||||
const customAltValue: ImageCompareValue = {
|
||||
before: 'https://example.com/before.jpg',
|
||||
after: 'https://example.com/after.jpg',
|
||||
beforeImages: ['https://example.com/before.jpg'],
|
||||
afterImages: ['https://example.com/after.jpg'],
|
||||
beforeAlt: 'Original design',
|
||||
afterAlt: 'Updated design'
|
||||
}
|
||||
@@ -73,8 +78,8 @@ describe('WidgetImageCompare Display', () => {
|
||||
|
||||
// Test default alt text
|
||||
const defaultAltValue: ImageCompareValue = {
|
||||
before: 'https://example.com/before.jpg',
|
||||
after: 'https://example.com/after.jpg'
|
||||
beforeImages: ['https://example.com/before.jpg'],
|
||||
afterImages: ['https://example.com/after.jpg']
|
||||
}
|
||||
const defaultWrapper = mountComponent(createMockWidget(defaultAltValue))
|
||||
const defaultImages = defaultWrapper.findAll('img')
|
||||
@@ -83,8 +88,8 @@ describe('WidgetImageCompare Display', () => {
|
||||
|
||||
// Test empty string alt text (falls back to default)
|
||||
const emptyAltValue: ImageCompareValue = {
|
||||
before: 'https://example.com/before.jpg',
|
||||
after: 'https://example.com/after.jpg',
|
||||
beforeImages: ['https://example.com/before.jpg'],
|
||||
afterImages: ['https://example.com/after.jpg'],
|
||||
beforeAlt: '',
|
||||
afterAlt: ''
|
||||
}
|
||||
@@ -96,12 +101,10 @@ describe('WidgetImageCompare Display', () => {
|
||||
|
||||
it('handles partial image URLs gracefully', () => {
|
||||
// Only before image provided
|
||||
const beforeOnlyValue: ImageCompareValue = {
|
||||
before: 'https://example.com/before.jpg',
|
||||
after: ''
|
||||
}
|
||||
const beforeOnlyWrapper = mountComponent(
|
||||
createMockWidget(beforeOnlyValue)
|
||||
createMockWidget({
|
||||
beforeImages: ['https://example.com/before.jpg']
|
||||
})
|
||||
)
|
||||
const beforeOnlyImages = beforeOnlyWrapper.findAll('img')
|
||||
expect(beforeOnlyImages).toHaveLength(1)
|
||||
@@ -110,11 +113,11 @@ describe('WidgetImageCompare Display', () => {
|
||||
)
|
||||
|
||||
// Only after image provided
|
||||
const afterOnlyValue: ImageCompareValue = {
|
||||
before: '',
|
||||
after: 'https://example.com/after.jpg'
|
||||
}
|
||||
const afterOnlyWrapper = mountComponent(createMockWidget(afterOnlyValue))
|
||||
const afterOnlyWrapper = mountComponent(
|
||||
createMockWidget({
|
||||
afterImages: ['https://example.com/after.jpg']
|
||||
})
|
||||
)
|
||||
const afterOnlyImages = afterOnlyWrapper.findAll('img')
|
||||
expect(afterOnlyImages).toHaveLength(1)
|
||||
expect(afterOnlyImages[0].attributes('src')).toBe(
|
||||
@@ -139,8 +142,8 @@ describe('WidgetImageCompare Display', () => {
|
||||
describe('Readonly Mode', () => {
|
||||
it('renders normally in readonly mode', () => {
|
||||
const value: ImageCompareValue = {
|
||||
before: 'https://example.com/before.jpg',
|
||||
after: 'https://example.com/after.jpg'
|
||||
beforeImages: ['https://example.com/before.jpg'],
|
||||
afterImages: ['https://example.com/after.jpg']
|
||||
}
|
||||
const widget = createMockWidget(value)
|
||||
const wrapper = mountComponent(widget, true)
|
||||
@@ -160,8 +163,11 @@ describe('WidgetImageCompare Display', () => {
|
||||
expect(wrapper.text()).toContain('imageCompare.noImages')
|
||||
})
|
||||
|
||||
it('shows no images message when both URLs are empty', () => {
|
||||
const value: ImageCompareValue = { before: '', after: '' }
|
||||
it('shows no images message when both arrays are empty', () => {
|
||||
const value: ImageCompareValue = {
|
||||
beforeImages: [],
|
||||
afterImages: []
|
||||
}
|
||||
const widget = createMockWidget(value)
|
||||
const wrapper = mountComponent(widget)
|
||||
|
||||
@@ -181,42 +187,40 @@ describe('WidgetImageCompare Display', () => {
|
||||
})
|
||||
|
||||
it('handles special content - long URLs, special characters, and long alt text', () => {
|
||||
// Test very long URLs
|
||||
const longUrl = 'https://example.com/' + 'a'.repeat(1000) + '.jpg'
|
||||
const longUrlValue: ImageCompareValue = {
|
||||
before: longUrl,
|
||||
after: longUrl
|
||||
}
|
||||
const longUrlWrapper = mountComponent(createMockWidget(longUrlValue))
|
||||
const longUrlWrapper = mountComponent(
|
||||
createMockWidget({
|
||||
beforeImages: [longUrl],
|
||||
afterImages: [longUrl]
|
||||
})
|
||||
)
|
||||
const longUrlImages = longUrlWrapper.findAll('img')
|
||||
expect(longUrlImages[0].attributes('src')).toBe(longUrl)
|
||||
expect(longUrlImages[1].attributes('src')).toBe(longUrl)
|
||||
|
||||
// Test special characters in URLs
|
||||
const specialUrl =
|
||||
'https://example.com/path with spaces & symbols!@#$.jpg'
|
||||
const specialUrlValue: ImageCompareValue = {
|
||||
before: specialUrl,
|
||||
after: specialUrl
|
||||
}
|
||||
const specialUrlWrapper = mountComponent(
|
||||
createMockWidget(specialUrlValue)
|
||||
createMockWidget({
|
||||
beforeImages: [specialUrl],
|
||||
afterImages: [specialUrl]
|
||||
})
|
||||
)
|
||||
const specialUrlImages = specialUrlWrapper.findAll('img')
|
||||
expect(specialUrlImages[0].attributes('src')).toBe(specialUrl)
|
||||
expect(specialUrlImages[1].attributes('src')).toBe(specialUrl)
|
||||
|
||||
// Test very long alt text
|
||||
const longAlt =
|
||||
'Very long alt text that exceeds normal length: ' +
|
||||
'description '.repeat(50)
|
||||
const longAltValue: ImageCompareValue = {
|
||||
before: 'https://example.com/before.jpg',
|
||||
after: 'https://example.com/after.jpg',
|
||||
beforeAlt: longAlt,
|
||||
afterAlt: longAlt
|
||||
}
|
||||
const longAltWrapper = mountComponent(createMockWidget(longAltValue))
|
||||
const longAltWrapper = mountComponent(
|
||||
createMockWidget({
|
||||
beforeImages: ['https://example.com/before.jpg'],
|
||||
afterImages: ['https://example.com/after.jpg'],
|
||||
beforeAlt: longAlt,
|
||||
afterAlt: longAlt
|
||||
})
|
||||
)
|
||||
const longAltImages = longAltWrapper.findAll('img')
|
||||
expect(longAltImages[0].attributes('alt')).toBe(longAlt)
|
||||
expect(longAltImages[1].attributes('alt')).toBe(longAlt)
|
||||
@@ -226,16 +230,14 @@ describe('WidgetImageCompare Display', () => {
|
||||
describe('Template Structure', () => {
|
||||
it('correctly renders after image as background and before image as overlay', () => {
|
||||
const value: ImageCompareValue = {
|
||||
before: 'https://example.com/before.jpg',
|
||||
after: 'https://example.com/after.jpg'
|
||||
beforeImages: ['https://example.com/before.jpg'],
|
||||
afterImages: ['https://example.com/after.jpg']
|
||||
}
|
||||
const widget = createMockWidget(value)
|
||||
const wrapper = mountComponent(widget)
|
||||
|
||||
const images = wrapper.findAll('img')
|
||||
// After image is rendered first as background
|
||||
expect(images[0].attributes('src')).toBe('https://example.com/after.jpg')
|
||||
// Before image is rendered second as overlay with clipPath
|
||||
expect(images[1].attributes('src')).toBe('https://example.com/before.jpg')
|
||||
expect(images[1].classes()).toContain('absolute')
|
||||
})
|
||||
@@ -243,26 +245,26 @@ describe('WidgetImageCompare Display', () => {
|
||||
|
||||
describe('Integration', () => {
|
||||
it('works with various URL types - data URLs and blob URLs', () => {
|
||||
// Test data URLs
|
||||
const dataUrl =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='
|
||||
const dataUrlValue: ImageCompareValue = {
|
||||
before: dataUrl,
|
||||
after: dataUrl
|
||||
}
|
||||
const dataUrlWrapper = mountComponent(createMockWidget(dataUrlValue))
|
||||
const dataUrlWrapper = mountComponent(
|
||||
createMockWidget({
|
||||
beforeImages: [dataUrl],
|
||||
afterImages: [dataUrl]
|
||||
})
|
||||
)
|
||||
const dataUrlImages = dataUrlWrapper.findAll('img')
|
||||
expect(dataUrlImages[0].attributes('src')).toBe(dataUrl)
|
||||
expect(dataUrlImages[1].attributes('src')).toBe(dataUrl)
|
||||
|
||||
// Test blob URLs
|
||||
const blobUrl =
|
||||
'blob:http://example.com/12345678-1234-1234-1234-123456789012'
|
||||
const blobUrlValue: ImageCompareValue = {
|
||||
before: blobUrl,
|
||||
after: blobUrl
|
||||
}
|
||||
const blobUrlWrapper = mountComponent(createMockWidget(blobUrlValue))
|
||||
const blobUrlWrapper = mountComponent(
|
||||
createMockWidget({
|
||||
beforeImages: [blobUrl],
|
||||
afterImages: [blobUrl]
|
||||
})
|
||||
)
|
||||
const blobUrlImages = blobUrlWrapper.findAll('img')
|
||||
expect(blobUrlImages[0].attributes('src')).toBe(blobUrl)
|
||||
expect(blobUrlImages[1].attributes('src')).toBe(blobUrl)
|
||||
@@ -272,8 +274,8 @@ describe('WidgetImageCompare Display', () => {
|
||||
describe('Slider Element', () => {
|
||||
it('renders slider divider when images are present', () => {
|
||||
const value: ImageCompareValue = {
|
||||
before: 'https://example.com/before.jpg',
|
||||
after: 'https://example.com/after.jpg'
|
||||
beforeImages: ['https://example.com/before.jpg'],
|
||||
afterImages: ['https://example.com/after.jpg']
|
||||
}
|
||||
const widget = createMockWidget(value)
|
||||
const wrapper = mountComponent(widget)
|
||||
@@ -291,4 +293,140 @@ describe('WidgetImageCompare Display', () => {
|
||||
expect(slider.exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Batch Navigation', () => {
|
||||
const beforeImages = [
|
||||
'https://example.com/a1.jpg',
|
||||
'https://example.com/a2.jpg',
|
||||
'https://example.com/a3.jpg'
|
||||
]
|
||||
const afterImages = [
|
||||
'https://example.com/b1.jpg',
|
||||
'https://example.com/b2.jpg'
|
||||
]
|
||||
|
||||
it('shows batch nav when either side has multiple images', () => {
|
||||
const value: ImageCompareValue = { beforeImages, afterImages }
|
||||
const wrapper = mountComponent(createMockWidget(value))
|
||||
|
||||
expect(wrapper.find('[data-testid="batch-nav"]').exists()).toBe(true)
|
||||
|
||||
const beforeBatch = wrapper.find('[data-testid="before-batch"]')
|
||||
const afterBatch = wrapper.find('[data-testid="after-batch"]')
|
||||
expect(beforeBatch.find('[data-testid="batch-counter"]').text()).toBe(
|
||||
'1 / 3'
|
||||
)
|
||||
expect(afterBatch.find('[data-testid="batch-counter"]').text()).toBe(
|
||||
'1 / 2'
|
||||
)
|
||||
})
|
||||
|
||||
it('hides batch nav for single images', () => {
|
||||
const value: ImageCompareValue = {
|
||||
beforeImages: ['https://example.com/a1.jpg'],
|
||||
afterImages: ['https://example.com/b1.jpg']
|
||||
}
|
||||
const wrapper = mountComponent(createMockWidget(value))
|
||||
|
||||
expect(wrapper.find('[data-testid="batch-nav"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('hides batch nav when no batch arrays are provided', () => {
|
||||
const wrapper = mountComponent(createMockWidget({} as ImageCompareValue))
|
||||
|
||||
expect(wrapper.find('[data-testid="batch-nav"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('navigates before images with prev/next buttons', async () => {
|
||||
const value: ImageCompareValue = { beforeImages, afterImages }
|
||||
const wrapper = mountComponent(createMockWidget(value))
|
||||
const beforeBatch = wrapper.find('[data-testid="before-batch"]')
|
||||
|
||||
// Initially shows first before image
|
||||
expect(wrapper.findAll('img')[1].attributes('src')).toBe(
|
||||
'https://example.com/a1.jpg'
|
||||
)
|
||||
|
||||
// Click next on before
|
||||
await beforeBatch.find('[data-testid="batch-next"]').trigger('click')
|
||||
expect(wrapper.findAll('img')[1].attributes('src')).toBe(
|
||||
'https://example.com/a2.jpg'
|
||||
)
|
||||
expect(beforeBatch.find('[data-testid="batch-counter"]').text()).toBe(
|
||||
'2 / 3'
|
||||
)
|
||||
|
||||
// Click next again
|
||||
await beforeBatch.find('[data-testid="batch-next"]').trigger('click')
|
||||
expect(wrapper.findAll('img')[1].attributes('src')).toBe(
|
||||
'https://example.com/a3.jpg'
|
||||
)
|
||||
expect(beforeBatch.find('[data-testid="batch-counter"]').text()).toBe(
|
||||
'3 / 3'
|
||||
)
|
||||
|
||||
// Next button should be disabled at last index
|
||||
expect(
|
||||
beforeBatch.find('[data-testid="batch-next"]').attributes('disabled')
|
||||
).toBeDefined()
|
||||
|
||||
// Click prev
|
||||
await beforeBatch.find('[data-testid="batch-prev"]').trigger('click')
|
||||
expect(wrapper.findAll('img')[1].attributes('src')).toBe(
|
||||
'https://example.com/a2.jpg'
|
||||
)
|
||||
})
|
||||
|
||||
it('navigates after images independently from before images', async () => {
|
||||
const value: ImageCompareValue = { beforeImages, afterImages }
|
||||
const wrapper = mountComponent(createMockWidget(value))
|
||||
const afterBatch = wrapper.find('[data-testid="after-batch"]')
|
||||
|
||||
// Navigate after to index 1
|
||||
await afterBatch.find('[data-testid="batch-next"]').trigger('click')
|
||||
expect(afterBatch.find('[data-testid="batch-counter"]').text()).toBe(
|
||||
'2 / 2'
|
||||
)
|
||||
|
||||
// After image should be b2, before image should still be a1
|
||||
const images = wrapper.findAll('img')
|
||||
expect(images[0].attributes('src')).toBe('https://example.com/b2.jpg')
|
||||
expect(images[1].attributes('src')).toBe('https://example.com/a1.jpg')
|
||||
})
|
||||
|
||||
it('disables prev button at first index', () => {
|
||||
const value: ImageCompareValue = { beforeImages, afterImages }
|
||||
const wrapper = mountComponent(createMockWidget(value))
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-testid="before-batch"]')
|
||||
.find('[data-testid="batch-prev"]')
|
||||
.attributes('disabled')
|
||||
).toBeDefined()
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-testid="after-batch"]')
|
||||
.find('[data-testid="batch-prev"]')
|
||||
.attributes('disabled')
|
||||
).toBeDefined()
|
||||
})
|
||||
|
||||
it('only shows controls for the side with multiple images', () => {
|
||||
const value: ImageCompareValue = {
|
||||
beforeImages,
|
||||
afterImages: ['https://example.com/b1.jpg']
|
||||
}
|
||||
const wrapper = mountComponent(createMockWidget(value))
|
||||
|
||||
expect(wrapper.find('[data-testid="batch-nav"]').exists()).toBe(true)
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-testid="before-batch"]')
|
||||
.find('[data-testid="batch-counter"]')
|
||||
.exists()
|
||||
).toBe(true)
|
||||
expect(wrapper.find('[data-testid="after-batch"]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
<template>
|
||||
<div ref="containerRef" class="relative size-full min-h-32 overflow-hidden">
|
||||
<div v-if="beforeImage || afterImage" class="relative size-full">
|
||||
<div class="flex size-full min-h-32 flex-col overflow-hidden">
|
||||
<div
|
||||
v-if="showBatchNav"
|
||||
class="flex shrink-0 justify-between px-2 py-1 text-xs"
|
||||
data-testid="batch-nav"
|
||||
>
|
||||
<BatchNavigation
|
||||
v-model="beforeIndex"
|
||||
:count="beforeBatchCount"
|
||||
data-testid="before-batch"
|
||||
>
|
||||
<template #label>{{ $t('imageCompare.batchLabelA') }}</template>
|
||||
</BatchNavigation>
|
||||
<div v-if="beforeBatchCount <= 1" />
|
||||
|
||||
<BatchNavigation
|
||||
v-model="afterIndex"
|
||||
:count="afterBatchCount"
|
||||
data-testid="after-batch"
|
||||
>
|
||||
<template #label>{{ $t('imageCompare.batchLabelB') }}</template>
|
||||
</BatchNavigation>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="beforeImage || afterImage"
|
||||
ref="containerRef"
|
||||
class="relative min-h-0 flex-1"
|
||||
>
|
||||
<img
|
||||
v-if="afterImage"
|
||||
:src="afterImage"
|
||||
@@ -25,7 +52,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex size-full items-center justify-center">
|
||||
<div v-else class="flex min-h-0 flex-1 items-center justify-center">
|
||||
{{ $t('imageCompare.noImages') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,9 +64,11 @@ import { computed, ref, watch } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import BatchNavigation from './BatchNavigation.vue'
|
||||
|
||||
export interface ImageCompareValue {
|
||||
before: string
|
||||
after: string
|
||||
beforeImages?: string[]
|
||||
afterImages?: string[]
|
||||
beforeAlt?: string
|
||||
afterAlt?: string
|
||||
initialPosition?: number
|
||||
@@ -52,6 +81,8 @@ const props = defineProps<{
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const sliderPosition = ref(50)
|
||||
const beforeIndex = ref(0)
|
||||
const afterIndex = ref(0)
|
||||
|
||||
const { elementX, elementWidth, isOutside } = useMouseInElement(containerRef)
|
||||
|
||||
@@ -61,14 +92,48 @@ watch([elementX, elementWidth, isOutside], ([x, width, outside]) => {
|
||||
}
|
||||
})
|
||||
|
||||
const parsedValue = computed(() => {
|
||||
const value = props.widget.value
|
||||
return typeof value === 'string' ? null : value
|
||||
})
|
||||
|
||||
const beforeBatchCount = computed(
|
||||
() => parsedValue.value?.beforeImages?.length ?? 0
|
||||
)
|
||||
|
||||
const afterBatchCount = computed(
|
||||
() => parsedValue.value?.afterImages?.length ?? 0
|
||||
)
|
||||
|
||||
const showBatchNav = computed(
|
||||
() => beforeBatchCount.value > 1 || afterBatchCount.value > 1
|
||||
)
|
||||
|
||||
// Reset indices when batch data changes
|
||||
watch(
|
||||
() => parsedValue.value?.beforeImages,
|
||||
() => {
|
||||
beforeIndex.value = 0
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => parsedValue.value?.afterImages,
|
||||
() => {
|
||||
afterIndex.value = 0
|
||||
}
|
||||
)
|
||||
|
||||
const beforeImage = computed(() => {
|
||||
const value = props.widget.value
|
||||
return typeof value === 'string' ? value : value?.before || ''
|
||||
if (typeof value === 'string') return value
|
||||
return value?.beforeImages?.[beforeIndex.value] ?? ''
|
||||
})
|
||||
|
||||
const afterImage = computed(() => {
|
||||
const value = props.widget.value
|
||||
return typeof value === 'string' ? '' : value?.after || ''
|
||||
if (typeof value === 'string') return ''
|
||||
return value?.afterImages?.[afterIndex.value] ?? ''
|
||||
})
|
||||
|
||||
const beforeAlt = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user