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;
}
}