编辑文件:hero-carousel.js
/** * ClothB2B Child — homepage carousel. * * cb-carousel.php renders .cb-carousel > .cb-carousel__track > .cb-carousel__slide * (one per admin-managed slide). This script wires up fade transitions, autoplay, * pagination dots, prev/next arrows, keyboard arrows, touch swipe, and pause-on- * hover/focus. Vanilla, no dependencies, footer-loaded, front-page only. * * No-JS fallback: the first slide carries .is-active in the markup, so the banner * shows a static image with full content even before or without JS. * * Autoplay ALWAYS runs (marketing requirement). prefers-reduced-motion only * disables the fade animation — handled in components.css via transition:none, * so reduced-motion users still get the rotating content, just without motion. */ ( function () { 'use strict'; /** ms between auto-advances. */ var INTERVAL = 5000; /** * @param {HTMLElement} root The .cb-carousel element. */ function initCarousel( root ) { var track = root.querySelector( '.cb-carousel__track' ); if ( ! track ) { return; } var slides = track.children; var count = slides.length; // 0 or 1 slide: nothing to rotate. Make sure the lone slide is visible. if ( count < 2 ) { if ( slides[0] ) { slides[0].classList.add( 'is-active' ); } return; } var slideLabel = root.getAttribute( 'data-slide-label' ) || 'Slide'; var current = 0; var timer = null; // Honor whichever slide the markup marked active (the first one), so the // initial paint and JS agree and there is no flash on init. for ( var i = 0; i < count; i++ ) { if ( slides[i].classList.contains( 'is-active' ) ) { current = i; } } setActive( current, true ); // Pagination dots — built in JS so PHP stays markup-light. var dotsContainer = root.querySelector( '.cb-carousel__dots' ); var dots = []; if ( dotsContainer ) { dotsContainer.innerHTML = ''; for ( var d = 0; d < count; d++ ) { /* eslint-disable no-loop-func */ ( function ( idx ) { var btn = document.createElement( 'button' ); btn.type = 'button'; btn.className = 'cb-carousel__dot'; btn.setAttribute( 'aria-label', slideLabel + ' ' + ( idx + 1 ) ); btn.addEventListener( 'click', function () { go( idx ); restart(); } ); dotsContainer.appendChild( btn ); dots.push( btn ); } )( d ); /* eslint-enable no-loop-func */ } } // Prev / next arrows (rendered by the template). var prev = root.querySelector( '.cb-carousel__arrow--prev' ); var next = root.querySelector( '.cb-carousel__arrow--next' ); if ( prev ) { prev.addEventListener( 'click', function () { go( current - 1 + count ); restart(); } ); } if ( next ) { next.addEventListener( 'click', function () { go( current + 1 ); restart(); } ); } // Keyboard: ←/→ advance while focus is within the carousel. root.addEventListener( 'keydown', function ( e ) { if ( 'ArrowLeft' === e.key ) { e.preventDefault(); go( current - 1 + count ); restart(); } else if ( 'ArrowRight' === e.key ) { e.preventDefault(); go( current + 1 ); restart(); } } ); // Pause autoplay on hover / keyboard focus; resume on leave. root.addEventListener( 'mouseenter', stop ); root.addEventListener( 'mouseleave', start ); root.addEventListener( 'focusin', stop ); root.addEventListener( 'focusout', start ); // Pause when the tab is hidden so the slide doesn't jump on return. document.addEventListener( 'visibilitychange', function () { if ( document.hidden ) { stop(); } else { start(); } } ); // Touch / pointer swipe — ignore tiny movements so taps on CTAs pass through. var startX = null; track.addEventListener( 'pointerdown', function ( e ) { startX = e.clientX; } ); window.addEventListener( 'pointerup', function ( e ) { if ( null === startX ) { return; } var dx = e.clientX - startX; startX = null; if ( Math.abs( dx ) > 40 ) { if ( dx < 0 ) { go( current + 1 ); } else { go( current - 1 + count ); } restart(); } } ); /** * Show slide idx; toggles .is-active + aria-hidden on every slide and * mirrors state onto the dots. * @param {number} idx * @param {boolean} immediate Skip the transition (used on init). */ function setActive( idx, immediate ) { for ( var j = 0; j < count; j++ ) { var on = ( j === idx ); slides[j].classList.toggle( 'is-active', on ); slides[j].setAttribute( 'aria-hidden', on ? 'false' : 'true' ); if ( immediate ) { slides[j].style.transitionDuration = '0ms'; } else { slides[j].style.transitionDuration = ''; } } for ( var k = 0; k < dots.length; k++ ) { var active = ( k === idx ); dots[k].classList.toggle( 'is-active', active ); dots[k].setAttribute( 'aria-current', active ? 'true' : 'false' ); } current = idx; } /** Advance to idx (wraps both directions); no-op if already there. */ function go( idx ) { var n = ( ( idx % count ) + count ) % count; if ( n === current ) { return; } setActive( n, false ); } // Autoplay always runs (see file header). Pause/resume is manual via the // hover/focus/visibility listeners above. function start() { stop(); timer = window.setInterval( function () { go( current + 1 ); }, INTERVAL ); } function stop() { if ( timer ) { window.clearInterval( timer ); timer = null; } } function restart() { stop(); start(); } start(); } document.addEventListener( 'DOMContentLoaded', function () { var carousels = document.querySelectorAll( '.cb-carousel' ); for ( var i = 0; i < carousels.length; i++ ) { initCarousel( carousels[i] ); } } ); }() );
保存
返回