1/** 2 * Slideshow Height Fix v2.2 (Production) 3 * 4 * Handles both image and text slides for jQuery Cycle2 slideshows. 5 * 6 * Strategy: 7 * - IMAGE slides (pure image, no text): Loads the background image, 8 * derives aspect ratio from natural dimensions, scales proportionally. 9 * - CONTENT slides (text, or mixed text + image): Uses the server-provided 10 * configured height (from data attributes) to calculate a deterministic 11 * height at any viewport width. Above a reference width, the configured 12 * CSS height is used directly. Below it, the height scales proportionally. 13 * A content measurement safety floor prevents text clipping. 14 * - FALLBACK: Uses a configurable default aspect ratio. 15 * 16 * Deployment: 17 * Place AFTER jquery.cycle2.min-new.js, both with defer: 18 * <script defer src="/scripts/jquery.cycle2.min-new.js"></script> 19 * <script defer src="/js/slideshow-fix-v2.js"></script> 20 * 21 * Data attributes read from the .cycle-slideshow element: 22 * data-slideshow-height Configured height value (e.g. "33", "400") 23 * data-slideshow-height-unit Unit: "vh" or "px" 24 * data-max-height-mobile Optional max height in px for viewport <= 768px 25 */ 26(function () { 27 'use strict'; 28 29 var CONFIG = { 30 slideshowSelector: '.cycle-slideshow', 31 bgIdPrefix: 'rotatorBG-', 32 fallbackAspectRatio: 2.5, 33 minHeight: 120, 34 mobileBreakpoint: 768, 35 referenceWidth: 1200, // Below this width, height scales proportionally 36 resizeDebounceMs: 120, 37 maxInitAttempts: 10, 38 initRetryMs: 300 39 }; 40 41 var instances = []; 42 43 // ââ Helpers ââââââââââââââââââââââââââââââââââââââââââââââ 44 45 function getBackgroundImageUrl(el) { 46 var bg; 47 try { 48 bg = window.getComputedStyle(el).backgroundImage; 49 } catch (e) { 50 return null; 51 } 52 if (!bg || bg === 'none') return null; 53 var match = bg.match(/url\(["']?(.*?)["']?\)/); 54 return match ? match[1] : null; 55 } 56 57 function detectSlideType(slides) { 58 var hasImage = false; 59 var hasContent = false; 60 61 for (var i = 0; i < slides.length; i++) { 62 if (getBackgroundImageUrl(slides[i])) { 63 hasImage = true; 64 } 65 var text = slides[i].textContent.trim(); 66 if (text.length > 0 || slides[i].querySelector('span, p, h1, h2, h3, h4, h5, h6, div:not(.cycle-slide)')) { 67 hasContent = true; 68 } 69 } 70 71 if (hasContent) return 'content'; 72 if (hasImage) return 'image'; 73 return 'fallback'; 74 } 75 76 /** 77 * Computes the configured slideshow height in pixels. 78 * Converts vh to px using current window.innerHeight. 79 * Returns 0 if no configured height data is available. 80 */ 81 function getConfiguredHeightPx(instance) { 82 var val = instance.configuredHeight; 83 var unit = instance.configuredHeightUnit; 84 if (!val || val <= 0) return 0; 85 if (unit === 'vh') return Math.round(window.innerHeight * val / 100); 86 return val; // px or unknown unit â treat as pixels 87 } 88 89 // ââ Measurement âââââââââââââââââââââââââââââââââââââââââ 90 91 /** 92 * Measure the tallest slide's natural content height. 93 * Sets instance.isMeasuring to prevent observer feedback loops. 94 */ 95 function measureContentHeight(instance) { 96 var ss = instance.slideshow; 97 var slides = ss.querySelectorAll('.cycle-slide:not(.cycle-sentinel)'); 98 if (!slides.length) return 0; 99 100 instance.isMeasuring = true; 101 102 var origSSHeight = ss.style.height; 103 var origSSMinHeight = ss.style.minHeight; 104 var origSSMaxHeight = ss.style.maxHeight; 105 var origSSOverflow = ss.style.overflow; 106 107 ss.style.setProperty('height', 'auto', 'important'); 108 ss.style.setProperty('min-height', '0', 'important'); 109 ss.style.setProperty('max-height', 'none', 'important'); 110 ss.style.setProperty('overflow', 'visible', 'important'); 111 112 var maxHeight = 0; 113 var savedStyles = []; 114
115 slides.forEach(function (slide) { 116 savedStyles.push({ 117 height: slide.style.height, 118 minHeight: slide.style.minHeight, 119 maxHeight: slide.style.maxHeight, 120 position: slide.style.position, 121 visibility: slide.style.visibility, 122 display: slide.style.display, 123 opacity: slide.style.opacity, 124 top: slide.style.top, 125 left: slide.style.left, 126 zIndex: slide.style.zIndex, 127 alignItems: slide.style.alignItems 128 }); 129 130 slide.style.setProperty('height', 'auto', 'important'); 131 slide.style.setProperty('min-height', '0', 'important'); 132 slide.style.setProperty('max-height', 'none', 'important'); 133 slide.style.setProperty('position', 'relative', 'important'); 134 slide.style.setProperty('visibility', 'hidden', 'important'); 135 slide.style.setProperty('display', 'flex', 'important'); 136 slide.style.setProperty('opacity', '1', 'important'); 137 slide.style.setProperty('top', 'auto', 'important'); 138 slide.style.setProperty('left', 'auto', 'important'); 139 slide.style.setProperty('z-index', '-1', 'important'); 140 }); 141 142 void ss.offsetHeight; 143 144 slides.forEach(function (slide, idx) { 145 var h = slide.scrollHeight; 146 if (h > maxHeight) maxHeight = h; 147 148 var s = savedStyles[idx]; 149 slide.style.height = s.height; 150 slide.style.minHeight = s.minHeight; 151 slide.style.maxHeight = s.maxHeight; 152 slide.style.position = s.position; 153 slide.style.visibility = s.visibility; 154 slide.style.display = s.display; 155 slide.style.opacity = s.opacity; 156 slide.style.top = s.top; 157 slide.style.left = s.left; 158 slide.style.zIndex = s.zIndex; 159 slide.style.alignItems = s.alignItems; 160 }); 161 162 ss.style.height = origSSHeight; 163 ss.style.minHeight = origSSMinHeight; 164 ss.style.maxHeight = origSSMaxHeight; 165 ss.style.overflow = origSSOverflow; 166 167 instance.isMeasuring = false; 168 169 return maxHeight; 170 } 171 172 // ââ Height Application ââââââââââââââââââââââââââââââââââ 173 174 function applyFixedHeight(instance, height) { 175 var ss = instance.slideshow; 176 var bg = instance.bg; 177 if (!ss || height <= 0) return; 178 179 var finalHeight = Math.max(height, CONFIG.minHeight); 180 181 // Apply mobile max-height cap if set and viewport is narrow 182 if (instance.maxHeightMobile > 0 && window.innerWidth <= CONFIG.mobileBreakpoint) { 183 finalHeight = Math.min(finalHeight, instance.maxHeightMobile); 184 } 185 186 var h = finalHeight + 'px'; 187 188 instance.isApplying = true; 189 190 var targets = [ss]; 191 if (bg) targets.push(bg); 192 193 targets.forEach(function (el) { 194 el.style.setProperty('height', h, 'important'); 195 el.style.setProperty('min-height', h, 'important'); 196 el.style.setProperty('max-height', h, 'important'); 197 }); 198 199 if (instance.strategy === 'content') { 200 ss.querySelectorAll('.cycle-slide:not(.cycle-sentinel)').forEach(function (slide) { 201 slide.style.setProperty('height', h, 'important'); 202 slide.style.setProperty('min-height', h, 'important'); 203 slide.style.setProperty('align-items', 'stretch', 'important'); 204 205 var wrapper = slide.querySelector('div'); 206 if (wrapper) { 207 wrapper.style.setProperty('height', '100%', 'important'); 208 wrapper.style.setProperty('min-height', '100%', 'important'); 209 } 210 }); 211 } else { 212 ss.querySelectorAll('div').forEach(function (d) { 213 d.style.setProperty('height', h, 'important'); 214 d.style.setProperty('min-height', h, 'important'); 215 }); 216 217 ss.querySelectorAll('img').forEach(function (img) { 218 img.style.setProperty('height', h, 'important'); 219 }); 220 221 ss.querySelectorAll('a').forEach(function (a) { 222 a.style.setProperty('height', h, 'important'); 223 }); 224 } 225 226 requestAnimationFrame(function () { 227 instance.isApplying = false; 228 }); 229 } 230 231 function applyHeight(instance) { 232 var ss = instance.slideshow; 233 if (!ss) return; 234 235 var width = ss.offsetWidth; 236 if (width <= 0) return; 237 238 switch (instance.strategy) { 239 case 'image': 240 if (!instance.aspectRatio) return; 241 applyFixedHeight(instance, Math.round(width / instance.aspectRatio)); 242 break; 243 244 case 'content': 245 var configuredPx = getConfiguredHeightPx(instance); 246 var targetHeight; 247 248 if (configuredPx > 0) { 249 // Deterministic approach using server-provided height. 250 // Above the reference width, use the CSS-configured height. 251 // Below it, scale proportionally â this gives consistent 252 // results regardless of what width the page loaded at. 253 if (width >= CONFIG.referenceWidth) { 254 targetHeight = configuredPx; 255 } else { 256 var ratio = CONFIG.referenceWidth / configuredPx; 257 targetHeight = Math.round(width / ratio); 258 } 259 } else if (instance.aspectRatio) { 260 // Fallback: ratio-based (legacy, no data attributes) 261 targetHeight = Math.round(width / instance.aspectRatio); 262 } else { 263 return; 264 } 265 266 // Safety floor: text reflow on narrow screens may need more space 267 var contentHeight = measureContentHeight(instance); 268 var finalHeight = Math.max(targetHeight, contentHeight); 269 applyFixedHeight(instance, finalHeight); 270 break; 271 272 case 'fallback': 273 instance.aspectRatio = CONFIG.fallbackAspectRatio; 274 applyFixedHeight(instance, Math.round(width / instance.aspectRatio)); 275 break; 276 } 277 } 278
279 // ââ Background Wrapper Discovery ââââââââââââââââââââââââ 280 281 function findBgWrapper(slideshowEl) { 282 var parent = slideshowEl.parentElement; 283 while (parent && parent !== document.body) { 284 if (parent.id && parent.id.indexOf(CONFIG.bgIdPrefix) === 0) return parent; 285 if (parent.id && /rotator/i.test(parent.id)) return parent; 286 parent = parent.parentElement; 287 } 288 return null; 289 } 290 291 // ââ Initialization ââââââââââââââââââââââââââââââââââââââ 292 293 function initInstance(instance) { 294 if (instance.initialized) return; 295 instance.initAttempts++; 296 297 var ss = instance.slideshow; 298 var slides = ss.querySelectorAll('.cycle-slide:not(.cycle-sentinel)'); 299 300 if (slides.length === 0) { 301 retryInit(instance); 302 return; 303 } 304 305 var type = detectSlideType(slides); 306 instance.strategy = type; 307 308 if (type === 'image') { 309 var imageUrl = null; 310 for (var i = 0; i < slides.length; i++) { 311 imageUrl = getBackgroundImageUrl(slides[i]); 312 if (imageUrl) break; 313 } 314 315 var img = new Image(); 316 img.onload = function () { 317 if (img.naturalWidth && img.naturalHeight) { 318 instance.aspectRatio = img.naturalWidth / img.naturalHeight; 319 instance.initialized = true; 320 applyHeight(instance); 321 startObserver(instance); 322 } 323 }; 324 img.onerror = function () { 325 instance.strategy = 'content'; 326 initContentStrategy(instance); 327 }; 328 img.src = imageUrl; 329 330 } else if (type === 'content') { 331 initContentStrategy(instance); 332 333 } else { 334 instance.initialized = true; 335 applyHeight(instance); 336 startObserver(instance); 337 } 338 } 339 340 function retryInit(instance) { 341 if (instance.initAttempts < CONFIG.maxInitAttempts) { 342 setTimeout(function () { initInstance(instance); }, CONFIG.initRetryMs); 343 } 344 } 345 346 /** 347 * Initialize content strategy. If configured height data attributes 348 * are present, no measurement is needed â applyHeight uses them 349 * deterministically. Otherwise, fall back to capturing aspect ratio 350 * from the rendered container height. 351 */ 352 function initContentStrategy(instance) { 353 if (instance.configuredHeight > 0) { 354 // Data attributes present â deterministic mode, no capture needed 355 instance.initialized = true; 356 applyHeight(instance); 357 startObserver(instance); 358 return; 359 } 360 361 // Fallback: capture ratio from rendered height (legacy behavior) 362 var ss = instance.slideshow; 363 var width = ss.offsetWidth; 364 var height = ss.offsetHeight; 365 366 if (height <= CONFIG.minHeight && instance.initAttempts < CONFIG.maxInitAttempts) { 367 retryInit(instance); 368 return; 369 } 370 371 if (width > 0 && height > 0) { 372 instance.aspectRatio = width / height; 373 } else { 374 instance.aspectRatio = CONFIG.fallbackAspectRatio; 375 } 376 377 instance.initialized = true; 378 applyHeight(instance); 379 startObserver(instance); 380 } 381 382 // ââ MutationObserver ââââââââââââââââââââââââââââââââââââ 383 384 function startObserver(instance) { 385 if (typeof MutationObserver === 'undefined') return; 386 387 var observer = new MutationObserver(function () { 388 if (!instance.isApplying && !instance.isMeasuring) { 389 applyHeight(instance); 390 } 391 }); 392 393 observer.observe(instance.slideshow, { 394 attributes: true, 395 attributeFilter: ['style'], 396 subtree: true 397 }); 398 399 instance.observer = observer; 400 } 401 402 // ââ Discovery & Resize ââââââââââââââââââââââââââââââââââ 403 404 function discoverSlideshows() { 405 var slideshows = document.querySelectorAll(CONFIG.slideshowSelector); 406 407 slideshows.forEach(function (ss) { 408 var alreadyTracked = instances.some(function (inst) { 409 return inst.slideshow === ss; 410 }); 411 if (alreadyTracked) return; 412 413 var instance = { 414 slideshow: ss, 415 bg: findBgWrapper(ss), 416 strategy: null, 417 aspectRatio: null, 418 configuredHeight: parseFloat(ss.getAttribute('data-sli
418deshow-height')) || 0, 419 configuredHeightUnit: (ss.getAttribute('data-slideshow-height-unit') || '').toLowerCase(), 420 maxHeightMobile: parseInt(ss.getAttribute('data-max-height-mobile'), 10) || 0, 421 isApplying: false, 422 isMeasuring: false, 423 initialized: false, 424 initAttempts: 0, 425 observer: null 426 }; 427 428 instances.push(instance); 429 initInstance(instance); 430 }); 431 } 432 433 var resizeTimer; 434 function onResize() { 435 clearTimeout(resizeTimer); 436 resizeTimer = setTimeout(function () { 437 instances.forEach(function (instance) { 438 if (instance.initialized) { 439 applyHeight(instance); 440 } 441 }); 442 }, CONFIG.resizeDebounceMs); 443 } 444 445 document.addEventListener('DOMContentLoaded', discoverSlideshows); 446 window.addEventListener('load', discoverSlideshows); 447 window.addEventListener('resize', onResize); 448 449})();
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.