UNPKG

@ozenselin/image-carousel

Version:

A lightweight image carousel with smooth transitions, multi-control navigation, dynamic scaling, and shadow effects

481 lines (398 loc) 14.6 kB
'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; }, }; }; module.exports = createCarousel;