UNPKG

omni-carousel

Version:

A lightweight carousel to enhance scrollable areas

1,426 lines (1,393 loc) 47.1 kB
// core/internal-config.ts var internalConfig = { ioRootMargin: "0px 1px 0px 1px", ioThresholds: [0, 1], scrollBehavior: "smooth", scrollBlock: "nearest", centerTolerance: 1, scrollendTimeoutDelay: 250, classes: { setupComplete: "is-omni-ready", itemCentered: "is-omni-centered", itemGroupCentered: "is-omni-centered", indicatorCurrent: "is-omni-current", indicatorPartCurrent: "is-omni-part-current", indicatorOverflowing: "is-omni-indicator-overflow", itemCurrent: "is-omni-current", itemPartCurrent: "is-omni-part-current", itemEntering: "is-omni-entering", itemExiting: "is-omni-exiting" }, selectors: { invisibleAnchor: ".omni-invisible-anchor" }, dataAttributes: { carouselInstance: "data-omni-carousel" } }; // core/carousel.ts import { createNanoEvents } from "nanoevents"; // core/defaults.ts var defaultSelectors = { track: "[data-omni-track]", slide: "[data-omni-slide]", prevButton: "[data-omni-button-prev]", nextButton: "[data-omni-button-next]", startButton: "[data-omni-button-start]", endButton: "[data-omni-button-end]", indicatorArea: "[data-omni-indicators]", indicator: "[data-omni-indicator]" }; var defaults = { scrollAlign: "start", scrollSteps: "one", selectors: defaultSelectors, hasEqualWidths: true, hasCenterMode: false, indicatorNumbers: false, transitionHelpers: false, preloadAdjacentImages: false }; // core/config.ts var mergeConfig = (options) => { if (!options) { return { ...defaults }; } const { selectors, ...rest } = options; return { ...defaults, ...rest, selectors: { ...defaults.selectors, ...selectors } }; }; // dom/attributes.ts var captureInitialAttributes = (context, initialState) => { const { prevButton, nextButton, startButton, endButton } = context.elements; if (initialState.buttonAttributes?.size > 0) { return; } [prevButton, nextButton, startButton, endButton].filter((button) => button !== void 0).forEach( (button) => { initialState.buttonAttributes.set(button, { disabled: button.hasAttribute("disabled"), hidden: button.hasAttribute("hidden") }); } ); }; var setElementAttributes = (context) => { const { root, indicators, prevButton, nextButton, startButton, endButton } = context.elements; root.classList.add(internalConfig.classes.setupComplete); indicators.forEach((indicator, index) => { indicator.setAttribute("aria-label", `Go to slide ${index + 1}`); }); [prevButton, nextButton, startButton, endButton].filter((button) => button !== void 0).forEach((button) => { button.removeAttribute("hidden"); }); }; var resetElementAttributes = (context, initialState) => { const { root, track } = context.elements; const { state } = context; root.classList.remove(internalConfig.classes.setupComplete); initialState.buttonAttributes.forEach((initialState2, button) => { button.toggleAttribute("disabled", initialState2.disabled); button.toggleAttribute("hidden", initialState2.hidden); }); if (state.addedTrackCSSPosition && initialState.trackCSSPosition !== void 0) { track.style.position = initialState.trackCSSPosition; state.addedTrackCSSPosition = false; } }; var ensureTrackPositioned = (context, initialState) => { const { track } = context.elements; const { state } = context; const computedPosition = window.getComputedStyle(track).position; if (computedPosition === "static") { initialState.trackCSSPosition = "static"; track.style.position = "relative"; state.addedTrackCSSPosition = true; } else { state.addedTrackCSSPosition = false; } }; // dom/elements.ts var getElements = (root, selectors) => { const track = root.querySelector(selectors.track); const slides = [...root.querySelectorAll(selectors.slide)]; const prevButton = root.querySelector(selectors.prevButton) || void 0; const nextButton = root.querySelector(selectors.nextButton) || void 0; const startButton = root.querySelector(selectors.startButton) || void 0; const endButton = root.querySelector(selectors.endButton) || void 0; const indicatorArea = root.querySelector(selectors.indicatorArea) || void 0; if (!track || slides.length < 2) { throw new Error("Carousel requires a track and at least 2 slides."); } return { root, track, slides, prevButton, nextButton, startButton, endButton, indicatorArea }; }; // dom/styles.ts var getScrollSnapAlign = (element) => { const computedStyle = window.getComputedStyle(element); const alignValue = computedStyle.scrollSnapAlign; if (!alignValue) { return void 0; } return alignValue.includes("center") ? "center" : "start"; }; var updateItem = (context, slide, fullIntersecting, partIntersecting, wasFullIntersecting, wasPartIntersecting) => { const { transitionHelpers } = context.config; const { classes } = internalConfig; slide.classList.toggle(classes.itemCurrent, fullIntersecting); slide.classList.toggle(classes.itemPartCurrent, partIntersecting); if (!transitionHelpers) { return; } const wasIntersecting = wasFullIntersecting || wasPartIntersecting; if (wasFullIntersecting && partIntersecting) { slide.classList.remove(classes.itemEntering); slide.classList.add(classes.itemExiting); } else if (!wasIntersecting && partIntersecting) { slide.classList.remove(classes.itemExiting); slide.classList.add(classes.itemEntering); } }; var updateCenterItem = (context) => { const { slides } = context.elements; const { centeredItemIndex, previousCenteredItemIndex } = context.state; const { itemCentered } = internalConfig.classes; if (previousCenteredItemIndex !== void 0) { slides[previousCenteredItemIndex].classList.remove(itemCentered); } if (centeredItemIndex !== void 0) { slides[centeredItemIndex].classList.add(itemCentered); } }; var updateCenterGroupItem = (context) => { const { elements, state, config } = context; const { slides } = elements; const { previousCenteredGroupItems, centeredGroupItems } = state; const { hasEqualWidths, scrollSteps, scrollAlign } = config; const { itemGroupCentered } = internalConfig.classes; if (hasEqualWidths == true || scrollAlign !== "center" || scrollSteps !== "auto") { return; } previousCenteredGroupItems.forEach((index) => { slides[index].classList.remove(itemGroupCentered); }); centeredGroupItems.forEach((index) => { slides[index].classList.add(itemGroupCentered); }); }; var clearItemAttributes = (slide) => { const { classes } = internalConfig; slide.classList.remove( classes.itemCurrent, classes.itemPartCurrent, classes.itemCentered, classes.itemGroupCentered ); }; // handlers/event-click.ts var handleClick = (context, event) => { const target = event.target; if (!target || !(target instanceof Element)) { return; } const button = target instanceof HTMLButtonElement ? target : target.closest("button"); if (!button) { return; } const { eventEmitter } = context; const { prevButton, nextButton, startButton, endButton } = context.elements; if (button === prevButton) { eventEmitter.emit("omni:nav:prev"); } else if (button === nextButton) { eventEmitter.emit("omni:nav:next"); } else if (button === startButton) { eventEmitter.emit("omni:nav:index", { index: 0 }); } else if (button === endButton) { eventEmitter.emit("omni:nav:index", { index: context.elements.slides.length - 1 }); } else if (button.parentElement === context.elements.indicatorArea) { const indicatorIndex = context.elements.indicators.indexOf(button); if (indicatorIndex !== -1 && indicatorIndex < context.elements.slides.length) { eventEmitter.emit("omni:nav:index", { index: indicatorIndex }); } } }; // handlers/event-keyboard.ts var handleKeyboard = (context, event) => { const { elements, state, eventEmitter } = context; const { root, track } = elements; const activeElement = document.activeElement; if (!root.contains(activeElement) || activeElement !== track) { return; } if (event.key === "ArrowLeft" && !state.startItemFullIntersecting) { event.preventDefault(); eventEmitter.emit("omni:nav:prev"); } else if (event.key === "ArrowRight" && !state.endItemFullIntersecting) { event.preventDefault(); eventEmitter.emit("omni:nav:next"); } else if (event.key === "Home" && !state.startItemFullIntersecting) { event.preventDefault(); eventEmitter.emit("omni:nav:index", { index: 0 }); } else if (event.key === "End" && !state.endItemFullIntersecting) { event.preventDefault(); eventEmitter.emit("omni:nav:index", { index: elements.slides.length - 1 }); } }; // features/utils.ts var getRectCenterX = (element) => { const rect = element.getBoundingClientRect(); return rect.left + rect.width / 2; }; var calculateCenterScrollPosition = (context, container, startItem, width) => { const measurements = (() => { const containerWidth = context.state.width; const containerLeft = context.utils.getContainerLeft(); const startItemRect = startItem.getBoundingClientRect(); return { containerWidth, // // Calculate the left position of the start item relative to the container // startLeft: startItemRect.left - containerLeft + container.scrollLeft }; })(); const leftOffset = Math.max(0, (measurements.containerWidth - width) / 2); return measurements.startLeft - leftOffset; }; var getFallbackItem = (context, direction) => { const { fullItems, partItems } = context.state; const { slides } = context.elements; const goingLeft = direction === "left"; if (fullItems.length > 0) { return goingLeft ? fullItems[0] : fullItems[fullItems.length - 1]; } else if (partItems.length > 0) { return goingLeft ? partItems[0] : partItems[partItems.length - 1]; } return goingLeft ? slides[0] : slides[slides.length - 1]; }; // features/indicators.ts var clearIndicators = (context) => { const { indicatorArea } = context.elements; if (!indicatorArea) { return; } indicatorArea.replaceChildren(); }; var addIndicators = (context) => { const { indicatorArea } = context.elements; if (!indicatorArea) { return []; } const { slides, track } = context.elements; const { indicatorNumbers } = context.config; const trackID = track.id; const fragment = document.createDocumentFragment(); const slidesLength = slides.length; const indicators = Array(slidesLength); for (let index = 0; index < slidesLength; index++) { const indicator = document.createElement("button"); indicator.type = "button"; indicator.setAttribute("data-omni-indicator", `${index}`); indicator.setAttribute("aria-label", `Go to slide ${index + 1}`); if (indicatorNumbers) { const number = document.createElement("span"); number.textContent = `${index + 1}`; indicator.appendChild(number); } if (trackID) { indicator.setAttribute("aria-controls", trackID); } fragment.appendChild(indicator); indicators[index] = indicator; } indicatorArea.appendChild(fragment); return indicators; }; var updateIndicator = (context, index, fullIntersecting, partIntersecting) => { const indicator = context.elements.indicators[index]; const { classes } = internalConfig; indicator.toggleAttribute("aria-current", fullIntersecting); indicator.classList.toggle(classes.indicatorCurrent, fullIntersecting); indicator.classList.toggle(classes.indicatorPartCurrent, partIntersecting); }; var updateIndicatorOverflow = (context) => { const { state } = context; const { indicatorArea } = context.elements; const overflow = indicatorArea ? indicatorArea.scrollWidth > indicatorArea.clientWidth : false; const overflowChanged = state.indicatorOverflow !== overflow; if (overflowChanged) { state.indicatorOverflow = overflow; context.elements.root.classList.toggle(internalConfig.classes.indicatorOverflowing, overflow); } return overflowChanged; }; var centerIndicators = (context) => { const { state } = context; const { indicatorArea } = context.elements; if (!indicatorArea) { return; } const { indicators } = context.elements; const currentIndicators = state.fullItems.map((slide) => { const index = context.utils.getItemIndex(slide); return indicators[index]; }).filter((indicator) => indicator !== void 0); if (currentIndicators.length === 0) { return; } let indicatorWidth; let indicatorSpacing; if (state.indicatorWidth && state.indicatorSpacing) { indicatorWidth = state.indicatorWidth; indicatorSpacing = state.indicatorSpacing; } else { const rect1 = indicators[0].getBoundingClientRect(); const rect2 = indicators[1].getBoundingClientRect(); indicatorSpacing = rect2.left - rect1.right; indicatorWidth = rect1.width; state.indicatorSpacing = indicatorSpacing; state.indicatorWidth = indicatorWidth; } const firstItem = currentIndicators[0]; const count = currentIndicators.length; const totalWidth = indicatorWidth * count + indicatorSpacing * (count - 1); const scrollPosition = calculateCenterScrollPosition( context, indicatorArea, firstItem, totalWidth ); indicatorArea.scrollTo({ left: scrollPosition, behavior: internalConfig.scrollBehavior }); }; var determineAlignmentForIndicator = (context, index) => { const { fullItems, partItems } = context.state; const { getItemIndex } = context.utils; let lastVisibleItemIndex = -1; if (fullItems.length > 0) { const lastFullItem = fullItems[fullItems.length - 1]; lastVisibleItemIndex = getItemIndex(lastFullItem); } else if (partItems.length > 0) { const lastPartItem = partItems[partItems.length - 1]; lastVisibleItemIndex = getItemIndex(lastPartItem); } return index > lastVisibleItemIndex ? "end" : "start"; }; var storeIndicators = (context, indicators) => { context.elements.indicators = indicators; }; // handlers/event-scrollend.ts var startIndicatorCentering = (context) => { if (!context.state.indicatorOverflow) { return; } setTimeout(() => { centerIndicators(context); }, 1); }; var removeInvisibleAnchors = (context) => { if (!context.state.hasOldInvisibleAnchors) { return; } const { track } = context.elements; const oldAnchors = track.querySelectorAll(".omni-invisible-anchor:not(:last-child)"); oldAnchors.forEach((anchor) => anchor.remove()); context.state.hasOldInvisibleAnchors = false; }; // handlers/event-transitionend.ts var updateItemTransitionClasses = (context, event) => { if (!event.target) { return; } const target = event.target; const { selectors } = context.config; const slide = target.closest(selectors.slide); if (!slide) { return; } const { classes } = internalConfig; slide.classList.remove(classes.itemEntering, classes.itemExiting); }; // handlers/omni-event-init.ts var handleInit = (context, initialState, lazyInitObserver) => { const { elements } = context; captureInitialAttributes(context, initialState); lazyInitObserver.observe(elements.root); return true; }; // features/buttons.ts var updateBackwardButtons = (context) => { const { prevButton, startButton } = context.elements; const { startItemFullIntersecting, centeredItemIndex } = context.state; const { hasCenterMode } = context.config; let atStart; if (hasCenterMode) { atStart = startItemFullIntersecting && centeredItemIndex === 0; } else { atStart = startItemFullIntersecting; } prevButton?.toggleAttribute("disabled", atStart); startButton?.toggleAttribute("disabled", atStart); }; var updateForwardButtons = (context) => { const { nextButton, endButton } = context.elements; const { endItemFullIntersecting, centeredItemIndex } = context.state; const { hasCenterMode } = context.config; const { slides } = context.elements; let atEnd; if (hasCenterMode) { atEnd = endItemFullIntersecting && centeredItemIndex === slides.length - 1; } else { atEnd = endItemFullIntersecting; } nextButton?.toggleAttribute("disabled", atEnd); endButton?.toggleAttribute("disabled", atEnd); }; // utils/arrays.ts var insertInOrder = (array, element, getOriginalIndex) => { const elementOriginalIndex = getOriginalIndex(element); const insertionPoint = array.findIndex( (item) => getOriginalIndex(item) > elementOriginalIndex ); const insertAt = insertionPoint === -1 ? array.length : insertionPoint; array.splice(insertAt, 0, element); }; var removeAtIndex = (array, index) => { if (index < 0 || index >= array.length) { return; } array.splice(index, 1); }; // utils/browser-support.ts var supportsRequirements = () => { const scrollBehavior = CSS.supports("scroll-behavior", "smooth"); const aspectRatio = CSS.supports("aspect-ratio", "1"); return { supported: scrollBehavior && aspectRatio, details: { scrollBehavior, aspectRatio } }; }; var supportsScrollend = () => { return "onscrollend" in window; }; var hasBlinkEngine = () => { return ( // // Chrome, Edge, Opera (Blink-based browsers) // CSS.supports("-webkit-app-region", "none") && !CSS.supports("-apple-trailing-word", "none") ); }; // utils/debounce.ts function debounce(fn, wait) { let controller = new AbortController(); return function(...args) { controller.abort(); controller = new AbortController(); const signal = controller.signal; setTimeout(() => { if (!signal.aborted) { fn.apply(this, args); } }, wait); }; } // utils/measurements.ts var getItemSpacing = (context) => { const { slides } = context.elements; const { itemSpacing } = context.state; if (itemSpacing) { return itemSpacing; } const rect1 = slides[0].getBoundingClientRect(); const rect2 = slides[1].getBoundingClientRect(); const spacing = rect2.left - rect1.right; context.state.itemSpacing = spacing; return spacing; }; // features/steps-auto-align-all.ts var getInboundItems = (context, direction) => { const { fullItems } = context.state; const { slides } = context.elements; const { getItemIndex } = context.utils; if (fullItems.length === 0) { return false; } const goingLeft = direction === "left"; const goingRight = !goingLeft; const firstFullItemIndex = fullItems.length > 0 ? getItemIndex(fullItems[0]) : -1; const lastFullItemIndex = fullItems.length > 0 ? getItemIndex(fullItems[fullItems.length - 1]) : -1; if (goingLeft && firstFullItemIndex === 0 || goingRight && lastFullItemIndex === slides.length - 1) { return false; } return calculateInboundItems(context, direction); }; var validateInboundItems = (context, result, goingLeft) => { const { slides } = context.elements; if (result.count === 0) { return false; } if (result.startIndex < 0 || result.startIndex >= slides.length) { return false; } const lastItemIndex = goingLeft ? result.startIndex : result.startIndex + result.count - 1; if (lastItemIndex < 0 || lastItemIndex >= slides.length) { return false; } return result; }; var calculateInboundItems = (context, direction) => { const { state, elements } = context; const { hasEqualWidths } = context.config; const { fullItems } = state; const { slides } = elements; const { getItemIndex, getItemWidth } = context.utils; const goingLeft = direction === "left"; const firstFullItemIndex = fullItems.length > 0 ? getItemIndex(fullItems[0]) : -1; const lastFullItemIndex = fullItems.length > 0 ? getItemIndex(fullItems[fullItems.length - 1]) : -1; const itemSpacing = getItemSpacing(context); const availableWidth = state.width; const initialIndex = goingLeft ? firstFullItemIndex - 1 : lastFullItemIndex + 1; let startIndex = initialIndex; let accumulatedWidth = 0; let count = 0; let currentIndex = initialIndex; let i = 0; while (goingLeft ? currentIndex >= 0 : currentIndex < slides.length) { let itemWidth; if (i === 0) { if (hasEqualWidths && state.itemWidth !== void 0) { itemWidth = state.itemWidth; } else { itemWidth = getItemWidth(currentIndex); if (hasEqualWidths) { state.itemWidth = itemWidth; } } } else { const itemBaseWidth = hasEqualWidths && state.itemWidth !== void 0 ? state.itemWidth : getItemWidth(currentIndex); itemWidth = itemBaseWidth + itemSpacing; } if (accumulatedWidth + itemWidth <= availableWidth) { accumulatedWidth += itemWidth; count++; if (goingLeft) { startIndex = currentIndex; } currentIndex += goingLeft ? -1 : 1; i++; } else { break; } } const result = { count, width: accumulatedWidth, startIndex }; return validateInboundItems(context, result, goingLeft); }; // features/invisible-anchor.ts var createInvisibleAnchor = (context, forceNew = false) => { const { elements } = context; const { track } = elements; const { selectors } = internalConfig; const selector = selectors.invisibleAnchor; if (!forceNew) { const existingInvisibleAnchor = track.querySelector(selector); if (existingInvisibleAnchor) { return existingInvisibleAnchor; } } const invisibleAnchor = document.createElement("div"); invisibleAnchor.classList.add("omni-invisible-anchor"); invisibleAnchor.style.position = "absolute"; invisibleAnchor.style.pointerEvents = "none"; invisibleAnchor.style.top = "50%"; invisibleAnchor.style.transform = "translateY(-50%)"; invisibleAnchor.style.scrollSnapAlign = "center"; track.appendChild(invisibleAnchor); elements.invisibleAnchor = invisibleAnchor; return invisibleAnchor; }; var placeInvisibleAnchor = (context, position, width, forceNew = false) => { const invisibleAnchor = createInvisibleAnchor(context, forceNew); invisibleAnchor.style.left = `${position}px`; if (width !== void 0) { invisibleAnchor.style.width = `${width}px`; } return invisibleAnchor; }; // features/scroll.ts var scrollToCenter = (context, element, direction) => { const { config, elements, state } = context; const { track } = elements; const { hasEqualWidths } = config; const { scrollBehavior, scrollBlock } = internalConfig; const inboundItems = getInboundItems(context, direction); if (!inboundItems || inboundItems.width >= state.width + 1) { element.scrollIntoView({ behavior: scrollBehavior, block: scrollBlock, inline: "center" }); return; } if (hasEqualWidths && inboundItems.count % 2 === 1) { const middleIndex = inboundItems.startIndex + Math.floor(inboundItems.count / 2); const middleItem = elements.slides[middleIndex]; middleItem.scrollIntoView({ behavior: scrollBehavior, block: scrollBlock, inline: "center" }); return; } if (!hasEqualWidths) { context.state.previousCenteredGroupItems = [...context.state.centeredGroupItems]; context.state.centeredGroupItems = []; for (let i = 0; i < inboundItems.count; i++) { const slideIndex = inboundItems.startIndex + i; context.state.centeredGroupItems.push(slideIndex); } } const firstSlideInGroup = elements.slides[inboundItems.startIndex]; const firstRect = firstSlideInGroup.getBoundingClientRect(); const trackRect = track.getBoundingClientRect(); const leftPosition = firstRect.left - trackRect.left + track.scrollLeft; if (context.state.detectedBlinkEngine) { const oldInvisibleAnchor = context.elements.invisibleAnchor; const invisibleAnchor = placeInvisibleAnchor( context, leftPosition, inboundItems.width, true ); context.state.hasOldInvisibleAnchors = true; invisibleAnchor.scrollIntoView({ behavior: scrollBehavior, block: scrollBlock, inline: "center" }); if (oldInvisibleAnchor) { if (supportsScrollend()) { } else { setTimeout(() => oldInvisibleAnchor.remove(), 0); } } } else { const invisibleAnchor = placeInvisibleAnchor( context, leftPosition, inboundItems.width ); invisibleAnchor.scrollIntoView({ behavior: scrollBehavior, block: scrollBlock, inline: "center" }); } }; // features/steps-all-align-start.ts var getAdjacentItem = (context, direction) => { const { fullItems } = context.state; const { slides } = context.elements; const { getItemIndex } = context.utils; const goingLeft = direction === "left"; const anchor = fullItems[0]; const anchorIndex = getItemIndex(anchor); const targetIndex = goingLeft ? Math.max(0, anchorIndex - 1) : Math.min(slides.length - 1, anchorIndex + 1); return targetIndex !== anchorIndex ? slides[targetIndex] : anchor; }; // features/steps-all-align-center.ts var getCentermostItem = (context, direction) => { const { slides } = context.elements; const { fullItems, centeredItemIndex } = context.state; const { getItemIndex } = context.utils; const goingLeft = direction === "left"; let item; let itemIndex; const oneFullItem = fullItems.length === 1; const validCenterIndex = centeredItemIndex !== -1 && fullItems.some((item2) => getItemIndex(item2) === centeredItemIndex); if (oneFullItem || validCenterIndex) { const anchorIndex = oneFullItem ? getItemIndex(fullItems[0]) : centeredItemIndex ?? 0; itemIndex = goingLeft ? Math.max(0, anchorIndex - 1) : Math.min(slides.length - 1, anchorIndex + 1); item = slides[itemIndex]; } else { item = calculateCentermostItem(context, direction); itemIndex = getItemIndex(item); } if (context.state.centeredItemIndex !== itemIndex) { context.state.previousCenteredItemIndex = context.state.centeredItemIndex; context.state.centeredItemIndex = itemIndex; if (context.config.hasCenterMode) { updateBackwardButtons(context); updateForwardButtons(context); } } return item; }; var calculateCentermostItem = (context, direction) => { const { track, slides } = context.elements; const { fullItems, partItems } = context.state; const { getItemIndex } = context.utils; const goingLeft = direction === "left"; const { centerTolerance } = internalConfig; if (fullItems.length === 0 && partItems.length === 0) { return getFallbackItem(context, direction); } const visibleItems = [...fullItems, ...partItems].sort( (a, b) => getItemIndex(a) - getItemIndex(b) ); const trackCenter = getRectCenterX(track); let item; if (goingLeft) { const startIndex = getItemIndex(visibleItems[visibleItems.length - 2]); const itemsToSearch = slides.slice(0, startIndex + 1).reverse(); item = itemsToSearch.find((item2) => { const itemCenter = getRectCenterX(item2); return itemCenter < trackCenter - centerTolerance; }); } else { const startIndex = getItemIndex(visibleItems[1]); const itemsToSearch = slides.slice(startIndex); item = itemsToSearch.find((item2) => { const itemCenter = getRectCenterX(item2); return itemCenter > trackCenter + centerTolerance; }); } return item || getFallbackItem(context, direction); }; // state/set.ts var createState = (trackWidth, trackScrollWidth, config) => { const debouncedCenterIndicators = !supportsScrollend() && config ? debounce((context) => centerIndicators(context), internalConfig.scrollendTimeoutDelay) : void 0; return { width: trackWidth, scrollWidth: trackScrollWidth, // containerLeft: undefined by default, will be calculated lazily when needed fullItems: [], partItems: [], startItemFullIntersecting: false, endItemFullIntersecting: false, hasIndicators: false, indicatorOverflow: false, slideIndexMap: /* @__PURE__ */ new Map(), itemWidthMap: /* @__PURE__ */ new Map(), detectedBlinkEngine: hasBlinkEngine(), centeredGroupItems: [], previousCenteredGroupItems: [], debouncedCenterIndicators }; }; var populateMaps = (context) => { const { elements, state } = context; const { slides } = elements; state.slideIndexMap = new Map( slides.map((slide, index) => [slide, index]) ); }; // state/update.ts var updateIntersectionState = (context, data) => { const { state } = context; const { slides } = context.elements; const { getItemIndex } = context.utils; const { slide, fullIntersecting, partIntersecting } = data; const index = getItemIndex(slide); const isStartItem = index === 0; const isEndItem = index === slides.length - 1; const wasStartItemFullIntersecting = state.startItemFullIntersecting; const wasEndItemFullIntersecting = state.endItemFullIntersecting; const indexInFullItems = state.fullItems.findIndex((item) => item === slide); const indexInPartItems = state.partItems.findIndex((item) => item === slide); const wasFullIntersecting = indexInFullItems !== -1; const wasPartIntersecting = indexInPartItems !== -1; let visibilityChanged = false; let startBoundaryChanged = false; let endBoundaryChanged = false; if (wasFullIntersecting !== fullIntersecting) { visibilityChanged = true; if (fullIntersecting) { insertInOrder(state.fullItems, slide, getItemIndex); if (isStartItem && !wasStartItemFullIntersecting) { startBoundaryChanged = true; state.startItemFullIntersecting = true; } if (isEndItem && !wasEndItemFullIntersecting) { endBoundaryChanged = true; state.endItemFullIntersecting = true; } } else { removeAtIndex(state.fullItems, indexInFullItems); if (isStartItem && wasStartItemFullIntersecting) { startBoundaryChanged = true; state.startItemFullIntersecting = false; } if (isEndItem && wasEndItemFullIntersecting) { endBoundaryChanged = true; state.endItemFullIntersecting = false; } } } if (wasPartIntersecting !== partIntersecting) { visibilityChanged = true; if (partIntersecting) { insertInOrder(state.partItems, slide, getItemIndex); } else { removeAtIndex(state.partItems, indexInPartItems); } } return { changed: visibilityChanged || startBoundaryChanged || endBoundaryChanged, slide, fullIntersecting, partIntersecting, wasFullIntersecting, wasPartIntersecting, startBoundaryChanged, endBoundaryChanged }; }; // handlers/omni-event-setup.ts var handleSetup = (context, initialState, intersectionObserver, addEventListeners) => { const { elements, state, config } = context; const detectedAlignment = getScrollSnapAlign(elements.slides[0]); if (detectedAlignment) { config.scrollAlign = detectedAlignment; } const indicators = addIndicators(context); storeIndicators(context, indicators); state.hasIndicators = indicators.length > 0; if (state.hasIndicators) { updateIndicatorOverflow(context); } populateMaps(context); if (config.scrollAlign === "center" && config.scrollSteps === "auto") { ensureTrackPositioned(context, initialState); } setElementAttributes(context); elements.slides.forEach((slide) => intersectionObserver.observe(slide)); addEventListeners(); }; // handlers/omni-event-destroy.ts var handleDestroy = (context, initialState, intersectionObserver, resizeObserver, removeEventListeners, eventEmitter, data, lazyInitObserver) => { const { state } = context; const { fullItems, partItems } = context.state; const mode = data?.mode || "full"; intersectionObserver.disconnect(); removeEventListeners(); resetElementAttributes(context, initialState); clearIndicators(context); storeIndicators(context, []); [...fullItems, ...partItems].forEach((slide) => clearItemAttributes(slide)); state.fullItems = []; state.partItems = []; state.startItemFullIntersecting = false; state.endItemFullIntersecting = false; state.hasIndicators = false; state.indicatorOverflow = false; state.centeredItemIndex = void 0; state.hasOldInvisibleAnchors = false; if (context.elements.invisibleAnchor) { context.elements.invisibleAnchor.remove(); context.elements.invisibleAnchor = void 0; } state.debouncedCenterIndicators = void 0; state.slideIndexMap.clear(); state.itemWidthMap.clear(); if (mode === "partial") { return true; } initialState.buttonAttributes.clear(); initialState.trackCSSPosition = void 0; resizeObserver.disconnect(); lazyInitObserver?.disconnect(); eventEmitter.events = {}; context.elements.root.removeAttribute(internalConfig.dataAttributes.carouselInstance); return false; }; // handlers/omni-event-dimensions-change.ts var updateLayoutData = (context, intersectionObserver) => { const { state } = context; const { slides } = context.elements; state.fullItems = []; state.partItems = []; state.startItemFullIntersecting = false; state.endItemFullIntersecting = false; state.itemWidth = void 0; state.containerLeft = void 0; state.itemWidthMap.clear(); if (state.hasIndicators) { updateIndicatorOverflow(context); } intersectionObserver.disconnect(); slides.forEach((slide) => intersectionObserver.observe(slide)); }; // handlers/omni-event-visibility-change.ts var updateUI = (context, data) => { const { utils } = context; const { slide, fullIntersecting, partIntersecting, wasFullIntersecting, wasPartIntersecting } = data; updateItem( context, slide, fullIntersecting, partIntersecting, wasFullIntersecting, wasPartIntersecting ); if (!context.config.hasCenterMode) { if (data.startBoundaryChanged) { updateBackwardButtons(context); } if (data.endBoundaryChanged) { updateForwardButtons(context); } } if (context.state.hasIndicators) { const index = utils.getItemIndex(slide); updateIndicator(context, index, fullIntersecting, partIntersecting); if (context.state.indicatorOverflow && !supportsScrollend() && context.state.debouncedCenterIndicators) { context.state.debouncedCenterIndicators(context); } } if (context.config.hasEqualWidths === false && context.config.scrollSteps === "auto" && context.config.scrollAlign === "center") { updateCenterGroupItem(context); } }; var preloadImages = (context) => { const { elements, state } = context; const { slides } = elements; const visibleSlides = [...state.fullItems, ...state.partItems]; if (visibleSlides.length === 0) { return; } const firstVisibleIndex = slides.indexOf(visibleSlides[0]); const lastVisibleIndex = slides.indexOf(visibleSlides[visibleSlides.length - 1]); const adjacentIndexes = /* @__PURE__ */ new Set(); if (firstVisibleIndex > 0) { adjacentIndexes.add(firstVisibleIndex - 1); } if (lastVisibleIndex < slides.length - 1) { adjacentIndexes.add(lastVisibleIndex + 1); } adjacentIndexes.forEach((index) => { const slide = slides[index]; const lazyImages = slide.querySelectorAll('img[loading="lazy"]'); lazyImages.forEach((img) => { img.removeAttribute("loading"); }); }); }; // handlers/observation-intersection.ts var handleIntersection = (context, entries) => { const { eventEmitter, state } = context; for (const entry of entries) { const slide = entry.target; const fullIntersecting = entry.isIntersecting && entry.intersectionRatio === 1; const partIntersecting = entry.isIntersecting && entry.intersectionRatio < 1; const result = updateIntersectionState(context, { slide, fullIntersecting, partIntersecting }); if (result.changed) { eventEmitter.emit("omni:visibility:change", { state, slide: result.slide, fullIntersecting: result.fullIntersecting, partIntersecting: result.partIntersecting, wasPartIntersecting: result.wasPartIntersecting, wasFullIntersecting: result.wasFullIntersecting, startBoundaryChanged: result.startBoundaryChanged, endBoundaryChanged: result.endBoundaryChanged }); } } }; // handlers/observation-resize.ts var handleResize = (context) => { const { track } = context.elements; const { state, eventEmitter } = context; const width = track.clientWidth; const scrollWidth = track.scrollWidth; if (width !== state.width || scrollWidth !== state.scrollWidth) { const storedOverflow = state.scrollWidth > state.width; const overflow = scrollWidth > width; const overflowChanged = storedOverflow !== overflow; state.width = width; state.scrollWidth = scrollWidth; if (overflowChanged && overflow) { eventEmitter.emit("omni:setup"); } else if (overflowChanged && !overflow) { eventEmitter.emit("omni:destroy", { mode: "partial" }); } else if (overflow) { eventEmitter.emit("omni:dimensions:change", { width, scrollWidth }); } } }; // navigation/index.ts var determineScrollMode = (index, scrollAlign, scrollSteps) => { if (index !== "none") { return "indicator"; } if (scrollSteps === "one") { return scrollAlign === "center" ? "steps-one-align-center" : "steps-one-align-start"; } else { return scrollAlign === "center" ? "steps-auto-align-center" : "steps-auto-align-start"; } }; var navigate = (context, direction = "none", index = "none") => { if (index === "none" && direction === "none") { return; } const { slides } = context.elements; const { scrollAlign, scrollSteps } = context.config; let position = context.config.scrollAlign; let destination; let shouldCenterForMany = false; let shouldUpdateCenterItem = false; if (context.state.fullItems.length < 1 && index === "none") { destination = getFallbackItem(context, direction); } else { const mode = determineScrollMode(index, scrollAlign, scrollSteps); if (mode === "indicator") { if (index !== "none" && index >= 0 && index < slides.length) { destination = slides[index]; if (scrollAlign === "start") { position = determineAlignmentForIndicator(context, index); } else { context.state.previousCenteredItemIndex = context.state.centeredItemIndex; context.state.centeredItemIndex = index; shouldUpdateCenterItem = true; } } else { return; } } else if (mode === "steps-one-align-center") { destination = getCentermostItem(context, direction); shouldUpdateCenterItem = true; } else if (mode === "steps-one-align-start") { destination = getAdjacentItem(context, direction); } else if (mode === "steps-auto-align-center") { const inboundItems = getInboundItems(context, direction); if (inboundItems) { destination = slides[inboundItems.startIndex]; shouldCenterForMany = true; } else { destination = getCentermostItem(context, direction); } } else if (mode === "steps-auto-align-start") { const inboundItems = getInboundItems(context, direction); if (inboundItems) { destination = slides[inboundItems.startIndex]; } else { destination = getAdjacentItem(context, direction); } } } destination = destination || getFallbackItem(context, direction); if (shouldUpdateCenterItem) { updateCenterItem(context); } if (shouldCenterForMany) { scrollToCenter(context, destination, direction); } else { destination.scrollIntoView({ behavior: internalConfig.scrollBehavior, block: internalConfig.scrollBlock, inline: position }); } }; // core/carousel.ts var createOmniCarousel = (root, options) => { const requirements = supportsRequirements(); if (!requirements.supported) { const missing = []; if (!requirements.details.scrollBehavior) { missing.push("scroll-behavior"); } if (!requirements.details.aspectRatio) { missing.push("aspect-ratio"); } throw new Error(`Browser requirements not met: ${missing.join(", ")} CSS support missing`); } if (root.hasAttribute(internalConfig.dataAttributes.carouselInstance)) { throw new Error("Carousel instance already exists on this element. Each element can only have one carousel instance."); } root.setAttribute(internalConfig.dataAttributes.carouselInstance, "true"); const onEvent = (event, handler) => { eventEmitter.on(event, handler); }; const config = mergeConfig(options); const elements = getElements(root, config.selectors); const { track, slides } = elements; const state = createState(track.clientWidth, track.scrollWidth, config); const eventEmitter = createNanoEvents(); const utils = { // // @neededfor scrollAlign:'center' + scrollSteps:'auto' // @neededfor indicators // getElementRect: (element, property) => { const { width, left } = element.getBoundingClientRect(); if (property === "width") { return width; } else if (property === "left") { return left; } return { width, left }; }, getItemIndex: (slide) => { return slide ? state.slideIndexMap.get(slide) ?? -1 : -1; }, // // @neededfor scrollSteps:'auto' // getItemWidth: (index) => { if (!state.itemWidthMap.has(index)) { const width = slides[index].offsetWidth; state.itemWidthMap.set(index, width); } return state.itemWidthMap.get(index); }, // // @neededfor scrollAlign:'center' + scrollSteps:'auto' // // Lazily calculates and caches the container's left position // getContainerLeft: () => { if (state.containerLeft === void 0) { state.containerLeft = track.getBoundingClientRect().left; } return state.containerLeft; } }; const getContext = (() => { const context = { config, elements: { ...elements, indicators: [] }, state, utils, eventEmitter }; return () => context; })(); const initialState = { buttonAttributes: /* @__PURE__ */ new Map() }; const eventHandlers = [ { handler: (event) => handleClick(getContext(), event), event: "click", element: "root", options: { passive: true } }, { handler: (event) => handleKeyboard(getContext(), event), event: "keydown", element: "track", options: {} }, // // @neededfor indicators // { handler: () => startIndicatorCentering(getContext()), event: "scrollend", element: "track", options: { passive: true }, condition: () => { const context = getContext(); return supportsScrollend() && context.state.indicatorOverflow; } }, // // @neededfor scrollAlign:'center' + scrollSteps:'auto' // { handler: () => removeInvisibleAnchors(getContext()), event: "scrollend", element: "track", options: { passive: true }, condition: () => { const context = getContext(); return !!context.state.detectedBlinkEngine && supportsScrollend() && context.config.scrollAlign === "center" && context.config.scrollSteps === "auto"; } }, // // @neededfor transitionHelpers:true // { handler: (event) => updateItemTransitionClasses(getContext(), event), event: "transitionend", element: "track", options: { passive: true }, condition: () => { const context = getContext(); return context.config.transitionHelpers; } } ]; const addEventListeners = () => { const { elements: elements2 } = getContext(); eventHandlers.filter((config2) => !config2.condition || config2.condition()).forEach((config2) => { elements2[config2.element].addEventListener( config2.event, config2.handler, config2.options ); }); }; const removeEventListeners = () => { const { elements: elements2 } = getContext(); eventHandlers.forEach((config2) => { elements2[config2.element].removeEventListener( config2.event, config2.handler ); }); }; const setupOmniEventListeners = () => { onEvent("omni:visibility:change", (data) => { updateUI(getContext(), data); if (getContext().config.preloadAdjacentImages) { preloadImages(getContext()); } }); onEvent("omni:dimensions:change", () => { updateLayoutData(getContext(), intersectionObserver); }); onEvent("omni:nav:prev", () => { navigate(getContext(), "left", "none"); }); onEvent("omni:nav:next", () => { navigate(getContext(), "right", "none"); }); onEvent("omni:nav:index", (data) => { navigate(getContext(), "none", data.index); }); }; let initialized = false; const init = () => { if (initialized) { console.warn("Carousel already initialized. Use setup() instead if needed."); return; } eventEmitter.emit("omni:init"); }; const destroy = () => { eventEmitter.emit("omni:destroy", { mode: "full" }); }; const resizeObserver = new ResizeObserver( () => { handleResize(getContext()); } ); const lazyInitObserver = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { resizeObserver.observe(elements.track); if (state.scrollWidth > state.width) { eventEmitter.emit("omni:setup"); } lazyInitObserver.disconnect(); } } ); const intersectionObserver = new IntersectionObserver( (entries) => handleIntersection(getContext(), entries), { root: track, rootMargin: internalConfig.ioRootMargin, threshold: internalConfig.ioThresholds } ); onEvent("omni:init", () => { initialized = handleInit( getContext(), initialState, lazyInitObserver ); }); onEvent("omni:setup", () => { handleSetup( getContext(), initialState, intersectionObserver, addEventListeners ); }); onEvent("omni:destroy", (data) => { initialized = handleDestroy( getContext(), initialState, intersectionObserver, resizeObserver, removeEventListeners, eventEmitter, data, lazyInitObserver ); }); setupOmniEventListeners(); const setup = () => { if (!initialized) { init(); return; } const { width, scrollWidth } = state; if (scrollWidth > width) { eventEmitter.emit("omni:setup"); } }; return { // // Lifecycle methods // init, setup, destroy: () => { if (initialized) { destroy(); } }, // // Navigation methods // goTo: (index) => eventEmitter.emit("omni:nav:index", { index }), next: () => eventEmitter.emit("omni:nav:next"), prev: () => eventEmitter.emit("omni:nav:prev"), // // Event subscription // on: (event, callback) => eventEmitter.on(event, callback) }; }; export { createOmniCarousel }; //# sourceMappingURL=index.js.map