/** * Palmetto Carousel — progressive enhancement for any `.palmetto-carousel` * wrapper. The plain markup is `.wp-block-columns` with snap-scrolling * (CSS-only, works without JS). This script adds: * * - Prev / Next arrow buttons (scroll by N cards based on * --palmetto-carousel-per-view) * - Dots indicator (one per card group) * - Active-dot tracking on scroll * - Edge-aware arrow disabling * * Markup contract: * * * Idempotent — re-running init() on the same wrapper is a no-op (guard * checks `data-palmetto-carousel-enhanced`). */ ( function () { 'use strict'; function ready( fn ) { if ( document.readyState !== 'loading' ) { fn(); } else { document.addEventListener( 'DOMContentLoaded', fn ); } } function perView( track ) { var raw = getComputedStyle( track.parentElement ) .getPropertyValue( '--palmetto-carousel-per-view' ) .trim(); var n = parseInt( raw, 10 ); return ( !isNaN( n ) && n > 0 ) ? n : 1; } function init( wrap ) { if ( wrap.dataset.palmettoCarouselEnhanced === '1' ) { return; } wrap.dataset.palmettoCarouselEnhanced = '1'; var track = wrap.querySelector( '.wp-block-columns' ); if ( !track ) { return; } var cards = track.querySelectorAll( '.wp-block-column' ); if ( cards.length < 2 ) { return; } // --- Arrow buttons --- var prev = document.createElement( 'button' ); var next = document.createElement( 'button' ); prev.type = 'button'; next.type = 'button'; prev.className = 'palmetto-carousel__arrow palmetto-carousel__arrow--prev'; next.className = 'palmetto-carousel__arrow palmetto-carousel__arrow--next'; prev.setAttribute( 'aria-label', 'Previous' ); next.setAttribute( 'aria-label', 'Next' ); prev.innerHTML = '‹'; // ‹ next.innerHTML = '›'; // › wrap.appendChild( prev ); wrap.appendChild( next ); function scrollByCards( direction ) { var first = cards[0]; if ( !first ) { return; } var gap = parseFloat( getComputedStyle( track ).columnGap ) || 0; var step = first.getBoundingClientRect().width + gap; track.scrollBy( { left: direction * step * perView( track ), behavior: 'smooth' } ); } prev.addEventListener( 'click', function () { scrollByCards( -1 ); } ); next.addEventListener( 'click', function () { scrollByCards( 1 ); } ); // --- Dots --- var dots = document.createElement( 'div' ); dots.className = 'palmetto-carousel__dots'; dots.setAttribute( 'role', 'tablist' ); var dotButtons = []; Array.prototype.forEach.call( cards, function ( card, i ) { var d = document.createElement( 'button' ); d.type = 'button'; d.className = 'palmetto-carousel__dot'; d.setAttribute( 'role', 'tab' ); d.setAttribute( 'aria-label', 'Go to slide ' + ( i + 1 ) ); d.addEventListener( 'click', function () { card.scrollIntoView( { behavior: 'smooth', inline: 'start', block: 'nearest' } ); } ); dots.appendChild( d ); dotButtons.push( d ); } ); wrap.appendChild( dots ); // --- Active state on scroll / edge-aware arrow disable --- function update() { var first = cards[0]; if ( !first ) { return; } var cardWidth = first.getBoundingClientRect().width; var gap = parseFloat( getComputedStyle( track ).columnGap ) || 0; var index = Math.round( track.scrollLeft / ( cardWidth + gap ) ); dotButtons.forEach( function ( d, i ) { d.classList.toggle( 'is-active', i === index ); d.setAttribute( 'aria-selected', i === index ? 'true' : 'false' ); } ); var atStart = track.scrollLeft <= 2; var atEnd = track.scrollLeft + track.clientWidth >= track.scrollWidth - 2; prev.disabled = atStart; next.disabled = atEnd; prev.setAttribute( 'aria-disabled', atStart ? 'true' : 'false' ); next.setAttribute( 'aria-disabled', atEnd ? 'true' : 'false' ); } track.addEventListener( 'scroll', update, { passive: true } ); window.addEventListener( 'resize', update ); update(); } function initAll() { document.querySelectorAll( '.palmetto-carousel' ).forEach( init ); } ready( initAll ); // Also expose for blocks added dynamically (editor preview, AJAX loads). window.PalmettoCarousel = { init: init, initAll: initAll }; } )();