PageSourceSearch

https://onyxroom.com/wp-content/plugins/custom-elements-social-fee…?ver=1.13.1&bd_ver=3.0.0-beta.9

js onyxroom.com collected 2026-10-08 07:56:27 UTC 12,892 bytes, 331 lines download raw bytes

1/**
2 * Custom Elements - Social Feed: Instagram Feed front-end engine.
3 *
4 * - Ticker: JS-driven marquee (replaces the CSS fallback when it can run):
5 *   seamless infinite wrap, click-and-drag with momentum, and eased speed
6 *   changes — pause-on-hover glides to a stop instead of freezing.
7 * - Video tiles: play on hover (muted, looped), pause on leave.
8 * - Lightbox: click opens the post large in an overlay with a caption and a
9 *   "View on Instagram" link.
10 *
11 * Idempotent: every initialized node is marked and skipped on re-scan.
12 */
13(function () {
14    'use strict';
15
16    if (window.__cesfFeedInit) {
17        return;
18    }
19    window.__cesfFeedInit = true;
20
21    var mod = function (n, m) {
22        return ((n % m) + m) % m;
23    };
24
25    // ----- ticker -----
26
27    function initTicker(grid) {
28        if (grid.dataset.cesfTickerInit) {
29            return;
30        }
31        grid.dataset.cesfTickerInit = '1';
32
33        var belt = grid.querySelector('.cesf-belt');
34        var firstTrack = grid.querySelector('.cesf-track');
35        if (!belt || !firstTrack) {
36            return;
37        }
38
39        var styles = getComputedStyle(grid);
40        var duration = parseFloat(styles.getPropertyValue('--cesf-speed')) || 40;
41        var reverse = (styles.getPropertyValue('--cesf-direction') || '').trim() === 'reverse';
42        var pauseOnHover = (styles.getPropertyValue('--cesf-pause') || '').trim() === '1';
43        var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
44
45        grid.classList.add('is-js');
46
47        // With fewer/narrower posts than the container, a two-copy belt shows
48        // an empty tail at the loop point — clone the item set into every
49        // track until one track genuinely fills the container. Clones are
50        // real (clickable) tiles, hidden from assistive tech + tab order.
51        var originals = Array.prototype.slice.call(firstTrack.children);
52        var lastFillW = 0;
53        var ensureFill = function () {
54            // Clamp to the viewport: if the container ever reports a
55            // content-driven (unconstrained) width, filling "to fit" would
56            // clone forever as each round widens the container again.
57            var viewportW = document.documentElement.clientWidth || 1600;
58            var containerW = Math.min(grid.offsetWidth, viewportW * 2);
59            if (containerW === lastFillW) {
60                return;
61            }
62            lastFillW = containerW;
63            var guard = 0;
64            while (firstTrack.offsetWidth < containerW && guard++ < 8 && originals.length
65                && firstTrack.children.length <= 60) {
66                grid.querySelectorAll('.cesf-track').forEach(function (track) {
67                    originals.forEach(function (original) {
68                        var clone = original.cloneNode(true);
69                        clone.setAttribute('tabindex', '-1');
70                        clone.setAttribute('aria-hidden', 'true');
71                        track.appendChild(clone);
72                    });
73                });
74            }
75        };
76        ensureFill();
77
78        var trackW = 0;
79        var measure = function () {
80            trackW = firstTrack.offsetWidth || 1;
81        };
82        measure();
83        if (window.ResizeObserver) {
84            new ResizeObserver(measure).observe(firstTrack);
85            // A grown container may need more clones (never fewer).
86            new ResizeObserver(function () {
87                ensureFill();
88                measure();
89            }).observe(grid);
90        }
91
92        var offset = 0;
93        var baseSpeed = function () {
94            // Loop Duration = seconds for one full track width to pass.
95            var v = trackW / Math.max(1, duration);
96            return reverse ? -v : v;
97        };
98        var speed = reducedMotion ? 0 : baseSpeed();
99        var target = speed;
100        var dragging = false;
101        var dragMoved = 0;
102        var lastX = 0;
103        var lastT = 0;
104        var dragV = 0;
105        var last = performance.now();
106
107        var frame = function (now) {
108            var dt = Math.min(0.05, (now - last) / 1000);
109            last = now;
110            if (!dragging) {
111                // Ease toward the target speed — this is what makes hover
112                // pause (and momentum recovery) glide instead of snapping.
113                speed += (target - speed) * Math.min(1, dt * 5);
114                offset = mod(offset + speed * dt, trackW);
115                belt.style.transform = 'translate3d(' + -offset + 'px,0,0)';
116            }
117            requestAnimationFrame(frame);
118        };
119        requestAnimationFrame(frame);
120
121        if (pauseOnHover && !reducedMotion) {
122            grid.addEventListener('pointerenter', function (e) {
123                if (e.pointerType === 'mouse') {
124                    target = 0;
125                }
126            });
127            grid.addEventListener('pointerleave', function (e) {
128                if (e.pointerType === 'mouse' && !dragging) {
129                    target = baseSpeed();
130                }
131            });
132        }
133
134        // Click-and-drag (mouse + touch). While dragging the belt follows the
135        // pointer 1:1; release hands the pointer's velocity to the loop and it
136        // eases back to the autoplay speed.
137        grid.addEventListener('pointerdown', function (e) {
138            if (e.button !== undefined && e.button !== 0) {
139                return;
140            }
141            dragging = true;
142            dragMoved = 0;
143            lastX = e.clientX;
144            lastT = performance.now();
145            dragV = 0;
146        });
147        grid.addEventListener('pointermove', function (e) {
148            if (!dragging) {
149                return;
150            }
151            // The button is no longer down — the release happened outside the
152            // grid before capture engaged, so no pointerup ever reached us.
153            // Without this, "dragging" sticks forever and every later mouse
154            // movement shoves the belt around (seen in production).
155            if (e.pointerType !== 'touch' && e.buttons === 0) {
156                endDrag();
157                return;
158            }
159            var dx = e.clientX - lastX;
160            var now = performance.now();
161            var dt = Math.max(1, now - lastT) / 1000;
162            dragV = -dx / dt;
163            lastX = e.clientX;
164            lastT = now;
165            dragMoved += Math.abs(dx);
166            // Only treat it as a drag (capture the pointer, move the belt)
167            // once the pointer has really moved — capturing on pointerdown
168            // makes the browser retarget the click to the grid, which
169            // swallowed tile clicks (links + lightbox) in ticker mode.
170            if (dragMoved <= 6) {
171                return;
172            }
173            if (!grid.classList.contains('is-dragging')) {
174                grid.classList.add('is-dragging');
175                grid.setPointerCapture && grid.setPointerCapture(e.pointerId);
176            }
177            offset = mod(offset - dx, trackW);
178            belt.style.transform = 'translate3d(' + -offset + 'px,0,0)';
179        });
180        var endDrag = function () {
181            if (!dragging) {
182                return;
183            }
184            dragging = false;
185            grid.classList.remove('is-dragging');
186            last = performance.now();
187            if (!reducedMotion) {
188                // Momentum capped at 4× autoplay, easing back to normal.
189                var cap = Math.abs(baseSpeed()) * 4;
190                speed = Math.max(-cap, Math.min(cap, dragV));
191                target = baseSpeed();
192            }
193        };
194        // Releases can land outside the grid (capture only engages after 6px
195        // of movement) — listen at the window level so the drag always ends.
196        window.addEventListener('pointerup', endDrag);
197        window.addEventListener('pointercancel', endDrag);
198        grid.addEventListener('lostpointercapture', endDrag);
199
200        // A real drag must not count as a click on a tile.
201        grid.addEventListener('click', function (e) {
202            if (dragMoved > 6) {
203                e.preventDefault();
204                e.stopPropagation();
205                dragMoved = 0;
206            }
207        }, true);
208
209        grid.addEventListener('dragstart', function (e) {
210            e.preventDefault();
211        });
212    }
213
214    // ----- video on hover -----
215
216    function bindVideoHover() {
217        document.addEventListener('mouseover', function (e) {
218            var item = e.target.closest && e.target.closest('.cesf-item');
219            if (!item || !item.closest('[data-cesf-vh]')) {
220                return;
221            }
222            var video = item.querySelector('video');
223            if (video && video.paused) {
224                // The <img> thumbnail stays as the resting layer (guaranteed
225                // object-fit:cover); the video fades in over it while playing.
226                item.classList.add('is-playing');
227                video.play().catch(function () {
228                    item.classList.remove('is-playing');
229                });
230            }
231        });
232        document.addEventListener('mouseout', function (e) {
233            var item = e.target.closest && e.target.closest('.cesf-item');
234            if (!item || !item.closest('[data-cesf-vh]')) {
235                return;
236            }
237            if (item.contains(e.relatedTarget)) {
238                return;
239            }
240            var video = item.querySelector('video');
241            if (video && !video.paused) {
242                video.pause();
243                item.classList.remove('is-playing');
244            }
245        });
246    }
247
248    // ----- lightbox -----
249
250    function openLightbox(item) {
251        var full = item.getAttribute('data-cesf-full') || '';
252        var videoSrc = item.getAttribute('data-cesf-video') || '';
253        var caption = item.getAttribute('data-cesf-caption') || '';
254        var href = item.getAttribute('href') || '#';
255
256        var overlay = document.createElement('div');
257        overlay.className = 'cesf-lightbox';
258        overlay.innerHTML =
259            '<div class="cesf-lightbox__backdrop"></div>' +
260            '<figure class="cesf-lightbox__box" role="dialog" aria-modal="true">' +
261            '<button type="button" class="cesf-lightbox__close" aria-label="Close">&times;</button>' +
262            (videoSrc
263                ? '<video class="cesf-lightbox__media" src="' + videoSrc + '" controls autoplay playsinline loop></video>'
264                : '<img class="cesf-lightbox__media" src="' + full + '" alt="" />') +
265            '<figcaption class="cesf-lightbox__bar">' +
266            (caption ? '<span class="cesf-lightbox__caption"></span>' : '') +
267            '<a class="cesf-lightbox__link" target="_blank" rel="noopener noreferrer">View on Instagram &rarr;</a>' +
268            '</figcaption>' +
269            '</figure>';
270
271        // Caption + href set via DOM APIs (never string-injected).
272        var cap = overlay.querySelector('.cesf-lightbox__caption');
273        if (cap) {
274            cap.textContent = caption;
275        }
276        overlay.querySelector('.cesf-lightbox__link').href = href;
277
278        var close = function () {
279            document.removeEventListener('keydown', onKey);
280            var video = overlay.querySelector('video');
281            if (video) {
282                video.pause();
283            }
284            overlay.remove();
285        };
286        var onKey = function (e) {
287            if (e.key === 'Escape') {
288                close();
289            }
290        };
291        overlay.querySelector('.cesf-lightbox__backdrop').addEventListener('click', close);
292        overlay.querySelector('.cesf-lightbox__close').addEventListener('click', close);
293        document.addEventListener('keydown', onKey);
294
295        document.body.appendChild(overlay);
296        overlay.querySelector('.cesf-lightbox__close').focus();
297    }
298
299    function bindLightbox() {
300        document.addEventListener('click', function (e) {
301            var item = e.target.closest && e.target.closest('.cesf-item');
302            if (!item || !item.closest('[data-cesf-lightbox]')) {
303                return;
304            }
305            e.preventDefault();
306            openLightbox(item);
307        });
308    }
309
310    // ----- boot -----
311
312    function scan() {
313        document.querySelectorAll('.cesf-grid--ticker[data-cesf-ticker]').forEach(initTicker);
314    }
315
316    var boot = function () {
317        bindVideoHover();
318        bindLightbox();
319        scan();
320        // Catch grids rendered after load (AJAX-injected sections etc.).
321        if (window.MutationObserver) {
322            new MutationObserver(scan).observe(document.body, { childList: true, subtree: true });
323        }
324    };
325
326    if (document.readyState !== 'loading') {
327        boot();
328    } else {
329        document.addEventListener('DOMContentLoaded', boot);
330    }
331})();

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.