UNPKG

@ngfly/carousel

Version:

A smooth, customizable carousel component for Angular 17+ applications

1,103 lines (1,100 loc) 71.3 kB
import * as i0 from '@angular/core'; import { Injectable, inject, ChangeDetectorRef, NgZone, EventEmitter, Component, Input, Output, ContentChild, ViewChild, Directive, NgModule } from '@angular/core'; import { __rest } from 'tslib'; import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import { BehaviorSubject, Subject, fromEvent } from 'rxjs'; import { debounceTime, takeUntil } from 'rxjs/operators'; /** * Default carousel configuration values */ const CAROUSEL_DEFAULTS = { NAVIGATION_SIZE: '32px', CONTENT_PADDING: '10px', ANIMATION_DURATION: '300ms', ANIMATION_TIMING: 'ease', EMPTY_STATE_HEIGHT: '200px', NAVIGATION_PREV_ICON: '❮', NAVIGATION_NEXT_ICON: '❯', INDICATOR_SIZE: '10px', INDICATOR_SPACING: '5px', INDICATOR_ACTIVE_COLOR: '#333', INDICATOR_INACTIVE_COLOR: '#ccc', INDICATOR_ACTIVE_OPACITY: '1', INDICATOR_INACTIVE_OPACITY: '0.5', INDICATOR_ANIMATION_DURATION: '250ms', INDICATOR_ANIMATION_TIMING: 'ease', INDICATOR_TRANSITION: 'all 0.3s ease-in-out' }; /** * Service for carousel-related functionality and state management */ class CarouselService { constructor() { // Visibility state this.isVisibleSubject = new BehaviorSubject(true); this.isVisible$ = this.isVisibleSubject.asObservable(); } /** * Set carousel visibility * @param visible Whether carousel is visible */ setVisibility(visible) { this.isVisibleSubject.next(visible); } /** * Get button shape styles based on the configured shape * @param shape Button shape type * @returns Style object */ getButtonShapeStyles(shape) { const styles = {}; switch (shape) { case 'circle': styles['borderRadius'] = '50%'; break; case 'rounded': styles['borderRadius'] = '8px'; break; case 'square': default: styles['borderRadius'] = '0'; } return styles; } /** * Get indicator styles * @param config Indicator style configuration * @param isActive Whether to get active or inactive styles * @returns Style object */ getIndicatorStyles(config, isActive = false) { var _a, _b, _c, _d; // Base styles for both active and inactive const baseStyles = { width: CAROUSEL_DEFAULTS.INDICATOR_SIZE, height: CAROUSEL_DEFAULTS.INDICATOR_SIZE, display: 'inline-block', transition: (config === null || config === void 0 ? void 0 : config.transition) || (((_a = config === null || config === void 0 ? void 0 : config.animation) === null || _a === void 0 ? void 0 : _a.timing) ? `all ${config.animation.duration || CAROUSEL_DEFAULTS.INDICATOR_ANIMATION_DURATION} ${config.animation.timing}` : CAROUSEL_DEFAULTS.INDICATOR_TRANSITION), cursor: 'pointer', margin: `0 ${(config === null || config === void 0 ? void 0 : config.spacing) || CAROUSEL_DEFAULTS.INDICATOR_SPACING}`, borderRadius: '50%' // Default circle shape }; // Active/inactive specific styles if (isActive) { baseStyles['backgroundColor'] = CAROUSEL_DEFAULTS.INDICATOR_ACTIVE_COLOR; baseStyles['opacity'] = CAROUSEL_DEFAULTS.INDICATOR_ACTIVE_OPACITY; baseStyles['transform'] = 'scale(1.2)'; // Add animation if enabled and not explicitly disabled const animEnabled = ((_b = config === null || config === void 0 ? void 0 : config.animation) === null || _b === void 0 ? void 0 : _b.enabled) !== false; const animType = ((_c = config === null || config === void 0 ? void 0 : config.animation) === null || _c === void 0 ? void 0 : _c.type) || 'pulse'; if (animEnabled && animType !== 'none') { if (animType === 'custom' && ((_d = config === null || config === void 0 ? void 0 : config.animation) === null || _d === void 0 ? void 0 : _d.custom)) { baseStyles['animation'] = config.animation.custom; } else if (animType === 'pulse') { baseStyles['animation'] = `indicator-pulse 1s infinite alternate`; } } // Apply custom active styles if provided (these override defaults) if (config === null || config === void 0 ? void 0 : config.active) { Object.assign(baseStyles, config.active); } } else { baseStyles['backgroundColor'] = CAROUSEL_DEFAULTS.INDICATOR_INACTIVE_COLOR; baseStyles['opacity'] = CAROUSEL_DEFAULTS.INDICATOR_INACTIVE_OPACITY; baseStyles['transform'] = 'scale(1)'; // Apply custom inactive styles if provided (these override defaults) if (config === null || config === void 0 ? void 0 : config.inactive) { Object.assign(baseStyles, config.inactive); } } return baseStyles; } /** * Get indicator container styles based on configuration * @param config Indicator style configuration * @returns Style object for container */ getIndicatorContainerStyles(config) { var _a, _b; const styles = { position: 'absolute', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: '100', padding: '10px', pointerEvents: 'auto' }; // Default position (bottom center) styles['bottom'] = ((_a = config === null || config === void 0 ? void 0 : config.position) === null || _a === void 0 ? void 0 : _a.bottom) || '4px'; styles['left'] = ((_b = config === null || config === void 0 ? void 0 : config.position) === null || _b === void 0 ? void 0 : _b.left) || '50%'; styles['transform'] = 'translateX(-50%)'; // Override with custom positions if provided if (config === null || config === void 0 ? void 0 : config.position) { Object.keys(config.position).forEach(key => { const position = config.position; if (position[key]) { styles[key] = position[key]; } }); // Handle transformations for centered positioning if (config.position.left === '50%' && !styles['transform']) { styles['transform'] = 'translateX(-50%)'; } else if (config.position.top === '50%' && !styles['transform']) { styles['transform'] = 'translateY(-50%)'; } } // Apply custom container styles if provided if (config === null || config === void 0 ? void 0 : config.container) { Object.assign(styles, config.container); } return styles; } /** * Get navigation icons based on orientation * @param isVertical Whether carousel is vertical * @param icons Custom icon configuration * @returns Previous and next icons */ getNavigationIcons(isVertical, icons) { const defaultIcons = { horizontal: { prev: CAROUSEL_DEFAULTS.NAVIGATION_PREV_ICON, next: CAROUSEL_DEFAULTS.NAVIGATION_NEXT_ICON }, vertical: { prev: CAROUSEL_DEFAULTS.NAVIGATION_PREV_ICON, next: CAROUSEL_DEFAULTS.NAVIGATION_NEXT_ICON } }; const customIcons = icons || {}; const verticalIcons = customIcons.vertical || {}; return { prev: isVertical ? (verticalIcons.prev || defaultIcons.vertical.prev) : (customIcons.prev || defaultIcons.horizontal.prev), next: isVertical ? (verticalIcons.next || defaultIcons.vertical.next) : (customIcons.next || defaultIcons.horizontal.next) }; } /** * Parse time string to milliseconds * @param time Time string (e.g., '300ms', '0.5s') * @returns Time in milliseconds */ parseTimeToMs(time) { if (!time) return 300; // Default 300ms if (time.endsWith('ms')) { return parseInt(time.slice(0, -2), 10); } if (time.endsWith('s')) { return parseFloat(time.slice(0, -1)) * 1000; } return parseInt(time, 10); } } CarouselService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: CarouselService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); CarouselService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: CarouselService, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: CarouselService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Easing functions for animations */ const easings = { linear: (t) => t, ease: (t) => t, easeInQuad: (t) => t * t, easeOutQuad: (t) => t * (2 - t), easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, easeInCubic: (t) => t * t * t, easeOutCubic: (t) => (--t) * t * t + 1, easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1, }; /** * Converts a CSS timing function to its JavaScript equivalent * @param timingFunction CSS timing function string */ function getCssTimingFunction(timingFunction) { switch (timingFunction) { case 'linear': return easings.linear; case 'ease-in': return easings.easeInQuad; case 'ease-out': return easings.easeOutQuad; case 'ease-in-out': return easings.easeInOutQuad; case 'ease': return easings.ease; default: return easings.easeInOutQuad; } } /** * Creates a CSS transform string for translating elements * * @param position - Translation position in pixels * @param isVertical - Whether to use vertical translation * @returns CSS transform string */ function createTranslation(position, isVertical) { return isVertical ? `translateY(-${position}px)` : `translateX(-${position}px)`; } /** * Parse time string to milliseconds * @param time Time string in the format: '300ms' or '0.3s' * @param defaultValue Default value in ms * @returns Time in milliseconds */ function parseTimeToMs(time, defaultValue = 300) { if (!time) return defaultValue; if (time.endsWith('ms')) { return parseInt(time, 10); } else if (time.endsWith('s')) { return parseFloat(time) * 1000; } return parseInt(time, 10) || defaultValue; } /** * Calculate scroll amount based on configuration * @param scrollSize Scroll size specification * @param containerSize Container width or height * @param scrollSizeMap Map of predefined scroll sizes * @param defaultPercentage Default percentage if no size specified * @returns Scroll amount in pixels */ function calculateScrollAmount(scrollSize, containerSize, scrollSizeMap, defaultPercentage = 0.8) { if (!scrollSize) return containerSize * defaultPercentage; // If size is a percentage if (scrollSize.endsWith('%')) { const percentage = parseFloat(scrollSize) / 100; return containerSize * percentage; } // If size is a predefined value if (scrollSizeMap[scrollSize]) { return scrollSizeMap[scrollSize]; } // If size is a pixel value if (scrollSize.endsWith('px')) { return parseFloat(scrollSize); } return containerSize * defaultPercentage; } /** * Performs a smooth scroll animation * @param element Element to scroll * @param to Target scroll position * @param duration Duration in milliseconds * @param easing Easing function */ function smoothScroll(element, to, duration = 300, easing = easings.easeInOutQuad) { return new Promise(resolve => { const start = element.scrollLeft; const change = to - start; const startTime = performance.now(); function animateScroll(currentTime) { const elapsedTime = currentTime - startTime; if (elapsedTime >= duration) { element.scrollLeft = to; resolve(); return; } const progress = elapsedTime / duration; const easedProgress = easing(progress); element.scrollLeft = start + change * easedProgress; requestAnimationFrame(animateScroll); } requestAnimationFrame(animateScroll); }); } /** * Performs a smooth vertical scroll animation * @param element Element to scroll * @param to Target scroll position * @param duration Duration in milliseconds * @param easing Easing function */ function smoothScrollVertical(element, to, duration = 300, easing = easings.easeInOutQuad) { return new Promise(resolve => { const start = element.scrollTop; const change = to - start; const startTime = performance.now(); function animateScroll(currentTime) { const elapsedTime = currentTime - startTime; if (elapsedTime >= duration) { element.scrollTop = to; resolve(); return; } const progress = elapsedTime / duration; const easedProgress = easing(progress); element.scrollTop = start + change * easedProgress; requestAnimationFrame(animateScroll); } requestAnimationFrame(animateScroll); }); } class CarouselComponent { constructor() { this.cdr = inject(ChangeDetectorRef); this.ngZone = inject(NgZone); this.carouselService = inject(CarouselService); this.slides = []; this.configs = {}; this.activeIndex = 0; this.onPrevClick = new EventEmitter(); this.onNextClick = new EventEmitter(); // Private state variables this.currentTranslate = 0; this.currentIndex = this.activeIndex || 0; this.destroy$ = new Subject(); this.itemWidths = []; this.itemHeights = []; this.containerWidth = 0; this.containerHeight = 0; this.intersectionObserver = null; this.resizeObserver = null; this.visibilityChangeTimeout = null; this.initialized = false; // Scroll size definitions this.scrollSizeMap = { xs: 50, sm: 100, md: 150, lg: 200, xl: 250, '2xl': 300, '3xl': 350, '4xl': 400, '5xl': 450, '6xl': 500, '7xl': 550, '8xl': 600, '9xl': 650, '10xl': 700, full: 1, }; this.showPrevButton = false; this.showNextButton = false; this.filteredItems = []; this.emptyStateText = 'No items found'; this.emptyStateIcon = '📭'; this.emptyStateTextColor = '#666'; this.showEmptyStateIcon = true; } ngOnInit() { this.filteredItems = this.slides || []; this.currentIndex = this.activeIndex || 0; // Initialize navigation button visibility early const hasMultipleItems = this.filteredItems.length > 1; if (hasMultipleItems) { this.showPrevButton = true; this.showNextButton = true; } if (this.configs.emptyState) { this.emptyStateText = this.configs.emptyState.text || this.emptyStateText; this.emptyStateIcon = this.configs.emptyState.icon || this.emptyStateIcon; this.emptyStateTextColor = this.configs.emptyState.textColor || this.emptyStateTextColor; this.showEmptyStateIcon = this.configs.emptyState.hideIcon ? false : true; } } ngOnChanges(changes) { if (changes['slides']) { this.filteredItems = this.slides || []; if (this.initialized) { setTimeout(() => { this.updateContainerDimensions(); this.calculateItemDimensions(); this.checkOverflow(); this.cdr.detectChanges(); }); } } if (changes['activeIndex']) { this.goToSlide(this.activeIndex); } if (changes['configs'] && this.initialized) { setTimeout(() => { this.updateContainerDimensions(); this.calculateItemDimensions(); this.checkOverflow(); this.setupAutoplay(); this.cdr.detectChanges(); }); } } setupAutoplay() { this.clearAutoplayInterval(); if (!this.configs.autoplay) return; const delay = this.carouselService.parseTimeToMs(this.configs.autoplayDelay || '3000ms'); this.autoplayInterval = setInterval(() => { var _a, _b; const track = (_a = this.trackElement) === null || _a === void 0 ? void 0 : _a.nativeElement; const wrapper = (_b = this.wrapperElement) === null || _b === void 0 ? void 0 : _b.nativeElement; // Exit if elements not ready if (!track || !wrapper) return; const max = this.isVertical ? track.offsetHeight - wrapper.offsetHeight : track.offsetWidth - wrapper.offsetWidth; if (this.currentTranslate >= max) { if (!this.configs.loop) { this.clearAutoplayInterval(); return; } this.currentTranslate = 0; this.currentIndex = 0; } else { this.next(); } this.cdr.detectChanges(); }, delay); } clearAutoplayInterval() { if (this.autoplayInterval) { clearInterval(this.autoplayInterval); this.autoplayInterval = undefined; } } ngAfterViewInit() { // Initialize early to avoid ExpressionChangedAfterItHasBeenCheckedError this.ngZone.runOutsideAngular(() => { setTimeout(() => { this.ngZone.run(() => { this.initializeCarousel(); this.setupResizeListener(); this.setupIntersectionObserver(); this.setupResizeObserver(); this.updateContainerDimensions(); this.calculateItemDimensions(); this.checkOverflow(); this.initialized = true; // Ensure changes are applied before next change detection cycle this.cdr.detectChanges(); // Setup autoplay after initialization to avoid change detection issues setTimeout(() => { this.setupAutoplay(); }); // Force another update after layout is complete setTimeout(() => { this.updateContainerDimensions(); this.calculateItemDimensions(); this.checkOverflow(); this.cdr.detectChanges(); }, 100); }); }); }); } ngOnDestroy() { // Clear any interval timers if (this.autoplayInterval) { clearInterval(this.autoplayInterval); this.autoplayInterval = undefined; } // Disconnect observers if (this.intersectionObserver) { this.intersectionObserver.disconnect(); this.intersectionObserver = null; } if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; } // Clear any timeouts if (this.visibilityChangeTimeout) { clearTimeout(this.visibilityChangeTimeout); this.visibilityChangeTimeout = null; } // Complete all observables this.destroy$.next(); this.destroy$.complete(); // Clear arrays this.itemWidths = []; this.itemHeights = []; this.filteredItems = []; } get isVertical() { return this.configs.orientation === 'vertical'; } get containerStyles() { const styles = this.configs.containerStyle || {}; if (!('width' in styles)) { styles['width'] = this.configs.containerWidth || '100%'; } if (!('height' in styles)) { styles['height'] = this.configs.containerHeight || 'auto'; } return styles; } get trackStyles() { const transform = createTranslation(this.currentTranslate, this.isVertical); const base = { transform }; return this.isVertical ? Object.assign(Object.assign({}, base), { flexDirection: 'column' }) : base; } initializeCarousel() { if (!this.trackElement || !this.wrapperElement) return; this.currentTranslate = 0; this.currentIndex = 0; // Initialize navigation buttons this.checkOverflow(); } /** * Set up window resize listener */ setupResizeListener() { fromEvent(window, 'resize') .pipe(debounceTime(200), takeUntil(this.destroy$)) .subscribe(() => { this.updateContainerDimensions(); this.calculateItemDimensions(); this.checkOverflow(); }); } /** * Set up intersection observer to detect when carousel becomes visible */ setupIntersectionObserver() { if ('IntersectionObserver' in window) { this.ngZone.runOutsideAngular(() => { var _a; this.intersectionObserver = new IntersectionObserver((entries) => { var _a; const isVisible = (_a = entries[0]) === null || _a === void 0 ? void 0 : _a.isIntersecting; if (isVisible && this.initialized) { this.ngZone.run(() => { this.visibilityChangeTimeout = setTimeout(() => { this.updateContainerDimensions(); this.calculateItemDimensions(); this.checkOverflow(); }, 50); }); } }, { threshold: 0.1 }); if ((_a = this.wrapperElement) === null || _a === void 0 ? void 0 : _a.nativeElement) { this.intersectionObserver.observe(this.wrapperElement.nativeElement); } }); } } /** * Set up resize observer to detect when carousel's size changes */ setupResizeObserver() { if ('ResizeObserver' in window) { this.ngZone.runOutsideAngular(() => { var _a; this.resizeObserver = new ResizeObserver(() => { if (this.initialized) { this.ngZone.run(() => { this.updateContainerDimensions(); this.calculateItemDimensions(); this.checkOverflow(); }); } }); if ((_a = this.wrapperElement) === null || _a === void 0 ? void 0 : _a.nativeElement) { this.resizeObserver.observe(this.wrapperElement.nativeElement); } }); } } updateContainerDimensions() { if (!this.wrapperElement) return; this.containerWidth = this.wrapperElement.nativeElement.offsetWidth; this.containerHeight = this.wrapperElement.nativeElement.offsetHeight; } /** * Check if content overflows and update button visibility */ checkOverflow() { var _a, _b, _c; // Skip check if not yet initialized to avoid change detection errors if (!this.initialized && !((_a = this.trackElement) === null || _a === void 0 ? void 0 : _a.nativeElement)) return; // Always show navigation in one-item mode with more than one slide if (this.filteredItems.length > 1 && this.configs.singleItemMode) { // In one-item mode with loop, always show both buttons if we have more than one slide if (this.configs.loop) { this.showPrevButton = true; this.showNextButton = true; return; } // Otherwise, buttons depend on current index this.showPrevButton = this.currentIndex > 0; this.showNextButton = this.currentIndex < this.filteredItems.length - 1; return; } // First set default visibility based on showNavigation config if (!this.showNavigation || this.filteredItems.length <= 1) { this.showPrevButton = false; this.showNextButton = false; return; } const track = (_b = this.trackElement) === null || _b === void 0 ? void 0 : _b.nativeElement; const wrapper = (_c = this.wrapperElement) === null || _c === void 0 ? void 0 : _c.nativeElement; if (!track || !wrapper) return; // Skip if element has zero dimensions (may be hidden) if (wrapper.offsetWidth === 0 || wrapper.offsetHeight === 0) return; // Check if there's room to scroll in either direction // When loop is enabled, always show both buttons if there are items if (this.configs.loop && this.filteredItems.length > 1) { this.showPrevButton = true; this.showNextButton = true; } else { // Standard overflow checking (no loop) this.showPrevButton = this.currentTranslate > 0; if (this.isVertical) { // For vertical carousels const trackHeight = track.offsetHeight; const wrapperHeight = wrapper.offsetHeight; this.showNextButton = trackHeight - this.currentTranslate > wrapperHeight + 1; } else { // For horizontal carousels const trackWidth = track.offsetWidth; const wrapperWidth = wrapper.offsetWidth; this.showNextButton = trackWidth - this.currentTranslate > wrapperWidth + 1; } } this.cdr.detectChanges(); } /** * Calculate dimensions of carousel items */ calculateItemDimensions() { if (!this.trackElement || !this.wrapperElement) return; const wrapper = this.wrapperElement.nativeElement; const containerHeight = wrapper.offsetHeight; const containerWidth = wrapper.offsetWidth; // Update stored container dimensions this.containerWidth = containerWidth; this.containerHeight = containerHeight; const items = Array.from(this.trackElement.nativeElement.children); if (items.length === 0) return; // Add a small delay to ensure images are loaded and measured correctly setTimeout(() => { if (this.configs.singleItemMode) { // In single item mode, set all items to wrapper dimensions if (this.isVertical) { // For vertical mode, each item should be full height this.itemHeights = items.map(() => containerHeight); this.itemWidths = items.map((item) => { const style = window.getComputedStyle(item); const width = item.offsetWidth; const marginLeft = parseInt(style.marginLeft || '0', 10); const marginRight = parseInt(style.marginRight || '0', 10); return width + marginLeft + marginRight; }); } else { // For horizontal mode, each item should be full width this.itemWidths = items.map(() => containerWidth); this.itemHeights = items.map((item) => { const style = window.getComputedStyle(item); const height = item.offsetHeight; const marginTop = parseInt(style.marginTop || '0', 10); const marginBottom = parseInt(style.marginBottom || '0', 10); return height + marginTop + marginBottom; }); } // Apply the calculated dimensions immediately this.updateTranslatePosition(); this.cdr.detectChanges(); return; } // For multi-item mode, calculate actual dimensions this.itemWidths = items.map((item) => { const style = window.getComputedStyle(item); const width = item.offsetWidth; const marginLeft = parseInt(style.marginLeft || '0', 10); const marginRight = parseInt(style.marginRight || '0', 10); return width + marginLeft + marginRight; }); this.itemHeights = items.map((item) => { const style = window.getComputedStyle(item); const height = item.offsetHeight; const marginTop = parseInt(style.marginTop || '0', 10); const marginBottom = parseInt(style.marginBottom || '0', 10); return height + marginTop + marginBottom; }); this.cdr.detectChanges(); }, 50); // Small delay to ensure images are loaded } /** * Calculate scroll amount based on configuration */ getScrollAmount() { var _a; const wrapper = (_a = this.wrapperElement) === null || _a === void 0 ? void 0 : _a.nativeElement; if (!wrapper) return 0; // For one-item-scroll mode, return item dimension if (this.configs.singleItemMode) { if (this.isVertical && this.itemHeights.length > 0) { // Return height of current item or default to wrapper height return this.currentIndex < this.itemHeights.length ? this.itemHeights[this.currentIndex] : this.itemHeights[0] || wrapper.offsetHeight; } else if (this.itemWidths.length > 0) { // Return width of current item or default to wrapper width return this.currentIndex < this.itemWidths.length ? this.itemWidths[this.currentIndex] : this.itemWidths[0] || wrapper.offsetWidth; } } // Otherwise, use configured scroll size const size = this.configs.scrollSize || 'sm'; if (size === 'full') { // Full size returns container dimension return this.isVertical ? wrapper.offsetHeight : wrapper.offsetWidth; } // If size is a percentage, calculate based on container size if (typeof size === 'string' && size.endsWith('%')) { const percent = parseInt(size, 10) / 100; return this.isVertical ? wrapper.offsetHeight * percent : wrapper.offsetWidth * percent; } // Return pixel value from map or default to small return this.scrollSizeMap[size] || this.scrollSizeMap['sm']; } /** * Navigate to previous item */ previous() { if (!this.showPrevButton) return; if (this.configs.singleItemMode) { if (this.currentIndex > 0) { this.currentIndex--; this.updateTranslatePosition(); } else if (this.configs.loop && this.filteredItems.length > 0) { // For loop mode, go to the last slide this.currentIndex = this.filteredItems.length - 1; this.updateTranslatePosition(); } } else { const scrollAmount = this.getScrollAmount(); this.currentTranslate = Math.max(0, this.currentTranslate - scrollAmount); } this.checkOverflow(); this.onPrevClick.emit(this.currentIndex); } /** * Navigate to next item */ next() { var _a, _b; if (!this.showNextButton) return; const track = (_a = this.trackElement) === null || _a === void 0 ? void 0 : _a.nativeElement; const wrapper = (_b = this.wrapperElement) === null || _b === void 0 ? void 0 : _b.nativeElement; if (!track || !wrapper) return; if (this.configs.singleItemMode) { if (this.currentIndex < this.filteredItems.length - 1) { this.currentIndex++; this.updateTranslatePosition(); } else if (this.configs.loop && this.filteredItems.length > 0) { // For loop mode, go back to the first slide this.currentIndex = 0; this.currentTranslate = 0; } } else { const scrollAmount = this.getScrollAmount(); const maxTranslate = this.isVertical ? track.offsetHeight - wrapper.offsetHeight : track.offsetWidth - wrapper.offsetWidth; this.currentTranslate = Math.min(maxTranslate, this.currentTranslate + scrollAmount); } this.checkOverflow(); this.onNextClick.emit(this.currentIndex); } updateTranslatePosition() { if (this.currentIndex === 0) { this.currentTranslate = 0; return; } const gap = this.configs.itemGap ? parseInt(this.configs.itemGap.replace('px', ''), 10) : 0; let position = 0; // Calculate cumulative position based on item dimensions for (let i = 0; i < this.currentIndex; i++) { if (this.isVertical) { position += (this.itemHeights[i] || 0) + gap; } else { position += (this.itemWidths[i] || 0) + gap; } } this.currentTranslate = position; } getItemStyle(index) { const styles = { flexShrink: '0', flexGrow: '0', boxSizing: 'border-box', overflow: 'hidden', borderRadius: 'inherit', }; if (this.configs.itemWidth) { if (this.configs.itemWidth === '100%' && this.containerWidth > 0) { styles['width'] = this.containerWidth + 'px'; styles['maxWidth'] = '100%'; } else { styles['width'] = this.configs.itemWidth; } } if (this.configs.itemHeight) { if ((this.configs.itemHeight === '100%' || parseInt(this.configs.itemHeight, 10) === this.containerHeight) && this.containerHeight > 0) { styles['height'] = this.containerHeight + 'px'; styles['minHeight'] = this.containerHeight + 'px'; styles['maxHeight'] = '100%'; styles['display'] = 'flex'; styles['flexDirection'] = 'column'; styles['alignItems'] = 'stretch'; styles['justifyContent'] = 'stretch'; } else { styles['height'] = this.configs.itemHeight; } } else if (this.isVertical && this.configs.containerHeight) { styles['height'] = this.containerHeight + 'px'; styles['minHeight'] = this.containerHeight + 'px'; } // Add margin for gap between items (except first item) if (!this.configs.itemGap) return styles; const marginProperty = this.isVertical ? 'marginTop' : 'marginLeft'; styles[marginProperty] = index === 0 ? '0' : this.configs.itemGap; return styles; } get contentPadding() { return this.configs.contentPadding || CAROUSEL_DEFAULTS.CONTENT_PADDING; } get animationDuration() { return (this.configs.animationDuration || CAROUSEL_DEFAULTS.ANIMATION_DURATION); } get animationTiming() { return this.configs.animationTiming || CAROUSEL_DEFAULTS.ANIMATION_TIMING; } get showNavigation() { var _a; return (_a = this.configs.showNavigation) !== null && _a !== void 0 ? _a : true; } getEmptyStateContainerStyle() { var _a; return { width: '100%', boxSizing: 'border-box', borderRadius: 'inherit', backgroundColor: ((_a = this.configs.emptyState) === null || _a === void 0 ? void 0 : _a.backgroundColor) || 'transparent', }; } hasItems() { var _a; return ((_a = this.filteredItems) === null || _a === void 0 ? void 0 : _a.length) > 0; } getNavControlsClass() { return 'carousel__nav-controls'; } getNavControlsStyle() { var _a; const styles = { position: 'absolute', top: '0', left: '0', width: '100%', height: '100%', pointerEvents: 'none', }; // Apply custom z-index if specified in config if ((_a = this.configs.navigationStyle) === null || _a === void 0 ? void 0 : _a.zIndex) { styles['--carousel-z-index'] = this.configs.navigationStyle.zIndex; } return styles; } get prevIcon() { var _a; const icons = this.carouselService.getNavigationIcons(this.isVertical, (_a = this.configs.navigationStyle) === null || _a === void 0 ? void 0 : _a.icons); return icons.prev; } get nextIcon() { var _a; const icons = this.carouselService.getNavigationIcons(this.isVertical, (_a = this.configs.navigationStyle) === null || _a === void 0 ? void 0 : _a.icons); return icons.next; } getIconStyles(isNext) { var _a, _b, _c, _d; const color = isNext ? ((_b = (_a = this.configs.navigationStyle) === null || _a === void 0 ? void 0 : _a.nextButton) === null || _b === void 0 ? void 0 : _b.color) || '#666' : ((_d = (_c = this.configs.navigationStyle) === null || _c === void 0 ? void 0 : _c.prevButton) === null || _d === void 0 ? void 0 : _d.color) || '#666'; return { color }; } /** * Calculate position styles for navigation buttons */ getButtonPositionStyle(button) { const navConfig = this.configs.navigationStyle; const buttonStyle = button === 'prev' ? (navConfig === null || navConfig === void 0 ? void 0 : navConfig.prevButton) || {} : (navConfig === null || navConfig === void 0 ? void 0 : navConfig.nextButton) || {}; const style = { position: 'absolute', pointerEvents: 'auto' }; // Handle positioning properties ['top', 'bottom', 'left', 'right'].forEach(prop => { const value = buttonStyle[prop]; if (value !== undefined) { style[prop] = value === 0 || value === '0' ? '0px' : value; } }); // Handle transforms based on positioning const transformMap = { 'left=50%': 'translateX(-50%)', 'right=50%': 'translateX(50%)', 'top=50%': 'translateY(-50%)', 'bottom=50%': 'translateY(50%)' }; // Find matching transform for (const [position, transform] of Object.entries(transformMap)) { const [prop, value] = position.split('='); if (buttonStyle[prop] === value) { style['transform'] = buttonStyle.transform ? `${buttonStyle.transform} ${transform}` : transform; break; } } // Use custom transform if specified if (!style['transform'] && buttonStyle.transform) { style['transform'] = buttonStyle.transform; } // Apply default positioning if none specified const hasPosition = ['top', 'bottom', 'left', 'right'].some(prop => buttonStyle[prop] !== undefined); if (!hasPosition) { if (this.isVertical) { style['left'] = '50%'; style['transform'] = 'translateX(-50%)'; style[button === 'prev' ? 'top' : 'bottom'] = '0px'; } else { style['top'] = '50%'; style['transform'] = 'translateY(-50%)'; style[button === 'prev' ? 'left' : 'right'] = '0px'; } } return style; } /** * Get full styles for navigation buttons * @param buttonType - Type of button ('prev' or 'next') */ getButtonFullStyles(buttonType) { var _a, _b, _c, _d; // Get base styles including shape styles const styles = Object.assign({}, this.carouselService.getButtonShapeStyles((_a = this.configs.navigationStyle) === null || _a === void 0 ? void 0 : _a.buttonShape)); // Apply custom styles from config const buttonConfig = (_b = this.configs.navigationStyle) === null || _b === void 0 ? void 0 : _b[`${buttonType}Button`]; if (buttonConfig) { // Apply position styles first const positionStyles = this.getButtonPositionStyle(buttonType); Object.assign(styles, positionStyles); // Apply custom button styles but preserve shape styles const buttonShape = (_c = this.configs.navigationStyle) === null || _c === void 0 ? void 0 : _c.buttonShape; if (buttonShape) { const { borderRadius } = buttonConfig, otherButtonConfig = __rest(buttonConfig, ["borderRadius"]); Object.assign(styles, otherButtonConfig); } else { Object.assign(styles, buttonConfig); } // Set the CSS variable for z-index if specified in button config if (buttonConfig.zIndex) { styles['--carousel-z-index'] = buttonConfig.zIndex; } // If no button-specific z-index, but global navigation z-index exists, use that else if ((_d = this.configs.navigationStyle) === null || _d === void 0 ? void 0 : _d.zIndex) { styles['--carousel-z-index'] = this.configs.navigationStyle.zIndex; } } return styles; } getPrevButtonFullStyles() { return this.getButtonFullStyles('prev'); } getNextButtonFullStyles() { return this.getButtonFullStyles('next'); } getPrevIndex(activeIndex) { var _a; const itemsCount = ((_a = this.filteredItems) === null || _a === void 0 ? void 0 : _a.length) || 0; if (itemsCount === 0) return activeIndex; return activeIndex > 0 ? activeIndex - 1 : this.configs.loop ? itemsCount - 1 : activeIndex; } goToPrevSlide() { const prevIndex = this.getPrevIndex(this.currentIndex); if (prevIndex !== this.currentIndex) { this.currentIndex = prevIndex; this.updateTranslatePosition(); this.checkOverflow(); } } getIndicatorContainerStyles() { return this.carouselService.getIndicatorContainerStyles(this.configs.indicatorStyle); } /** * Get styles for an individual indicator * @param index Index of the indicator */ getIndicatorItemStyles(index) { const isActive = index === this.currentIndex; return this.carouselService.getIndicatorStyles(this.configs.indicatorStyle, isActive); } /** * Navigate to a specific slide when indicator is clicked * @param index Target slide index */ goToSlide(index) { var _a, _b, _c; if (index === this.currentIndex || index < 0 || index >= this.filteredItems.length) return; this.currentIndex = index; if (this.configs.singleItemMode) { this.updateTranslatePosition(); this.checkOverflow(); return; } const track = (_a = this.trackElement) === null || _a === void 0 ? void 0 : _a.nativeElement; const wrapper = (_b = this.wrapperElement) === null || _b === void 0 ? void 0 : _b.nativeElement; if (!track || !wrapper) return; const gap = parseInt(((_c = this.configs.itemGap) === null || _c === void 0 ? void 0 : _c.replace('px', '')) || '0', 10); const dimensions = this.isVertical ? this.itemHeights : this.itemWidths; // Calculate cumulative position up to target index const position = dimensions .slice(0, index) .reduce((sum, size) => sum + (size || 0) + gap, 0); const maxTranslate = this.isVertical ? track.offsetHeight - wrapper.offsetHeight : track.offsetWidth - wrapper.offsetWidth; this.currentTranslate = Math.min(maxTranslate, Math.max(0, position)); this.checkOverflow(); } /** * Whether to show indicators */ get showIndicators() { var _a; return (_a = this.configs.showIndicators) !== null && _a !== void 0 ? _a : false; } } CarouselComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: CarouselComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); CarouselComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: CarouselComponent, isStandalone: true, selector: "carousel", inputs: { slides: "slides", configs: "configs", activeIndex: "activeIndex" }, outputs: { onPrevClick: "onPrevClick", onNextClick: "onNextClick" }, providers: [CarouselService], queries: [{ propertyName: "itemTemplate", first: true, predicate: ["carouselItem"], descendants: true }, { propertyName: "emptyStateTemplate", first: true, predicate: ["emptyState"], descendants: true }], viewQueries: [{ propertyName: "trackElement", first: true, predicate: ["track"], descendants: true }, { propertyName: "wrapperElement", first: true, predicate: ["wrapper"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"carousel\" \n [class.carousel--vertical]=\"isVertical\" \n [ngClass]=\"configs.containerClass\" \n [ngStyle]=\"containerStyles\"\n [style.height]=\"configs.containerHeight || 'auto'\"\n [style.width]=\"configs.containerWidth || 'auto'\">\n <div\n #wrapper\n [style.--content-padding]=\"contentPadding\"\n class=\"carousel__wrapper\">\n <div\n #track\n [ngStyle]=\"trackStyles\"\n [class.carousel__track--vertical]=\"isVertical\"\n [style.--animation-duration]=\"animationDuration\"\n [style.--animation-timing]=\"animationTiming\"\n class=\"carousel__track\">\n <ng-container *ngIf=\"hasItems(); else emptyState\">\n <ng-container *ngFor=\"let slide of filteredItems; let i = index\">\n <div class=\"carousel__item\" \n [ngClass]=\"configs.itemClass\" \n [ngStyle]=\"getItemStyle(i)\" \n [style.--item-width]=\"configs.itemWidth || 'auto'\"\n [style.--item-height]=\"configs.itemHeight || 'auto'\">\n <ng-container *ngIf=\"itemTemplate; else defaultTemplate\">\n <ng-container *ngTemplateOutlet=\"itemTemplate; context: { $implicit: slide, index: i }\"></ng-container>\n </ng-container>\n <ng-template #defaultTemplate>\n <div class=\"carousel__item-default\">\n {{ slide }}\n </div>\n </ng-template>\n </div>\n </ng-container>\n </ng-container>\n \n <ng-template #emptyState>\n <div class=\"carousel__empty-container\" [ngStyle]=\"getEmptyStateContainerStyle()\">\n <ng-container *ngIf=\"emptyStateTemplate; else defaultEmptyState\">\n <ng-container *ngTemplateOutlet=\"emptyStateTemplate; context: { $implicit: emptyStateText }\"></ng-container>\n </ng-container>\n <ng-template #defaultEmptyState>\n <div class=\"carousel__empty-state\" [style.color]=\"emptyStateTextColor\">\n <div *ngIf=\"showEmptyStateIcon\" class=\"carousel__empty-icon\">{{emptyStateIcon}}</div>\n <div class=\"carousel__empty-text\">{{emptyStateText}}</div>\n </div>\n </ng-template>\n </div>\n </ng-template>\n </div>\n </div>\n\n <div *ngIf=\"showNavigation && hasItems()\" [ngClass]=\"getNavControlsClass()\" [ngStyle]=\"getNavControlsStyle()\"> \n <button \n [class.carousel__nav-button--disabled]=\"!showPrevButton\" \n [disabled]=\"!showPrevButton\" \n [ngStyle]=\"getPrevButtonFullStyles()\" \n (click)=\"previous()\" \n style=\"pointer-events:auto;\"\n class=\"carousel__nav-button\">\n <span class=\"carousel__nav-icon\" [ngStyle]=\"getIconStyles(false)\">{{ prevIcon }}</span>\n </button>\n \n <button\n [class.carousel__nav-button--disabled]=\"!showNextButton\"\n [disabled]=\"!showNextButton\"\n [ngStyle]=\"getNextButtonFullStyles()\"\n (click)=\"next()\"\n style=\"pointer-events:auto;\"\n class=\"carousel__nav-button\">\n <span class=\"carousel__nav-icon\" [ngStyle]=\"getIconStyles(true)\">{{ nextIcon }}</span>\n </button>\n </div>\n\n <!-- Carousel indicators -->\n <div\n *ngIf=\"showIndicators && hasItems() && filteredItems.length > 1\" \n class=\"carousel__indicators\" \n [ngStyle]=\"getIndicatorContaine