@ozenselin/image-carousel
Version:
A lightweight image carousel with smooth transitions, multi-control navigation, dynamic scaling, and shadow effects
497 lines (413 loc) • 18.9 kB
JavaScript
var Carousel = (function () {
'use strict';
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (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 --item-width: 14rem;\n --item-height: 14rem;\n --shadow-color: rgba(255, 255, 255, 0.5);\n\n --panel-margin-top: 6em;\n --panel-margin-bottom: 6em;\n \n --dot-padding: 3.5px;\n --dot-border-width: 2px;\n --dot-gap: 0.25em;\n --dot-bg-color: transparent;\n --dot-border-color: rgb(127, 127, 127);\n --dot-selected-bg-color: rgb(127, 127, 127);\n --dot-selected-border-color: rgb(127, 127, 127);\n --dot-transition-period: 0.3s;\n \n --icon-width: 1.75rem;\n --icon-fill-color: rgb(70, 70, 70);\n --nav-overlay-padding: 2rem;\n --nav-overlay-button-bg-color:rgb(245, 245, 245);\n --nav-overlay-button-bg-opacity: 0.9;\n --nav-overlay-button-bg-scale: 2.2;\n --nav-overlay-button-bg-blur: 10px;\n --nav-overlay-icon-fill-color: rgb(70, 70, 70);\n --nav-overlay-icon-width: 1.75rem;\n \n --nav-bottom-gap: 0.25em;\n --nav-bottom-icon-fill-color: rgb(127, 127, 127);\n --nav-bottom-icon-width: 1.25rem;\n\n --transition-period: 0.5s;\n --transition-timing-function: ease-in-out;\n\n font-family:\n system-ui, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif,\n \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n display: flex;\n flex-direction: column;\n overflow-x: hidden;\n position: relative;\n}\n\n.carousel__panel {\n margin-top: var(--panel-margin-top);\n margin-bottom: var(--panel-margin-bottom);\n position: relative;\n width: min-content;\n max-width: 200vw;\n top: 50%;\n left: 50%;\n /* Paneli tam ortalamak için eleman genişliğinin yarısı kadar sola kaydırıyoruz */\n transform: translateX(calc(var(--item-width) / -2));\n}\n\n.carousel__list {\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 var(--transition-period) var(--transition-timing-function);\n}\n\n.carousel__item {\n color: rgb(99, 99, 99);\n font-size: 2rem;\n width: var(--item-width);\n height: var(--item-height);\n background-color: rgb(171, 164, 164);\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 1px var(--shadow-color),\n 0 2px 4px 2px var(--shadow-color),\n 0 4px 8px 4px var(--shadow-color),\n 0 8px 16px 8px var(--shadow-color),\n 0 16px 32px 8px var(--shadow-color);\n}\n\n.carousel__indicators {\n display: grid;\n grid-auto-flow: column;\n gap: var(--dot-gap);\n}\n\n.carousel__dot {\n display: inline-block;\n border: var(--dot-border-width) solid var(--dot-border-color);\n background-color: var(--dot-bg-color);\n padding: var(--dot-padding);\n border-radius: 50%;\n transition: \n transform var(--dot-transition-period) ease-out;\n}\n\n.carousel__dot--selected {\n border-color: var(--dot-selected-border-color);\n background-color: var(--dot-selected-bg-color);\n}\n\n.carousel__navigation--bottom {\n display: flex;\n flex-direction: row;\n justify-content: center;\n align-items: center;\n gap: var(--nav-bottom-gap);\n}\n\n.carousel__button {\n border: unset;\n background-color: transparent;\n font: inherit;\n display: inline-grid;\n place-items: center;\n}\n\n.carousel__icon {\n width: var(--icon-width);\n}\n\n.carousel__navigation--bottom .carousel__icon {\n fill: var(--nav-bottom-icon-fill-color);\n width: var(--nav-bottom-icon-width);\n}\n\n.carousel__navigation--overlay .carousel__icon {\n fill: var(--nav-overlay-icon-fill-color);\n width: var(--nav-overlay-icon-width);\n}\n\n.carousel__navigation--overlay {\n position: absolute;\n width: 100%;\n transform: translateY(-50%);\n margin-top: calc(var(--item-height)/2 + var(--panel-margin-top));\n display: flex;\n flex-direction: row;\n justify-content: space-between;\n padding: var(--nav-overlay-padding);\n z-index: 1;\n}\n\n.carousel__navigation--overlay .carousel__button {\n position: relative;\n}\n\n.carousel__navigation--overlay .carousel__button::before,\n.carousel__navigation--overlay .carousel__button::after {\n content: '';\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: calc(var(--icon-width) * var(--nav-overlay-button-bg-scale));\n height: calc(var(--icon-width) * var(--nav-overlay-button-bg-scale));\n border-radius: 50%;\n background-color: var(--nav-overlay-button-bg-color);\n z-index: -1;\n opacity: var(--nav-overlay-button-bg-opacity);\n}\n\n.carousel__navigation--overlay .carousel__button::before {\n filter: blur(var(--nav-overlay-button-bg-blur));\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}`),
nextButtons: rootElement.querySelectorAll(`.${config.classes.nextButton}`),
previousButtons: rootElement.querySelectorAll(
`.${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="carousel__navigation carousel__navigation--overlay">
<button class="${config.classes.button} ${config.classes.previousButton}" type="button">
<svg class="carousel__icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M400-80 0-480l400-400 71 71-329 329 329 329-71 71Z"/></svg>
</button>
<button class="${config.classes.button} ${config.classes.nextButton}" type="button">
<svg class="carousel__icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="m321-80-71-71 329-329-329-329 71-71 400 400L321-80Z"/></svg>
</button>
</div>
<div class="${config.classes.panel}">
<ul class="${config.classes.list}"></ul>
</div>
<div class="carousel__navigation carousel__navigation--bottom">
<button class="${config.classes.button} ${config.classes.previousButton}" type="button">
<svg class="carousel__icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="m313-440 224 224-57 56-320-320 320-320 57 56-224 224h487v80H313Z"/></svg>
</button>
<div class="${config.classes.dotsContainer}"></div>
<button class="${config.classes.button} ${config.classes.nextButton}" type="button">
<svg class="carousel__icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px"><path d="M647-440H160v-80h487L423-744l57-56 320 320-320 320-57-56 224-224Z"/></svg>
</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);
elements.previousButtons.forEach((previousButton) => {
addEventListener(previousButton, "click", handlePrevious);
});
elements.nextButtons.forEach((nextButton) => {
addEventListener(nextButton, "click", handleNext);
});
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;
})();