1/** 2 * Exchange Partner Mobile - Scroll Animation & Load More 3 * Titolo si blocca a sinistra, items scorrono a destra come dipartimenti 4 */ 5(function() { 6 'use strict'; 7 8 let scrollTriggerInstance = null; 9 10 function calculateScrollDuration(numCards) { 11 // Calcola la durata dello scroll in base al numero di card 12 // Più card = scroll più lungo per farle passare tutte sopra il titolo fisso 13 const baseEnd = 200; // Durata base per le prime 3 card 14 const additionalPerCard = 40; // +40% per ogni card aggiuntiva (per garantire visibilità ) 15 16 if (numCards <= 3) { 17 return baseEnd; 18 } 19 20 // Per ogni card oltre le prime 3, aggiungi scroll extra 21 // Es: 6 card = 200 + (3 * 40) = 320% 22 // Es: 12 card = 200 + (9 * 40) = 560% 23 return baseEnd + ((numCards - 3) * additionalPerCard); 24 } 25 26 function initAnimation() { 27 const partnerBlock = document.querySelector('.exchange-partner-mobile'); 28 29 if (!partnerBlock) return; 30 31 const container = partnerBlock.querySelector('.exchange-partner-mobile__container'); 32 const titleWrapper = partnerBlock.querySelector('.exchange-partner-mobile__title-wrapper'); 33 const itemsWrapper = partnerBlock.querySelector('.exchange-partner-mobile__items');
34 const loadMoreWrapper = partnerBlock.querySelector('.exchange-partner-mobile__load-more'); 35 36 if (!titleWrapper || !itemsWrapper) return; 37 38 // Registra ScrollTrigger plugin 39 gsap.registerPlugin(ScrollTrigger); 40 41 const viewportHeight = window.innerHeight; 42 43 // Container resta sempre 100vh (fisso) 44 // Solo lo scroll duration cambia in base al numero di card 45 46 // Pulisci animazione precedente se esiste 47 if (scrollTriggerInstance) { 48 scrollTriggerInstance.kill(); 49 } 50 51 // === STATO INIZIALE === 52 // Titolo: posizionato direttamente nel contenitore (nessuna animazione di ingresso) 53 // Non applichiamo transform Y per mantenere il centraggio verticale del CSS 54 gsap.set(titleWrapper, { 55 opacity: 1, 56 clearProps: "transform" // Rimuove eventuali transform precedenti, lascia che il CSS centri 57 }); 58 59 // Items wrapper parte dal basso 60 gsap.set(itemsWrapper, { 61 y: viewportHeight, 62 opacity: 1 63 }); 64 65 // === ANIMAZIONE CON PIN (titolo centrato FISSO, items scrollano sopra) === 66 // Calcola dinamicamente l'end in base al numero di card 67 const cards = itemsWrapper.querySelectorAll('.partner-card-mobile'); 68 const dynamicEnd = calculateScrollDuration(cards.length); 69 70 //console.log('Inizializzazione con', cards.length, 'partner, scroll duration:', dynamicEnd + '%'); 71 72 const mainTimeline = gsap.timeline({ 73 scrollTrigger: { 74 trigger: partnerBlock, 75 start: 'top top', 76 end: `+=${dynamicEnd}%`, // End dinamico: più card = scroll più lungo 77 pin: true, 78 scrub: 1, 79 anticipatePin: 1, 80 invalidateOnRefresh: true, 81 // markers: true, // Debug 82 onLeave: () => { 83 if (container) container.classList.add('animation-complete'); 84 }, 85 onEnterBack: () => { 86 if (container) container.classList.remove('animation-complete'); 87 } 88 } 89 }); 90 91 // Salva per cleanup 92 scrollTriggerInstance = mainTimeline.scrollTrigger; 93 94 // FASE 1: Titolo rimane fisso e centrato (non animiamo, il CSS lo centra) 95 // Non aggiungiamo animazione per mantenere il centraggio verticale del CSS 96 97 // FASE 2: Pausa minima (0-5% timeline) 98 mainTimeline.to({}, { duration: 0.05 }); 99 100 // FASE 3: Items salgono dal basso e vanno sopra il titolo (5-100% timeline) - Inizia subito 101 // Calcola dinamicamente quanto devono salire per mostrare tutte le card 102 const itemsTargetY = 20; 103 104 // Rileva se siamo su tablet per usare l'altezza corretta delle card 105 const isTablet = window.innerWidth >= 1000 && window.innerWidth <= 1499; 106 const cardHeight = isTablet ? 250 : 200; // 250px per tablet, 200px per mobile 107 108 // Calcola l'altezza totale del contenuto items 109 const gap = 10;
110 const loadMoreBtn = itemsWrapper.querySelector('.exchange-partner-mobile__load-more'); 111 const buttonHeight = loadMoreBtn ? 70 : 0; 112 const safetyMargin = 100; // Margine di sicurezza 113 const totalItemsHeight = (cards.length * cardHeight) + ((cards.length - 1) * gap) + buttonHeight + safetyMargin; 114 115 // Items devono salire per mostrare tutto 116 // L'ultima card + bottone devono essere visibili alla fine dello scroll 117 const itemsTravel = totalItemsHeight - viewportHeight + itemsTargetY; 118 119 //console.log('Items travel:', itemsTravel, 'px per', cards.length, 'card (total height:', totalItemsHeight, 'px)'); 120 121 mainTimeline.to(itemsWrapper, { 122 y: -itemsTravel, 123 duration: 0.95, 124 ease: 'none', 125 }, 0.05); 126 127 //console.log('Exchange Partner Mobile animation initialized'); 128 } 129 130 // === LOAD MORE FUNCTIONALITY === 131 function handleLoadMore() { 132 const partnerBlock = document.querySelector('.exchange-partner-mobile');
133 const loadMoreBtn = partnerBlock.querySelector('.exchange-partner-load-more-btn'); 134 const itemsWrapper = partnerBlock.querySelector('.exchange-partner-mobile__items'); 135 const loadMoreWrapper = partnerBlock.querySelector('.exchange-partner-mobile__load-more'); 136 137 if (!loadMoreBtn) return; 138 139 loadMoreBtn.addEventListener('click', function() { 140 // Recupera i partner rimanenti dai data attributes 141 const remainingPartnersJson = partnerBlock.dataset.remainingPartners; 142 143 if (!remainingPartnersJson) return; 144 145 let remainingPartners = JSON.parse(remainingPartnersJson); 146 147 if (remainingPartners.length === 0) return; 148 149 // Prendi i prossimi 6 150 const partnersPerPage = parseInt(partnerBlock.dataset.partnersPerPage) || 6; 151 const nextBatch = remainingPartners.slice(0, partnersPerPage); 152 remainingPartners = remainingPartners.slice(partnersPerPage); 153 154 // Disabilita bottone durante caricamento 155 loadMoreBtn.disabled = true; 156 loadMoreBtn.textContent = 'Caricamento...'; 157 158 // Aggiungi le nuove card prima del bottone 159 const currentIndex = itemsWrapper.querySelectorAll('.partner-card-mobile').length; 160 161 nextBatch.forEach((partner, index) => { 162 const card = document.createElement('div'); 163 card.className = 'partner-card-mobile'; 164 card.dataset.index = currentIndex + index; 165 card.innerHTML = ` 166 <img src="${partner.thumbnail}" 167 alt="${partner.title}" 168 class="partner-card-mobile__image"> 169 `; 170 // Inserisci prima del wrapper del bottone 171 itemsWrapper.insertBefore(card, loadMoreWrapper); 172 }); 173 174 // Aggiorna i dati rimanenti 175 partnerBlock.dataset.remainingPartners = JSON.stringify(remainingPartners); 176 177 // Se non ci sono più partner, nascondi il bottone 178 if (remainingPartners.length === 0) { 179 if (loadMoreWrapper) loadMoreWrapper.style.display = 'none'; 180 } else { 181 loadMoreBtn.disabled = false; 182 loadMoreBtn.textContent = 'Visualizza Partner'; 183 } 184 185 // Aspetta che le card siano nel DOM 186 setTimeout(() => { 187 // Container resta 100vh (fisso) 188 // Aggiorna solo la durata dello scroll (end) 189 const totalCards = itemsWrapper.querySelectorAll('.partner-card-mobile').length; 190 const newScrollDuration = calculateScrollDuration(totalCards); 191 192 //console.log('Caricati', nextBatch.length, 'partner. Totale:', totalCards, 'Nuova scroll duration:', newScrollDuration + '%'); 193 194 // Kill e ricrea ScrollTrigger con nuovo end 195 // Ma mantieni la posizione corrente degli elementi 196 if (scrollTriggerInstance) { 197 const container = partnerBlock.querySelector('.exchange-partner-mobile__container'); 198 const titleWrapper = partnerBlock.querySelector('.exchange-partner-mobile__title-wrapper'); 199 const itemsWrapper = partnerBlock.querySelector('.exchange-partner-mobile__items'); 200 201 // Salva le posizioni correnti 202 const titleCurrentY = gsap.getProperty(titleWrapper, 'y'); 203 const itemsCurrentY = gsap.getProperty(itemsWrapper, 'y'); 204 205 //console.log('Posizioni correnti - Title Y:', titleCurrentY, 'Items Y:', itemsCurrentY); 206 207 // Kill il vecchio ScrollTrigger 208 scrollTriggerInstance.kill(); 209 210 // Mantieni le posizioni correnti (non resettare) 211 // Per il titolo, non applichiamo transform Y per mantenere il centraggio CSS 212 gsap.set(titleWrapper, { clearProps: "transform" }); 213 gsap.set(itemsWrapper, { y: itemsCurrentY }); 214 215 // Crea nuovo ScrollTrigger con end aggiornato
216 const viewportHeight = window.innerHeight; 217 const itemsTargetY = 20; 218 219 // Calcola dinamicamente l'altezza totale del contenuto 220 const allCards = itemsWrapper.querySelectorAll('.partner-card-mobile'); 221 // Rileva se siamo su tablet per usare l'altezza corretta delle card 222 const isTablet = window.innerWidth >= 1000 && window.innerWidth <= 1499; 223 const cardHeight = isTablet ? 250 : 200; // 250px per tablet, 200px per mobile 224 const gap = 10; 225 const loadMoreBtn = itemsWrapper.querySelector('.exchange-partner-mobile__load-more'); 226 const buttonHeight = loadMoreBtn && loadMoreBtn.offsetParent !== null ? 70 : 0; 227 const safetyMargin = 100; // Margine di sicurezza 228 const totalItemsHeight = (allCards.length * cardHeight) + ((allCards.length - 1) * gap) + buttonHeight + safetyMargin; 229 230 // Items devono salire per mostrare tutto 231 const itemsTravel = totalItemsHeight - viewportHeight + itemsTargetY; 232 233 //console.log('Ricreato - Items travel:', itemsTravel, 'px per', allCards.length, 'card (total height:', totalItemsHeight, 'px)'); 234 235 const mainTimeline = gsap.timeline({ 236 scrollTrigger: { 237 trigger: partnerBlock, 238 start: 'top top', 239 end: `+=${newScrollDuration}%`, 240 pin: true, 241 scrub: 1, 242 anticipatePin: 1, 243 invalidateOnRefresh: true, 244 onLeave: () => { 245 if (container) container.classList.add('animation-complete'); 246 }, 247 onEnterBack: () => { 248 if (container) container.classList.remove('animation-complete'); 249 } 250 } 251 }); 252 253 // Timeline: titolo rimane fisso e centrato (non animiamo) 254 mainTimeline.to({}, { duration: 0.05 }); 255 256 mainTimeline.to(itemsWrapper, { 257 y: -itemsTravel, 258 duration: 0.95, 259 ease: 'none', 260 }, 0.05); 261 262 // Salva nuovo instance 263 scrollTriggerInstance = mainTimeline.scrollTrigger; 264 265 //console.log('ScrollTrigger ricreato con end:', newScrollDuration + '%'); 266 } 267 }, 50); 268 269 //console.log('Rimanenti:', remainingPartners.length, 'partner'); 270 }); 271 } 272 273 // Inizializza 274 document.addEventListener('DOMContentLoaded', () => { 275 const partnerBlock = document.querySelector('.exchange-partner-mobile'); 276 277 if (!partnerBlock) return; 278 279 // Verifica che l'elemento sia visibile (non nascosto da display:none) 280 const isVisible = window.getComputedStyle(partnerBlock).display !== 'none'; 281 if (!isVisible) { 282 // Se non è visibile, aspetta che diventi visibile (per template responsive dinamici) 283 const checkVisibility = () => { 284 const checkVisible = window.getComputedStyle(partnerBlock).display !== 'none'; 285 if (checkVisible) { 286 initAnimation(); 287 handleLoadMore(); 288 } else { 289 setTimeout(checkVisibility, 100); 290 } 291 }; 292 checkVisibility(); 293 return; 294 } 295 296 initAnimation(); 297 handleLoadMore(); 298 }); 299 300})(); 301
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.