/* NativeSlider - Zero-dependency infinite-loop carousel */ (function () { 'use strict'; class NativeSlider { constructor(container, options = {}) { this.container = container; this.options = { autoplay: options.autoplay !== false, delay: options.delay || 5000, loop: options.loop !== false, pagination: options.pagination !== false, navigation: options.navigation !== false, pauseOnHover: options.pauseOnHover !== false }; this.track = container.querySelector('.slider__track'); this.slides = Array.from(this.track ? this.track.children : []); this.realSlides = this.slides.slice(); this.realSlideCount = this.slides.length; // State this.currentIndex = 0; this.realIndex = 0; this.isAnimating = false; this.autoplayInterval = null; this.isDragging = false; this.pointerStartX = 0; this.pointerStartY = 0; this.currentTranslate = 0; this.startTranslate = 0; this.pointerId = null; this.dragThreshold = 0.2; this.cloneCount = 1; // Calculate slideWidth early var rect = this.container.getBoundingClientRect(); this.slideWidth = rect.width; // DOM references for cleanup this.pagination = null; this.dots = []; this.prevButton = null; this.nextButton = null; this.liveRegion = null; // Event handlers (bound for cleanup) this._boundHandlers = {}; // Single slide edge case if (this.realSlideCount <= 1) { this._setupSingleSlide(); return; } this._cloneSlides(); // Set initial position to first real slide this.currentIndex = this.cloneCount; this.realIndex = 0; this._createPagination(); this._createNavigation(); this._setupEvents(); this._setupKeyboard(); this._setupResize(); this._setupA11y(); this._setPosition(false); this._updateActiveState(); if (this.options.autoplay) { this.startAutoplay(); } this._setupShopifyUnload(); } _setupSingleSlide() { if (this.track) { this.track.style.transform = 'none'; } this._updateActiveState(); var navButtons = this.container.querySelectorAll('.slider__button--prev, .slider__button--next'); navButtons.forEach(function(btn) { btn.style.display = 'none'; }); var pag = this.container.querySelector('.slider__pagination'); if (pag) pag.style.display = 'none'; } _cloneSlides() { if (!this.options.loop || this.realSlideCount < 2) return; for (var i = 0; i < this.cloneCount; i++) { var cloneBefore = this.slides[this.realSlideCount - 1 - i].cloneNode(true); cloneBefore.classList.add('slider__slide--clone', 'slider__slide--clone-before'); this.track.insertBefore(cloneBefore, this.track.firstChild); } for (var j = 0; j < this.cloneCount; j++) { var cloneAfter = this.slides[j].cloneNode(true); cloneAfter.classList.add('slider__slide--clone', 'slider__slide--clone-after'); this.track.appendChild(cloneAfter); } this.slides = Array.from(this.track.children); } _createPagination() { if (!this.options.pagination) return; this.pagination = this.container.querySelector('.slider__pagination'); if (!this.pagination) return; this.pagination.innerHTML = ''; this.dots = []; for (var i = 0; i < this.realSlideCount; i++) { var dot = document.createElement('button'); dot.className = 'slider__dot'; dot.setAttribute('role', 'tab'); dot.setAttribute('aria-label', 'Go to slide ' + (i + 1)); if (i === 0) { dot.classList.add('slider__dot--active'); dot.setAttribute('aria-current', 'true'); } (function(slider, index) { var handler = function (e) { e.preventDefault(); slider.goToSlide(index); }; dot.addEventListener('click', handler); slider._boundHandlers['dot-' + index] = { element: dot, event: 'click', handler: handler }; })(this, i); this.pagination.appendChild(dot); this.dots.push(dot); } } _createNavigation() { if (!this.options.navigation) return; this.prevButton = this.container.querySelector('.slider__button--prev'); this.nextButton = this.container.querySelector('.slider__button--next'); if (this.prevButton) { var self = this; var prevHandler = function (e) { e.preventDefault(); self.prev(); }; this.prevButton.addEventListener('click', prevHandler); this._boundHandlers['prev'] = { element: this.prevButton, event: 'click', handler: prevHandler }; } if (this.nextButton) { var self2 = this; var nextHandler = function (e) { e.preventDefault(); self2.next(); }; this.nextButton.addEventListener('click', nextHandler); this._boundHandlers['next'] = { element: this.nextButton, event: 'click', handler: nextHandler }; } } _setupEvents() { var self = this; var pointerStart = function (e) { self._handleTouchStart(e); }; var pointerMove = function (e) { self._handleTouchMove(e); }; var pointerEnd = function (e) { self._handleTouchEnd(e); }; this.track.addEventListener('pointerdown', pointerStart); this.track.addEventListener('pointermove', pointerMove); this.track.addEventListener('pointerup', pointerEnd); this.track.addEventListener('pointercancel', pointerEnd); this.track.addEventListener('pointerleave', pointerEnd); this._boundHandlers['track-pointerdown'] = { element: this.track, event: 'pointerdown', handler: pointerStart }; this._boundHandlers['track-pointermove'] = { element: this.track, event: 'pointermove', handler: pointerMove }; this._boundHandlers['track-pointerup'] = { element: this.track, event: 'pointerup', handler: pointerEnd }; this._boundHandlers['track-pointercancel'] = { element: this.track, event: 'pointercancel', handler: pointerEnd }; this._boundHandlers['track-pointerleave'] = { element: this.track, event: 'pointerleave', handler: pointerEnd }; if (this.options.pauseOnHover) { var self3 = this; var enterHandler = function () { self3.stopAutoplay(); }; var leaveHandler = function () { self3.startAutoplay(); }; this.container.addEventListener('mouseenter', enterHandler); this.container.addEventListener('mouseleave', leaveHandler); this.container.addEventListener('focusin', enterHandler); this.container.addEventListener('focusout', leaveHandler); this._boundHandlers['mouseenter'] = { element: this.container, event: 'mouseenter', handler: enterHandler }; this._boundHandlers['mouseleave'] = { element: this.container, event: 'mouseleave', handler: leaveHandler }; this._boundHandlers['focusin'] = { element: this.container, event: 'focusin', handler: enterHandler }; this._boundHandlers['focusout'] = { element: this.container, event: 'focusout', handler: leaveHandler }; } var self4 = this; var transitionEnd = function (e) { self4._handleTransitionEnd(e); }; this.track.addEventListener('transitionend', transitionEnd); this._boundHandlers['transitionend'] = { element: this.track, event: 'transitionend', handler: transitionEnd }; } _setupKeyboard() { var self = this; var keyHandler = function (e) { if (e.key === 'ArrowLeft') { e.preventDefault(); self.prev(); } else if (e.key === 'ArrowRight') { e.preventDefault(); self.next(); } }; this.container.addEventListener('keydown', keyHandler); this._boundHandlers['keydown'] = { element: this.container, event: 'keydown', handler: keyHandler }; } _setupResize() { var self = this; this.resizeObserver = new ResizeObserver(function () { self._onResize(); }); this.resizeObserver.observe(this.container); } _setupA11y() { this.container.setAttribute('role', 'region'); this.container.setAttribute('aria-roledescription', 'carousel'); this.container.setAttribute('aria-label', 'Banner carousel'); this.liveRegion = document.createElement('div'); this.liveRegion.className = 'slider__live-region visually-hidden'; this.liveRegion.setAttribute('aria-live', 'polite'); this.liveRegion.setAttribute('aria-atomic', 'true'); this.container.appendChild(this.liveRegion); var self = this; this.slides.forEach(function (slide, index) { if (!slide.classList.contains('slider__slide--clone')) { slide.setAttribute('role', 'group'); slide.setAttribute('aria-roledescription', 'slide'); slide.setAttribute('aria-label', 'Slide ' + (index + 1) + ' of ' + self.realSlideCount); } }); } _setupShopifyUnload() { var self = this; var unloadHandler = function (event) { if (event.target.contains(self.container)) { self.destroy(); } }; document.addEventListener('shopify:section:unload', unloadHandler); this._boundHandlers['shopify-unload'] = { element: document, event: 'shopify:section:unload', handler: unloadHandler }; } _handleTouchStart(e) { if (this.isAnimating) return; if (this.pointerId !== null) return; this.pointerId = e.pointerId; this.pointerStartX = e.clientX; this.pointerStartY = e.clientY; this.startTranslate = this.currentTranslate; this._pointerStartTime = e.timeStamp; } _handleTouchMove(e) { if (e.pointerId !== this.pointerId) return; var deltaX = e.clientX - this.pointerStartX; var deltaY = e.clientY - this.pointerStartY; if (!this.isDragging) { if (Math.abs(deltaX) < 8 || Math.abs(deltaX) <= Math.abs(deltaY)) return; this.isDragging = true; this.track.style.transition = 'none'; this.track.setPointerCapture(e.pointerId); } e.preventDefault(); this.currentTranslate = this.startTranslate + deltaX; this._updateTrackPosition(false); } _handleTouchEnd(e) { if (e.pointerId !== this.pointerId) return; var wasDragging = this.isDragging; this.isDragging = false; this.pointerId = null; if (!wasDragging) { this._pointerStartTime = null; return; } try { this.track.releasePointerCapture(e.pointerId); } catch (err) {} this.track.style.transition = ''; var deltaX = e.clientX - this.pointerStartX; var duration = e.timeStamp - (this._pointerStartTime || e.timeStamp); var velocity = Math.abs(deltaX) / Math.max(duration, 1); var movedEnough = Math.abs(deltaX) > 50 || velocity > 0.3; if (movedEnough) { if (deltaX > 0) { this.prev(); } else { this.next(); } } else { this._setPosition(true); } this._pointerStartTime = null; } _handleTransitionEnd(e) { if (e.propertyName !== 'transform') return; this.isAnimating = false; if (this.options.loop && this.realSlideCount > 1) { // Derive realIndex from currentIndex this.realIndex = this.currentIndex - this.cloneCount; // Check clone boundaries if (this.realIndex < 0) { // We're on a clone-before slide, snap to last real slide this.realIndex = this.realSlideCount - 1; this.currentIndex = this.realIndex + this.cloneCount; this._setPosition(false, true); // no animation, force instant reset } else if (this.realIndex >= this.realSlideCount) { // We're on a clone-after slide, snap to first real slide this.realIndex = 0; this.currentIndex = this.realIndex + this.cloneCount; this._setPosition(false, true); // no animation, force instant reset } } else { // Non-loop mode: derive realIndex and clamp this.realIndex = this.currentIndex - this.cloneCount; if (this.realIndex < 0) { this.realIndex = 0; this.currentIndex = 0; } if (this.realIndex >= this.realSlideCount) { this.realIndex = this.realSlideCount - 1; this.currentIndex = this.realSlideCount - 1; } } this._updateActiveState(); } _onResize() { if (this.realSlideCount <= 1) return; var rect = this.container.getBoundingClientRect(); this.slideWidth = rect.width; this._setPosition(false); } _setPosition(animate, noTransition) { if (this.realSlideCount <= 1) return; if (animate === undefined) animate = true; if (noTransition === undefined) noTransition = false; this.currentTranslate = -this.currentIndex * this.slideWidth; this._updateTrackPosition(animate, noTransition); } _updateTrackPosition(animate, noTransition) { if (animate === undefined) animate = true; if (noTransition === undefined) noTransition = false; if (noTransition) { // Instant position change - no animation this.track.style.transition = 'none'; } else { this.track.style.transition = animate ? 'transform 0.3s ease' : 'none'; } this.track.style.transform = 'translate3d(' + this.currentTranslate + 'px, 0, 0)'; if (noTransition) { // Force reflow so the browser processes the transform without transition void this.track.offsetHeight; } } _updateActiveState() { var self = this; var activeSlide = this.realSlides[this.realIndex]; if (activeSlide && activeSlide.dataset.nav) { this.container.style.setProperty('--top-banner-nav-color', activeSlide.dataset.nav); } this.dots.forEach(function (dot, index) { if (index === self.realIndex) { dot.classList.add('slider__dot--active'); dot.setAttribute('aria-current', 'true'); } else { dot.classList.remove('slider__dot--active'); dot.removeAttribute('aria-current'); } }); if (this.liveRegion) { this.liveRegion.textContent = 'Slide ' + (this.realIndex + 1) + ' of ' + this.realSlideCount; } } goToSlide(index) { if (this.isAnimating || this.realSlideCount <= 1) return; var realIdx = Math.max(0, Math.min(index, this.realSlideCount - 1)); this.currentIndex = realIdx + this.cloneCount; this.realIndex = realIdx; this.isAnimating = true; this._setPosition(true); this._updateActiveState(); } next() { if (this.isAnimating || this.realSlideCount <= 1) return; this.isAnimating = true; this.currentIndex++; this.realIndex = (this.realIndex + 1) % this.realSlideCount; this._setPosition(true); this._updateActiveState(); } prev() { if (this.isAnimating || this.realSlideCount <= 1) return; this.isAnimating = true; this.currentIndex--; this.realIndex = (this.realIndex - 1 + this.realSlideCount) % this.realSlideCount; this._setPosition(true); this._updateActiveState(); } startAutoplay() { if (this.autoplayInterval || !this.options.autoplay || this.realSlideCount <= 1) return; var self = this; this.autoplayInterval = setInterval(function () { self.next(); }, this.options.delay); } stopAutoplay() { if (this.autoplayInterval) { clearInterval(this.autoplayInterval); this.autoplayInterval = null; } } destroy() { this.stopAutoplay(); if (this.resizeObserver) { this.resizeObserver.disconnect(); } var self = this; Object.keys(this._boundHandlers).forEach(function (key) { var h = self._boundHandlers[key]; h.element.removeEventListener(h.event, h.handler); }); this._boundHandlers = {}; var clones = this.track.querySelectorAll('.slider__slide--clone'); clones.forEach(function (clone) { clone.remove(); }); if (this.liveRegion && this.liveRegion.parentNode) { this.liveRegion.parentNode.removeChild(this.liveRegion); } this.slides.forEach(function (slide) { slide.removeAttribute('role'); slide.removeAttribute('aria-roledescription'); slide.removeAttribute('aria-label'); }); if (this.track) { this.track.style.transform = ''; this.track.style.transition = ''; } this.container.removeAttribute('role'); this.container.removeAttribute('aria-roledescription'); this.container.removeAttribute('aria-label'); } } window.NativeSlider = NativeSlider; // ============================================================ // Countdown Timer Manager // ============================================================ function pad2(value) { return value < 10 ? '0' + value : '' + value; } var CountdownManager = { intervalId: null, init: function () { if (this.intervalId) return; // Si no queda ningún countdown vivo, ni siquiera arrancamos el intervalo. if (this.tick() === 0) return; var self = this; this.intervalId = setInterval(function () { self.tick(); }, 1000); }, // Devuelve cuántos countdowns siguen corriendo. Cuando llega a 0 se apaga // solo: el top banner está en TODAS las páginas y no tiene sentido escanear // el DOM cada segundo para siempre. tick: function () { var nowTs = Math.floor(Date.now() / 1000); var self = this; var alive = 0; // Actualiza TODOS los countdowns: slides reales y clones document.querySelectorAll('.banner-countdown').forEach(function (el) { var targetTs = parseInt(el.dataset.countdownTs, 10); if (!targetTs) return; var diffSeconds = targetTs - nowTs; if (diffSeconds <= 0) { el.style.display = 'none'; return; } var textEl = el.querySelector('.countdown-text'); if (!textEl) return; self.render(textEl, diffSeconds, el.dataset.countdownFormat); alive++; }); if (alive === 0) this.stop(); return alive; }, // format viene del metafield (countdown_format). Solo 'hours' cambia el // comportamiento: acumula las horas y nunca muestra días (76:15:22). // Cualquier otro valor —incluido ausente o vacío— conserva el formato original. render: function (element, diffSeconds, format) { var asTotalHours = format === 'hours'; var days = asTotalHours ? 0 : Math.floor(diffSeconds / 86400); var hours = asTotalHours ? Math.floor(diffSeconds / 3600) : Math.floor((diffSeconds % 86400) / 3600); var minutes = Math.floor((diffSeconds % 3600) / 60); var seconds = diffSeconds % 60; var clock = pad2(hours) + ':' + pad2(minutes) + ':' + pad2(seconds); element.textContent = days > 0 ? days + 'd ' + clock : clock; }, stop: function () { if (!this.intervalId) return; clearInterval(this.intervalId); this.intervalId = null; } }; // ============================================================ // Top Banner — Slider initialization // ============================================================ function initTopBannerSliders() { document.querySelectorAll('.top-banner-slider').forEach(function (container) { var delay = parseInt(container.dataset.delay) || 5000; var showArrows = container.dataset.arrows === 'true'; var showDots = container.dataset.dots === 'true'; var config = { autoplay: container.dataset.delay !== 'false', delay: delay, loop: true, pagination: showDots, navigation: showArrows, pauseOnHover: true }; var slider = new NativeSlider(container, config); // Limpieza en el editor de Shopify document.addEventListener('shopify:section:unload', function (event) { if (event.target.contains(container)) { slider.destroy(); } }); }); } initTopBannerSliders(); // Después de initTopBannerSliders para que los clones del slider ya existan // en el DOM y su countdown también se pinte desde el primer tick. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { CountdownManager.init(); }); } else { CountdownManager.init(); } })();