PageSourceSearch

https://nelson4pv.com/js/slideshow.js

js nelson4pv.com collected 2026-10-07 22:06:46 UTC 17,118 bytes, 449 lines download raw bytes

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.