/* 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.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); if (this.options.autoplay) { this.startAutoplay(); } this._setupShopifyUnload(); } _setupSingleSlide() { if (this.track) { this.track.style.transform = 'none'; } 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) { // Phase 3: Eliminar reflujo forzado - usar getComputedStyle en lugar de offsetHeight var computed = window.getComputedStyle(this.track); if (computed.display) { /* solo lectura, no forzar reflow */ } } } _updateActiveState() { var self = this; 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.isAnimating = true; this._setPosition(true); this._updateActiveState(); } next() { if (this.isAnimating || this.realSlideCount <= 1) return; this.isAnimating = true; this.currentIndex++; this._setPosition(true); } prev() { if (this.isAnimating || this.realSlideCount <= 1) return; this.isAnimating = true; this.currentIndex--; this._setPosition(true); } 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; // ============================================================ // Banner Home — Parallax Effect + Slider Init (optimizado) // ============================================================ // Keep editor discovery available even before the first parallax section exists. { // Phase 3: Cache para getComputedStyle - evitar lectura repetida var overflowCache = new Map(); var parallaxStyles = new Map(); var stickySections = new Set(); var unloadedContainers = new WeakSet(); function writeParallaxStyle(element, property, value) { var current = element.style.getPropertyValue(property); var priority = element.style.getPropertyPriority(property); if (current === value && !priority) return; if (!parallaxStyles.has(element)) parallaxStyles.set(element, new Map()); var properties = parallaxStyles.get(element); if (!properties.has(property)) properties.set(property, { value: current, priority: priority }); element.style.setProperty(property, value); var entry = properties.get(property); entry.written = element.style.getPropertyValue(property); entry.writtenPriority = element.style.getPropertyPriority(property); } function restoreParallax() { parallaxStyles.forEach(function (properties, element) { properties.forEach(function (entry, property) { if (element.style.getPropertyValue(property) !== entry.written || element.style.getPropertyPriority(property) !== entry.writtenPriority) return; if (entry.value) element.style.setProperty(property, entry.value, entry.priority); else element.style.removeProperty(property); }); }); parallaxStyles.clear(); stickySections.forEach(function (section) { section.classList.remove('banner-home-sticky-section'); }); stickySections.clear(); overflowCache.clear(); } // Phase 3: Debounce para resize var resizeTimeout; function debouncedResizeHandler() { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(function() { initParallax(); }, 250); } function hasHeightConstraint(element) { if (element.computedStyleMap) { var height = element.computedStyleMap().get('height').toString(); if (height === 'auto') return false; if (Number.isFinite(parseFloat(height)) && !height.includes('%')) return true; } // Without computed sizing values, test expansion synchronously and leave no styles behind. var heightBefore = element.clientHeight; var probe = document.createElement('div'); probe.style.cssText = 'display:block!important;position:static!important;box-sizing:content-box!important;height:' + (heightBefore + 1) + 'px!important;min-height:' + (heightBefore + 1) + 'px!important;flex-shrink:0!important;margin:0!important;padding:0!important;border:0!important;'; try { element.appendChild(probe); return element.clientHeight <= heightBefore; } finally { probe.remove(); } } function initParallax() { // Remove our overrides before reading the current breakpoint's theme styles. restoreParallax(); var activeCount = 0; document.querySelectorAll('.banner-home-slider[data-parallax="true"]').forEach(function (container) { if (unloadedContainers.has(container)) return; var section = container.closest('.shopify-section'); if (!section) return; activeCount++; if (!section.classList.contains('banner-home-sticky-section')) { stickySections.add(section); section.classList.add('banner-home-sticky-section'); } var parent = section.parentElement; while (parent && parent !== document.body) { if (!overflowCache.has(parent)) { var style = window.getComputedStyle(parent); overflowCache.set(parent, { overflow: style.overflow, overflowX: style.overflowX, overflowY: style.overflowY, maxHeight: style.maxHeight }); } var cached = overflowCache.get(parent); // Equal viewport geometry alone also matches expandable, auto-height wrappers. var constrained = parent.clientHeight > 0 && ( (Math.abs(parent.clientHeight - window.innerHeight) <= 1 && hasHeightConstraint(parent)) || (cached.maxHeight !== 'none' && Number.isFinite(parseFloat(cached.maxHeight))) ); var scrolling = /^(auto|scroll|overlay)$/.test(cached.overflowY); var locked = cached.overflowY === 'hidden' && constrained && window.getComputedStyle(document.documentElement).overflowY === 'hidden'; if (locked || (scrolling && (constrained || cached.overflowY === 'scroll' || parent.scrollHeight > parent.clientHeight))) { break; } if (cached.overflowX !== 'visible') writeParallaxStyle(parent, 'overflow-x', 'visible'); if (cached.overflowY !== 'visible') writeParallaxStyle(parent, 'overflow-y', 'visible'); parent = parent.parentElement; } // Phase 3: Elevar secciones siguientes - cache getComputedStyle var nextSection = section.nextElementSibling; while (nextSection) { if (nextSection.classList.contains('shopify-section')) { if (!overflowCache.has(nextSection)) { var nextStyle = window.getComputedStyle(nextSection); overflowCache.set(nextSection, { zIndex: nextStyle.zIndex, backgroundColor: nextStyle.backgroundColor }); } var cachedNext = overflowCache.get(nextSection); var currentZ = parseInt(cachedNext.zIndex); var newZ = isNaN(currentZ) ? 1 : currentZ + 1; writeParallaxStyle(nextSection, 'position', 'relative'); writeParallaxStyle(nextSection, 'z-index', newZ.toString()); if (cachedNext.backgroundColor === 'rgba(0, 0, 0, 0)' || cachedNext.backgroundColor === 'transparent') { writeParallaxStyle(nextSection, 'background-color', 'var(--color-background, #ffffff)'); } } nextSection = nextSection.nextElementSibling; } }); if (activeCount) window.addEventListener('resize', debouncedResizeHandler); else window.removeEventListener('resize', debouncedResizeHandler); } // Apply synchronously when present; editor discovery below is unconditional. if (document.querySelector('.banner-home-slider[data-parallax="true"]')) initParallax(); document.addEventListener('shopify:section:unload', function (event) { document.querySelectorAll('.banner-home-slider[data-parallax="true"]').forEach(function (container) { if (event.target.contains(container)) unloadedContainers.add(container); }); clearTimeout(resizeTimeout); initParallax(); }); document.addEventListener('shopify:section:load', function (event) { document.querySelectorAll('.banner-home-slider[data-parallax="true"]').forEach(function (container) { if (event.target.contains(container)) unloadedContainers.delete(container); }); initParallax(); }); } // Phase 3: Inicializar slider (más crítico, ejecutar primero) function initSlider() { document.querySelectorAll('.banner-home-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); // Shopify editor cleanup document.addEventListener('shopify:section:unload', function (event) { if (event.target.contains(container)) { slider.destroy(); } }); }); } // Slider initialization is independent of parallax discovery. initSlider(); })();