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;