#169 - Autoplay slider with an optional manual selection. v0.1

Add on scroll autoplay to your Webflow sliders with optional pause-on-hover, custom slider dots navigation.

Ver demostración

<!-- 💙 MEMBERSCRIPT #169 v0.1 💙 - AUTOPLAY SLIDER WITH OPTIONAL MANUAL SELECTION -->
<script>
(function() {
    'use strict';
    
    // Wait for DOM to be ready
    function initSliders() {
        const sliders = document.querySelectorAll('[data-ms-code="auto-slider"]');
        
        sliders.forEach(slider => {
            new AutoSlider(slider);
        });
    }
    
    class AutoSlider {
        constructor(element) {
            this.slider = element;
            this.track = this.slider.querySelector('[data-ms-code="slider-track"]');
            this.slides = this.slider.querySelectorAll('[data-ms-code="slider-slide"]');
            this.dotsContainer = this.slider.querySelector('[data-ms-code="slider-dots"]');
            this.dots = this.slider.querySelectorAll('[data-ms-code="slider-dot"]');
            this.collectDots();
            
            // Configuration
            this.currentSlide = 0;
            this.interval = parseInt(this.slider.dataset.msInterval) || 3000;
            this.pauseOnHover = this.slider.dataset.msPauseOnHover !== 'false';
            this.resumeDelay = parseInt(this.slider.dataset.msResumeDelay) || 3000;
            this.autoplayOnVisible = this.slider.dataset.msAutoplayOnVisible === 'true';
            this.visibleThreshold = Number.isNaN(parseFloat(this.slider.dataset.msVisibleThreshold))
                ? 0.3
                : Math.min(1, Math.max(0, parseFloat(this.slider.dataset.msVisibleThreshold)));
            this.dotsActiveClass = this.dotsContainer?.dataset.msDotActiveClass || '';
            this.dotsInactiveClass = this.dotsContainer?.dataset.msDotInactiveClass || '';
            this.defaultActiveClass = '';
            this.defaultInactiveClass = '';
            
            // State
            this.autoplayTimer = null;
            this.resumeTimer = null;
            this.isUserInteracting = false;
            this.isPaused = false;
            this.isInView = false;
            this.visibilityObserver = null;
            
            // Validate required elements
            if (!this.track || this.slides.length === 0) {
                console.warn('AutoSlider: Required elements not found');
                return;
            }
            
            this.init();
        }
        
        init() {
            // Set up initial styles
            this.setupStyles();
            
            // Detect default dot classes from markup if no attributes provided
            this.detectDotClasses();

            // Sync with Webflow's current state
            this.syncWithWebflow();
            
            // Bind event listeners
            this.bindEvents();
            
            // Start autoplay (optionally only when visible)
            if (this.autoplayOnVisible) {
                this.setupVisibilityObserver();
            } else {
                this.startAutoplay();
            }
            
            console.log('AutoSlider initialized with', this.slides.length, 'slides');
        }
        
        setupStyles() {
            // No CSS modifications - work with existing Webflow slider styles
            // Only add data attributes to slides for tracking
            this.slides.forEach((slide, index) => {
                slide.dataset.slideIndex = index;
            });

            // Improve accessibility for custom dots without altering styles
            if (this.dots && this.dots.length) {
                this.dots.forEach((dot, index) => {
                    if (!dot.hasAttribute('role')) dot.setAttribute('role', 'button');
                    if (!dot.hasAttribute('tabindex')) dot.setAttribute('tabindex', '0');
                    if (!dot.hasAttribute('aria-label')) dot.setAttribute('aria-label', `Show slide ${index + 1}`);
                    // If data-ms-slide is missing, infer from position
                    if (!dot.dataset.msSlide) dot.dataset.msSlide = String(index);
                });
            }
        }
        
        bindEvents() {
            // No custom prev/next controls
            
            // Custom dot navigation (data-ms-code dots). Use event delegation if container exists.
            if (this.dotsContainer) {
                this.dotsContainer.addEventListener('click', (e) => {
                    const dot = e.target.closest('[data-ms-code="slider-dot"]');
                    if (!dot || !this.dotsContainer.contains(dot)) return;
                    e.preventDefault();
                    let slideIndex = parseInt(dot.dataset.msSlide);
                    if (Number.isNaN(slideIndex)) {
                        // Recollect in case DOM changed
                        this.dots = this.dotsContainer.querySelectorAll('[data-ms-code="slider-dot"]');
                        slideIndex = Array.from(this.dots).indexOf(dot);
                    }
                    if (!Number.isNaN(slideIndex)) {
                        this.handleUserInteraction();
                        this.goToSlide(slideIndex);
                    }
                });
            }

            // Also attach direct listeners to cover cases without a container
            this.dots.forEach(dot => {
                dot.addEventListener('click', (e) => {
                    e.preventDefault();
                    let slideIndex = parseInt(dot.dataset.msSlide);
                    if (Number.isNaN(slideIndex)) {
                        slideIndex = Array.from(this.dots).indexOf(dot);
                    }
                    if (!Number.isNaN(slideIndex)) {
                        this.handleUserInteraction();
                        this.goToSlide(slideIndex);
                    }
                });

                // Keyboard support for custom dots
                dot.addEventListener('keydown', (e) => {
                    const key = e.key;
                    if (key === 'Enter' || key === ' ') {
                        e.preventDefault();
                        let slideIndex = parseInt(dot.dataset.msSlide);
                        if (Number.isNaN(slideIndex)) {
                            slideIndex = Array.from(this.dots).indexOf(dot);
                        }
                        if (!Number.isNaN(slideIndex)) {
                            this.handleUserInteraction();
                            this.goToSlide(slideIndex);
                        }
                    }
                });
            });
            
            // Listen for Webflow slider interactions to pause autoplay
            const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
            const webflowArrows = this.slider.querySelectorAll('.w-slider-arrow-left, .w-slider-arrow-right');
            
            webflowDots.forEach(dot => {
                dot.addEventListener('click', () => {
                    this.handleUserInteraction();
                    // Update our current slide based on Webflow's active dot
                    const activeDotIndex = Array.from(webflowDots).indexOf(dot);
                    if (activeDotIndex !== -1) {
                        this.currentSlide = activeDotIndex;
                        this.updateActiveStates();
                    }
                    // Schedule resume after inactivity
                    clearTimeout(this.resumeTimer);
                    this.resumeTimer = setTimeout(() => {
                        this.isUserInteracting = false;
                        this.resumeAutoplay();
                    }, this.resumeDelay);
                });
            });
            
            webflowArrows.forEach(arrow => {
                arrow.addEventListener('click', () => {
                    this.handleUserInteraction();
                    // Let Webflow handle the navigation, then sync our state
                    setTimeout(() => {
                        this.syncWithWebflow();
                        // Schedule resume after inactivity
                        clearTimeout(this.resumeTimer);
                        this.resumeTimer = setTimeout(() => {
                            this.isUserInteracting = false;
                            this.resumeAutoplay();
                        }, this.resumeDelay);
                    }, 100);
                });
            });
            
            // Hover events
            if (this.pauseOnHover) {
                this.slider.addEventListener('mouseenter', () => {
                    this.pauseAutoplay();
                });
                this.slider.addEventListener('mouseleave', () => {
                    this.isUserInteracting = false;
                    this.resumeAutoplay();
                });
            }
            
            // Touch/swipe support (only if not handled by Webflow)
            this.setupTouchEvents();
            
            // Keyboard navigation
            this.slider.addEventListener('keydown', (e) => {
                if (e.key === 'ArrowLeft') {
                    this.handleUserInteraction();
                    this.previousSlide();
                } else if (e.key === 'ArrowRight') {
                    this.handleUserInteraction();
                    this.nextSlide();
                }
            });
            
            // Focus management
            this.slider.addEventListener('focus', () => {
                this.pauseAutoplay();
            }, true);
            
            this.slider.addEventListener('blur', () => {
                if (!this.isUserInteracting) {
                    this.resumeAutoplay();
                }
            }, true);
        }
        
        setupTouchEvents() {
            let startX = 0;
            let currentX = 0;
            let isDragging = false;
            
            this.slider.addEventListener('touchstart', (e) => {
                startX = e.touches[0].clientX;
                isDragging = true;
                this.pauseAutoplay();
            });
            
            this.slider.addEventListener('touchmove', (e) => {
                if (!isDragging) return;
                currentX = e.touches[0].clientX;
            });
            
            this.slider.addEventListener('touchend', () => {
                if (!isDragging) return;
                isDragging = false;
                
                const deltaX = startX - currentX;
                const threshold = 50;
                
                if (Math.abs(deltaX) > threshold) {
                    this.handleUserInteraction();
                    if (deltaX > 0) {
                        this.nextSlide();
                    } else {
                        this.previousSlide();
                    }
                }
            });
        }
        
        handleUserInteraction() {
            this.isUserInteracting = true;
            this.pauseAutoplay();
            
            // Clear any existing resume timer
            clearTimeout(this.resumeTimer);
            
            // Set timer to resume autoplay after period of inactivity
            this.resumeTimer = setTimeout(() => {
                this.isUserInteracting = false;
                this.resumeAutoplay();
            }, this.resumeDelay);
        }
        
        startAutoplay() {
            if (this.slides.length <= 1) return;
            if (this.autoplayTimer) return; // already running
            if (this.autoplayOnVisible && !this.isInView) return; // respect visibility
            
            this.autoplayTimer = setInterval(() => {
                this.nextSlide();
            }, this.interval);
            
            this.isPaused = false;
        }
        
        pauseAutoplay() {
            if (this.autoplayTimer) {
                clearInterval(this.autoplayTimer);
                this.autoplayTimer = null;
            }
            this.isPaused = true;
        }
        
        resumeAutoplay() {
            if (!this.isPaused || this.isUserInteracting) return;
            this.startAutoplay();
        }
        
        goToSlide(index) {
            // Ensure index is within bounds
            if (index < 0) {
                this.currentSlide = this.slides.length - 1;
            } else if (index >= this.slides.length) {
                this.currentSlide = 0;
            } else {
                this.currentSlide = index;
            }
            
            // Use Webflow's native slider navigation if available
            const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
            const webflowRight = this.slider.querySelector('.w-slider-arrow-right');
            const webflowLeft = this.slider.querySelector('.w-slider-arrow-left');
            if (webflowDots.length > 0) {
                // Clicking dots is reliable for direct index navigation
                const target = webflowDots[this.currentSlide];
                if (target) target.click();
            } else if (webflowRight && webflowLeft) {
                // Fallback: use arrows to move stepwise toward target
                const direction = this.currentSlide > 0 ? 1 : -1;
                (direction > 0 ? webflowRight : webflowLeft).click();
            } else {
                // Fallback: calculate slide position manually
                const slideWidth = this.slides[0].offsetWidth;
                const translateX = -(this.currentSlide * slideWidth);
                this.track.style.transform = `translateX(${translateX}px)`;
            }
            
            // Update active states
            this.updateActiveStates();
            
            // Trigger custom event
            this.slider.dispatchEvent(new CustomEvent('slideChanged', {
                detail: {
                    currentSlide: this.currentSlide,
                    totalSlides: this.slides.length
                }
            }));

            // If the user has left the dots/nav area, ensure autoplay resumes after delay
            if (!this.pauseOnHover && !this.isUserInteracting && !this.autoplayTimer) {
                this.startAutoplay();
            }
        }
        
        nextSlide() {
            this.goToSlide(this.currentSlide + 1);
            // If autoplay is running, keep it seamless after manual advance
            if (!this.autoplayTimer && !this.isUserInteracting) {
                this.startAutoplay();
            }
        }
        
        previousSlide() {
            this.goToSlide(this.currentSlide - 1);
            if (!this.autoplayTimer && !this.isUserInteracting) {
                this.startAutoplay();
            }
        }
        
        updateActiveStates() {
            // Update slides
            this.slides.forEach((slide, index) => {
                slide.classList.toggle('active', index === this.currentSlide);
                slide.setAttribute('aria-hidden', index !== this.currentSlide);
            });
            
            // Update dots
            this.dots.forEach((dot, index) => {
                let slideIndex = parseInt(dot.dataset.msSlide);
                if (Number.isNaN(slideIndex)) slideIndex = index;
                const isActive = slideIndex === this.currentSlide;

                // ARIA and data state
                dot.setAttribute('aria-pressed', String(isActive));
                if (isActive) {
                    dot.setAttribute('data-active', 'true');
                } else {
                    dot.removeAttribute('data-active');
                }

                // Generic active class toggle (if they style it)
                dot.classList.toggle('active', isActive);

                // Optional custom classes provided via attributes
                const activeClass = dot.dataset.msActiveClass || this.dotsActiveClass || this.defaultActiveClass;
                const inactiveClass = dot.dataset.msInactiveClass || this.dotsInactiveClass || this.defaultInactiveClass;
                if (activeClass) dot.classList.toggle(activeClass, isActive);
                if (inactiveClass) dot.classList.toggle(inactiveClass, !isActive);
            });
        }

        detectDotClasses() {
            if (!this.dots || this.dots.length === 0) return;
            // If classes are already provided via attributes, skip detection
            if (this.dotsActiveClass || this.dotsInactiveClass) return;
            // Find a class containing 'active' and 'inactive' among dot elements
            const classCounts = new Map();
            this.dots.forEach((dot) => {
                dot.classList.forEach((cls) => {
                    classCounts.set(cls, (classCounts.get(cls) || 0) + 1);
                });
            });
            // Prefer classes that explicitly include 'active'/'inactive'
            const allClasses = Array.from(classCounts.keys());
            const activeCandidate = allClasses.find((c) => /active/i.test(c));
            const inactiveCandidate = allClasses.find((c) => /inactive/i.test(c));
            if (activeCandidate) this.defaultActiveClass = activeCandidate;
            if (inactiveCandidate) this.defaultInactiveClass = inactiveCandidate;
        }
        
        syncWithWebflow() {
            // Sync our current slide with Webflow's active slide
            const activeWebflowDot = this.slider.querySelector('.w-slider-dot.w-active');
            if (activeWebflowDot) {
                const webflowDots = this.slider.querySelectorAll('.w-slider-dot');
                const activeIndex = Array.from(webflowDots).indexOf(activeWebflowDot);
                if (activeIndex !== -1 && activeIndex !== this.currentSlide) {
                    this.currentSlide = activeIndex;
                    this.updateActiveStates();
                }
            }
        }

        collectDots() {
            // If dots are not inside the slider, look for a sibling container in the same wrapper
            if (!this.dots || this.dots.length === 0) {
                let container = this.dotsContainer;
                if (!container && this.slider.parentElement) {
                    container = this.slider.parentElement.querySelector('[data-ms-code="slider-dots"]');
                }
                if (!container) {
                    // Try the closest ancestor wrapper then find dots within it that are siblings
                    const wrapper = this.slider.closest('[data-ms-slider-wrapper], .feature-slider-wrapper, section, div');
                    if (wrapper) {
                        // Prefer immediate sibling dots container
                        const siblingDots = Array.from(wrapper.querySelectorAll('[data-ms-code="slider-dots"]'))
                            .find((el) => el !== this.slider);
                        if (siblingDots) container = siblingDots;
                    }
                }
                if (container) {
                    this.dotsContainer = container;
                    this.dots = container.querySelectorAll('[data-ms-code="slider-dot"]');
                }
            }
        }

        // No custom nav buttons
        
        // Public methods for external control
        pause() {
            this.pauseAutoplay();
        }
        
        resume() {
            this.isUserInteracting = false;
            this.resumeAutoplay();
        }
        
        destroy() {
            this.pauseAutoplay();
            clearTimeout(this.resumeTimer);
            if (this.visibilityObserver) {
                try { this.visibilityObserver.disconnect(); } catch (e) {}
                this.visibilityObserver = null;
            }
            
            // Remove event listeners would go here if needed
            // This is a simplified version
        }

        setupVisibilityObserver() {
            if (!('IntersectionObserver' in window)) {
                // Fallback: start immediately
                this.startAutoplay();
                return;
            }
            const thresholds = [];
            const step = 0.1;
            for (let t = 0; t <= 1; t += step) thresholds.push(parseFloat(t.toFixed(1)));
            if (!thresholds.includes(this.visibleThreshold)) thresholds.push(this.visibleThreshold);
            thresholds.sort((a, b) => a - b);

            this.visibilityObserver = new IntersectionObserver((entries) => {
                entries.forEach((entry) => {
                    this.isInView = entry.isIntersecting && entry.intersectionRatio >= this.visibleThreshold;
                    if (this.isInView) {
                        // Resume/start autoplay only if not interacting
                        if (!this.isUserInteracting) {
                            this.startAutoplay();
                        }
                    } else {
                        this.pauseAutoplay();
                    }
                });
            }, { threshold: thresholds });

            this.visibilityObserver.observe(this.slider);
        }
    }
    
    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initSliders);
    } else {
        initSliders();
    }
    
    // Expose the class globally for external access if needed
    window.MemberScript169 = {
        AutoSlider: AutoSlider,
        init: initSliders
    };
})();

