@ozenselin/image-carousel
Version:
A lightweight image carousel with smooth transitions, multi-control navigation, dynamic scaling, and shadow effects
484 lines (400 loc) • 15.4 kB
JavaScript
var Carousel = (function () {
'use strict';
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = ".carousel * {\n padding: 0;\n margin: 0;\n box-sizing: border-box;\n}\n\n.carousel {\n --clr-lighter: rgb(226, 216, 216);\n --clr-light: rgb(171, 164, 164);\n --clr-dark: rgb(99, 99, 99);\n --clr-darker: rgb(43, 43, 43);\n color: var(--clr-font-dark);\n font-family:\n system-ui, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif,\n \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 1rem;\n padding: 0;\n margin: 0;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n overflow-x: hidden;\n}\n\n.carousel__panel {\n margin-top: 6em;\n margin-bottom: 6em;\n position: relative;\n width: min-content;\n max-width: 200vw;\n top: 50%;\n left: 50%;\n transform: translateX(-7em);\n}\n\n.carousel__list {\n --clr-shadow-light: rgba(255, 255, 255, 0.5);\n --clr-shadow-dark: rgba(0, 0, 0, 0.07);\n --clr-background-dark: rgb(171, 164, 164);\n position: relative;\n top: 0;\n left: 0;\n display: grid;\n grid-auto-flow: column;\n justify-items: center;\n list-style: none;\n transition: left 0.5s ease-in-out;\n}\n\n.carousel__item {\n color: var(--clr-dark);\n --transition-period: 0.5s;\n font-size: 2rem;\n width: 7em;\n height: 7em;\n background-color: var(--clr-light);\n display: grid;\n align-content: center;\n justify-content: center;\n transition:\n box-shadow var(--transition-period) ease,\n transform var(--transition-period) ease;\n}\n\n.carousel__item--selected {\n z-index: 1;\n transform-origin: center;\n box-shadow:\n 0 1px 2px var(--clr-shadow-light),\n 0 2px 4px var(--clr-shadow-light),\n 0 4px 8px var(--clr-shadow-light),\n 0 8px 16px var(--clr-shadow-light),\n 0 16px 32px var(--clr-shadow-light),\n 0 32px 64px var(--clr-shadow-light);\n}\n\n.carousel__indicators {\n display: grid;\n grid-auto-flow: column;\n gap: 0.25em;\n justify-content: center;\n margin-bottom: 3em;\n}\n\n.carousel__dot {\n display: inline-block;\n border: 2px solid var(--clr-dark);\n padding: 3.5px;\n border-radius: 50%;\n transition: background-color 0.3s;\n}\n\n.carousel__dot--selected {\n background-color: var(--clr-dark);\n}\n\n.carousel__actions {\n display: flex;\n flex-direction: row;\n justify-content: center;\n gap: 1em;\n}\n\n.carousel__button {\n background-color: var(--clr-darker);\n border: unset;\n color: var(--clr-lighter);\n border-radius: 1em;\n font: inherit;\n padding: 0em 1em;\n}\n";
styleInject(css_248z);
const DEFAULT_SCALE = {
HIGHEST: 1.4,
STEP: 0.2,
};
const DEFAULT_CLASSES = {
carousel: "carousel",
panel: "carousel__panel",
list: "carousel__list",
item: "carousel__item",
dotsContainer: "carousel__indicators",
dot: "carousel__dot",
buttonsContainer: "carousel__actions",
button: "carousel__button",
nextButton: "carousel__button--next",
previousButton: "carousel__button--previous",
selectedItem: "carousel__item--selected",
selectedDot: "carousel__dot--selected",
};
const DEFAULT_IMAGES = [
{ src: "./images/image01.png", alt: "", width: "100%" },
{ src: "./images/image02.jpg", alt: "", width: "100%" },
{ src: "./images/image03.jpg", alt: "", width: "100%" },
{ src: "./images/image04.png", alt: "", width: "100%" },
{ src: "./images/image05.png", alt: "", width: "100%" },
{ src: "./images/image06.png", alt: "", width: "100%" },
{ src: "./images/image07.png", alt: "", width: "100%" },
];
const CarouselConfig = {
scale: DEFAULT_SCALE,
classes: DEFAULT_CLASSES,
images: DEFAULT_IMAGES,
};
const createConfig = (userConfig = {}) => {
const config = {
...CarouselConfig,
...userConfig,
classes: { ...CarouselConfig.classes, ...userConfig.classes },
scale: { ...CarouselConfig.scale, ...userConfig.scale },
};
config.maxItems = config.images.length;
return config;
};
const createCarouselState = (config) => {
if (!config || !config.maxItems) {
throw new Error("Config with maxItems is required");
}
let currentIndex = 0;
let previousIndex = 0;
const maxItems = config.maxItems;
const isValidIndex = (index) => {
return Number.isInteger(index) && index >= 0 && index < maxItems;
};
const getCurrentIndex = () => currentIndex;
const getPreviousIndex = () => previousIndex;
const getMaxItems = () => maxItems;
const setCurrentIndex = (newIndex) => {
if (!isValidIndex(newIndex)) {
return false;
}
previousIndex = currentIndex;
currentIndex = newIndex;
return true;
};
const goToNext = () => {
return setCurrentIndex(currentIndex + 1);
};
const goToPrevious = () => {
return setCurrentIndex(currentIndex - 1);
};
const canGoNext = () => currentIndex < maxItems - 1;
const canGoPrevious = () => currentIndex > 0;
const reset = () => {
previousIndex = currentIndex;
currentIndex = 0;
};
const destroy = () => {
// basic cleanup
};
return {
getCurrentIndex,
getPreviousIndex,
getMaxItems,
setCurrentIndex,
goToNext,
goToPrevious,
canGoNext,
canGoPrevious,
reset,
destroy,
};
};
const createCarouselDOM = (rootElement, config) => {
if (!(rootElement instanceof HTMLElement)) {
throw new Error("rootElement must be a valid HTMLElement");
}
if (!config || !config.classes || !config.images) {
throw new Error("config must contain classes and images");
}
let elements = null;
let isInitialized = false;
const cacheElements = () => {
elements = {
list: rootElement.querySelector(`.${config.classes.list}`),
nextButton: rootElement.querySelector(`.${config.classes.nextButton}`),
previousButton: rootElement.querySelector(
`.${config.classes.previousButton}`
),
dotsContainer: rootElement.querySelector(
`.${config.classes.dotsContainer}`
),
items: Array.from(
rootElement.querySelectorAll(`.${config.classes.item}`)
),
dots: Array.from(rootElement.querySelectorAll(`.${config.classes.dot}`)),
};
};
const createImage = (imageConfig) => {
const image = document.createElement("img");
Object.entries(imageConfig).forEach(([attribute, value]) => {
if (value !== undefined && value !== null) {
image.setAttribute(attribute, value);
}
});
return image;
};
const createDot = () => {
const dot = document.createElement("span");
dot.classList.add(config.classes.dot);
return dot;
};
const createCarouselItem = (imageConfig) => {
const item = document.createElement("li");
item.classList.add(config.classes.item);
const image = createImage(imageConfig);
item.appendChild(image);
return item;
};
const injectHTML = () => {
rootElement.innerHTML = `
<div class="${config.classes.carousel}">
<div class="${config.classes.panel}">
<ul class="${config.classes.list}"></ul>
</div>
<div class="${config.classes.dotsContainer}"></div>
<div class="${config.classes.buttonsContainer}">
<button class="${config.classes.button} ${config.classes.previousButton}" type="button">
Previous
</button>
<button class="${config.classes.button} ${config.classes.nextButton}" type="button">
Next
</button>
</div>
</div>
`;
const list = rootElement.querySelector(`.${config.classes.list}`);
const dotsContainer = rootElement.querySelector(
`.${config.classes.dotsContainer}`
);
config.images.forEach((imageConfig) => {
const item = createCarouselItem(imageConfig);
const dot = createDot();
list.appendChild(item);
dotsContainer.appendChild(dot);
});
};
const updateClasses = (previousIndex, currentIndex) => {
if (!elements) return;
const selectedItemClass = config.classes.selectedItem;
const selectedDotClass = config.classes.selectedDot;
if (elements.items[previousIndex]) {
elements.items[previousIndex].classList.remove(selectedItemClass);
}
if (elements.items[currentIndex]) {
elements.items[currentIndex].classList.add(selectedItemClass);
}
if (elements.dots[previousIndex]) {
elements.dots[previousIndex].classList.remove(selectedDotClass);
}
if (elements.dots[currentIndex]) {
elements.dots[currentIndex].classList.add(selectedDotClass);
}
};
const moveItems = (targetIndex) => {
if (!elements?.items?.[0]) return;
const itemWidth = parseFloat(getComputedStyle(elements.items[0]).width);
const leftPosition = -targetIndex * itemWidth;
elements.list.style.left = `${leftPosition}px`;
};
const computeScale = (itemIndex, selectedIndex) => {
const difference = Math.abs(selectedIndex - itemIndex);
return config.scale.HIGHEST - config.scale.STEP * difference;
};
const resizeItems = (selectedIndex) => {
if (!elements?.items) return;
elements.items.forEach((item, index) => {
const scale = computeScale(index, selectedIndex);
item.style.transform = `scale(${scale})`;
});
};
const initialize = () => {
if (isInitialized) return;
injectHTML();
cacheElements();
resizeItems(0);
updateClasses(0, 0);
isInitialized = true;
};
const destroy = () => {
elements = null;
isInitialized = false;
rootElement.innerHTML = "";
};
const getElements = () => {
if (!elements) {
cacheElements();
}
return elements;
};
return {
initialize,
destroy,
moveItems,
resizeItems,
updateClasses,
getElements,
get isInitialized() {
return isInitialized;
},
};
};
const createCarouselEvents = (rootElement, state, dom, config) => {
if (!(rootElement instanceof HTMLElement)) {
throw new Error("rootElement must be a valid HTMLElement");
}
if (!state || !dom || !config) {
throw new Error("state, dom, and config are required");
}
const eventListeners = [];
let isInitialized = false;
const handleIndexChange = () => {
dom.moveItems(state.getCurrentIndex());
dom.resizeItems(state.getCurrentIndex());
dom.updateClasses(state.getPreviousIndex(), state.getCurrentIndex());
};
const handleNext = () => {
if (!state.canGoNext()) return;
state.goToNext();
handleIndexChange();
};
const handlePrevious = () => {
if (!state.canGoPrevious()) return;
state.goToPrevious();
handleIndexChange();
};
const getDotIndex = (targetDot) => {
const elements = dom.getElements();
return elements.dots.findIndex((dot) => dot === targetDot);
};
const handleDotClick = (event) => {
const dotIndex = getDotIndex(event.target);
if (dotIndex === -1 || dotIndex === state.getCurrentIndex()) {
return;
}
state.setCurrentIndex(dotIndex);
handleIndexChange();
};
const handleKeydown = (event) => {
switch (event.key) {
case "ArrowLeft":
event.preventDefault();
handlePrevious();
break;
case "ArrowRight":
event.preventDefault();
handleNext();
break;
}
};
const addEventListener = (element, eventType, handler) => {
if (!element) return;
element.addEventListener(eventType, handler);
eventListeners.push({ element, eventType, handler });
};
const setupEventListeners = () => {
const elements = dom.getElements();
addEventListener(elements.nextButton, "click", handleNext);
addEventListener(elements.previousButton, "click", handlePrevious);
addEventListener(elements.dotsContainer, "click", handleDotClick);
addEventListener(document, "keydown", handleKeydown);
};
const removeEventListeners = () => {
eventListeners.forEach(({ element, eventType, handler }) => {
element.removeEventListener(eventType, handler);
});
eventListeners.length = 0;
};
const initialize = () => {
if (isInitialized) return;
setupEventListeners();
isInitialized = true;
};
const destroy = () => {
removeEventListeners();
isInitialized = false;
};
return {
initialize,
destroy,
handleNext,
handlePrevious,
handleIndexChange,
get isInitialized() {
return isInitialized;
},
};
};
const createCarousel = ({ rootElement, config: userConfig = {} }) => {
if (!(rootElement instanceof HTMLElement)) {
throw new Error("rootElement must be a valid HTMLElement");
}
const config = createConfig(userConfig);
let state = null;
let dom = null;
let events = null;
let isInitialized = false;
const initialize = () => {
if (isInitialized) return;
state = createCarouselState(config);
dom = createCarouselDOM(rootElement, config);
events = createCarouselEvents(rootElement, state, dom, config);
dom.initialize();
events.initialize();
isInitialized = true;
};
const destroy = () => {
if (events?.destroy) events.destroy();
if (dom?.destroy) dom.destroy();
if (state?.destroy) state.destroy();
state = null;
dom = null;
events = null;
isInitialized = false;
};
const goToNext = () => {
if (!isInitialized) throw new Error("Must call initialize() first");
events.handleNext();
};
const goToPrevious = () => {
if (!isInitialized) throw new Error("Must call initialize() first");
events.handlePrevious();
};
const goToIndex = (index) => {
if (!isInitialized) throw new Error("Must call initialize() first");
state.setCurrentIndex(index);
events.handleIndexChange();
};
const getCurrentIndex = () => {
if (!isInitialized) throw new Error("Must call initialize() first");
return state.getCurrentIndex();
};
return {
initialize,
destroy,
goToNext,
goToPrevious,
goToIndex,
getCurrentIndex,
get isInitialized() {
return isInitialized;
},
};
};
return createCarousel;
})();