import { Swiper } from "swiper"; import { A11y, Autoplay, Keyboard, Navigation, Pagination, } from "swiper/modules"; import type { SwiperOptions } from "swiper/types"; import { CLASS_ACTIVE, CLASS_AUTOPLAY, CLASS_DOT, CLASS_DOT_CIRCLE, CLASS_DOT_SVG, CLASS_SHOWING_INVERTED_SLIDE, CLASS_SLIDE, CLASS_TRACK, SELECTOR_DOTS, SELECTOR_VIEWPORT, } from "./constants"; export const defaultConfig: SwiperOptions = { pagination: { el: SELECTOR_DOTS, clickable: true, bulletClass: CLASS_DOT, bulletActiveClass: CLASS_ACTIVE, renderBullet: (index, className) => { return ``; }, }, slidesPerView: 1, loop: true, a11y: { enabled: true, prevSlideMessage: "Predchádzajúci snímok", nextSlideMessage: "Nasledujúci snímok", paginationBulletMessage: "Prejsť na snímok {index}", containerMessage: "Carousel so snímkami", containerRoleDescriptionMessage: "carousel", itemRoleDescriptionMessage: "snímok", firstSlideMessage: "Prvý snímok", lastSlideMessage: "Posledný snímok", slideLabelMessage: "Snímok", }, cssMode: false, direction: "horizontal", wrapperClass: CLASS_TRACK, slideClass: CLASS_SLIDE, slideActiveClass: CLASS_ACTIVE, autoplay: false, }; export default class CarouselPromotions { element: HTMLElement; config: SwiperOptions; viewport!: HTMLElement; instance!: Swiper; interval: number = 0; autoplayTimer?: ReturnType; private resizeRafId?: number; private boundWindowResizeHandler: () => void; private boundPaginationClickHandler: () => void; constructor(element: HTMLElement, config?: Partial) { this.element = element; this.config = { ...defaultConfig, ...config }; this.interval = 0; this.handleSlideChange = this.handleSlideChange.bind(this); this.scrollToNextSlide = this.scrollToNextSlide.bind(this); this.autoplayAnimationEnd = this.autoplayAnimationEnd.bind(this); this.boundWindowResizeHandler = this.handleWindowResize.bind(this); this.boundPaginationClickHandler = this.handlePaginationClick.bind(this); (this.element as any).ODS_CarouselPromotions = this; // Defer initialization to ensure DOM is ready // requestAnimationFrame(() => { this.init(); // }); return this; } init() { this.getElements(); if (this.element.hasAttribute("data-swiper-options")) { this.getCustomOptions(); } if (this.element.hasAttribute("data-interval")) { this.interval = parseInt(this.element.getAttribute("data-interval")!); } const isIntervalValid = this.isIntervalValid(); if (isIntervalValid) { this.element.classList.add(CLASS_AUTOPLAY); this.config.autoplay = { delay: this.interval, disableOnInteraction: false, }; } else { this.interval = 0; this.config.autoplay = false; } this.instance = new Swiper(this.viewport, { ...this.config, modules: [Navigation, Pagination, A11y, Keyboard, Autoplay], on: { slideChange: this.handleSlideChange, init: () => { this.fixPaginationButtonType(); if (this.isAutoplayEnabled) { this.renderSvgDotsForAnimation(); } }, update: () => { this.fixPaginationButtonType(); if (this.isAutoplayEnabled) { this.renderSvgDotsForAnimation(); } }, }, }); const paginationEl = this.element.querySelector( SELECTOR_DOTS, ) as HTMLElement | null; if (paginationEl && isIntervalValid) { paginationEl.addEventListener("animationend", this.autoplayAnimationEnd); paginationEl.addEventListener("click", this.boundPaginationClickHandler); } window.addEventListener("resize", this.boundWindowResizeHandler); } private handlePaginationClick() { this.stopAutoplay(); } private handleWindowResize() { if (this.resizeRafId) { cancelAnimationFrame(this.resizeRafId); } this.resizeRafId = requestAnimationFrame(() => { this.resizeRafId = undefined; if (!this.instance || !this.viewport) { return; } if (this.viewport.clientWidth <= 0) { return; } this.instance.update(); this.fixPaginationButtonType(); if (this.isAutoplayEnabled) { this.renderSvgDotsForAnimation(); } }); } stopAutoplay() { this.interval = 0; this.element.classList.remove(CLASS_AUTOPLAY); if (this.instance && this.instance.autoplay) { this.instance.autoplay.stop(); } } /** * Handles the slide change event on the carousel. * Updates the inverted slide class based on slide index. */ handleSlideChange() { this.element.classList.remove(CLASS_SHOWING_INVERTED_SLIDE); if (this.instance && this.instance.activeIndex !== undefined) { const slideIndex = this.instance.activeIndex; if (slideIndex % 2 === 1) { this.element.classList.add(CLASS_SHOWING_INVERTED_SLIDE); } } } autoplayAnimationEnd(e: AnimationEvent) { if (e instanceof AnimationEvent && e.animationName === "countdown") { this.scrollToNextSlide(); } } get isAutoplayEnabled() { return this.interval > 0; } isIntervalValid(): boolean { const interval = parseInt(String(this.interval)); return !isNaN(interval) && interval >= 1000; } scrollToNextSlide() { if (!this.instance || !this.instance.slides) { return; } const slidesCount = this.instance.slides.length; if (slidesCount <= 0) { return; } const currentSlideIndex = this.instance.activeIndex || 0; this.instance.slideTo((currentSlideIndex + 1) % slidesCount); } getElements() { this.viewport = this.element.querySelector(SELECTOR_VIEWPORT)!; // Ensure pagination container has proper role const paginationEl = this.element.querySelector( SELECTOR_DOTS, ) as HTMLElement | null; if (paginationEl && !paginationEl.hasAttribute("role")) { paginationEl.setAttribute("role", "tablist"); } const elements = { pagination: { ...((this.config.pagination ?? {}) as object), el: paginationEl, clickable: true, bulletClass: CLASS_DOT, bulletActiveClass: CLASS_ACTIVE, }, }; this.config = { ...this.config, ...elements, }; } getCustomOptions() { const passedSwiperOptions = this.element.getAttribute( "data-swiper-options", ); if (!passedSwiperOptions) return; const parsedSwiperOptions = JSON.parse(passedSwiperOptions); this.config = { ...this.config, ...parsedSwiperOptions, }; } fixPaginationButtonType() { const dots = Array.from( this.element.querySelectorAll(`${SELECTOR_DOTS} > *`), ); dots.forEach((dot) => (dot as HTMLElement).setAttribute("type", "button")); } static createSvgDot(): SVGSVGElement { const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("class", CLASS_DOT_SVG); svg.setAttribute("width", "12"); svg.setAttribute("height", "12"); svg.setAttribute("viewBox", "0 0 12 12"); svg.setAttribute("fill", "none"); svg.setAttribute("xmlns", svgNS); const circle = document.createElementNS(svgNS, "circle"); circle.setAttribute("class", CLASS_DOT_CIRCLE); circle.setAttribute("r", "5"); circle.setAttribute("cx", "6"); circle.setAttribute("cy", "6"); svg.appendChild(circle); return svg; } renderSvgDotsForAnimation() { const dots = Array.from( this.element.querySelectorAll(`${SELECTOR_DOTS} > *`), ); const svg = CarouselPromotions.createSvgDot(); (svg.querySelector("circle") as SVGCircleElement).style.animationDuration = `${this.interval}ms`; dots.forEach((dot) => { dot.innerHTML = ""; dot.appendChild(svg.cloneNode(true)); }); } destroy() { window.removeEventListener("resize", this.boundWindowResizeHandler); if (this.resizeRafId) { cancelAnimationFrame(this.resizeRafId); this.resizeRafId = undefined; } const paginationEl = this.element.querySelector( SELECTOR_DOTS, ) as HTMLElement | null; if (paginationEl) { paginationEl.removeEventListener( "animationend", this.autoplayAnimationEnd, ); paginationEl.removeEventListener( "click", this.boundPaginationClickHandler, ); } if (this.instance) { this.instance.destroy(); } if ((this.element as any).ODS_CarouselPromotions === this) { delete (this.element as any).ODS_CarouselPromotions; } } update() { if (this.instance) { this.instance.update(); } } static getInstance(el: HTMLElement): CarouselPromotions | null { return el && (el as any).ODS_CarouselPromotions ? (el as any).ODS_CarouselPromotions : null; } }