1(function() { 2 'use strict'; 3 4 // Prevent execution if config is missing 5 if (!window.ExtendableAnimations) { 6 return; 7 } 8 9 const config = window.ExtendableAnimations || { map: {}, defaults: {} }; 10 const staggerDelay = parseFloat(config.defaults && config.defaults.stagger) || 0.08; 11 const maxStagger = 0.6; 12 let observer = null; 13 let trackedElements = []; 14 15 const speeds = Object.freeze({ 16 slow: 1, 17 medium: 0.6, 18 fast: 0.45 19 }); 20 const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); 21 let currentSpeed = config.speed || 'medium'; 22 let currentType = config.type || 'none'; 23 let currentMap = config.map || {}; 24 const initialType = config.type || 'none'; // Track if PHP added FOUC CSS 25 26 function initAnimations() { 27 if (currentType === 'none' || prefersReducedMotion.matches) { 28 return; 29 } 30 31 if (!('IntersectionObserver' in window)) { 32 Object.keys(currentMap).forEach(selector => { 33 try { 34 document.querySelectorAll(selector).forEach(el => el.style.opacity = '1'); 35 } catch (e) {} 36 }); 37 return; 38 } 39 40 if (observer) { 41 observer.disconnect(); 42 } 43 44 observer = new IntersectionObserver((entries) => { 45 entries.forEach(entry => { 46 if (entry.isIntersecting) { 47 const element = entry.target; 48 const animationType = element.dataset.extAnimate; 49 50 if (animationType) { 51 element.classList.add(`ext-animated-${animationType}`); 52 } 53 } 54 }); 55 }, { 56 threshold: 0.01, 57 rootMargin: '0px' 58 }); 59 60 trackedElements = []; 61 62 const pending = []; 63 Object.keys(currentMap).forEach(selector => { 64 try { 65 const elements = document.querySelectorAll(selector); 66 const animationType = currentMap[selector]; 67 const elementsByParent = new Map(); 68 elements.forEach(element => { 69 if (element.classList.contains('ext-animate--off')) { 70 return; 71 } 72 const parent = element.parentElement; 73 if (!elementsByParent.has(parent)) { 74 elementsByParent.set(parent, []); 75 } 76 elementsByParent.get(parent).push(element); 77 }); 78 79 elementsByParent.forEach(siblingElements => { 80 siblingElements.forEach((element, index) => { 81 const rect = element.getBoundingClientRect(); 82 pending.push({ 83 element, 84 animationType, 85 delay: Math.min(index * staggerDelay, maxStagger), 86 // Elements already in viewport (like headers) - reveal immediately 87 inViewport: rect.top < window.innerHeight && rect.bottom > 0, 88 }); 89 }); 90 }); 91 } catch (e) {} 92 }); 93 94 // Batch all writes into one frame so layout happens once, not once per element. 95 const duration = speeds[currentSpeed]; 96 requestAnimationFrame(() => { 97 pending.forEach(({ element, animationType, delay, inViewport }) => { 98 element.classList.add('ext-animate'); 99 element.dataset.extAnimate = animationType; 100 element.style.animationDuration = `${duration}s`; 101 element.style.animationDelay = `${delay}s`; 102 103 // Clear stacking context after animation completes 104 element.addEventListener('animationend', () => { 105 element.dataset.extAnimated = 'true'; 106 }, { once: true }); 107 108 if (inViewport) { 109 element.classList.add(`ext-animated-${animationType}`); 110 } 111 112 observer.observe(element); 113 trackedElements.push(element); 114 }); 115 }); 116 } 117 118 function resetAnimations() { 119 // Early return for reduced motion - nothing to reset 120 if (prefersReducedMotion.matches) { 121 return true; 122 } 123
124 trackedElements.forEach(element => { 125 element.classList.remove( 126 'ext-animated-fade', 127 'ext-animated-fade-up', 128 'ext-animated-fade-down', 129 'ext-animated-fade-left', 130 'ext-animated-fade-right', 131 'ext-animated-zoom-in' 132 ); 133 delete element.dataset.extAnimated; 134 }); 135 136 requestAnimationFrame(() => { 137 const visible = trackedElements.map(element => { 138 const rect = element.getBoundingClientRect(); 139 return rect.top < window.innerHeight && rect.bottom > 0; 140 }); 141 142 trackedElements.forEach((element, index) => { 143 if (!visible[index]) { 144 return; 145 } 146 const animationType = element.dataset.extAnimate; 147 if (animationType) { 148 element.classList.add(`ext-animated-${animationType}`); 149 } 150 }); 151 }); 152 153 return true; 154 } 155 156 function setSpeed(speed) { 157 // Early return for reduced motion - speed changes are irrelevant 158 if (prefersReducedMotion.matches) { 159 return true; 160 } 161 162 if (!speeds[speed]) { 163 speed = 'medium'; 164 } 165 currentSpeed = speed; 166 167 trackedElements.forEach(element => { 168 element.style.animationDuration = `${speeds[currentSpeed]}s`; 169 }); 170 171 return true; 172 } 173 174 function setType(type) { 175 currentType = type || 'none'; 176 177 if (initialType === 'none') { 178 const existingStyle = document.getElementById('ext-fouc-styles'); 179 if (existingStyle) { 180 existingStyle.remove(); 181 } 182 } 183 184 trackedElements.forEach(element => { 185 element.classList.remove( 186 'ext-animate', 187 'ext-animated-fade', 188 'ext-animated-fade-up', 189 'ext-animated-fade-down', 190 'ext-animated-fade-left', 191 'ext-animated-fade-right', 192 'ext-animated-zoom-in' 193 ); 194 delete element.dataset.extAnimated; 195 element.style.opacity = ''; 196 element.style.transform = ''; 197 element.style.animationDuration = ''; 198 element.style.animationDelay = ''; 199 }); 200 201 trackedElements = []; 202 203 if (observer) { 204 observer.disconnect(); 205 } 206 207 if (currentType === 'none') { 208 // Force elements visible (override PHP FOUC CSS) 209 Object.keys(config.allTypes?.['fade'] || currentMap || {}).forEach(selector => { 210 try { 211 document.querySelectorAll(selector).forEach(el => { 212 el.style.opacity = '1'; 213 el.style.transform = 'none'; 214 }); 215 } catch (e) {} 216 }); 217 return true; 218 } 219 220 if (config.allTypes && config.allTypes[currentType]) { 221 currentMap = config.allTypes[currentType]; 222 } 223 224 Object.keys(currentMap).forEach(selector => { 225 try { 226 document.querySelectorAll(selector).forEach(el => { 227 el.style.opacity = ''; 228 }); 229 } catch (e) {} 230 }); 231 232 // Only inject FOUC CSS if PHP didn't add it 233 if (initialType === 'none') { 234 const styleEl = document.createElement('style'); 235 styleEl.id = 'ext-fouc-styles'; 236 let css = ''; 237 Object.keys(currentMap).forEach(selector => { 238 css += `@media (prefers-reduced-motion: no-preference) { ${selector}:not(.ext-animate--off) { opacity: 0; } } `; 239 }); 240 styleEl.textContent = css; 241 document.head.appendChild(styleEl); 242 } 243 244 requestAnimationFrame(() => { 245 initAnimations(); 246 }); 247 248 return true; 249 } 250 251 window.ExtendableAnimations = window.ExtendableAnimations || {}; 252 window.ExtendableAnimations.reset = resetAnimations; 253 window.ExtendableAnimations.setSpeed = setSpeed; 254 window.ExtendableAnimations.setType = setType; 255 256 if (document.readyState === 'loading') { 257 document.addEventListener('DOMContentLoaded', initAnimations); 258 } else { 259 initAnimations(); 260 } 261})();
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.