UNPKG

ngx-carousel-ease

Version:

ngx-carousel-ease is a versatile Angular library providing a feature-rich, simple, and performant carousel component. This library supports infinite and responsive mode, mouse and touch event. Attention has been put to accessibility, performance, and frie

1,178 lines (1,171 loc) 66.4 kB
import * as i0 from '@angular/core'; import { signal, Injectable, input, computed, PLATFORM_ID, Component, ChangeDetectionStrategy, Inject, ViewChild } from '@angular/core'; import { isPlatformBrowser, CommonModule } from '@angular/common'; class Carousel { constructor(carousel, maxWidthCarousel, slideToShow, slideWidth, slideMaxWidth, gap, responsive, loop) { this.carousel = carousel; this.maxWidthCarousel = maxWidthCarousel; this.slideToShow = slideToShow; this.slideWidth = slideWidth; this.slideMaxWidth = slideMaxWidth; this.gap = gap; this.responsive = responsive; this.loop = loop; this.numberDots = signal(0); this.maxScrollableContent = signal(0); this.arrayNumberDots = signal([]); this.totalSlides = 0; this.initialSlideToShow = 1; this.paddingCarousel = 0; this.arrayOfSlides = []; this.init(); } get carouselElement() { return this.carousel; } init() { this.paddingCarousel = this.getPaddingCarousel(); this.initialSlideToShow = this.slideToShow; this.slidesContainer = this.selectSlidesContainer(); this.slides = this.selectSlides(); this.arrayOfSlides = this.slidesToArray(); this.totalSlides = this.slides.length; this.originalSlideWidth = this.slideWidth; this.setWidthSlides(); this.setMaxWidthCarousel(); this.updateProperties(); this.slidesContainer.style.gap = `${this.gap}px`; this.setDraggableImgToFalse(); } /** * Set the slide width and max width * NB: In responsive mode, width is automatically adapted through the setAutoColumnSlideContainer() method. * */ setWidthSlides() { for (const slide of this.slides) { slide.style.maxWidth = `${this.slideMaxWidth}px`; slide.style.width = '100%'; slide.style.userSelect = 'none'; if (!this.responsive) { slide.style.width = `${this.slideWidth}px`; } } } /** * Update carousel properties * Occurs at start and at resizing. */ updateProperties() { this.carouselWidth = this.carousel.clientWidth; if (this.responsive) { this.updateSlideToShowResponsive(); this.setAutoColumnSlideContainer(); } else { this.updateSlideToShowNotResponsive(); } this.numberDots.set(this.setNumberDots()); this.arrayNumberDots.set([...Array(this.numberDots()).keys()]); this.slideWidthWithGap = this.slideWidth + this.gap; this.setWidthSlideContainer(); this.maxScrollableContent.set(this.getMaxScroll()); } /** * Update slide to show (responsive mode) * Computes the number of slide fitting */ updateSlideToShowResponsive() { const slideWidthPlusGap = this.originalSlideWidth + this.gap; const referenceWidth = Math.min(this.carouselWidth, this.maxWidthCarousel || Infinity, window.innerWidth); const slideFitting = Math.floor(referenceWidth / slideWidthPlusGap) || 1; this.determineSlideToShow(slideFitting); } /** * Determine slide to show * Useful to compute the slide displayed on screen and for the slider class. * In not responsive mode, the maximum slides to show is determined by the maximum available space and the width of the slide set by the user. */ determineSlideToShow(slideFitting) { const maxSlidesToShow = this.responsive ? this.initialSlideToShow : this.totalSlides; this.slideToShow = Math.min(slideFitting, maxSlidesToShow); } /** * Update slide to show (not responsive mode) * Computes the number of slide fitting. The number of slides to be shown are determined by the number of slides fitting within its container. */ updateSlideToShowNotResponsive() { const slideWidthPlusGap = this.slideWidth + this.gap; const referenceWidth = Math.min(this.carouselWidth, this.maxWidthCarousel || Infinity, window.innerWidth); const numberOfSlidesComputed = Math.floor(referenceWidth / slideWidthPlusGap) || 1; const slideFitting = Math.min(numberOfSlidesComputed, this.totalSlides); this.determineSlideToShow(slideFitting); } getPaddingCarousel() { const computedStyle = window.getComputedStyle(this.carousel); const paddingLeft = computedStyle.getPropertyValue('padding-left'); const paddingRight = computedStyle.getPropertyValue('padding-right'); return parseFloat(paddingLeft) + parseFloat(paddingRight); } setMaxWidthCarousel() { this.carousel.style.maxWidth = `${this.maxWidthCarousel}px`; } /** * Set the with of a slide, responsive mode * There are [n cards - 1] gaps (3 cards, 2 gaps) */ setAutoColumnSlideContainer() { const windowWidth = this.carouselWidth - this.paddingCarousel - (this.slideToShow - 1) * this.gap; const widthPerSlide = windowWidth / this.slideToShow; this.slidesContainer.style.gridAutoColumns = `${widthPerSlide}px`; this.slideWidth = widthPerSlide; } /** * Define the slide container width * Make non visible gaps of non visible cards scrollable and is used to compute the maxScrollableContent (strechingEffect) */ setWidthSlideContainer() { this.widthSlideContainer = this.selectSlides().length * this.slideWidthWithGap - this.gap; this.slidesContainer.style.width = `${this.widthSlideContainer}px`; } selectSlides() { return this.carousel.querySelectorAll('.carousel-slide'); } slidesToArray() { return Array.from(this.slides); } selectSlidesContainer() { return this.carousel.querySelector('.slides-container'); } /** * Set number of dots * If infinite mode, one more window than normal mode. */ setNumberDots() { if (this.loop) return this.totalSlides; return this.slideToShow > 1 ? this.totalSlides - this.slideToShow + 1 : this.totalSlides; } /** * Get the max scrollable content * Useful for the streching effect (not infinite mode), end of the slides */ getMaxScroll() { return (this.numberDots() - 1) * this.slideWidthWithGap; } setDraggableImgToFalse() { const images = this.slidesContainer.querySelectorAll('img'); images?.forEach((image) => { image.setAttribute('draggable', 'false'); }); } } class Validation { constructor(carousel, slideWidth, slideMaxWidth, gap, slideToScroll) { this.carousel = carousel; this.slideWidth = slideWidth; this.slideMaxWidth = slideMaxWidth; this.gap = gap; this.slideToScroll = slideToScroll; this.carouselSlides = this.carousel.querySelectorAll('.carousel-slide'); this.slideMaxWidthShouldBeGreaterThanSlideWidth(); this.slideWidthAndGapShouldBeGreaterThanZero(); this.requiredClassShouldBeAdded(); this.slideToScrollNotGreaterThanTotalSlides(); } slideMaxWidthShouldBeGreaterThanSlideWidth() { if (this.slideMaxWidth < this.slideWidth) { throw new Error(`slideMaxWidth (value: ${this.slideMaxWidth}) is lower than slideWidth (value: ${this.slideWidth}). Please increase the max width or decrease the slide width.`); } } slideWidthAndGapShouldBeGreaterThanZero() { const slideWidthPlusGap = this.slideWidth + this.gap; if (slideWidthPlusGap <= 0) { throw new Error('Unable to construct Carousel. SlideWidth and gap lower or equal than zero. Please add a positive value for the slideWidth and gap.'); } } requiredClassShouldBeAdded() { if (this.carouselSlides === undefined || this.carouselSlides.length === 0) { throw new Error('No elements with "carousel-slide" as class have been found. Please add this class to each of your slides.'); } } slideToScrollNotGreaterThanTotalSlides() { if (this.carouselSlides && this.slideToScroll > this.carouselSlides.length) { throw new Error(`slideToScroll value (${this.slideToScroll}) is greater than the total amount of slide (${this.carouselSlides.length}). This can cause invisible cards in infinite mode. Please lower the slideToScroll value.`); } } } class CommunSlider { constructor(carousel, responsive, slideToScroll, LIMIT_AUTO_SLIDE, strechingLimit, autoSlide, animationTimingFn, animationTimingMs, enableMouseDrag, enableTouch, autoPlay, autoPlayInterval, autoPlayAtStart, playDirection, autoplaySlideToScroll, carouselService, cd) { this.carousel = carousel; this.responsive = responsive; this.slideToScroll = slideToScroll; this.LIMIT_AUTO_SLIDE = LIMIT_AUTO_SLIDE; this.strechingLimit = strechingLimit; this.autoSlide = autoSlide; this.animationTimingFn = animationTimingFn; this.animationTimingMs = animationTimingMs; this.enableMouseDrag = enableMouseDrag; this.enableTouch = enableTouch; this.autoPlay = autoPlay; this.autoPlayInterval = autoPlayInterval; this.autoPlayAtStart = autoPlayAtStart; this.playDirection = playDirection; this.autoplaySlideToScroll = autoplaySlideToScroll; this.carouselService = carouselService; this.cd = cd; this.dragging = signal(false); this.currentSlide = signal(0); this.lastWindow = 0; this.currentTranslation = signal(0); this.previousTranslation = 0; this.direction = 'right'; this.startX = 0; this.previousX = 0; this.currentX = 0; this.positionChange = 0; this.prevLimit = 0; this.nextLimit = 0; this.fullWidthInf = 0; this.lastWindowTranslation = 0; this.totalSlides = 0; this.DOMLimitReached = false; this.visibleOffsetCardNotResponsive = 0; this.invisibleOffsetCardNotResponsive = 0; this.accumulatedSlide = 0; this.currentCarouselID = 0; this.playActive = signal(false); this.playButtonDisabled = signal(false); this.directionAutoPlay = (slides) => { }; this.initProperties(); this.updateProperties(); } initProperties() { this.slidesContainer = this.carousel.slidesContainer; this.arrayOfSlides = this.carousel.arrayOfSlides; this.totalSlides = this.carousel.totalSlides; this.totalAmountOfSlides = this.totalSlides; this.nextLimit = Math.floor(this.carousel.slideWidthWithGap); this.prevLimit = -this.carousel.slideWidthWithGap; if (this.autoPlayAtStart) this.launchAutoPlay(); } launchAutoPlay() { if (!this.autoPlay) return; this.playActive.set(true); this.autoInterval = window.setInterval(() => { this.directionAutoPlay(this.autoplaySlideToScroll); this.cd.markForCheck(); }, this.autoPlayInterval); } stopAutoPlay() { if (!this.autoPlay) return; this.playActive.set(false); clearInterval(this.autoInterval); } /** * Update properties of the slider * Fired at start and at resizing. */ updateProperties() { this.updateNotResponsive(); this.lastWindow = this.carousel.numberDots() - 1; this.fullWidthInf = this.totalSlides * this.carousel.slideWidthWithGap; this.lastWindowTranslation = this.slidesContainer.clientWidth - this.carousel.slideToShow * this.carousel.slideWidthWithGap + this.carousel.gap; this.lastWindowTranslation += this.responsive ? 0 : -this.visibleOffsetCardNotResponsive - this.carousel.gap; const maxScrollable = this.carousel.widthSlideContainer - this.carousel.carouselWidth + this.carousel.paddingCarousel; this.carousel.maxScrollableContent.set(maxScrollable); if (this.carousel.numberDots() === 1) { // if all slides visible in one window, max scrollable content equals 0 this.carousel.maxScrollableContent.set(0); } } updateNotResponsive() { if (this.responsive) return; // visible part of the offset of the card in px this.visibleOffsetCardNotResponsive = this.carousel.carouselWidth - this.carousel.slideToShow * this.carousel.slideWidthWithGap - this.carousel.paddingCarousel; this.invisibleOffsetCardNotResponsive = this.carousel.slideWidth - this.visibleOffsetCardNotResponsive; } /** * Fired at drag start * Instantiate property of the starting drag point on the X axis. Used to compute the translation. * Disabling the transition while applying the transformation because of the attached animation. */ dragStart(event) { if (this.currentEventIsDisabled(event)) return; clearInterval(this.autoInterval); this.dragging.set(true); this.startX = event instanceof MouseEvent ? event.pageX : event.touches[0].pageX; // Useful for direction detection this.previousX = this.startX; this.slidesContainer.style.transition = 'none'; } /** * Checks if current event is allowed by user. * TouchEvent partially supported on Firefox (and working on Safari despite the MDN docs). */ currentEventIsDisabled(event) { const isMouseEvent = event instanceof MouseEvent; return ((isMouseEvent && !this.enableMouseDrag) || (!isMouseEvent && !this.enableTouch)); } /** * Update the direction * Do not update the direction in case of the same previous position. */ setDirection() { if (this.previousX > this.currentX) { this.direction = 'right'; } else if (this.previousX < this.currentX) { this.direction = 'left'; } } /** * Update the last window translation * Useful to get the max translation at the end of the slides. * In not responsive mode, there is possibly a not fully displayed card (card offset). */ updateLastWindowTranslation() { const total = this.totalAmountOfSlides * this.carousel.slideWidthWithGap; this.lastWindowTranslation = total - this.carousel.slideToShow * this.carousel.slideWidthWithGap; this.lastWindowTranslation += this.responsive ? 0 : -this.visibleOffsetCardNotResponsive - this.carousel.gap; } /** * Decrease limit (movement to the left) * In infinite mode, take full width of a set if on the first slide as new slides are created to the left (a whole set offset). * Exception: if not responsive (card offset) and finite carousel, the next limit is at the maximum (the end of the carousel) */ decreaseLimits(slidesCreatedOnTheLeft = false) { let translationCorrectionAfterClone = this.prevLimit; if (slidesCreatedOnTheLeft) { translationCorrectionAfterClone = this.fullWidthInf; } this.prevLimit = translationCorrectionAfterClone - this.carousel.slideWidthWithGap; this.nextLimit = this.prevLimit + 2 * this.carousel.slideWidthWithGap; this.prevLimit = Math.floor(this.prevLimit); this.nextLimit = Math.floor(this.nextLimit); } /** * Increase limit on basis of previous computed limits (movement to the right) * Schema: || previous | current || next * Exception: if not responsive (card offset) and finite carousel, the next limit is at the maximum (the end of the carousel) */ increaseLimits() { this.nextLimit += Math.floor(this.carousel.slideWidthWithGap); this.prevLimit = Math.floor(this.nextLimit - this.carousel.slideWidthWithGap * 2); // console.log(this.prevLimit, this.nextLimit); } /** * Change prev and next limit on basis of the provided slide number * Prev and next limit are always calculated as the following: * || <= prev | current || <= next */ changePrevAndNextLimits(slideNumber) { const limitInPX = slideNumber * this.carousel.slideWidthWithGap; this.nextLimit = Math.floor(limitInPX + this.carousel.slideWidthWithGap); this.prevLimit = Math.floor(this.nextLimit - this.carousel.slideWidthWithGap * 2); // console.log(this.prevLimit, this.nextLimit); } fireSlideChangeEvent(slideHasChanged, slide) { if (!slideHasChanged) return; this.carouselService.emit(slide, this.carousel.carouselElement); } applyTransformation(transformation) { this.slidesContainer.style.transition = `transform ${this.animationTimingMs}ms ${this.animationTimingFn}`; this.applyTranslation(-transformation); this.dragging.set(false); this.previousTranslation = -transformation; this.currentTranslation.set(-transformation); } applyTranslation(transformation) { this.slidesContainer.style.transform = `translate3d(${transformation}px, 0, 0)`; } } class InfiniteSlider extends CommunSlider { constructor(carousel, responsive, slideToScroll, LIMIT_AUTO_SLIDE, strechingLimit, autoSlide, animationTimingFn, animationTimingMs, MAX_DOM_SIZE, enableMouseDrag, enableTouch, autoPlay, autoPlayInterval, autoPlayAtStart, playDirection, autoplaySlideToScroll, carouselService, cd) { super(carousel, responsive, slideToScroll, LIMIT_AUTO_SLIDE, strechingLimit, autoSlide, animationTimingFn, animationTimingMs, enableMouseDrag, enableTouch, autoPlay, autoPlayInterval, autoPlayAtStart, playDirection, autoplaySlideToScroll, carouselService, cd); this.MAX_DOM_SIZE = MAX_DOM_SIZE; this.initProperties(); this.updateProperties(); this.addSlidesToRightAtStart(); } initProperties() { this.defineAutoPlayDirection(); super.initProperties(); } /** * next and prev are defined separatively */ defineAutoPlayDirection() { if (this.playDirection === 'ltr') { this.directionAutoPlay = this.next.bind(this); } else { this.directionAutoPlay = this.prev.bind(this); } } /** * Add slides to the right at start * If only one window (number of dots === 1) and not responsive mode, there is possibly space at start for slides to the right (even though this configuration does not make a lot of sense) * TODO: ajouter des slides si slidesToShow > total slides */ addSlidesToRightAtStart() { if (this.carousel.numberDots() > 1 || this.responsive) { return; } this.addSlidesToTheRight(); } /** * Fired at drag start * Instantiate property of the starting drag point on the X axis. Used to compute the translation. * Disabling the transition while applying the transformation because of the attached animation. */ dragStart(event) { if (super.currentEventIsDisabled(event)) return; super.dragStart(event); } /** * Relaunch autoPlay if not disabled or infinite mode (never disabled by limits) * Restart only if started (playActive). */ relaunchAutoPlay() { if (this.playActive()) { clearInterval(this.autoInterval); this.launchAutoPlay(); } } /** * Fired at drag end */ dragStop(event) { if (this.currentEventIsDisabled(event)) return; this.dragging.set(false); this.previousTranslation = this.currentTranslation(); this.autoSlider(); } autoSlider() { if (!this.autoSlide) return; const referenceWidth = Math.min(this.carousel.slideWidth, this.carousel.slideMaxWidth || Infinity); const currentLimit = this.prevLimit + this.carousel.slideWidthWithGap; // previousTranslation always a negative number, currentLimit always positive const currentPositionChange = this.previousTranslation + currentLimit; const moveComparedToSlide = (currentPositionChange / referenceWidth) * 100; if (moveComparedToSlide < -this.LIMIT_AUTO_SLIDE || moveComparedToSlide > this.LIMIT_AUTO_SLIDE) { if (moveComparedToSlide > this.LIMIT_AUTO_SLIDE) { this.changeSlideNumber(-1); this.decreaseLimits(); } else { this.changeSlideNumber(1); this.increaseLimits(); } } if (-this.currentTranslation() > this.lastWindowTranslation) { this.addSlidesToTheRight(); } this.computeTransformation(this.accumulatedSlide); } /** * Fired at dragging * Compute the translation, change the slide number, update the direction. */ dragMove(event) { if (this.currentEventIsDisabled(event)) return; if (!this.dragging()) return; this.currentX = event instanceof MouseEvent ? event.pageX : event.changedTouches[0].pageX; this.setDirection(); this.previousX = this.currentX; this.positionChange = this.currentX - this.startX; this.currentTranslation.set(this.positionChange + this.previousTranslation); this.applyTranslation(this.currentTranslation()); this.modifyCurrentSlide(); } /** * Responsible for changing slide number and updating the limits. * If createSlidesInfiniteModeIfLimitsReached() doesn't take action, slide change according to previous computed limits. */ modifyCurrentSlide() { if (this.createSlidesInfiniteModeIfLimits()) return; if (-this.currentTranslation() < this.prevLimit) { this.changeSlideNumber(-1); this.decreaseLimits(); } else if (-this.currentTranslation() >= this.nextLimit) { this.changeSlideNumber(1); this.increaseLimits(); } } /** * Handle slide creation in infinite mode if limits reached * Mouse or touch drag. */ createSlidesInfiniteModeIfLimits() { if (this.currentTranslation() > 0) { this.addSlidesToTheLeft(); this.decreaseLimits(true); // not enabled at start if (this.currentSlide() > 0) { this.changeSlideNumber(-1); } return true; } else if (-this.currentTranslation() > this.lastWindowTranslation) { this.addSlidesToTheRight(); if (this.DOMLimitReached) { this.changePrevAndNextLimits(this.accumulatedSlide); return true; } } return false; } /** * Append or prepend new slides according to the direction * If new slides prepended, update the translation to the correct place (appending new slides do not change the translation). * Limit DOM growth or update last window translation if applicable. */ appendOrPrependNElements() { if (this.direction === 'left') { for (let i = this.arrayOfSlides.length - 1; i >= 0; i--) { const clonedElement = this.arrayOfSlides[i].cloneNode(true); this.slidesContainer.prepend(clonedElement); } this.accumulatedSlide += this.totalSlides; this.resetViewLeftDirection(); } else { for (let i = 0; i < this.arrayOfSlides.length; i++) { const clonedElement = this.arrayOfSlides[i].cloneNode(true); this.slidesContainer.append(clonedElement); } } if (this.totalAmountOfSlides >= this.MAX_DOM_SIZE * this.totalSlides) { // Limit DOM growth, max X times original DOM // console.log('dom limit reached'); this.limitDOMGrowth(); this.DOMLimitReached = true; } else { this.totalAmountOfSlides += this.totalSlides; this.DOMLimitReached = false; this.updateLastWindowTranslation(); } } /** * Limit DOM growth * Reset the view accordingly. */ limitDOMGrowth() { const slides = this.carousel.selectSlides(); if (this.direction === 'right') { for (let i = 0; i < this.totalSlides; i++) { slides[i].remove(); } this.resetViewRightDirection(); this.accumulatedSlide -= this.totalSlides; } else { for (let i = slides.length - 1; i > slides.length - this.totalSlides - 1; i--) { slides[i].remove(); } } } /** * Reset the view in a movement to the left * New slides added to the left, so the view has to be updated accordingly. * If the carousel is moved with the mouse | touch event (dragging is true), the offset should be equal to a full carousel width. Otherwise (with the buttons), the computed translation should be taken into account. * Side comment: the view does not have to be updated for slides added to the right, since relative order does not change. * getBoundingClientRect triggers reflow of the element. */ resetViewLeftDirection() { const translation = Math.abs(this.previousTranslation) + this.fullWidthInf; this.previousTranslation = -translation; this.slidesContainer.style.transition = 'none'; const transformation = this.dragging() ? -this.fullWidthInf : -translation; this.applyTranslation(transformation); this.slidesContainer.getBoundingClientRect(); } /** * Reset the view in a movement to the right * First set of slides have been deleted, so the translation has to be updated accordingly. * currentTranslation is a negative number so it will be decreased by a set of slides. * getBoundingClientRect triggers reflow of the element. */ resetViewRightDirection() { const translation = this.currentTranslation() + this.fullWidthInf; this.slidesContainer.style.transition = 'none'; this.applyTranslation(translation); this.previousTranslation = translation - this.positionChange; this.slidesContainer.getBoundingClientRect(); } addSlidesToTheLeft() { this.appendOrPrependNElements(); } addSlidesToTheRight() { this.appendOrPrependNElements(); } /** * Increase limit on basis of previous computed limits (movement to the right) * Schema: || previous | current || next */ increaseLimits() { this.nextLimit += Math.floor(this.carousel.slideWidthWithGap); this.prevLimit = Math.floor(this.nextLimit - this.carousel.slideWidthWithGap * 2); } /** * Previous button navigation */ prev(slides = this.slideToScroll) { this.direction = 'left'; this.handleBtnInfinite(-slides); this.changeSlideNumber(-slides); this.changePrevAndNextLimits(this.accumulatedSlide); this.computeTransformation(this.accumulatedSlide); this.relaunchAutoPlay(); } /** * Next button navigation */ next(slides = this.slideToScroll) { this.direction = 'right'; this.handleBtnInfinite(slides); this.changeSlideNumber(slides); this.changePrevAndNextLimits(this.accumulatedSlide); this.computeTransformation(this.accumulatedSlide); this.relaunchAutoPlay(); } /** * Buttons navigation in infinite mode * Create new slide if limits reached (start or end). Update slide, limits and apply transformation accordingly. */ handleBtnInfinite(step) { let cardOffset = 0; const goingTo = this.accumulatedSlide + step; // there is (possibly) a card offset if not responsive if (!this.responsive) cardOffset = 1; if (goingTo < 0) { this.addSlidesToTheLeft(); } else if (goingTo + this.carousel.slideToShow + cardOffset > this.totalAmountOfSlides) { this.addSlidesToTheRight(); } } /** * Navigation with bullet points * Update values accordingly. */ goTo(bullet) { const slideHasChanged = this.currentSlide() !== bullet; this.direction = this.currentSlide() < bullet ? 'right' : 'left'; this.currentSlide.set(bullet); this.fireSlideChangeEvent(slideHasChanged, this.currentSlide()); this.relaunchAutoPlay(); this.navInfiniteBullets(bullet); } /** * Bullets navigation * Create new slides if exceeding end of carousel. */ navInfiniteBullets(bullet) { let cardOffset = 0; const positionOfCurrentSlide = this.accumulatedSlide % this.totalSlides; const difference = bullet - positionOfCurrentSlide; this.accumulatedSlide += difference; // there is (possibly) a card offset if not responsive if (!this.responsive) cardOffset = 1; if (this.accumulatedSlide + this.carousel.slideToShow + cardOffset > this.totalAmountOfSlides) { this.addSlidesToTheRight(); } this.computeTransformation(this.accumulatedSlide); this.changePrevAndNextLimits(this.accumulatedSlide); } /** * Exception: if only one window (numberDots === 1), update the accumulatedSlide to let the transformation occurs, but currentSlide should stay at 0. */ changeSlideNumber(step) { const slideNumberBeforeChange = this.currentSlide(); this.infiniteChangeSlideNumber(step); if (this.carousel.numberDots() === 1) { this.currentSlide.set(0); } const current = this.carousel.numberDots() > 1 ? this.currentSlide() : this.accumulatedSlide; const slideHasChanged = slideNumberBeforeChange !== this.currentSlide(); this.fireSlideChangeEvent(slideHasChanged, current); } infiniteChangeSlideNumber(step) { this.accumulatedSlide += step; this.currentSlide.update((slide) => slide + step); const lastSlide = this.totalSlides - 1; if (this.currentSlide() > lastSlide) { this.currentSlide.update((slide) => slide % this.totalSlides); } else if (this.currentSlide() < 0) { this.currentSlide.update((slide) => slide + this.totalSlides); } } /** * Compute transformation that will be applied on basis of the provided slide number */ computeTransformation(slide) { const transformation = slide * this.carousel.slideWidthWithGap; this.applyTransformation(transformation); } } class FiniteSlider extends CommunSlider { constructor(carousel, responsive, slideToScroll, LIMIT_AUTO_SLIDE, strechingLimit, autoSlide, animationTimingFn, animationTimingMs, enableMouseDrag, enableTouch, autoPlay, autoPlayInterval, autoPlayAtStart, playDirection, autoplaySlideToScroll, carouselService, cd) { super(carousel, responsive, slideToScroll, LIMIT_AUTO_SLIDE, strechingLimit, autoSlide, animationTimingFn, animationTimingMs, enableMouseDrag, enableTouch, autoPlay, autoPlayInterval, autoPlayAtStart, playDirection, autoplaySlideToScroll, carouselService, cd); this.initProperties(); this.updateProperties(); } initProperties() { this.defineAutoPlayDirection(); super.initProperties(); this.disableAutoPlayBtn(); } /** * In finite carousel, clear autoPlay interval if limits are reached. */ disableAutoPlayBtn() { if (!this.autoPlay) return; this.playButtonDisabled.set(false); // start if (this.currentSlide() === 0 && this.playDirection === 'rtl' && this.currentTranslation() === 0) { this.playButtonDisabled.set(true); this.playActive.set(false); clearInterval(this.autoInterval); } // end if (this.currentSlide() === this.lastWindow && this.playDirection === 'ltr' && this.currentTranslation() <= -this.carousel.maxScrollableContent()) { this.playButtonDisabled.set(true); this.playActive.set(false); clearInterval(this.autoInterval); } if (this.carousel.numberDots() === 1) this.playButtonDisabled.set(true); } /** * Fired at drag start * Instantiate property of the starting drag point on the X axis. Used to compute the translation. * Disabling the transition while applying the transformation because of the attached animation. */ dragStart(event) { super.dragStart(event); this.disableAutoPlayBtn(); } /** * Fired at drag end * If limits reached (start or end), put back to the current slide. Updates previous translation. */ dragStop(event) { if (this.currentEventIsDisabled(event)) return; this.dragging.set(false); this.previousTranslation = this.currentTranslation(); this.autoSlider(); this.disableAutoPlayBtn(); const limit = this.currentTranslation() > 0 || -this.currentTranslation() > this.lastWindowTranslation; if (limit) { this.computeTransformation(this.currentSlide()); } } /** * Fired at dragging * Compute the translation, change the slide number, update the direction. */ dragMove(event) { if (this.currentEventIsDisabled(event)) return; if (!this.dragging()) return; this.currentX = event instanceof MouseEvent ? event.pageX : event.changedTouches[0].pageX; this.setDirection(); this.previousX = this.currentX; this.positionChange = this.currentX - this.startX; this.currentTranslation.set(this.positionChange + this.previousTranslation); // Current translation exceeding start or end limits if (this.strechingEffect()) return; this.applyTranslation(this.currentTranslation()); this.modifyCurrentSlide(); } strechingEffect() { return ((this.currentTranslation() > this.strechingLimit && this.currentSlide() === 0) || this.currentTranslation() < -this.carousel.maxScrollableContent() - this.strechingLimit); } /** * Responsible for changing slide number and updating the limits. * In finite mode, if all slides visible on one window or end of carousel, early return to not trigger change event. */ modifyCurrentSlide() { if (this.endOfCarousel()) return; if (-this.currentTranslation() < this.prevLimit) { this.changeSlideNumber(-1); this.decreaseLimits(); } else if (-this.currentTranslation() >= this.nextLimit) { this.changeSlideNumber(1); this.increaseLimits(); } } endOfCarousel() { return (this.carousel.numberDots() === 1 || (this.currentSlide() === this.lastWindow && this.direction === 'right')); } /** * Auto slide card if option enabled, applied on both directions. * Prevents auto slide on limits in finite mode (if streching < limit auto slide). * If only one slide is displayed (slideToShow === 1), the width of the slide corresponds to the window's width (a dot). Hence, taking the min between the two. * In non responsive and non infinite, there is possibly an offset of the current limit. */ autoSlider() { if (!this.autoSlide) return; if (this.currentTranslation() > 0 || -this.currentTranslation() > this.lastWindowTranslation) { return; } const referenceWidth = Math.min(this.carousel.slideWidth, this.carousel.slideMaxWidth || Infinity); let currentLimit = this.prevLimit + this.carousel.slideWidthWithGap; if (!this.responsive && this.currentSlide() > this.lastWindow - 1) { currentLimit = this.lastWindowTranslation; } // previousTranslation always a negative number, currentLimit always positive const currentPositionChange = this.previousTranslation + currentLimit; const moveComparedToSlide = (currentPositionChange / referenceWidth) * 100; if (moveComparedToSlide < -this.LIMIT_AUTO_SLIDE || moveComparedToSlide > this.LIMIT_AUTO_SLIDE) { if (moveComparedToSlide > this.LIMIT_AUTO_SLIDE) { this.changeSlideNumber(-1); this.decreaseLimits(); } else { this.changeSlideNumber(1); this.increaseLimits(); } } this.computeTransformation(this.accumulatedSlide); } /** * Decrease limit (movement to the left) * Exception: if not responsive (card offset) and finite carousel, the next limit is at the maximum (the end of the carousel) */ decreaseLimits() { super.decreaseLimits(); if (!this.responsive && this.currentSlide() >= this.lastWindow - 1) { this.nextLimit = this.lastWindowTranslation; } this.nextLimit = Math.floor(this.nextLimit); } /** * Increase limit on basis of previous computed limits (movement to the right) * Schema: || previous | current || next * Exception: if not responsive (card offset) and finite carousel, the next limit is at the maximum (the end of the carousel) */ increaseLimits() { super.increaseLimits(); if (!this.responsive && this.currentSlide() >= this.lastWindow - 1) { this.nextLimit = this.lastWindowTranslation; if (this.currentSlide() === this.lastWindow) { // only update previous limit if last slide reached this.prevLimit = Math.floor(this.nextLimit - this.invisibleOffsetCardNotResponsive); } } } /** * Previous button navigation */ prev(slides = this.slideToScroll) { this.direction = 'left'; this.changeSlideNumber(-slides); this.changePrevAndNextLimits(this.accumulatedSlide); this.computeTransformation(this.accumulatedSlide); this.disableAutoPlayBtn(); this.relaunchAutoPlay(); } /** * Relaunch autoPlay if not disabled. * Restart only if started (playActive). Play button disabled when limits reached (finite carousel). */ relaunchAutoPlay() { if (!this.playButtonDisabled() && this.playActive()) { clearInterval(this.autoInterval); this.launchAutoPlay(); } } defineAutoPlayDirection() { if (this.playDirection === 'ltr') { this.directionAutoPlay = this.next.bind(this); } else { this.directionAutoPlay = this.prev.bind(this); } } /** * Next button navigation */ next(slides = this.slideToScroll) { this.direction = 'right'; this.changeSlideNumber(slides); this.changePrevAndNextLimits(this.accumulatedSlide); this.computeTransformation(this.accumulatedSlide); this.disableAutoPlayBtn(); this.relaunchAutoPlay(); } /** * Navigation with bullet points * Update values accordingly. */ goTo(bullet) { const slideHasChanged = this.currentSlide() !== bullet; this.direction = this.currentSlide() < bullet ? 'right' : 'left'; this.currentSlide.set(bullet); this.fireSlideChangeEvent(slideHasChanged, this.currentSlide()); this.disableAutoPlayBtn(); this.relaunchAutoPlay(); this.accumulatedSlide = this.currentSlide(); this.changePrevAndNextLimits(bullet); this.computeTransformation(bullet); } /** * Exception: if only one window (numberDots === 1), update the accumulatedSlide to let the transformation occurs, but currentSlide should stay at 0. */ changeSlideNumber(step) { const slideNumberBeforeChange = this.currentSlide(); this.finiteChangeSlideNumber(step); if (this.carousel.numberDots() === 1) { this.currentSlide.set(0); } const current = this.carousel.numberDots() > 1 ? this.currentSlide() : this.accumulatedSlide; const slideHasChanged = slideNumberBeforeChange !== this.currentSlide(); this.fireSlideChangeEvent(slideHasChanged, current); } finiteChangeSlideNumber(step) { this.currentSlide.update((slide) => slide + step); if (this.currentSlide() > this.lastWindow) { this.currentSlide.set(this.lastWindow); } else if (this.currentSlide() < 0) { this.currentSlide.set(0); } this.accumulatedSlide = this.currentSlide(); } /** * Compute transformation that will be applied on basis of the provided slide number * Exception: if not responsive (card offset) and finite carousel, the next limit is the end of the carousel */ computeTransformation(slide) { let transformation = slide * this.carousel.slideWidthWithGap; if (!this.responsive && slide >= this.lastWindow - 1 && this.carousel.numberDots() > 1) { this.nextLimit = this.lastWindowTranslation; if (slide === this.lastWindow) { // if last window, go to maximum & update prev limit transformation = this.lastWindowTranslation; this.prevLimit = Math.floor(this.nextLimit - this.invisibleOffsetCardNotResponsive); } } this.applyTransformation(transformation); } } class CarouselService { emit(slide, carouselElement) { const event = new CustomEvent('slideChange', { detail: slide }); // dispatch event on the parent carouselElement.parentElement?.dispatchEvent(event); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CarouselService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CarouselService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CarouselService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); class CarouselComponent { constructor(cd, carouselService, platformId) { this.cd = cd; this.carouselService = carouselService; this.maxWidthCarousel = input(); this.infinite = input(false); this.responsive = input(true); this.autoSlide = input(true); this.slideToShow = input(3); this.slideToScroll = input(2); this.autoslideLimitPercentCard = input(30); this.strechingLimit = input(60); this.slideWidth = input(300); this.slideMaxWidth = input(500); this.dots = input(true); this.arrows = input(true); this.counter = input(true); this.enableMouseDrag = input(true); this.enableTouch = input(true); this.counterSeparator = input('/'); this.gapBetweenSlides = input(16); this.animationTimingMs = input(300); this.maxDomSize = input(4); this.animationTimingFn = input('ease-out'); this.autoPlay = input(false); this.autoPlayInterval = input(1500); this.autoPlayAtStart = input(false); this.displayAutoPlayControls = input(true); this.autoPlaySlideToScroll = input(1); this.autoPlayDirection = input('ltr'); this.previousWidth = 0; this.currentWidth = 0; this.isBrowser = true; // Template variables this.carouselDots = signal(0); this.carouselMaxScrollableContent = signal(0); this.carouselArrayNumberDots = signal([]); this.sliderCurrentSlide = signal(0); this.sliderCurrentTranslation = signal(0); this.sliderPlayActive = signal(false); this.sliderPlayButtonDisabled = signal(false); this.sliderDragging = signal(false); this.isBrowser = isPlatformBrowser(platformId); } ngAfterViewInit() { if (!this.isBrowser) return; const carouselContainer = this.carouselContainer.nativeElement; this.previousWidth = window.innerWidth; this.currentWidth = window.innerWidth; new Validation(carouselContainer, this.slideWidth(), this.slideMaxWidth(), this.gapBetweenSlides(), this.slideToScroll()); this.carousel = new Carousel(carouselContainer, this.maxWidthCarousel(), this.slideToShow(), this.slideWidth(), this.slideMaxWidth(), this.gapBetweenSlides(), this.responsive(), this.infinite()); this.instantiateSlider(); this.listeners(); this.cd.detectChanges(); } instantiateSlider() { if (this.infinite()) { this.slider = new InfiniteSlider(this.carousel, this.responsive(), this.slideToScroll(), this.autoslideLimitPercentCard(), this.strechingLimit(), this.autoSlide(), this.animationTimingFn(), this.animationTimingMs(), this.maxDomSize(), this.enableMouseDrag(), this.enableTouch(), this.autoPlay(), this.autoPlayInterval(), this.autoPlayAtStart(), this.autoPlayDirection(), this.autoPlaySlideToScroll(), this.carouselService, this.cd); } else { this.slider = new FiniteSlider(this.carousel, this.responsive(), this.slideToScroll(), this.autoslideLimitPercentCard(), this.strechingLimit(), this.autoSlide(), this.animationTimingFn(), this.animationTimingMs(), this.enableMouseDrag(), this.enableTouch(), this.autoPlay(), this.autoPlayInterval(), this.autoPlayAtStart(), this.autoPlayDirection(), this.autoPlaySlideToScroll(), this.carouselService, this.cd); } this.carouselDots = computed(() => this.carousel.numberDots()); this.carouselMaxScrollableContent = computed(() => this.carousel.maxScrollableContent()); this.carouselArrayNumberDots = computed(() => this.carousel.arrayNumberDots()); this.sliderCurrentSlide = computed(() => this.slider.currentSlide()); this.sliderCurrentTranslation = computed(() => this.slider.currentTranslation()); this.sliderCurrentTranslation = computed(() => this.slider.currentTranslation()); this.sliderPlayActive = computed(() => this.slider.playActive()); this.sliderPlayButtonDisabled = computed(() => this.slider.playButtonDisabled()); this.sliderDragging = computed(() => this.slider.dragging()); } listeners() { this.mouseUpEvent = (event) => { if (!this.sliderDragging()) return; this.slider.dragStop(event); this.slider.relaunchAutoPlay(); this.cd.markForCheck(); }; this.visibilityEvent = (event) => { const visibility = event.target.visibilityState; this.slider.dragStop(event); if (visibility === 'hidden') { clearInterval(this.slider.autoInterval); } if (visibility === 'visible') { this.slider.relaunchAutoPlay(); } this.cd.markForCheck(); }; this.resizeEvent = () => { this.resize(); }; window.addEventListener('mouseup', this.mouseUpEvent); window.addEventListener('resize', this.resizeEvent); document.addEventListener('visibilitychange', this.visibilityEvent); } /** * Reinitialise variables at resize */ resize() { // on smartphones a vertical scroll triggers a resize event this.currentWidth = window.innerWidth; if (this.currentWidth === this.previousWidth) return; this.previousWidth = window.innerWidth; this.carousel.updateProperties(); this.slider.updateProperties(); this.slider.stopAutoPlay(); const slideNumberBeforeChange = this.slider.currentSlide(); this.slider.currentSlide.set(0); const slideHasChanged = slideNumberBeforeChange !== this.slider.currentSlide(); this.slider.accumulatedSlide = 0; this.slider.computeTransformation(0); this.slider.changePrevAndNextLimits(0); this.slider.fireSlideChangeEvent(slideHasChanged, 0); this.cd.markForCheck(); } /** * Removes active subscriptions and clear interval autoplay. */ ngOnDestroy() { window.removeEventListener('resize', this.resizeEvent); window.removeEventListener('mouseup', this.mouseUpEvent); document.removeEventListener('visibilitychange', this.visibilityEvent); this.slider.stopAutoPlay(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CarouselComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: CarouselService }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: CarouselComponent, isStandalone: true, selector: "ca