</script>

Creación del escenario Make.com

1. Descargue el proyecto JSON a continuación para empezar.

2. Navegue hasta Make.com y Cree un nuevo escenario...

3. Haga clic en el pequeño cuadro con 3 puntos y luego Importar Blueprint...

4. Sube tu archivo y ¡voilá! Ya está listo para vincular sus propias cuentas.

¿Necesitas ayuda con este MemberScript?

Todos los clientes de Memberstack pueden solicitar asistencia en el Slack 2.0. Tenga en cuenta que no se trata de funciones oficiales y que no se puede garantizar la asistencia.

Únete al Slack 2.0
Notas de la versión
Atributos
Descripción
Atributo
No se han encontrado artículos.
Guías / Tutoriales
No se han encontrado artículos.
Tutorial
¿Qué es Memberstack?

Autenticación y pagos para sitios Webflow

Añada inicios de sesión, suscripciones, contenido cerrado y mucho más a su sitio Webflow: fácil y totalmente personalizable.

Más información

"Hemos estado utilizando Memberstack durante mucho tiempo, y nos ha ayudado a lograr cosas que nunca hubiéramos creído posible usando Webflow. Nos ha permitido construir plataformas con gran profundidad y funcionalidad y el equipo que hay detrás siempre ha sido súper servicial y receptivo a los comentarios"

