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
JavaScript
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