preline
Version:
Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.
933 lines (771 loc) • 26.2 kB
text/typescript
/*
* HSCarousel
* @version: 2.5.0
* @author: Preline Labs Ltd.
* @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html)
* Copyright 2024 Preline Labs Ltd.
*/
import { classToClassList, debounce, htmlToElement } from '../../utils';
import { ICarousel, ICarouselOptions } from './interfaces';
import { TCarouselOptionsSlidesQty } from './types';
import HSBasePlugin from '../base-plugin';
import { ICollectionItem } from '../../interfaces';
import { BREAKPOINTS } from '../../constants';
class HSCarousel extends HSBasePlugin<ICarouselOptions> implements ICarousel {
private currentIndex: number;
private readonly loadingClasses: string | string[];
private readonly dotsItemClasses: string;
private readonly isAutoHeight: boolean;
private readonly isAutoPlay: boolean;
private readonly isCentered: boolean;
private readonly isDraggable: boolean;
private readonly isInfiniteLoop: boolean;
private readonly isRTL: boolean;
private readonly isSnap: boolean;
private readonly hasSnapSpacers: boolean;
private readonly slidesQty: TCarouselOptionsSlidesQty | number;
private readonly speed: number;
private readonly updateDelay: number;
private readonly loadingClassesRemove: string | string[];
private readonly loadingClassesAdd: string | string[];
private readonly afterLoadingClassesAdd: string | string[];
private readonly container: HTMLElement | null;
private readonly inner: HTMLElement | null;
private readonly slides: NodeListOf<HTMLElement> | undefined[];
private readonly prev: HTMLElement | null;
private readonly next: HTMLElement | null;
private readonly dots: HTMLElement | null;
private dotsItems: NodeListOf<HTMLElement> | undefined[] | null;
private readonly info: HTMLElement | null;
private readonly infoTotal: HTMLElement | null;
private readonly infoCurrent: HTMLElement | null;
private sliderWidth: number;
private timer: any;
// Drag events' help variables
private isScrolling: ReturnType<typeof setTimeout>;
private isDragging: boolean;
private dragStartX: number | null;
private initialTranslateX: number | null;
// Touch events' help variables
private readonly touchX: {
start: number;
end: number;
};
// Resize events' help variables
private resizeContainer: HTMLElement;
public resizeContainerWidth: number;
constructor(el: HTMLElement, options?: ICarouselOptions) {
super(el, options);
const data = el.getAttribute('data-hs-carousel');
const dataOptions: ICarouselOptions = data ? JSON.parse(data) : {};
const concatOptions = {
...dataOptions,
...options,
};
this.currentIndex = concatOptions.currentIndex || 0;
this.loadingClasses = concatOptions.loadingClasses
? `${concatOptions.loadingClasses}`.split(',')
: null;
this.dotsItemClasses = concatOptions.dotsItemClasses
? concatOptions.dotsItemClasses
: null;
this.isAutoHeight =
typeof concatOptions.isAutoHeight !== 'undefined'
? concatOptions.isAutoHeight
: false;
this.isAutoPlay =
typeof concatOptions.isAutoPlay !== 'undefined'
? concatOptions.isAutoPlay
: false;
this.isCentered =
typeof concatOptions.isCentered !== 'undefined'
? concatOptions.isCentered
: false;
this.isDraggable =
typeof concatOptions.isDraggable !== 'undefined'
? concatOptions.isDraggable
: false;
this.isInfiniteLoop =
typeof concatOptions.isInfiniteLoop !== 'undefined'
? concatOptions.isInfiniteLoop
: false;
this.isRTL =
typeof concatOptions.isRTL !== 'undefined' ? concatOptions.isRTL : false;
this.isSnap =
typeof concatOptions.isSnap !== 'undefined'
? concatOptions.isSnap
: false;
this.hasSnapSpacers =
typeof concatOptions.hasSnapSpacers !== 'undefined'
? concatOptions.hasSnapSpacers
: true;
this.speed = concatOptions.speed || 4000;
this.updateDelay = concatOptions.updateDelay || 0;
this.slidesQty = concatOptions.slidesQty || 1;
this.loadingClassesRemove = this.loadingClasses?.[0]
? this.loadingClasses[0].split(' ')
: 'opacity-0';
this.loadingClassesAdd = this.loadingClasses?.[1]
? this.loadingClasses[1].split(' ')
: '';
this.afterLoadingClassesAdd = this.loadingClasses?.[2]
? this.loadingClasses[2].split(' ')
: '';
this.container = this.el.querySelector('.hs-carousel') || null;
this.inner = this.el.querySelector('.hs-carousel-body') || null;
this.slides = this.el.querySelectorAll('.hs-carousel-slide') || [];
this.prev = this.el.querySelector('.hs-carousel-prev') || null;
this.next = this.el.querySelector('.hs-carousel-next') || null;
this.dots = this.el.querySelector('.hs-carousel-pagination') || null;
this.info = this.el.querySelector('.hs-carousel-info') || null;
this.infoTotal =
this?.info?.querySelector('.hs-carousel-info-total') || null;
this.infoCurrent =
this?.info?.querySelector('.hs-carousel-info-current') || null;
this.sliderWidth = this.el.getBoundingClientRect().width;
// Drag events' help variables
this.isDragging = false;
this.dragStartX = null;
this.initialTranslateX = null;
// Touch events' help variables
this.touchX = {
start: 0,
end: 0,
};
// Resize events' help variables
this.resizeContainer = document.querySelector('body');
this.resizeContainerWidth = 0;
this.init();
}
private setIsSnap() {
const containerRect = this.container.getBoundingClientRect();
const containerCenter = containerRect.left + containerRect.width / 2;
let closestElement: HTMLElement | null = null;
let closestElementIndex: number | null = null;
let closestDistance = Infinity;
Array.from(this.inner.children).forEach((child: HTMLElement) => {
const childRect = child.getBoundingClientRect();
const innerContainerRect = this.inner.getBoundingClientRect();
const childCenter =
childRect.left + childRect.width / 2 - innerContainerRect.left;
const distance = Math.abs(
containerCenter - (innerContainerRect.left + childCenter),
);
if (distance < closestDistance) {
closestDistance = distance;
closestElement = child;
}
});
if (closestElement) {
closestElementIndex = Array.from(this.slides).findIndex(
(el) => el === closestElement,
);
}
this.setIndex(closestElementIndex);
if (this.dots) this.setCurrentDot();
}
private init() {
this.createCollection(window.$hsCarouselCollection, this);
if (this.inner) {
this.calculateWidth();
if (this.isDraggable && !this.isSnap) this.initDragHandling();
}
if (this.prev)
this.prev.addEventListener('click', () => {
this.goToPrev();
if (this.isAutoPlay) {
this.resetTimer();
this.setTimer();
}
});
if (this.next)
this.next.addEventListener('click', () => {
this.goToNext();
if (this.isAutoPlay) {
this.resetTimer();
this.setTimer();
}
});
if (this.dots) this.initDots();
if (this.info) this.buildInfo();
if (this.slides.length) {
this.addCurrentClass();
if (!this.isInfiniteLoop) this.addDisabledClass();
if (this.isAutoPlay) this.autoPlay();
}
setTimeout(() => {
if (this.isSnap) this.setIsSnap();
if (this.loadingClassesRemove) {
if (typeof this.loadingClassesRemove === 'string')
this.inner.classList.remove(this.loadingClassesRemove);
else this.inner.classList.remove(...this.loadingClassesRemove);
}
if (this.loadingClassesAdd) {
if (typeof this.loadingClassesAdd === 'string')
this.inner.classList.add(this.loadingClassesAdd);
else this.inner.classList.add(...this.loadingClassesAdd);
}
if (this.inner && this.afterLoadingClassesAdd) {
setTimeout(() => {
if (typeof this.afterLoadingClassesAdd === 'string')
this.inner.classList.add(this.afterLoadingClassesAdd);
else this.inner.classList.add(...this.afterLoadingClassesAdd);
});
}
}, 400);
if (this.isSnap) {
this.container.addEventListener('scroll', () => {
clearTimeout(this.isScrolling);
this.isScrolling = setTimeout(() => {
this.setIsSnap();
}, 100);
});
}
this.el.classList.add('init');
if (!this.isSnap) {
this.el.addEventListener('touchstart', (evt) => {
this.touchX.start = evt.changedTouches[0].screenX;
});
this.el.addEventListener('touchend', (evt) => {
this.touchX.end = evt.changedTouches[0].screenX;
this.detectDirection();
});
}
this.observeResize();
}
private initDragHandling(): void {
const scrollableElement = this.inner;
if (scrollableElement) {
scrollableElement.addEventListener(
'mousedown',
this.handleDragStart.bind(this),
);
scrollableElement.addEventListener(
'touchstart',
this.handleDragStart.bind(this),
{ passive: true },
);
document.addEventListener('mousemove', this.handleDragMove.bind(this));
document.addEventListener('touchmove', this.handleDragMove.bind(this), {
passive: false,
});
document.addEventListener('mouseup', this.handleDragEnd.bind(this));
document.addEventListener('touchend', this.handleDragEnd.bind(this));
}
}
private getTranslateXValue(): number {
const transformMatrix = window.getComputedStyle(this.inner).transform;
if (transformMatrix !== 'none') {
const matrixValues = transformMatrix
.match(/matrix.*\((.+)\)/)?.[1]
.split(', ');
if (matrixValues) {
let translateX = parseFloat(
matrixValues.length === 6 ? matrixValues[4] : matrixValues[12],
);
if (this.isRTL) translateX = -translateX;
return isNaN(translateX) || translateX === 0 ? 0 : -translateX;
}
}
return 0;
}
private removeClickEventWhileDragging(evt: MouseEvent) {
evt.preventDefault();
}
private handleDragStart(evt: MouseEvent | TouchEvent): void {
evt.preventDefault();
this.isDragging = true;
this.dragStartX = this.getEventX(evt);
this.initialTranslateX = this.isRTL
? this.getTranslateXValue()
: -this.getTranslateXValue();
this.inner.classList.add('dragging');
}
private handleDragMove(evt: MouseEvent | TouchEvent): void {
if (!this.isDragging) return;
this.inner.querySelectorAll('a:not(.prevented-click)').forEach((el) => {
el.classList.add('prevented-click');
el.addEventListener('click', this.removeClickEventWhileDragging);
});
const currentX = this.getEventX(evt);
let deltaX = currentX - this.dragStartX;
if (this.isRTL) deltaX = -deltaX;
const newTranslateX = this.initialTranslateX + deltaX;
const newTranslateXFunc = () => {
let calcWidth =
(this.sliderWidth * this.slides.length) / this.getCurrentSlidesQty() -
this.sliderWidth;
const containerWidth = this.sliderWidth;
const itemWidth = containerWidth / this.getCurrentSlidesQty();
const centeredOffset = (containerWidth - itemWidth) / 2;
const limitStart = this.isCentered ? centeredOffset : 0;
if (this.isCentered) calcWidth = calcWidth + centeredOffset;
const limitEnd = -calcWidth;
if (this.isRTL) {
if (newTranslateX < limitStart) return limitStart;
if (newTranslateX > calcWidth) return limitEnd;
else return -newTranslateX;
} else {
if (newTranslateX > limitStart) return limitStart;
else if (newTranslateX < -calcWidth) return limitEnd;
else return newTranslateX;
}
};
this.setTranslate(newTranslateXFunc());
}
private handleDragEnd(): void {
if (!this.isDragging) return;
this.isDragging = false;
const containerWidth = this.sliderWidth;
const itemWidth = containerWidth / this.getCurrentSlidesQty();
const currentTranslateX = this.getTranslateXValue();
let closestIndex = Math.round(currentTranslateX / itemWidth);
if (this.isRTL) closestIndex = Math.round(currentTranslateX / itemWidth);
this.inner.classList.remove('dragging');
setTimeout(() => {
this.calculateTransform(closestIndex);
if (this.dots) this.setCurrentDot();
this.dragStartX = null;
this.initialTranslateX = null;
this.inner.querySelectorAll('a.prevented-click').forEach((el) => {
el.classList.remove('prevented-click');
el.removeEventListener('click', this.removeClickEventWhileDragging);
});
});
}
private getEventX(event: MouseEvent | TouchEvent): number {
return event instanceof MouseEvent
? event.clientX
: event.touches[0].clientX;
}
private getCurrentSlidesQty(): number {
if (typeof this.slidesQty === 'object') {
const windowWidth = document.body.clientWidth;
let currentRes = 0;
Object.keys(this.slidesQty).forEach((key: string) => {
if (
windowWidth >=
(typeof key + 1 === 'number'
? (this.slidesQty as TCarouselOptionsSlidesQty)[key]
: BREAKPOINTS[key])
)
currentRes = (this.slidesQty as TCarouselOptionsSlidesQty)[key];
});
return currentRes;
} else {
return this.slidesQty as number;
}
}
private buildSnapSpacers() {
const existingBefore = this.inner.querySelector('.hs-snap-before');
const existingAfter = this.inner.querySelector('.hs-snap-after');
if (existingBefore) existingBefore.remove();
if (existingAfter) existingAfter.remove();
const containerWidth = this.sliderWidth;
const itemWidth = containerWidth / this.getCurrentSlidesQty();
const spacerWidth = containerWidth / 2 - itemWidth / 2;
const before = htmlToElement(
`<div class="hs-snap-before" style="height: 100%; width: ${spacerWidth}px"></div>`,
);
const after = htmlToElement(
`<div class="hs-snap-after" style="height: 100%; width: ${spacerWidth}px"></div>`,
);
this.inner.prepend(before);
this.inner.appendChild(after);
}
private initDots() {
if (this.el.querySelectorAll('.hs-carousel-pagination-item').length)
this.setDots();
else this.buildDots();
if (this.dots) this.setCurrentDot();
}
private buildDots() {
this.dots.innerHTML = '';
const slidesQty =
!this.isCentered && this.slidesQty
? this.slides.length - (this.getCurrentSlidesQty() - 1)
: this.slides.length;
for (let i = 0; i < slidesQty; i++) {
const singleDot = this.buildSingleDot(i);
this.dots.append(singleDot);
}
}
private setDots() {
this.dotsItems = this.dots.querySelectorAll('.hs-carousel-pagination-item');
this.dotsItems.forEach((dot, ind) => {
const targetIndex = dot.getAttribute(
'data-carousel-pagination-item-target',
);
this.singleDotEvents(dot, targetIndex ? +targetIndex : ind);
});
}
private goToCurrentDot() {
const container = this.dots;
const containerRect = container.getBoundingClientRect();
const containerScrollLeft = container.scrollLeft;
const containerScrollTop = container.scrollTop;
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const item = this.dotsItems[this.currentIndex];
const itemRect = item.getBoundingClientRect();
const itemLeft = itemRect.left - containerRect.left + containerScrollLeft;
const itemRight = itemLeft + item.clientWidth;
const itemTop = itemRect.top - containerRect.top + containerScrollTop;
const itemBottom = itemTop + item.clientHeight;
let scrollLeft = containerScrollLeft;
let scrollTop = containerScrollTop;
if (
itemLeft < containerScrollLeft ||
itemRight > containerScrollLeft + containerWidth
) {
scrollLeft = itemRight - containerWidth;
}
if (
itemTop < containerScrollTop ||
itemBottom > containerScrollTop + containerHeight
) {
scrollTop = itemBottom - containerHeight;
}
container.scrollTo({
left: scrollLeft,
top: scrollTop,
behavior: 'smooth',
});
}
private buildInfo() {
if (this.infoTotal) this.setInfoTotal();
if (this.infoCurrent) this.setInfoCurrent();
}
private setInfoTotal() {
this.infoTotal.innerText = `${this.slides.length}`;
}
private setInfoCurrent() {
this.infoCurrent.innerText = `${this.currentIndex + 1}`;
}
private buildSingleDot(ind: number) {
const singleDot = htmlToElement('<span></span>');
if (this.dotsItemClasses) classToClassList(this.dotsItemClasses, singleDot);
this.singleDotEvents(singleDot, ind);
return singleDot;
}
private singleDotEvents(dot: HTMLElement, ind: number) {
dot.addEventListener('click', () => {
this.goTo(ind);
if (this.isAutoPlay) {
this.resetTimer();
this.setTimer();
}
});
}
private observeResize() {
const resizeObserver = new ResizeObserver(
debounce((entries: ResizeObserverEntry[]) => {
for (let entry of entries) {
const newWidth = entry.contentRect.width;
if (newWidth !== this.resizeContainerWidth) {
this.recalculateWidth();
if (this.dots) this.initDots();
this.addCurrentClass();
this.resizeContainerWidth = newWidth;
}
}
}, this.updateDelay),
);
resizeObserver.observe(this.resizeContainer);
}
private calculateWidth() {
if (!this.isSnap)
this.inner.style.width = `${(this.sliderWidth * this.slides.length) / this.getCurrentSlidesQty()}px`;
this.slides.forEach((el) => {
el.style.width = `${this.sliderWidth / this.getCurrentSlidesQty()}px`;
});
this.calculateTransform();
}
private addCurrentClass() {
if (this.isSnap) {
const itemsQty = Math.floor(this.getCurrentSlidesQty() / 2);
for (let i = 0; i < this.slides.length; i++) {
const slide = this.slides[i];
if (
i <= this.currentIndex + itemsQty &&
i >= this.currentIndex - itemsQty
)
slide.classList.add('active');
else slide.classList.remove('active');
}
} else {
const maxIndex = this.isCentered
? this.currentIndex +
this.getCurrentSlidesQty() +
(this.getCurrentSlidesQty() - 1)
: this.currentIndex + this.getCurrentSlidesQty();
this.slides.forEach((el, i) => {
if (i >= this.currentIndex && i < maxIndex) {
el.classList.add('active');
} else {
el.classList.remove('active');
}
});
}
}
private setCurrentDot() {
const toggleDotActive = (el: HTMLElement | Element, i: number) => {
let statement = false;
const itemsQty = Math.floor(this.getCurrentSlidesQty() / 2);
if (this.isSnap && !this.hasSnapSpacers) {
statement =
i ===
(this.getCurrentSlidesQty() % 2 === 0
? this.currentIndex - itemsQty + 1
: this.currentIndex - itemsQty);
} else statement = i === this.currentIndex;
if (statement) el.classList.add('active');
else el.classList.remove('active');
};
if (this.dotsItems)
this.dotsItems.forEach((el, i) => toggleDotActive(el, i));
else
this.dots
.querySelectorAll(':scope > *')
.forEach((el, i) => toggleDotActive(el, i));
}
private setElementToDisabled(el: HTMLElement) {
el.classList.add('disabled');
if (el.tagName === 'BUTTON' || el.tagName === 'INPUT')
el.setAttribute('disabled', 'disabled');
}
private unsetElementToDisabled(el: HTMLElement) {
el.classList.remove('disabled');
if (el.tagName === 'BUTTON' || el.tagName === 'INPUT')
el.removeAttribute('disabled');
}
private addDisabledClass() {
if (!this.prev || !this.next) return false;
const gapValue = getComputedStyle(this.inner).getPropertyValue('gap');
const itemsQty = Math.floor(this.getCurrentSlidesQty() / 2);
let currentIndex = 0;
let maxIndex = 0;
let statementPrev = false;
let statementNext = false;
if (this.isSnap) {
currentIndex = this.currentIndex;
maxIndex = this.hasSnapSpacers
? this.slides.length - 1
: this.slides.length - itemsQty - 1;
statementPrev = this.hasSnapSpacers
? currentIndex === 0
: this.getCurrentSlidesQty() % 2 === 0
? currentIndex - itemsQty < 0
: currentIndex - itemsQty === 0;
statementNext =
currentIndex >= maxIndex &&
this.container.scrollLeft +
this.container.clientWidth +
(parseFloat(gapValue) || 0) >=
this.container.scrollWidth;
} else {
currentIndex = this.currentIndex;
maxIndex = this.isCentered
? this.slides.length -
this.getCurrentSlidesQty() +
(this.getCurrentSlidesQty() - 1)
: this.slides.length - this.getCurrentSlidesQty();
statementPrev = currentIndex === 0;
statementNext = currentIndex >= maxIndex;
}
if (statementPrev) {
this.unsetElementToDisabled(this.next);
this.setElementToDisabled(this.prev);
} else if (statementNext) {
this.unsetElementToDisabled(this.prev);
this.setElementToDisabled(this.next);
} else {
this.unsetElementToDisabled(this.prev);
this.unsetElementToDisabled(this.next);
}
}
private autoPlay() {
this.setTimer();
}
private setTimer() {
this.timer = setInterval(() => {
if (this.currentIndex === this.slides.length - 1) this.goTo(0);
else this.goToNext();
}, this.speed);
}
private resetTimer() {
clearInterval(this.timer);
}
private detectDirection() {
const { start, end } = this.touchX;
if (end < start) this.goToNext();
if (end > start) this.goToPrev();
}
// Public methods
public recalculateWidth() {
this.sliderWidth = this.inner.parentElement.getBoundingClientRect().width;
this.calculateWidth();
if (
this.sliderWidth !==
this.inner.parentElement.getBoundingClientRect().width
)
this.recalculateWidth();
}
private calculateTransform(currentIdx?: number | undefined): void {
if (currentIdx !== undefined) this.currentIndex = currentIdx;
if (
this.currentIndex > this.slides.length - this.getCurrentSlidesQty() &&
!this.isCentered
)
this.currentIndex = this.slides.length - this.getCurrentSlidesQty();
const containerWidth = this.sliderWidth;
const itemWidth = containerWidth / this.getCurrentSlidesQty();
let translateX = this.currentIndex * itemWidth;
// TODO:: need to test auto scrolling to the end if last on resize
if (this.isSnap && !this.isCentered) {
if (
this.container.scrollLeft < containerWidth &&
this.container.scrollLeft + itemWidth / 2 > containerWidth
)
this.container.scrollLeft = this.container.scrollWidth;
}
if (this.isCentered && !this.isSnap) {
const centeredOffset = (containerWidth - itemWidth) / 2;
if (this.currentIndex === 0) translateX = -centeredOffset;
else if (
this.currentIndex >=
this.slides.length -
this.getCurrentSlidesQty() +
(this.getCurrentSlidesQty() - 1)
) {
const totalSlideWidth = this.slides.length * itemWidth;
translateX = totalSlideWidth - containerWidth + centeredOffset;
} else translateX = this.currentIndex * itemWidth - centeredOffset;
}
if (!this.isSnap)
this.inner.style.transform = this.isRTL
? `translate(${translateX}px, 0px)`
: `translate(${-translateX}px, 0px)`;
if (this.isAutoHeight)
this.inner.style.height = `${this.slides[this.currentIndex].clientHeight}px`;
if (this.dotsItems) this.goToCurrentDot();
this.addCurrentClass();
if (!this.isInfiniteLoop) this.addDisabledClass();
if (this.isSnap && this.hasSnapSpacers) this.buildSnapSpacers();
if (this.infoCurrent) this.setInfoCurrent();
}
private setTranslate(val: number) {
this.inner.style.transform = this.isRTL
? `translate(${-val}px, 0px)`
: `translate(${val}px, 0px)`;
}
public goToPrev() {
if (this.currentIndex > 0) {
this.currentIndex--;
} else {
this.currentIndex = this.slides.length - this.getCurrentSlidesQty();
}
if (this.isSnap) {
const itemWidth = this.sliderWidth / this.getCurrentSlidesQty();
this.container.scrollBy({
left: Math.max(-this.container.scrollLeft, -itemWidth),
behavior: 'smooth',
});
this.addCurrentClass();
if (!this.isInfiniteLoop) this.addDisabledClass();
} else this.calculateTransform();
if (this.dots) this.setCurrentDot();
}
public goToNext() {
const statement = this.isCentered
? this.slides.length -
this.getCurrentSlidesQty() +
(this.getCurrentSlidesQty() - 1)
: this.slides.length - this.getCurrentSlidesQty();
if (this.currentIndex < statement) {
this.currentIndex++;
} else {
this.currentIndex = 0;
}
if (this.isSnap) {
const itemWidth = this.sliderWidth / this.getCurrentSlidesQty();
const maxScrollLeft =
this.container.scrollWidth - this.container.clientWidth;
this.container.scrollBy({
left: Math.min(itemWidth, maxScrollLeft - this.container.scrollLeft),
behavior: 'smooth',
});
this.addCurrentClass();
if (!this.isInfiniteLoop) this.addDisabledClass();
} else this.calculateTransform();
if (this.dots) this.setCurrentDot();
}
public goTo(i: number) {
const currentIndex = this.currentIndex;
this.currentIndex = i;
if (this.isSnap) {
const itemWidth = this.sliderWidth / this.getCurrentSlidesQty();
const index =
currentIndex > this.currentIndex
? currentIndex - this.currentIndex
: this.currentIndex - currentIndex;
const width =
currentIndex > this.currentIndex
? -(itemWidth * index)
: itemWidth * index;
this.container.scrollBy({
left: width,
behavior: 'smooth',
});
this.addCurrentClass();
if (!this.isInfiniteLoop) this.addDisabledClass();
} else this.calculateTransform();
if (this.dots) this.setCurrentDot();
}
private setIndex(i: number) {
this.currentIndex = i;
this.addCurrentClass();
if (!this.isInfiniteLoop) this.addDisabledClass();
}
// Static methods
static getInstance(target: HTMLElement | string, isInstance?: boolean) {
const elInCollection = window.$hsCarouselCollection.find(
(el) =>
el.element.el ===
(typeof target === 'string' ? document.querySelector(target) : target),
);
return elInCollection
? isInstance
? elInCollection
: elInCollection.element
: null;
}
static autoInit() {
if (!window.$hsCarouselCollection) window.$hsCarouselCollection = [];
document
.querySelectorAll('[data-hs-carousel]:not(.--prevent-on-load-init)')
.forEach((el: HTMLElement) => {
if (
!window.$hsCarouselCollection.find(
(elC) => (elC?.element?.el as HTMLElement) === el,
)
)
new HSCarousel(el);
});
}
}
declare global {
interface Window {
HSCarousel: Function;
$hsCarouselCollection: ICollectionItem<HSCarousel>[];
}
}
window.addEventListener('load', () => {
HSCarousel.autoInit();
// Uncomment for debug
// console.log('Carousel collection:', window.$hsCarouselCollection);
});
if (typeof window !== 'undefined') {
window.HSCarousel = HSCarousel;
}
export default HSCarousel;