UNPKG

flyonui

Version:

The easiest, free and open-source Tailwind CSS component library with semantic classes.

997 lines (797 loc) 33 kB
/* * 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