Jamie Debnam
39 Digital

"He estado construyendo un sitio de membresía con Memberstack y Jetboost para un cliente. Se siente como magia construir con estas herramientas. Como alguien que ha trabajado en una agencia donde algunas de estas aplicaciones fueron codificadas desde cero, finalmente entiendo el bombo ahora. Esto es mucho más rápido y mucho más barato."

Félix Meens
Estudio Webflix

"Uno de los mejores productos para iniciar un sitio de membresía - Me gusta la facilidad de uso de Memberstack. Yo era capaz de mi sitio de membresía en marcha y funcionando dentro de un día. No hay nada más fácil que eso. También proporciona la funcionalidad que necesito para hacer la experiencia del usuario más personalizada."

Eric McQuesten
Nerds de la tecnología sanitaria
Depósito Off World

"Mi negocio no sería lo que es sin Memberstack. Si crees que 30 $/mes es caro, prueba a contratar a un desarrollador para que integre recomendaciones personalizadas en tu sitio por ese precio. Increíblemente flexible conjunto de herramientas para aquellos dispuestos a poner en algunos esfuerzos mínimos para ver su documentación bien elaborado."

Riley Brown
Depósito Off World

"La comunidad de Slack es una de las más activas que he visto y los clientes están dispuestos a responder preguntas y ofrecer soluciones. He realizado evaluaciones en profundidad de herramientas alternativas y siempre volvemos a Memberstack: ahórrate el tiempo y dale una oportunidad"."

Abadía Burtis
Nerds de la tecnología sanitaria
Slack

¿Necesitas ayuda con este MemberScript? ¡Únete a nuestra comunidad Slack!

Únete al Slack de la comunidad Memberstack y ¡pregunta! Espera una respuesta rápida de un miembro del equipo, un experto de Memberstack o un compañero de la comunidad.

Únete a nuestro Slack