flyonui
Version:
The easiest, free and open-source Tailwind CSS component library with semantic classes.
997 lines (797 loc) • 33 kB
text/typescript
/*
* HSCarousel
* @version: 3.2.2
* @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
}
private readonly touchY: {
start: number
end: number
}
// Resize events' help variables
private resizeContainer: HTMLElement
public resizeContainerWidth: number
// Listeners
private onPrevClickListener: () => void
private onNextClickListener: () => void
private onContainerScrollListener: () => void
private onElementTouchStartListener: (evt: TouchEvent) => void
private onElementTouchEndListener: (evt: TouchEvent) => void
private onInnerMouseDownListener: (evt: MouseEvent | TouchEvent) => void
private onInnerTouchStartListener: (evt: MouseEvent | TouchEvent) => void
private onDocumentMouseMoveListener: (evt: MouseEvent | TouchEvent) => void
private onDocumentTouchMoveListener: (evt: MouseEvent | TouchEvent) => void
private onDocumentMouseUpListener: () => void
private onDocumentTouchEndListener: () => void
private onDotClickListener: () => void
constructor(el: HTMLElement, options?: ICarouselOptions) {
super(el, options)
const data = el.getAttribute('data-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('.carousel') || null
this.inner = this.el.querySelector('.carousel-body') || null
this.slides = this.el.querySelectorAll('.carousel-slide') || []
this.prev = this.el.querySelector('.carousel-prev') || null
this.next = this.el.querySelector('.carousel-next') || null
this.dots = this.el.querySelector('.carousel-pagination') || null
this.info = this.el.querySelector('.carousel-info') || null
this.infoTotal = this?.info?.querySelector('.carousel-info-total') || null
this.infoCurrent = this?.info?.querySelector('.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
}
this.touchY = {
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 prevClick() {
this.goToPrev()
if (this.isAutoPlay) {
this.resetTimer()
this.setTimer()
}
}
private nextClick() {
this.goToNext()
if (this.isAutoPlay) {
this.resetTimer()
this.setTimer()
}
}
private containerScroll() {
clearTimeout(this.isScrolling)
this.isScrolling = setTimeout(() => {
this.setIsSnap()
}, 100)
}
private elementTouchStart(evt: TouchEvent) {
this.touchX.start = evt.changedTouches[0].screenX
this.touchY.start = evt.changedTouches[0].screenY
}
private elementTouchEnd(evt: TouchEvent) {
this.touchX.end = evt.changedTouches[0].screenX
this.touchY.end = evt.changedTouches[0].screenY
this.detectDirection()
}
private innerMouseDown(evt: MouseEvent | TouchEvent) {
this.handleDragStart(evt)
}
private innerTouchStart(evt: MouseEvent | TouchEvent) {
this.handleDragStart(evt)
}
private documentMouseMove(evt: MouseEvent | TouchEvent) {
this.handleDragMove(evt)
}
private documentTouchMove(evt: MouseEvent | TouchEvent) {
this.handleDragMove(evt)
}
private documentMouseUp() {
this.handleDragEnd()
}
private documentTouchEnd() {
this.handleDragEnd()
}
private dotClick(ind: number) {
this.goTo(ind)
if (this.isAutoPlay) {
this.resetTimer()
this.setTimer()
}
}
private init() {
this.createCollection(window.$hsCarouselCollection, this)
if (this.inner) {
this.calculateWidth()
if (this.isDraggable && !this.isSnap) this.initDragHandling()
}
if (this.prev) {
this.onPrevClickListener = () => this.prevClick()
this.prev.addEventListener('click', this.onPrevClickListener)
}
if (this.next) {
this.onNextClickListener = () => this.nextClick()
this.next.addEventListener('click', this.onNextClickListener)
}
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.onContainerScrollListener = () => this.containerScroll()
this.container.addEventListener('scroll', this.onContainerScrollListener)
}
this.el.classList.add('init')
if (!this.isSnap) {
this.onElementTouchStartListener = (evt: TouchEvent) => this.elementTouchStart(evt)
this.onElementTouchEndListener = (evt: TouchEvent) => this.elementTouchEnd(evt)
this.el.addEventListener('touchstart', this.onElementTouchStartListener)
this.el.addEventListener('touchend', this.onElementTouchEndListener)
}
this.observeResize()
}
private initDragHandling(): void {
const scrollableElement = this.inner
this.onInnerMouseDownListener = evt => this.innerMouseDown(evt)
this.onInnerTouchStartListener = evt => this.innerTouchStart(evt)
this.onDocumentMouseMoveListener = evt => this.documentMouseMove(evt)
this.onDocumentTouchMoveListener = evt => this.documentTouchMove(evt)
this.onDocumentMouseUpListener = () => this.documentMouseUp()
this.onDocumentTouchEndListener = () => this.documentTouchEnd()
if (scrollableElement) {
scrollableElement.addEventListener('mousedown', this.onInnerMouseDownListener)
scrollableElement.addEventListener('touchstart', this.onInnerTouchStartListener, { passive: true })
document.addEventListener('mousemove', this.onDocumentMouseMoveListener)
document.addEventListener('touchmove', this.onDocumentTouchMoveListener, {
passive: false
})
document.addEventListener('mouseup', this.onDocumentMouseUpListener)
document.addEventListener('touchend', this.onDocumentTouchEndListener)
}
}
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('.snap-before')
const existingAfter = this.inner.querySelector('.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="snap-before" style="height: 100%; width: ${spacerWidth}px"></div>`)
const after = htmlToElement(`<div class="snap-after" style="height: 100%; width: ${spacerWidth}px"></div>`)
this.inner.prepend(before)
this.inner.appendChild(after)
}
private initDots() {
if (this.el.querySelectorAll('.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('.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) {
this.onDotClickListener = () => this.dotClick(ind)
dot.addEventListener('click', this.onDotClickListener)
}
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 deltaX = this.touchX.end - this.touchX.start
const deltaY = this.touchY.end - this.touchY.start
const absDeltaX = Math.abs(deltaX)
const absDeltaY = Math.abs(deltaY)
const SWIPE_THRESHOLD = 30
if (absDeltaX < SWIPE_THRESHOLD || absDeltaX < absDeltaY) return
const isSwipeToNext = this.isRTL ? deltaX > 0 : deltaX < 0
if (!this.isInfiniteLoop) {
if (isSwipeToNext && this.currentIndex < this.slides.length - this.getCurrentSlidesQty()) {
this.goToNext()
}
if (!isSwipeToNext && this.currentIndex > 0) {
this.goToPrev()
}
} else {
if (isSwipeToNext) this.goToNext()
else this.goToPrev()
}
}
private calculateTransform(currentIdx?: number | undefined): void {
if (currentIdx !== undefined) this.currentIndex = currentIdx
const containerWidth = this.sliderWidth
const itemWidth = containerWidth / this.getCurrentSlidesQty()
let translateX = this.currentIndex * itemWidth
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.setTransform(translateX)
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 setTransform(val: number) {
if (this.slides.length > this.getCurrentSlidesQty()) {
this.inner.style.transform = this.isRTL ? `translate(${val}px, 0px)` : `translate(${-val}px, 0px)`
} else this.inner.style.transform = 'translate(0px, 0px)'
}
private setTranslate(val: number) {
this.inner.style.transform = this.isRTL ? `translate(${-val}px, 0px)` : `translate(${val}px, 0px)`
}
private setIndex(i: number) {
this.currentIndex = i
this.addCurrentClass()
if (!this.isInfiniteLoop) this.addDisabledClass()
}
// Public methods
public recalculateWidth() {
this.sliderWidth = this.inner.parentElement.getBoundingClientRect().width
this.calculateWidth()
if (this.sliderWidth !== this.inner.parentElement.getBoundingClientRect().width) {
this.recalculateWidth()
}
}
public goToPrev() {
if (this.currentIndex > 0) {
this.currentIndex--
} else {
this.currentIndex = this.slides.length - this.getCurrentSlidesQty()
}
this.fireEvent('update', this.currentIndex)
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
}
this.fireEvent('update', this.currentIndex)
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
this.fireEvent('update', this.currentIndex)
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()
}
public destroy() {
// Remove classes
if (this.loadingClassesAdd) {
if (typeof this.loadingClassesAdd === 'string') {
this.inner.classList.remove(this.loadingClassesAdd)
} else this.inner.classList.remove(...this.loadingClassesAdd)
}
if (this.inner && this.afterLoadingClassesAdd) {
setTimeout(() => {
if (typeof this.afterLoadingClassesAdd === 'string') {
this.inner.classList.remove(this.afterLoadingClassesAdd)
} else this.inner.classList.remove(...this.afterLoadingClassesAdd)
})
}
this.el.classList.remove('init')
this.inner.classList.remove('dragging')
this.slides.forEach(el => el.classList.remove('active'))
if (this?.dotsItems?.length) {
this.dotsItems.forEach(el => el.classList.remove('active'))
}
this.prev.classList.remove('disabled')
this.next.classList.remove('disabled')
// Remove styles
this.inner.style.width = ''
this.slides.forEach(el => (el.style.width = ''))
if (!this.isSnap) this.inner.style.transform = ''
if (this.isAutoHeight) this.inner.style.height = ''
// Remove listeners
this.prev.removeEventListener('click', this.onPrevClickListener)
this.next.removeEventListener('click', this.onNextClickListener)
this.container.removeEventListener('scroll', this.onContainerScrollListener)
this.el.removeEventListener('touchstart', this.onElementTouchStartListener)
this.el.removeEventListener('touchend', this.onElementTouchEndListener)
this.inner.removeEventListener('mousedown', this.onInnerMouseDownListener)
this.inner.removeEventListener('touchstart', this.onInnerTouchStartListener)
document.removeEventListener('mousemove', this.onDocumentMouseMoveListener)
document.removeEventListener('touchmove', this.onDocumentTouchMoveListener)
document.removeEventListener('mouseup', this.onDocumentMouseUpListener)
document.removeEventListener('touchend', this.onDocumentTouchEndListener)
this.inner.querySelectorAll('a:not(.prevented-click)').forEach(el => {
el.classList.remove('prevented-click')
el.removeEventListener('click', this.removeClickEventWhileDragging)
})
if (this?.dotsItems?.length || this.dots.querySelectorAll(':scope > *').length) {
const dots = this?.dotsItems || this.dots.querySelectorAll(':scope > *')
dots.forEach(el => el.removeEventListener('click', this.onDotClickListener))
this.dots.innerHTML = null
}
// Remove elements
this.inner.querySelector('.snap-before').remove()
this.inner.querySelector('.snap-after').remove()
this.dotsItems = null
this.isDragging = false
this.dragStartX = null
this.initialTranslateX = null
window.$hsCarouselCollection = window.$hsCarouselCollection.filter(({ element }) => element.el !== this.el)
}
// 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 = []
if (window.$hsCarouselCollection) {
window.$hsCarouselCollection = window.$hsCarouselCollection.filter(({ element }) => document.contains(element.el))
}
document.querySelectorAll('[data-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