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">×</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 →</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.