From 54f6ea8faaf07958136d4a0fc23a39b95b640f8f Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Tue, 22 Jul 2025 22:00:58 +0800 Subject: [PATCH 1/6] improve carousel frontend performance removed swapping of slides --- src/block/carousel/frontend-carousel.js | 213 ++++++++++++++---------- 1 file changed, 124 insertions(+), 89 deletions(-) diff --git a/src/block/carousel/frontend-carousel.js b/src/block/carousel/frontend-carousel.js index d2cc058a28..252582fa3e 100644 --- a/src/block/carousel/frontend-carousel.js +++ b/src/block/carousel/frontend-carousel.js @@ -46,14 +46,6 @@ class _StackableCarousel { this.fixChildrenAccessibility() // This needs to be first before infinte scrolling clones slides. this.initProperties() - this.addEventListeners() - this.fixAccessibility( this.currentSlide ) - this.setDotActive( this.currentSlide ) - this.fixInlineScrollNavigation() - - this.slideEls[ this.currentSlide - 1 ].classList.add( 'stk-block-carousel__slide--active' ) - - this.unpauseAutoplay() } initProperties = () => { @@ -73,29 +65,91 @@ class _StackableCarousel { } } + // If we have infiniteScroll, call this after cloning the slides + const otherInitCalls = () => { + this.addEventListeners() + this.fixAccessibility( this.currentSlide ) + this.setDotActive( this.currentSlide ) + this.fixInlineScrollNavigation() + + this.slideEls[ this.currentSlide - 1 ].classList.add( 'stk-block-carousel__slide--active' ) + + this.unpauseAutoplay() + } + if ( this.infiniteScroll && ! this.el._StackableHasInitCarousel ) { // clone slides - this.clones = this.slideEls.map( node => node.cloneNode( true ) ) + this.clones = [] + const clonesToAdd = [] + let lastClone = null + let frame = 0 + + const runInitSteps = () => { + if ( frame === 0 ) { + this.slideEls.forEach( ( original, i ) => { + const clone = original.cloneNode( true ) + clone.classList.add( `stk-slide-clone-${ i + 1 }` ) + + this.clones.push( clone ) + + // Ensure click events on cloned slides are delegated to the corresponding original slide elements. + // This preserves expected interactivity for cloned slides in infinite scroll. + clone.addEventListener( 'click', e => { + const targetClassList = [ ...e.target.classList ] + if ( targetClassList.length ) { + const targetClasses = `.${ targetClassList.join( '.' ) }` + const originalTarget = original.querySelector( targetClasses ) + originalTarget.click() + } + } ) + + if ( i === this.slideEls.length - 1 ) { + lastClone = clone + + // Also add the last slide clone at the end + if ( this.slidesToShow === this.slideEls.length ) { + const lastCloneClone = clone.cloneNode( true ) + lastCloneClone.classList.add( `stk-slide-clone-${ i + 1 }-clone` ) + clonesToAdd.push( lastCloneClone ) + } + } else { + clonesToAdd.push( clone ) + } + } ) + } else if ( frame === 2 ) { + // Append clones at the end except for the last slide clone which will be placed at the front + this.sliderEl.appendChild( ...clonesToAdd ) + if ( lastClone ) { + this.sliderEl.insertBefore( lastClone, this.slideEls[ 0 ] ) + } + } else if ( frame === 3 ) { + // IMPORTANT: Do style reads before applying style change to improve performance + // https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing + const targetOffsetLeft = this.slideEls[ 0 ].offsetLeft + + // Scroll without animation to the first slide + this.sliderEl.style.scrollBehavior = 'unset' + this.sliderEl.scrollLeft = targetOffsetLeft + this.sliderEl.style.scrollBehavior = '' - this.clones.map( ( node, i ) => { - node.classList.add( `stk-slide-clone-${ i + 1 }` ) - if ( i === this.clones.length - 1 ) { - return this.sliderEl.insertBefore( node, this.slideEls[ 0 ] ) + this.currentSlide = 1 + this.updateDots() + } else if ( frame === 4 ) { + otherInitCalls() } - return this.sliderEl.appendChild( node ) - } ) - - // Scroll without animation to the first slide - this.sliderEl.style.scrollBehavior = 'unset' - this.sliderEl.scrollLeft = this.slideEls[ 0 ].offsetLeft - this.sliderEl.style.scrollBehavior = '' + frame++ + if ( frame < 5 ) { + requestAnimationFrame( runInitSteps ) + } + } - this.currentSlide = 1 - this.swappedSlides = 0 + requestAnimationFrame( runInitSteps ) + return } this.updateDots() + otherInitCalls() } updateDots = () => { @@ -198,7 +252,7 @@ class _StackableCarousel { nextSlide = () => { let newSlide = this.currentSlide + 1 - if ( this.infiniteScroll && newSlide > this.maxSlides() ) { + if ( this.infiniteScroll && newSlide > this.slideEls.length ) { this.swapSlides( newSlide, 'N' ) return } @@ -212,8 +266,7 @@ class _StackableCarousel { prevSlide = () => { let newSlide = this.currentSlide - 1 - if ( this.infiniteScroll && - ( newSlide < this.slideOffset || this.needToSwapCount( newSlide ) >= 0 ) ) { + if ( this.infiniteScroll && newSlide < this.slideOffset ) { this.swapSlides( newSlide, 'P' ) return } @@ -225,70 +278,43 @@ class _StackableCarousel { } swapSlides = ( slide, dir ) => { - let setScrollToClone = false - if ( this.slidesToShow === this.slideEls.length ) { - setScrollToClone = true - } - - if ( dir === 'N' && slide > this.slideEls.length ) { + if ( dir === 'N' ) { slide = this.slideOffset - setScrollToClone = true - } else if ( dir === 'P' && slide < this.slideOffset ) { + } else { slide = this.slideEls.length - setScrollToClone = true } - const needToSwap = this.needToSwapCount( slide ) - if ( needToSwap > 0 && this.swappedSlides < needToSwap ) { - // swap original and clone slides - const original = [ ...this.slideEls.slice( this.swappedSlides, needToSwap ) ] - const clones = [ ...this.clones.slice( this.swappedSlides, needToSwap ) ] + let steps = 0 - original.map( node => this.sliderEl.insertBefore( node, this.clones[ needToSwap ] ) ) - clones.map( node => this.sliderEl.insertBefore( node, this.slideEls[ needToSwap ] ) ) + const runSteps = () => { + if ( steps === 0 ) { + const lastCloneSlide = this.clones[ this.currentSlide - 1 ].offsetLeft + const firstCloneSide = this.clones[ 0 ].offsetLeft - // This ensures that the cloned slides are in the right position when slides to show === number of slides - if ( this.slidesToShow === this.slideEls.length && dir === 'N' ) { - const children = this.sliderEl.children - this.sliderEl.append( children[ 0 ] ) - } else if ( this.slidesToShow === this.slideEls.length && dir === 'P' ) { - const children = [ ...Array.from( this.sliderEl.children ).slice( -2 ) ].reverse() - children.map( node => this.sliderEl.insertBefore( node, this.sliderEl.children[ 0 ] ) ) - } + let initSlide = null - this.swappedSlides = needToSwap - } else if ( this.swappedSlides > needToSwap ) { - // unswap original and clone slides that are not needed - const _needToSwap = needToSwap > 0 ? needToSwap : 0 - const original = [ ...this.slideEls.slice( _needToSwap, this.swappedSlides ) ] - const clones = [ ...this.clones.slice( _needToSwap, this.swappedSlides ) ] - original.map( node => this.sliderEl.insertBefore( node, this.slideEls[ this.swappedSlides ] ) ) - clones.map( node => this.sliderEl.insertBefore( node, this.clones[ this.swappedSlides ] ) ) - this.swappedSlides = _needToSwap - - // This ensures that the cloned slides are in the right position when slides to show === number of slides - if ( this.slidesToShow === this.slideEls.length ) { - const children = this.sliderEl.children - this.sliderEl.insertBefore( children[ children.length - 1 ], children[ 0 ] ) - } - } + if ( dir === 'N' ) { + initSlide = lastCloneSlide + } else if ( this.slidesToShow === 1 ) { + initSlide = lastCloneSlide + } else { + initSlide = firstCloneSide + } - if ( setScrollToClone ) { - // Move from the last slide to the first slide (N - next) or - // Move from the first slide to the last slide (P - prev) - this.sliderEl.style.scrollBehavior = 'unset' - this.sliderEl.scrollLeft = dir === 'N' - ? this.clones[ this.currentSlide - 1 ].offsetLeft // Go to the last clone slide - : ( this.slidesToShow === 1 - ? this.clones[ this.currentSlide - 1 ].offsetLeft // If slidesToShow is 1, go to the first clone slide - : this.slideEls[ this.currentSlide - 1 ].offsetLeft // Go to the original first slide which is swapped with the clone - ) - this.sliderEl.style.scrollBehavior = '' + // Move from the last slide to the first slide (N - next) or + // Move from the first slide to the last slide (P - prev) + this.sliderEl.style.scrollBehavior = 'unset' + this.sliderEl.scrollLeft = initSlide + this.sliderEl.style.scrollBehavior = '' + + steps++ + requestAnimationFrame( runSteps ) + } else { + this.goToSlide( slide ) + } } - setTimeout( () => { - this.goToSlide( slide ) - }, 1 ) + runSteps() } goToSlide = ( slide, force = false ) => { @@ -300,7 +326,8 @@ class _StackableCarousel { this.slideEls[ slide - 1 ].classList.add( 'stk-block-carousel__slide--active' ) if ( this.type === 'slide' ) { - this.sliderEl.scrollLeft = this.slideEls[ slide - 1 ].offsetLeft + const offsetLeft = this.slideEls[ slide - 1 ].offsetLeft + this.sliderEl.scrollLeft = offsetLeft } else if ( this.type === 'fade' ) { // fade const slidePrevEl = this.slideEls[ this.currentSlide - 1 ] slidePrevEl.style.opacity = 0 @@ -414,6 +441,9 @@ class _StackableCarousel { } onWheel = e => { + const sliderElScrollLeft = this.sliderEl.scrollLeft + const lastSlideOffset = this.slideEls[ this.slideEls.length - 1 ].offsetLeft + const firstCloneOffset = this.clones[ 0 ].offsetLeft if ( this.type === 'fade' ) { if ( this.wheelTimeout ) { return @@ -430,15 +460,17 @@ class _StackableCarousel { }, 500 ) } // For infinite scrolling, set the scroll position to the actual slide ( not to the clone of the slide ) - } else if ( this.infiniteScroll && e.deltaX <= -1 && this.sliderEl.scrollLeft === 0 ) { + } else if ( this.infiniteScroll && e.deltaX <= -1 && sliderElScrollLeft === 0 ) { this.sliderEl.style.scrollBehavior = 'unset' - this.sliderEl.scrollLeft = this.slideEls[ this.slideEls.length - 1 ].offsetLeft + this.sliderEl.scrollLeft = lastSlideOffset this.sliderEl.style.scrollBehavior = '' - } else if ( this.infiniteScroll && e.deltaX >= 1 && this.sliderEl.scrollLeft >= this.clones[ 0 ].offsetLeft ) { + } else if ( this.infiniteScroll && e.deltaX >= 1 && sliderElScrollLeft >= firstCloneOffset ) { this.clones.every( ( clone, i ) => { - if ( this.sliderEl.scrollLeft === clone.offsetLeft ) { + const cloneOffset = clone.offsetLeft + const slideOffset = this.slideEls[ i ].offsetLeft + if ( sliderElScrollLeft === cloneOffset ) { this.sliderEl.style.scrollBehavior = 'unset' - this.sliderEl.scrollLeft = this.slideEls[ i ].offsetLeft + this.sliderEl.scrollLeft = slideOffset this.sliderEl.style.scrollBehavior = '' return false } @@ -467,14 +499,17 @@ class _StackableCarousel { dragMouseMove = e => { // How far the mouse has been moved let dx = e.clientX - this.initialClientX + const sliderElScrollLeft = this.sliderEl.scrollLeft + const lastSlideOffsetLeft = this.slideEls[ this.slideEls.length - 1 ].offsetLeft + const firstCloneOffsetLeft = this.clones[ 0 ].offsetLeft if ( this.type === 'slide' ) { - if ( this.infiniteScroll && this.sliderEl.scrollLeft === 0 && dx > 0 ) { - this.initialScrollLeft = this.slideEls[ this.slideEls.length - 1 ].offsetLeft + if ( this.infiniteScroll && sliderElScrollLeft === 0 && dx > 0 ) { + this.initialScrollLeft = lastSlideOffsetLeft this.initialClientX = e.clientX dx = 0 - } else if ( this.infiniteScroll && this.sliderEl.scrollLeft >= this.clones[ 0 ].offsetLeft && dx < 0 ) { - this.initialScrollLeft = this.slideEls[ 0 ].offsetLeft + } else if ( this.infiniteScroll && sliderElScrollLeft >= firstCloneOffsetLeft && dx < 0 ) { + this.initialScrollLeft = firstCloneOffsetLeft this.initialClientX = e.clientX dx = 0 } From 56f22db941223964ad1ce2a742035cac7fb3a789 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 23 Jul 2025 01:04:40 +0800 Subject: [PATCH 2/6] clone necessary slides only --- src/block/carousel/frontend-carousel.js | 108 ++++++++++++------------ 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/src/block/carousel/frontend-carousel.js b/src/block/carousel/frontend-carousel.js index 252582fa3e..84be805806 100644 --- a/src/block/carousel/frontend-carousel.js +++ b/src/block/carousel/frontend-carousel.js @@ -82,47 +82,46 @@ class _StackableCarousel { this.clones = [] const clonesToAdd = [] let lastClone = null - let frame = 0 + let index = 0 + let step = 0 const runInitSteps = () => { - if ( frame === 0 ) { - this.slideEls.forEach( ( original, i ) => { - const clone = original.cloneNode( true ) - clone.classList.add( `stk-slide-clone-${ i + 1 }` ) - - this.clones.push( clone ) - - // Ensure click events on cloned slides are delegated to the corresponding original slide elements. - // This preserves expected interactivity for cloned slides in infinite scroll. - clone.addEventListener( 'click', e => { - const targetClassList = [ ...e.target.classList ] - if ( targetClassList.length ) { - const targetClasses = `.${ targetClassList.join( '.' ) }` - const originalTarget = original.querySelector( targetClasses ) - originalTarget.click() - } - } ) - - if ( i === this.slideEls.length - 1 ) { - lastClone = clone - - // Also add the last slide clone at the end - if ( this.slidesToShow === this.slideEls.length ) { - const lastCloneClone = clone.cloneNode( true ) - lastCloneClone.classList.add( `stk-slide-clone-${ i + 1 }-clone` ) - clonesToAdd.push( lastCloneClone ) - } - } else { - clonesToAdd.push( clone ) + if ( step === 0 ) { + // Clone only the last slide and the first N slides (where N equals to slidesToShow) + const slideIndex = index === this.slidesToShow ? this.slideEls.length - 1 : index + const original = this.slideEls[ slideIndex ] + const clone = original.cloneNode( true ) + clone.classList.add( `stk-slide-clone-${ slideIndex + 1 }` ) + + this.clones.push( clone ) + + // Ensure click events on cloned slides are delegated to the corresponding original slide elements. + // This preserves expected interactivity for cloned slides in infinite scroll. + clone.addEventListener( 'click', e => { + const targetClassList = [ ...e.target.classList ] + if ( targetClassList.length ) { + const targetClasses = `.${ targetClassList.join( '.' ) }` + const originalTarget = original.querySelector( targetClasses ) + originalTarget.click() } } ) - } else if ( frame === 2 ) { + + if ( index === this.slidesToShow ) { + lastClone = clone + step++ + } else { + clonesToAdd.push( clone ) + } + + index++ + } else if ( step === 1 ) { // Append clones at the end except for the last slide clone which will be placed at the front - this.sliderEl.appendChild( ...clonesToAdd ) + this.sliderEl.append( ...clonesToAdd ) if ( lastClone ) { this.sliderEl.insertBefore( lastClone, this.slideEls[ 0 ] ) } - } else if ( frame === 3 ) { + step++ + } else if ( step === 2 ) { // IMPORTANT: Do style reads before applying style change to improve performance // https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing const targetOffsetLeft = this.slideEls[ 0 ].offsetLeft @@ -130,16 +129,19 @@ class _StackableCarousel { // Scroll without animation to the first slide this.sliderEl.style.scrollBehavior = 'unset' this.sliderEl.scrollLeft = targetOffsetLeft + step++ + } else if ( step === 3 ) { this.sliderEl.style.scrollBehavior = '' this.currentSlide = 1 this.updateDots() - } else if ( frame === 4 ) { + step++ + } else if ( step === 4 ) { otherInitCalls() + step++ } - frame++ - if ( frame < 5 ) { + if ( step < 5 ) { requestAnimationFrame( runInitSteps ) } } @@ -252,7 +254,7 @@ class _StackableCarousel { nextSlide = () => { let newSlide = this.currentSlide + 1 - if ( this.infiniteScroll && newSlide > this.slideEls.length ) { + if ( this.type === 'slide' && this.infiniteScroll && newSlide > this.slideEls.length ) { this.swapSlides( newSlide, 'N' ) return } @@ -266,7 +268,7 @@ class _StackableCarousel { prevSlide = () => { let newSlide = this.currentSlide - 1 - if ( this.infiniteScroll && newSlide < this.slideOffset ) { + if ( this.type === 'slide' && this.infiniteScroll && newSlide < this.slideOffset ) { this.swapSlides( newSlide, 'P' ) return } @@ -288,7 +290,7 @@ class _StackableCarousel { const runSteps = () => { if ( steps === 0 ) { - const lastCloneSlide = this.clones[ this.currentSlide - 1 ].offsetLeft + const lastCloneSlide = this.clones[ this.clones.length - 1 ].offsetLeft const firstCloneSide = this.clones[ 0 ].offsetLeft let initSlide = null @@ -321,26 +323,26 @@ class _StackableCarousel { if ( slide === this.currentSlide && ! force ) { return } + const currentSlideEl = this.slideEls[ this.currentSlide - 1 ] + const newSlideEl = this.slideEls[ slide - 1 ] + const offsetLeft = newSlideEl.offsetLeft - this.slideEls[ this.currentSlide - 1 ].classList.remove( 'stk-block-carousel__slide--active' ) - this.slideEls[ slide - 1 ].classList.add( 'stk-block-carousel__slide--active' ) + currentSlideEl.classList.remove( 'stk-block-carousel__slide--active' ) + newSlideEl.classList.add( 'stk-block-carousel__slide--active' ) if ( this.type === 'slide' ) { - const offsetLeft = this.slideEls[ slide - 1 ].offsetLeft this.sliderEl.scrollLeft = offsetLeft } else if ( this.type === 'fade' ) { // fade - const slidePrevEl = this.slideEls[ this.currentSlide - 1 ] - slidePrevEl.style.opacity = 0 - - const slideEl = this.slideEls[ slide - 1 ] - slideEl.style.zIndex = ++this.currentZIndex - slideEl.style.transition = 'none' - slideEl.style.opacity = 0 - slideEl.style.visibility = 'visible' - slideEl.style.left = `${ this.isRTL ? '' : '-' }${ 100 * ( slide - 1 ) }%` + currentSlideEl.style.opacity = 0 + + newSlideEl.style.zIndex = ++this.currentZIndex + newSlideEl.style.transition = 'none' + newSlideEl.style.opacity = 0 + newSlideEl.style.visibility = 'visible' + newSlideEl.style.left = `${ this.isRTL ? '' : '-' }${ 100 * ( slide - 1 ) }%` setTimeout( () => { - slideEl.style.transition = '' - slideEl.style.opacity = 1 + newSlideEl.style.transition = '' + newSlideEl.style.opacity = 1 }, 1 ) } this.fixAccessibility( slide ) From 16f8019929615ea3d726020c5effa2bca22b8c90 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 23 Jul 2025 11:19:22 +0800 Subject: [PATCH 3/6] remove unnecessary fx --- src/block/carousel/frontend-carousel.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/block/carousel/frontend-carousel.js b/src/block/carousel/frontend-carousel.js index 84be805806..9a589e8684 100644 --- a/src/block/carousel/frontend-carousel.js +++ b/src/block/carousel/frontend-carousel.js @@ -247,10 +247,6 @@ class _StackableCarousel { return maxSlides } - needToSwapCount = slide => { - return this.slidesToShow - ( this.slideEls.length - slide + 1 ) - } - nextSlide = () => { let newSlide = this.currentSlide + 1 From 7012e61a2e799dfcd8f2490b8a70b34ce6906ba1 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 30 Jul 2025 20:03:19 +0800 Subject: [PATCH 4/6] use transform: TranslateX() --- src/block/carousel/frontend-carousel.js | 130 ++++++++++++++++-------- 1 file changed, 85 insertions(+), 45 deletions(-) diff --git a/src/block/carousel/frontend-carousel.js b/src/block/carousel/frontend-carousel.js index 9a589e8684..679b0b73c5 100644 --- a/src/block/carousel/frontend-carousel.js +++ b/src/block/carousel/frontend-carousel.js @@ -92,19 +92,13 @@ class _StackableCarousel { const original = this.slideEls[ slideIndex ] const clone = original.cloneNode( true ) clone.classList.add( `stk-slide-clone-${ slideIndex + 1 }` ) + original.style.willChange = 'transform' + original.style.transform = 'TranslateX( 0 )' - this.clones.push( clone ) + // prevents flickering when changing the value of TranslateX + original.style.transition = 'transform 0s' - // Ensure click events on cloned slides are delegated to the corresponding original slide elements. - // This preserves expected interactivity for cloned slides in infinite scroll. - clone.addEventListener( 'click', e => { - const targetClassList = [ ...e.target.classList ] - if ( targetClassList.length ) { - const targetClasses = `.${ targetClassList.join( '.' ) }` - const originalTarget = original.querySelector( targetClasses ) - originalTarget.click() - } - } ) + this.clones.push( clone ) if ( index === this.slidesToShow ) { lastClone = clone @@ -122,6 +116,15 @@ class _StackableCarousel { } step++ } else if ( step === 2 ) { + const numSlides = this.slideEls.length + const slideClientRect = this.slideEls[ 0 ].getBoundingClientRect() + const carouselSlideGap = window.getComputedStyle( this.el ).getPropertyValue( '--gap' ) + const slideWidth = slideClientRect.width + + this.slideTranslateX = `calc((${ slideWidth }px * ${ numSlides }) + (${ carouselSlideGap } * ${ numSlides }))` + + step++ + } else if ( step === 3 ) { // IMPORTANT: Do style reads before applying style change to improve performance // https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing const targetOffsetLeft = this.slideEls[ 0 ].offsetLeft @@ -130,18 +133,19 @@ class _StackableCarousel { this.sliderEl.style.scrollBehavior = 'unset' this.sliderEl.scrollLeft = targetOffsetLeft step++ - } else if ( step === 3 ) { + } else if ( step === 4 ) { this.sliderEl.style.scrollBehavior = '' this.currentSlide = 1 + this.swappedSlides = 0 this.updateDots() step++ - } else if ( step === 4 ) { + } else if ( step === 5 ) { otherInitCalls() step++ } - if ( step < 5 ) { + if ( step <= 5 ) { requestAnimationFrame( runInitSteps ) } } @@ -247,10 +251,14 @@ class _StackableCarousel { return maxSlides } + needToSwapCount = slide => { + return this.slidesToShow - ( this.slideEls.length - slide + 1 ) + } + nextSlide = () => { let newSlide = this.currentSlide + 1 - if ( this.type === 'slide' && this.infiniteScroll && newSlide > this.slideEls.length ) { + if ( this.type === 'slide' && this.infiniteScroll && newSlide > this.maxSlides() ) { this.swapSlides( newSlide, 'N' ) return } @@ -264,7 +272,8 @@ class _StackableCarousel { prevSlide = () => { let newSlide = this.currentSlide - 1 - if ( this.type === 'slide' && this.infiniteScroll && newSlide < this.slideOffset ) { + if ( this.type === 'slide' && this.infiniteScroll && + ( newSlide < this.slideOffset || this.needToSwapCount( newSlide ) >= 0 ) ) { this.swapSlides( newSlide, 'P' ) return } @@ -276,43 +285,74 @@ class _StackableCarousel { } swapSlides = ( slide, dir ) => { - if ( dir === 'N' ) { + let scrollToSlide = false + if ( dir === 'N' && slide > this.slideEls.length ) { slide = this.slideOffset - } else { + scrollToSlide = true + } else if ( dir === 'P' && slide < this.slideOffset ) { slide = this.slideEls.length + scrollToSlide = true + } + + const needToSwap = this.needToSwapCount( slide ) + let startIndex = 0 + let endIndex = 0 + let slideTranslateXValue = 0 + if ( needToSwap > 0 && this.swappedSlides < needToSwap ) { + startIndex = this.swappedSlides + endIndex = needToSwap + + slideTranslateXValue = this.slideTranslateX + + this.swappedSlides = endIndex + } else if ( this.swappedSlides > needToSwap ) { + startIndex = needToSwap > 0 ? needToSwap : 0 + endIndex = this.swappedSlides + + this.swappedSlides = startIndex } - let steps = 0 + let step = 0 const runSteps = () => { - if ( steps === 0 ) { - const lastCloneSlide = this.clones[ this.clones.length - 1 ].offsetLeft - const firstCloneSide = this.clones[ 0 ].offsetLeft + if ( step === 0 ) { + this.slideEls.slice( startIndex, endIndex ).forEach( slide => { + slide.style.transform = `TranslateX(${ slideTranslateXValue })` + } ) + step++ + requestAnimationFrame( runSteps ) + } else if ( step === 1 ) { + this.slideEls.slice( startIndex, endIndex ).forEach( slide => slide.offsetLeft ) - let initSlide = null + if ( scrollToSlide ) { + const lastCloneSlide = this.clones[ this.clones.length - 1 ].offsetLeft + const firstCloneSide = this.clones[ 0 ].offsetLeft - if ( dir === 'N' ) { - initSlide = lastCloneSlide - } else if ( this.slidesToShow === 1 ) { - initSlide = lastCloneSlide - } else { - initSlide = firstCloneSide - } + let initSlide = null - // Move from the last slide to the first slide (N - next) or - // Move from the first slide to the last slide (P - prev) - this.sliderEl.style.scrollBehavior = 'unset' - this.sliderEl.scrollLeft = initSlide - this.sliderEl.style.scrollBehavior = '' + if ( dir === 'N' ) { + initSlide = lastCloneSlide + } else if ( this.slidesToShow === 1 ) { + initSlide = lastCloneSlide + } else { + initSlide = firstCloneSide + } + + // Move from the last slide to the first slide (N - next) or + // Move from the first slide to the last slide (P - prev) + this.sliderEl.style.scrollBehavior = 'unset' + this.sliderEl.scrollLeft = initSlide + this.sliderEl.style.scrollBehavior = '' + } - steps++ + step++ requestAnimationFrame( runSteps ) } else { - this.goToSlide( slide ) + requestAnimationFrame( () => this.goToSlide( slide ) ) } } - runSteps() + requestAnimationFrame( runSteps ) } goToSlide = ( slide, force = false ) => { @@ -442,6 +482,8 @@ class _StackableCarousel { const sliderElScrollLeft = this.sliderEl.scrollLeft const lastSlideOffset = this.slideEls[ this.slideEls.length - 1 ].offsetLeft const firstCloneOffset = this.clones[ 0 ].offsetLeft + const slidesOffset = this.slideEls.map( slide => slide.offsetLeft ) + const clonesOffset = this.clones.map( slide => slide.offsetLeft ) if ( this.type === 'fade' ) { if ( this.wheelTimeout ) { return @@ -463,16 +505,14 @@ class _StackableCarousel { this.sliderEl.scrollLeft = lastSlideOffset this.sliderEl.style.scrollBehavior = '' } else if ( this.infiniteScroll && e.deltaX >= 1 && sliderElScrollLeft >= firstCloneOffset ) { - this.clones.every( ( clone, i ) => { - const cloneOffset = clone.offsetLeft - const slideOffset = this.slideEls[ i ].offsetLeft - if ( sliderElScrollLeft === cloneOffset ) { + clonesOffset.some( ( offset, i ) => { + if ( sliderElScrollLeft === offset ) { this.sliderEl.style.scrollBehavior = 'unset' - this.sliderEl.scrollLeft = slideOffset + this.sliderEl.scrollLeft = slidesOffset[ i ] this.sliderEl.style.scrollBehavior = '' - return false + return true } - return true + return false } ) } } From b5d0dad6b8defcbb1d10b9beb261436ddae6dd26 Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 30 Jul 2025 20:06:45 +0800 Subject: [PATCH 5/6] remove computedstyle --- src/block/carousel/frontend-carousel.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/block/carousel/frontend-carousel.js b/src/block/carousel/frontend-carousel.js index 679b0b73c5..bac3dc42cb 100644 --- a/src/block/carousel/frontend-carousel.js +++ b/src/block/carousel/frontend-carousel.js @@ -118,10 +118,9 @@ class _StackableCarousel { } else if ( step === 2 ) { const numSlides = this.slideEls.length const slideClientRect = this.slideEls[ 0 ].getBoundingClientRect() - const carouselSlideGap = window.getComputedStyle( this.el ).getPropertyValue( '--gap' ) const slideWidth = slideClientRect.width - this.slideTranslateX = `calc((${ slideWidth }px * ${ numSlides }) + (${ carouselSlideGap } * ${ numSlides }))` + this.slideTranslateX = `calc((${ slideWidth }px * ${ numSlides }) + (var(--gap) * ${ numSlides }))` step++ } else if ( step === 3 ) { From e021a16dcc519b1b0a4e649279194572e78944fc Mon Sep 17 00:00:00 2001 From: Mikhaela Tapia Date: Wed, 30 Jul 2025 20:10:05 +0800 Subject: [PATCH 6/6] add zIndex --- src/block/carousel/frontend-carousel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/block/carousel/frontend-carousel.js b/src/block/carousel/frontend-carousel.js index bac3dc42cb..969cf5c1ef 100644 --- a/src/block/carousel/frontend-carousel.js +++ b/src/block/carousel/frontend-carousel.js @@ -92,6 +92,7 @@ class _StackableCarousel { const original = this.slideEls[ slideIndex ] const clone = original.cloneNode( true ) clone.classList.add( `stk-slide-clone-${ slideIndex + 1 }` ) + clone.style.zIndex = -1 original.style.willChange = 'transform' original.style.transform = 'TranslateX( 0 )'