nicwands.com/src/components/strapi/Media.vue
2026-06-09 15:08:37 -04:00

139 lines
3.1 KiB
Vue

<template>
<div
v-if="image"
:class="['strapi-media', { 'fill-space': fillSpace }, `fit-${fit}`]"
:style="styles"
>
<div class="image-sizer">
<img
:src="addURLBase(image.url)"
:srcset="srcset"
:sizes="sizes"
:alt="image.alternativeText || ''"
:width="image.width"
:height="image.height"
loading="lazy"
decoding="async"
/>
<video
v-if="video?.url"
:src="addURLBase(video.url)"
muted
autoplay
playsinline
loop
/>
</div>
</div>
</template>
<script setup>
import { breakpoints } from '@/libs/theme'
import { computed } from 'vue'
const props = defineProps({
image: Object,
video: Object,
aspect: {
type: [String, Number],
default: () => -1,
},
fillSpace: {
type: Boolean,
default: () => false,
},
fit: {
type: String,
default: () => 'cover',
},
mobileSize: {
type: String,
default: () => '100vw',
},
desktopSize: {
type: String,
default: () => '100vw',
},
})
const addURLBase = (path) => `${import.meta.env.VITE_STRAPI_URL}${path}`
const formats = computed(() => props.image?.formats || {})
const ratio = computed(() => {
if (!props.image) return 1
return props.image.height / props.image.width
})
const aspect = computed(() => {
// calculate if no aspect provided
if (props.aspect === -1) {
return ratio.value * 100
}
// otherwise, parse provided aspect, handling both 56.25 and 0.5625 style
const toParse = parseFloat(props.aspect)
return toParse <= 1 ? toParse * 100 : toParse
})
const styles = computed(() => ({ '--aspect': aspect.value + '%' }))
const srcset = computed(() =>
Object.values(formats.value)
.map((f) => `${addURLBase(f.url)} ${f.width}w`)
.join(', '),
)
const sizes = computed(
() =>
`(max-width: ${breakpoints.mobile}) ${props.mobileSize}, ${props.desktopSize}`,
)
</script>
<style lang="scss">
.strapi-media {
position: relative;
width: 100%;
overflow: hidden;
.image-sizer {
overflow: hidden;
padding-bottom: var(--aspect);
& > * {
display: block;
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
}
.svg {
background-size: cover;
background-repeat: no-repeat;
}
}
// fill space
&.fill-space {
position: absolute;
bottom: 0;
right: 0;
left: 0;
top: 0;
.image-sizer {
padding-bottom: 0;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
// fits
&.fit-cover .image-sizer > * {
object-fit: cover;
}
&.fit-contain .image-sizer > * {
object-fit: contain;
}
}
</style>