Files
website/src/lib/components/carousel/Carousel.svelte
Jesse Winton d62337b244 updates
2024-07-16 17:17:07 -04:00

169 lines
5.4 KiB
Svelte

<script lang="ts">
import embla from 'embla-carousel-svelte';
import {
type EmblaCarouselType,
type EmblaEventType,
type EmblaOptionsType,
type EmblaPluginType
} from 'embla-carousel';
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
let emblaApi: EmblaCarouselType;
let options: EmblaOptionsType = {
align: 'center',
skipSnaps: true,
loop: true
};
let hasPrev: boolean = false;
let hasNext: boolean = true;
const togglePrevNextBtnsState = () => {
if (emblaApi.canScrollPrev()) hasPrev = true;
else hasPrev = false;
if (emblaApi.canScrollNext()) hasNext = true;
else hasNext = false;
};
let plugins: EmblaPluginType[] = [WheelGesturesPlugin()];
let selectedScrollIndex = 0;
const onSelect = (index: number) => {
emblaApi.scrollTo(index);
selectedScrollIndex = emblaApi.selectedScrollSnap();
};
const onPrev = () => {
emblaApi.scrollPrev();
selectedScrollIndex = emblaApi.selectedScrollSnap();
};
const onNext = () => {
emblaApi.scrollNext();
selectedScrollIndex = emblaApi.selectedScrollSnap();
};
const TWEEN_FACTOR_BASE = 0.52;
let tweenFactor = 0;
let tweenNodes: HTMLElement[] = [];
const numberWithinRange = (number: number, min: number, max: number) =>
Math.min(Math.max(number, min), max);
const setTweenNodes = (emblaApi: EmblaCarouselType): void => {
tweenNodes = emblaApi.slideNodes().map((slideNode) => {
return slideNode.querySelector('.embla__slide__number') as HTMLElement;
});
};
const setTweenFactor = (emblaApi: EmblaCarouselType): void => {
tweenFactor = TWEEN_FACTOR_BASE * emblaApi.scrollSnapList().length;
};
const tweenScale = (emblaApi: EmblaCarouselType, eventName?: EmblaEventType): void => {
const engine = emblaApi.internalEngine();
const scrollProgress = emblaApi.scrollProgress();
const slidesInView = emblaApi.slidesInView();
const isScrollEvent = eventName === 'scroll';
emblaApi.scrollSnapList().forEach((scrollSnap, snapIndex) => {
let diffToTarget = scrollSnap - scrollProgress;
const slidesInSnap = engine.slideRegistry[snapIndex];
slidesInSnap.forEach((slideIndex) => {
if (isScrollEvent && !slidesInView.includes(slideIndex)) return;
if (engine.options.loop) {
engine.slideLooper.loopPoints.forEach((loopItem) => {
const target = loopItem.target();
if (slideIndex === loopItem.index && target !== 0) {
const sign = Math.sign(target);
if (sign === -1) {
diffToTarget = scrollSnap - (1 + scrollProgress);
}
if (sign === 1) {
diffToTarget = scrollSnap + (1 - scrollProgress);
}
}
});
}
const tweenValue = 1 - Math.abs(diffToTarget * tweenFactor);
const scale = numberWithinRange(tweenValue, 0.8, 1).toString();
const tweenNode = tweenNodes[slideIndex];
tweenNode.style.transform = `scale(${scale})`;
});
});
};
const onEmblaInit = (event: CustomEvent<EmblaCarouselType>) => {
emblaApi = event.detail;
setTweenNodes(emblaApi);
setTweenFactor(emblaApi);
tweenScale(emblaApi);
emblaApi
.on('scroll', () => {
selectedScrollIndex = emblaApi.selectedScrollSnap();
})
.on('init', togglePrevNextBtnsState)
.on('select', togglePrevNextBtnsState)
.on('reInit', togglePrevNextBtnsState)
.on('reInit', setTweenNodes)
.on('reInit', setTweenFactor)
.on('reInit', tweenScale)
.on('scroll', tweenScale)
.on('slideFocus', tweenScale);
};
</script>
<div class="embla web-carousel">
{#if hasPrev}
<button class="web-carousel-button web-carousel-button-start" on:click={onPrev}>
<span class="web-icon-arrow-left" aria-hidden="true"></span>
</button>
{/if}
{#if hasNext}
<button class="web-carousel-button web-carousel-button-end" on:click={onNext}>
<span class="web-icon-arrow-right" aria-hidden="true"></span>
</button>
{/if}
<div class="embla__viewport" use:embla={{ options, plugins }} on:emblaInit={onEmblaInit}>
<ul class="embla__container">
<slot />
</ul>
</div>
</div>
<div class="web-carousel-bullets">
<ul class="web-carousel-bullets-list">
{#each Array.from({ length: emblaApi?.scrollSnapList().length }) as _, i}
<li class="web-carousel-bullets-item">
<button
class="web-carousel-bullets-button"
class:is-selected={selectedScrollIndex === i}
aria-label={`gallery item ${i + 1}`}
on:click={() => onSelect(i)}
></button>
</li>
{/each}
</ul>
</div>
<style>
.embla {
overflow: hidden;
position: relative;
}
.embla__container {
display: flex;
}
</style>