import { BaseComponent, exposeComponent, triggerEvent, parseJSON, } from "../../utils/dom"; /** * External dependencies */ import Swiper from "swiper"; import { Navigation, Pagination, Scrollbar, A11y, Autoplay, Thumbs, EffectFade, EffectCube, EffectFlip, EffectCoverflow, EffectCreative, EffectCards, } from "swiper/modules"; Swiper.use([ Navigation, Pagination, Scrollbar, A11y, Autoplay, Thumbs, EffectFade, EffectCube, EffectFlip, EffectCoverflow, EffectCreative, EffectCards, ]); /** * Internal dependencies */ import { deepMerge, compose, createStyleManager } from "./utils-for-frontend"; class CarouselScript extends BaseComponent { constructor(element, options = {}) { // Get this._element and this._config super(element, options); // Bail if the element is not a DOM node if (!element) { return; } // Already initialized. this.cbbCarouselId = element.dataset.cbbCarouselId; if (this.cbbCarouselId && this.slider) { return this; } // Store the main element this.sliderElement = this._element; // Store the original source this.originalSource = element.cloneNode(true); // Get the owner document this.document = element.ownerDocument; // Cache the config this.settings = this._config; const { datasetSelector } = this.settings; // Get the datasetElement this.datasetElement = datasetSelector ? element.querySelector(datasetSelector) || element : element; // Get slider options this.sliderOptions = this.getSliderOptions(options?.sliderOptions); // The slider instance this.slider = null; // Kick start this.init(); } static get NAME() { return "carousel"; } defaultConfig() { return { selector: ".js-carousel-layout", wrapperSelector: ".carousel__inner", itemSelector: ".is-carousel-item", containerClass: "swiper", wrapperClass: "swiper-wrapper", itemClass: "swiper-slide", paginationClass: "swiper-pagination", navigationNextClass: "swiper-button-next", navigationPrevClass: "swiper-button-prev", scrollbarClass: "swiper-scrollbar", preloadClass: "cbb-carousel-preload", instanceId: "cbb-carousel-1", datasetSelector: "", }; } getSliderOptions(options) { const slideChangeStart = (swiper) => triggerEvent(window, "cbb.carousel.slideChangeTransitionStart", { swiper, }); const slideChangeEnd = (swiper) => triggerEvent(window, "cbb.carousel.slideChangeTransitionEnd", { swiper, }); // Slider default options let sliderOptions = { // Optional parameters direction: "horizontal", loop: false, grabCursor: true, pagination: true, navigation: false, scrollbar: false, // Slides per view slidesPerView: 1, slidesPerGroup: 1, spaceBetween: 0, watchSlidesProgress: true, on: { afterInit(swiper) { triggerEvent(window, "cbb.carousel.afterInit", { swiper }); }, slideChangeTransitionStart: slideChangeStart, slideChangeTransitionEnd: slideChangeEnd, slideResetTransitionStart: slideChangeStart, slideResetTransitionEnd: slideChangeEnd, }, }; // Parse settings from attribute const carouselSettings = this.datasetElement.dataset?.carouselSettings; const attributeOptions = parseJSON(carouselSettings); // Merge with dataset attribute sliderOptions = deepMerge(sliderOptions, attributeOptions); // Merge with options if (options) { sliderOptions = deepMerge(sliderOptions, options); } // Get slider options return sliderOptions; } init() { if (this.cbbCarouselId) { this.restoreSlider(); return; } // Setup the slider. this.buildSlider(); } buildSlider() { const { sliderElement, sliderOptions } = this; const { itemSelector, itemClass, wrapperSelector, wrapperClass, containerClass, preloadClass, instanceId, } = this.settings; // Query slide items const sliderItems = Array.from( sliderElement.querySelectorAll(itemSelector), ); // Cache it this.sliderItems = sliderItems; // Add item class sliderItems.forEach((item) => this.addClass(item, itemClass)); // The wrapper element let sliderWrapper = sliderElement.querySelector(wrapperSelector); if (sliderWrapper) { this.hasSlideWrapper = true; } else { this.hasSlideWrapper = false; // Create wrapper element sliderWrapper = this.document.createElement("div"); } // Cache it this.sliderWrapper = sliderWrapper; // Add swiper class this.addClass(sliderWrapper, wrapperClass); const { equalHeight, effect, centeredSlides, centeredSlidesSettings = {}, } = sliderOptions; // Make all items have equal height if (equalHeight) { this.addClass(sliderWrapper, "is-equal-height"); } // Get slider style and add custom variables const sliderStyle = this.getSliderStyle(); // centeredSlides if ( effect === "slide" && centeredSlides && centeredSlidesSettings?.enable ) { let inactiveSize = parseFloat(centeredSlidesSettings?.inactiveSize); if (!isNaN(inactiveSize)) { this.addClass(sliderWrapper, "centered-active-slide"); sliderStyle.add("--swiper-inactive-scale", inactiveSize); } } if (!this.hasSlideWrapper) { // Attach items sliderWrapper.append(...sliderItems); } // Create the container const sliderContainer = Object.assign(this.document.createElement("div"), { className: containerClass, }); // Cache it this.sliderContainer = sliderContainer; // Wrap the slider wrapper inside a container sliderContainer.appendChild(sliderWrapper); sliderElement.appendChild(sliderContainer); // Build navigation this.buildNavigation(); // Build play/pause this.buildPlayPause(); // Build pagination this.buildPagination(); // Build scrollbar this.buildScrollbar(); // Mark it as initialized. this.addClass(sliderElement, "slider-initialized"); // Remove the preload class this.removeClass(sliderElement, preloadClass); // Apply style for all custom properties from sub build functions sliderStyle.apply(); // Initialize the slider. this.slider = new Swiper(sliderContainer, sliderOptions); // Save the instance to the element. sliderElement.setAttribute("data-cbb-carousel-id", instanceId); } restoreSlider() { const swiperElement = this._element.querySelector( `.${this.settings.containerClass}`, ); if (!swiperElement) { this.buildSlider(); return; } /** * Reconnect existing controls */ const { cbbCarouselId } = this; const sliderOptions = { ...this.sliderOptions }; // Navigation if (sliderOptions.navigation) { sliderOptions.navigation = { ...sliderOptions.navigationSettings, nextEl: this.document.getElementById( `${cbbCarouselId}-navigation-next`, ), prevEl: this.document.getElementById( `${cbbCarouselId}-navigation-prev`, ), }; } // Pagination if (sliderOptions.pagination) { sliderOptions.pagination = this.buildPaginationOptions({ ...sliderOptions.paginationSettings, el: this.document.getElementById(`${cbbCarouselId}-pagination`), }); } // Scrollbar if (sliderOptions.scrollbar) { sliderOptions.scrollbar = { ...sliderOptions.scrollbarSettings, el: this.document.getElementById(`${cbbCarouselId}-scrollbar`), }; } // Reconnect play/pause if (sliderOptions?.playPause?.enable) { this.sliderPlayPause = this.document.getElementById( `${cbbCarouselId}-play-pause`, ); if (this.sliderPlayPause) { // Bind events this.bindPlayPause(this.sliderPlayPause); } } // Recreate swiper instance only this.slider = new Swiper(swiperElement, sliderOptions); } dispose() { super.dispose(); if (this.slider) { // Swiper's destroy this.slider.destroy(); // Remove the instance this.slider = null; if (!this.settings?.isClone) { // Restore the original source this.sliderElement.replaceWith(this.originalSource); } } } /** * Build slider controls */ buildSliderControls() { if (!this.sliderControls) { // Create the control wrapper const sliderControls = this.document.createElement("div"); // Add id sliderControls.setAttribute("id", `${this.settings.instanceId}-controls`); // Add class sliderControls.classList.add("cbb-carousel-controls"); // Cache it this.sliderControls = sliderControls; // Add the controls to the slider element this.sliderElement.appendChild(sliderControls); } return this.sliderControls; } hasCarouselControls(controlType = "") { const { pagination, paginationSettings, direction, navigation, navigationSettings = {}, playPause = {}, } = this.sliderOptions; if (!pagination || direction !== "horizontal") { return false; } const withPagination = "with-pagination"; const hasNavigation = navigation && navigationSettings?.position === withPagination; const hasPlayPause = playPause?.enable && playPause?.position === withPagination; if (!controlType) { return !!(paginationSettings?.position || hasNavigation || hasPlayPause); } else { if (controlType === "navigation") { return hasNavigation; } else if (controlType === "playPause") { return hasPlayPause; } } } getSliderControls(controlType) { if (this.hasCarouselControls(controlType)) { return this.buildSliderControls(); } return this.sliderElement; } /** * Build pagination */ buildPagination() { if (this.sliderOptions.pagination) { const sliderControls = this.getSliderControls(); const { sliderElement, sliderOptions, settings: { instanceId, paginationClass } = {}, } = this; // Create pagination wrapper const sliderPagination = this.document.createElement("div"); // Cache it this.sliderPagination = sliderPagination; // Add id sliderPagination.setAttribute("id", `${instanceId}-pagination`); // Add class sliderPagination.classList.add(paginationClass); const { direction, paginationSettings = {} } = sliderOptions; const { position, xOffset, type = "bullets", size, // Deprecated bulletWidth = size, bulletHeight = size, activeBulletWidth, activeBulletHeight, spacing, color, inactiveColor, opacity, borderColor, fractionColor, horizontalGap, dynamicBullets = false, } = paginationSettings; // Get slider style and add custom variables, apply in the buildSlider const sliderStyle = this.getSliderStyle(); if (position && direction === "horizontal") { (this.hasCarouselControls() ? this.sliderControls : sliderPagination ).classList.add("pag-custom-position", `pag-is-${position}`); sliderStyle.add("--cbb--pag-x-offset", xOffset); } if (type === "bullets") { if (bulletWidth === bulletHeight) { sliderStyle.add("--swiper-pagination-bullet-size", bulletWidth); } else { sliderStyle.add("--swiper-pagination-bullet-width", bulletWidth); sliderStyle.add("--swiper-pagination-bullet-height", bulletHeight); } if (activeBulletWidth) { this.sliderPagination.classList.add("active-bullet-width"); sliderStyle.add( "--swiper-pagination-active-bullet-width", activeBulletWidth, ); } if (activeBulletHeight) { this.sliderPagination.classList.add("active-bullet-height"); sliderStyle.add( "--swiper-pagination-active-bullet-height", activeBulletHeight, ); } sliderStyle.addColor("--swiper-pagination-color", color); sliderStyle.addColor( "--swiper-pagination-bullet-inactive-color", inactiveColor, ); sliderStyle.addColor("--swiper-pagination-bullet-b-color", borderColor); sliderStyle.add("--swiper-pagination-bullet-inactive-opacity", opacity); } else { sliderStyle.addColor( "--swiper-pagination-fraction-color", fractionColor, ); } if (spacing) { const offsetProperty = direction === "vertical" ? "--swiper-pagination-right" : "--swiper-pagination-bottom"; sliderStyle.add(offsetProperty, spacing); } sliderStyle.add( "--swiper-pagination-bullet-horizontal-gap", horizontalGap, ); // Add pagination to the options this.sliderOptions.pagination = this.buildPaginationOptions({ el: sliderPagination, type, dynamicBullets, }); // Add the pagination to the slider element sliderControls.appendChild(sliderPagination); } } /** * Build pagination arguments * * @param {Object} options * @returns */ buildPaginationOptions(options) { return { ...options, renderBullet(i) { return ``; }, clickable: true, }; } /** * Build navigation */ buildNavigation() { if (this.sliderOptions.navigation) { const { sliderElement, settings: { instanceId, navigationNextClass, navigationPrevClass } = {}, } = this; const { nextIcon = {}, prevIcon = {}, size, position, xOffset, yOffset, spacing, color, ...otherProps } = this.sliderOptions.navigationSettings; const sliderControls = this.getSliderControls("navigation"); // Create naviation next button const sliderNavigationNext = this.document.createElement("div"); // Cache it this.sliderNavigationNext = sliderNavigationNext; // Add id sliderNavigationNext.setAttribute("id", `${instanceId}-navigation-next`); // Add class sliderNavigationNext.classList.add(navigationNextClass); if (nextIcon?.value) { sliderNavigationNext.classList.add("has-custom-icon"); sliderNavigationNext.insertAdjacentHTML("afterbegin", nextIcon.value); } // Create naviation prev button const sliderNavigationPrev = this.document.createElement("div"); // Cache it this.sliderNavigationPrev = sliderNavigationPrev; // Add id sliderNavigationPrev.setAttribute("id", `${instanceId}-navigation-prev`); // Add class sliderNavigationPrev.classList.add(navigationPrevClass); if (prevIcon?.value) { sliderNavigationPrev.classList.add("has-custom-icon"); sliderNavigationPrev.insertAdjacentHTML("afterbegin", prevIcon.value); } // Get slider style and add custom variables, apply in the buildSlider const sliderStyle = this.getSliderStyle(); sliderStyle.add("--swiper-navigation-size", size); // Add position settings if (position) { sliderNavigationNext.classList.add( "nav-custom-position", `is-${position}`, ); sliderNavigationPrev.classList.add( "nav-custom-position", `is-${position}`, ); sliderStyle.add("--cbb--nav-x-offset", xOffset); sliderStyle.add("--cbb--nav-y-offset", yOffset); } else { sliderStyle.add("--swiper-navigation-sides-offset", spacing); } sliderStyle.addColor("--swiper-navigation-color", color); // Add navigation to the options this.sliderOptions.navigation = { ...otherProps, nextEl: sliderNavigationNext, prevEl: sliderNavigationPrev, }; // Add the naviation button to the slider element sliderControls.appendChild(sliderNavigationNext); // Add the naviation button to the slider element sliderControls.appendChild(sliderNavigationPrev); // Add class to mark the slider have navigation buttons sliderElement.classList.add("swiper--has-navigation"); } } /** * Build play/pause */ buildPlayPause() { const { playPause = {} } = this.sliderOptions; if (playPause?.enable) { const { size, position = "bottom-right", xOffset, yOffset, color, trackColor, progressColor, } = playPause; const sliderControls = this.getSliderControls("playPause"); // Create play/pause button const sliderPlayPause = this.document.createElement("div"); // Set the markup sliderPlayPause.innerHTML = ''; // Add attributes sliderPlayPause.setAttribute( "id", `${this.settings.instanceId}-play-pause`, ); sliderPlayPause.setAttribute("tabindex", 0); sliderPlayPause.setAttribute("role", "button"); // Add class sliderPlayPause.classList.add( "cbb-play-pause", "cbb-carousel-play-pause", `is-${position}`, ); // Get style builder for PlayPause const sliderPPStyle = createStyleManager(sliderPlayPause); // Inject custom properties sliderPPStyle.add("--cbb--pp-size", size); sliderPPStyle.add("--cbb--pp-x-offset", xOffset); sliderPPStyle.add("--cbb--pp-y-offset", yOffset); sliderPPStyle.addColor("--cbb--pp-color", color); sliderPPStyle.addColor("--cbb--progress-track-color", trackColor); sliderPPStyle.addColor("--cbb--progress-color", progressColor); // Apply here sliderPPStyle.apply(); // Bind events this.bindPlayPause(sliderPlayPause); // Cache it this.sliderPlayPause = sliderPlayPause; // Add the play/pause button to the slider element sliderControls.appendChild(sliderPlayPause); } } bindPlayPause(sliderPlayPause) { const { autoplay, on = {} } = this.sliderOptions; const updatePlayPause = (sliderPlayPause, autoplay) => { sliderPlayPause.setAttribute( "aria-pressed", !autoplay ? "true" : "false", ); sliderPlayPause.classList.toggle("is-playing", autoplay); sliderPlayPause.classList.toggle("is-paused", !autoplay); sliderPlayPause.setAttribute("aria-label", autoplay ? "Pause" : "Play"); }; const setProgress = (progress) => sliderPlayPause.style.setProperty("--cbb--progress", progress.toFixed(5)); const handlePlayPause = () => { const { running, start, resume, pause } = this.slider.autoplay; if (!running) { start(); resume(); updatePlayPause(sliderPlayPause, true); this.autoplaying = true; } else { if (!this.autoplaying) { resume(); } else { pause(); } this.autoplaying = !this.autoplaying; updatePlayPause(sliderPlayPause, this.autoplaying); } }; // Add event listener sliderPlayPause.addEventListener("click", handlePlayPause); sliderPlayPause.addEventListener("keydown", (e) => { if (!["Enter", " ", "Spacebar"].includes(e.key)) return; e.preventDefault(); handlePlayPause(); }); // Play by default this.autoplaying = !!autoplay; updatePlayPause(sliderPlayPause, !!autoplay); // Set aria-controls on.afterInit = compose(on.afterInit, (swiper) => { sliderPlayPause.setAttribute( "aria-controls", swiper.wrapperEl.getAttribute("id"), ); }); on.autoplayStop = () => { updatePlayPause(sliderPlayPause, false); setProgress(0); }; on.autoplayResume = () => updatePlayPause(sliderPlayPause, true); on.slideChange = compose(on.slideChange, (s) => { if (!this.autoplaying) { s.autoplay.stop(); } }); on.lock = (s) => sliderPlayPause.classList.add("swiper-button-lock"); on.unlock = (s) => sliderPlayPause.classList.remove("swiper-button-lock"); on.autoplayTimeLeft = (s, time, progress) => { if (s.isLocked || s.autoplay.paused) { return; } const activeSlideEl = s.slides[s.activeIndex]; const currentSlideDelay = parseInt( activeSlideEl.getAttribute("data-swiper-autoplay"), 10, ); setProgress( 1 - (currentSlideDelay > 0 ? time / currentSlideDelay : progress), ); }; // Set on back to the options this.sliderOptions.on = on; } /** * Build scrollbar */ buildScrollbar() { if (this.sliderOptions.scrollbar) { const { sliderElement, settings: { instanceId, scrollbarClass } = {} } = this; // Create scrollbar element const sliderScrollbar = this.document.createElement("div"); // Cache it this.sliderScrollbar = sliderScrollbar; // Add id sliderScrollbar.setAttribute("id", `${instanceId}-scrollbar`); // Add class sliderScrollbar.classList.add(scrollbarClass); const { direction, scrollbarSettings = {} } = this.sliderOptions; const { trackSize, spacing, trackColor, sliderColor, trackOpacity, ...otherProps } = scrollbarSettings; // Get slider style and add custom variables, apply in the buildSlider const sliderStyle = this.getSliderStyle(); sliderStyle.add("--swiper-scrollbar-size", trackSize); if (spacing) { const offsetProperty = direction === "vertical" ? "--swiper-scrollbar-right" : "--swiper-scrollbar-bottom"; sliderStyle.add(offsetProperty, spacing); } sliderStyle.addColor("--swiper-scrollbar-track-color", trackColor); sliderStyle.addColor("--swiper-scrollbar-slider-color", sliderColor); sliderStyle.add("--swiper-scrollbar-track-opacity", trackOpacity); // Add scrollbar to the options this.sliderOptions.scrollbar = { ...otherProps, el: sliderScrollbar, }; // Add the scrollbar to the slider element this.sliderContainer.appendChild(sliderScrollbar); } } getSliderStyle() { if (!this.sliderStyle) { this.sliderStyle = createStyleManager(this.sliderElement); } return this.sliderStyle; } addClass(element, classname) { if (!element.classList.contains(classname)) { element.classList.add(classname); } } removeClass(element, classname) { if (classname) { element.classList.remove(classname); } } } /** * Get carousel manager object * * @returns {Array} */ export const getCarouselManager = () => { if (!window?.CBBCarousels) { window.CBBCarousels = {}; } return window.CBBCarousels; }; export const addCarousel = (element, args, carouselElements = []) => { const manager = getCarouselManager(); const { instanceId } = args; if (!manager[instanceId] || !manager[instanceId]?.slider) { if (carouselElements.length > 1) { const thumbs = buildThumbs(element, carouselElements); if (thumbs) { args.sliderOptions = { thumbs: { swiper: thumbs.slider } }; } } if (element.classList.contains("wp-block-query")) { args.wrapperSelector = "ul"; args.itemSelector = "li"; } const instance = new CarouselScript(element, args); manager[instance?.cbbCarouselId || instanceId] = instance; return instance; } return manager[instanceId]; }; const buildThumbs = (element, carouselElements) => { let carouselSettings = element?.dataset?.carouselSettings; if (!carouselSettings) { return null; } carouselSettings = parseJSON(carouselSettings); const thumbSelector = carouselSettings?.thumbsSettings?.enable ? carouselSettings.thumbsSettings?.selector : ""; if (!thumbSelector) { return null; } const thumbElement = element.ownerDocument.querySelector(thumbSelector); if (!thumbElement) { return null; } const thumbsIndex = carouselElements.findIndex((element) => element.isSameNode(thumbElement), ); if (thumbsIndex === -1) { return null; } return addCarousel( thumbElement, { instanceId: `cbb-carousel-${thumbsIndex + 1}`, }, carouselElements, ); }; exposeComponent("CarouselScript", CarouselScript); export default CarouselScript;