174 lines
3.7 KiB
Vue
174 lines
3.7 KiB
Vue
<template>
|
|
<div class="slider">
|
|
<div class="slider-container" ref="emblaNode">
|
|
<div class="slides">
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="controls" class="controls">
|
|
<button @click="onPrev">
|
|
<arrow />
|
|
</button>
|
|
<button @click="onNext">
|
|
<arrow />
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="nav && slideCount > 1" class="nav">
|
|
<btn
|
|
v-for="i in slideCount"
|
|
@click="onNavClick(i)"
|
|
:secondary="activeIndex !== i"
|
|
number
|
|
>{{ i }}</btn
|
|
>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref, watch } from 'vue'
|
|
import Btn from '@/components/Btn.vue'
|
|
import Arrow from '@/components/svg/util/Arrow.vue'
|
|
import embla from 'embla-carousel-vue'
|
|
import ClassNames from 'embla-carousel-class-names'
|
|
import Autoplay from 'embla-carousel-autoplay'
|
|
|
|
const props = defineProps({
|
|
emblaOptions: {
|
|
type: Object,
|
|
default: () => {},
|
|
},
|
|
autoplay: {
|
|
type: Boolean,
|
|
default: () => false,
|
|
},
|
|
autoplayOptions: {
|
|
type: Object,
|
|
default: () => {},
|
|
},
|
|
controls: {
|
|
type: Boolean,
|
|
default: () => false,
|
|
},
|
|
nav: {
|
|
type: Boolean,
|
|
default: () => false,
|
|
},
|
|
slideCount: {
|
|
type: Number,
|
|
default: () => 0,
|
|
},
|
|
})
|
|
|
|
const plugins = computed(() => {
|
|
const base = [ClassNames()]
|
|
if (props.autoplay) {
|
|
base.push(Autoplay(props.autoplayOptions))
|
|
}
|
|
return base
|
|
})
|
|
|
|
const [emblaNode, emblaApi] = embla(props.emblaOptions, plugins.value)
|
|
|
|
defineExpose({
|
|
api: emblaApi,
|
|
})
|
|
|
|
const activeIndex = ref(1)
|
|
const onNext = () => {
|
|
emblaApi.value.scrollNext()
|
|
}
|
|
const onPrev = () => {
|
|
emblaApi.value.scrollPrev()
|
|
}
|
|
const onNavClick = (i) => {
|
|
activeIndex.value = i
|
|
|
|
emblaApi.value.scrollTo(i - 1)
|
|
}
|
|
|
|
watch(emblaApi, () => {
|
|
if (!emblaApi.value) return
|
|
|
|
emblaApi.value.on('select', () => {
|
|
activeIndex.value = emblaApi.value.selectedScrollSnap() + 1
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.slider {
|
|
position: relative;
|
|
|
|
.slider-container {
|
|
max-width: 100vw;
|
|
overflow: hidden;
|
|
}
|
|
.slides {
|
|
display: flex;
|
|
|
|
> * {
|
|
flex-shrink: 0;
|
|
}
|
|
}
|
|
.controls {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 var(--layout-margin);
|
|
position: absolute;
|
|
inset: 0;
|
|
pointer-events: none;
|
|
|
|
button {
|
|
pointer-events: all;
|
|
border-radius: 50%;
|
|
background: var(--black);
|
|
color: var(--white);
|
|
width: desktop-vw(40px);
|
|
aspect-ratio: 1;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
|
|
svg {
|
|
width: desktop-vw(21px);
|
|
height: auto;
|
|
}
|
|
&:first-child {
|
|
transform: scaleX(-1);
|
|
}
|
|
&:disabled {
|
|
opacity: 0.5;
|
|
cursor: default;
|
|
}
|
|
}
|
|
|
|
@include mobile {
|
|
button {
|
|
width: mobile-vw(32px);
|
|
|
|
svg {
|
|
width: mobile-vw(16px);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.nav {
|
|
position: absolute;
|
|
left: 0;
|
|
bottom: desktop-vw(25px);
|
|
right: 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
|
|
@include mobile {
|
|
bottom: mobile-vw(25px);
|
|
}
|
|
}
|
|
}
|
|
</style>
|