1/** 2 * Layout Stability Optimization 3 * Prevents Cumulative Layout Shift (CLS) by managing dynamic content loading 4 * and font loading optimization 5 */ 6 7(function () { 8 'use strict'; 9 10 // Font loading optimization 11 function optimizeFontLoading() { 12 // Add font loading class to body 13 document.body.classList.add('font-loading'); 14 15 // Check if fonts are loaded 16 if (document.fonts && document.fonts.ready) { 17 document.fonts.ready.then(function () { 18 document.body.classList.remove('font-loading'); 19 document.body.classList.add('font-loaded'); 20 }); 21 } else { 22 // Fallback for older browsers 23 setTimeout(function () { 24 document.body.classList.remove('font-loading'); 25 document.body.classList.add('font-loaded'); 26 }, 1000); 27 } 28 } 29 30 // Image loading optimization 31 function optimizeImageLoading() { 32 // Only add loading="lazy" to images that don't have it and are below the fold 33 const images = document.querySelectorAll('img:not([loading])'); 34 images.forEach(function (img) { 35 // Only add lazy loading to images below the fold 36 if (img.getBoundingClientRect().top > window.innerHeight) { 37 img.setAttribute('loading', 'lazy'); 38 } 39 }); 40 41 // Handle image load events to prevent layout shifts 42 const allImages = document.querySelectorAll('img'); 43 allImages.forEach(function (img) { 44 // Don't interfere with images that already have loading states 45 if ( 46 img.classList.contains('loaded') || 47 img.classList.contains('loading') 48 ) { 49 return; 50 } 51 52 if (img.complete) { 53 img.classList.add('loaded'); 54 } else { 55 // Add loading class for images that are still loading 56 img.classList.add('loading'); 57 58 img.addEventListener('load', function () { 59 this.classList.remove('loading'); 60 this.classList.add('loaded'); 61 }); 62 63 img.addEventListener('error', function () { 64 this.classList.remove('loading'); 65 this.classList.add('error'); 66 }); 67 } 68 }); 69 } 70 71 // Banner gallery optimization 72 function optimizeBannerGallery() { 73 const galleries = document.querySelectorAll( 74 '.banner-gallery, .gallery-container' 75 ); 76 77 galleries.forEach(function (gallery) { 78 // Only add loading state if gallery doesn't already have content 79 if ( 80 gallery.children.length === 0 || 81 gallery.classList.contains('gallery-loading') 82 ) { 83 gallery.classList.add('gallery-loading'); 84 85 // Remove loading state when content is actually loaded 86 // This should be replaced with actual loading logic from the gallery component 87 const observer = new MutationObserver(function (mutations) { 88 mutations.forEach(function (mutation) { 89 if ( 90 mutation.type === 'childList' && 91 mutation.addedNodes.length > 0 92 ) { 93 gallery.classList.remove('gallery-loading'); 94 gallery.classList.add('gallery-loaded'); 95 observer.disconnect(); 96 } 97 }); 98 }); 99 100 observer.observe(gallery, { childList: true }); 101 102 // Fallback timeout in case content doesn't load 103 setTimeout(function () { 104 if (gallery.classList.contains('gallery-loading')) { 105 gallery.classList.remove('gallery-loading'); 106 gallery.classList.add('gallery-loaded'); 107 observer.disconnect(); 108 } 109 }, 2000); 110 } 111 }); 112 } 113 114 // Dynamic content loading optimization 115 function optimizeDynamicContent() { 116 // Handle dynamic lists 117 const dynamicLists = document.querySelectorAll('.dynamic-list');
118 dynamicLists.forEach(function (list) { 119 list.style.minHeight = '200px'; 120 }); 121 122 // Handle dynamic grids 123 const dynamicGrids = document.querySelectorAll('.stable-grid'); 124 dynamicGrids.forEach(function (grid) { 125 const items = grid.querySelectorAll('.grid-item'); 126 items.forEach(function (item) { 127 item.style.minHeight = '200px'; 128 }); 129 }); 130 } 131 132 // Prevent layout shifts from external content 133 function optimizeExternalContent() { 134 // Handle ads and external embeds 135 const adContainers = document.querySelectorAll( 136 '.ad-container, .ad-placeholder' 137 ); 138 adContainers.forEach(function (container) { 139 container.style.minHeight = '250px'; 140 }); 141 142 // Handle iframes 143 const iframes = document.querySelectorAll('iframe'); 144 iframes.forEach(function (iframe) { 145 if (!iframe.style.height) { 146 iframe.style.minHeight = '200px'; 147 } 148 }); 149 } 150 151 // Mobile-specific optimizations 152 function optimizeMobile() { 153 if (window.innerWidth <= 768) { 154 // Prevent horizontal scroll only if not already set 155 if (!document.body.style.overflowX) { 156 document.body.style.overflowX = 'hidden'; 157 } 158 159 // Optimize touch targets only for elements that need it 160 const touchTargets = document.querySelectorAll( 161 'button:not(.skip-link), .btn:not(.skip-link), .cta-button:not(.skip-link), a:not(.skip-link)' 162 ); 163 touchTargets.forEach(function (target) { 164 // Don't modify elements that already have proper sizing 165 if (target.style.minHeight || target.style.minWidth) { 166 return; 167 } 168 169 const rect = target.getBoundingClientRect(); 170 if (rect.height < 44 || rect.width < 44) { 171 // Use CSS classes instead of inline styles to avoid specificity issues 172 target.classList.add('touch-optimized'); 173 } 174 }); 175 } 176 } 177 178 // Container query polyfill for older browsers 179 function initContainerQueries() { 180 if (!CSS.supports('container-type: inline-size')) { 181 // Add fallback styles for browsers without container query support 182 const containers = document.querySelectorAll( 183 '.responsive-container' 184 ); 185 containers.forEach(function (container) { 186 if (container.offsetWidth >= 768) { 187 container.classList.add('container-wide'); 188 } else { 189 container.classList.add('container-narrow'); 190 } 191 }); 192 } 193 } 194 195 // Resize observer for responsive behavior 196 function initResizeObserver() { 197 if (window.ResizeObserver) { 198 const resizeObserver = new ResizeObserver(function (entries) { 199 entries.forEach(function (entry) { 200 const element = entry.target; 201 if (element.classList.contains('responsive-container')) { 202 if (entry.contentRect.width >= 768) { 203 element.classList.add('container-wide'); 204 element.classList.remove('container-narrow'); 205 } else { 206 element.classList.add('container-narrow'); 207 element.classList.remove('container-wide'); 208 } 209 } 210 }); 211 }); 212 213 // Observe responsive containers 214 const containers = document.querySelectorAll( 215 '.responsive-container' 216 ); 217 containers.forEach(function (container) { 218 resizeObserver.observe(container); 219 }); 220 } 221 } 222 223 // Initialize all optimizations 224 function init() { 225 // Only run optimizations if they haven't been applied already 226 if (!document.body.classList.contains('layout-stability-initialized')) { 227 optimizeFontLoading(); 228 optimizeImageLoading(); 229 optimizeBannerGallery(); 230 optimizeDynamicContent();
231 optimizeExternalContent(); 232 optimizeMobile(); 233 initContainerQueries(); 234 initResizeObserver(); 235 236 // Mark as initialized to prevent duplicate runs 237 document.body.classList.add('layout-stability-initialized'); 238 } 239 } 240 241 // Run on DOM ready 242 if (document.readyState === 'loading') { 243 document.addEventListener('DOMContentLoaded', init); 244 } else { 245 init(); 246 } 247 248 // Re-run on window resize 249 let resizeTimeout; 250 window.addEventListener('resize', function () { 251 clearTimeout(resizeTimeout); 252 resizeTimeout = setTimeout(function () { 253 optimizeMobile(); 254 }, 250); 255 }); 256 257 // Expose functions for external use 258 window.LayoutStability = { 259 optimizeFontLoading: optimizeFontLoading, 260 optimizeImageLoading: optimizeImageLoading, 261 optimizeBannerGallery: optimizeBannerGallery, 262 optimizeDynamicContent: optimizeDynamicContent, 263 optimizeExternalContent: optimizeExternalContent, 264 optimizeMobile: optimizeMobile 265 }; 266})();
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.