1/** 2 * Modular Photo & Video Carousel Component 3 * Features: 4 * - Lazy loading for all images, direct videos, and YouTube embeds (`loading="lazy"`) 5 * - YouTube iframe embed support with COEP `credentialless` compatibility (`https://www.youtube-nocookie.com/embed/...`) 6 * - Direct HTML5 video support (`.mp4`, `.webm`, `.ogg`) 7 * - Prev / Next slide navigation & keyboard arrow support 8 * - Native Autoplay Slideshow engine (`[ ⵠPLAY ]` / `[ ⸠PAUSE ]`) with Spacebar toggle 9 * - Counter badge updating 10 * - Auto-fading controls after 3 seconds of inactivity 11 * - Bottom-center button to open a 3-column grid thumbnail gallery modal with pagination across all cards 12 * - Video & YouTube icon badges on gallery thumbnails 13 * - Retro ImageViewer lightbox integration with full zoom and next/prev gallery navigation 14 */ 15(function() { 16 class PhotoCarousel { 17 static activeInstance = null; 18 static modalBound = false; 19 20 constructor(container) { 21 this.container = typeof container === 'string' ? document.querySelector(container) : container; 22 if (!this.container) return; 23 24 this.mainFrame = this.container.querySelector('.gallery-main-frame, .carousel-main-frame') || this.container.querySelector('#gallery-main-img, .carousel-main-img')?.parentElement; 25 this.mainImg = this.container.querySelector('#gallery-main-img, .carousel-main-img'); 26 this.prevBtn = this.container.querySelector('#gallery-prev-btn, .carousel-prev-btn'); 27 this.nextBtn = this.container.querySelector('#gallery-next-btn, .carousel-next-btn'); 28 this.playBtn = this.container.querySelector('#gallery-play-btn, .carousel-play-btn'); 29 this.counter = this.container.querySelector('#gallery-counter, .carousel-counter'); 30 this.thumbsToggleBtn = this.container.querySelector('#gallery-thumbs-toggle-btn, .carousel-thumbs-btn'); 31 32 this.thumbs = Array.from(this.container.querySelectorAll('.gallery-thumb, .carousel-thumb')); 33 this.currentIndex = 0; 34 this.itemsPerPage = 9; // 3 columns x 3 rows 35 this.modalCurrentPage = 0; 36 this.fadeTimer = null; 37 this.autoplayTimer = null; 38 this.isAutoplaying = false; 39 this.isBottomLayout = this.container.classList.contains('carousel-layout-bottom') || this.container.getAttribute('data-gui-layout') === 'bottom'; 40 if (this.isBottomLayout) { 41 this.fadeControls = []; 42 } else { 43 this.fadeControls = Array.from(this.container.querySelectorAll('.carousel-overlay-control, #gallery-prev-btn, #gallery-next-btn, #gallery-play-btn, #gallery-counter, #gallery-thumbs-toggle-btn, .carousel-prev-btn, .carousel-next-btn, .carousel-play-btn, .carousel-counter, .carousel-thumbs-btn')); 44 } 45 46 this.container._photoCarousel = this; 47 this.init(); 48 } 49 50 init() { 51 if (this.thumbs.length === 0) return; 52 53 if (this.mainFrame) { 54 this.mainFrame.style.cursor = 'pointer'; 55 this.mainFrame.addEventListener('click', (e) => { 56 if (e.target.closest('.carousel-overlay-control, .carousel-prev-btn, .carousel-next-btn, .carousel-play-btn, .carousel-thumbs-btn, iframe, video')) return; 57 this.stopAutoplay(); 58 if (window.ImageViewer) { 59 window.ImageViewer.getInstance().openFromCarousel(this, this.currentIndex); 60 } 61 }); 62 } 63 64 if (this.prevBtn) { 65 this.prevBtn.addEventListener('click', (e) => { 66 e.preventDefault(); 67 this.updateGallery(this.currentIndex - 1); 68 if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer(); 69 }); 70 } 71 72 if (this.nextBtn) { 73 this.nextBtn.addEventListener('click', (e) => { 74 e.preventDefault(); 75 this.updateGallery(this.currentIndex + 1); 76 if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer(); 77 }); 78 } 79 80 if (this.playBtn) { 81 this.playBtn.addEventListener('click', (e) => { 82 e.preventDefault(); 83 e.stopPropagation(); 84 this.toggleAutoplay(); 85 if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer(); 86 }); 87 } 88
89 this.thumbs.forEach((t, idx) => { 90 t.setAttribute('loading', 'lazy'); 91 t.addEventListener('click', () => { 92 this.updateGallery(idx); 93 }); 94 }); 95 96 // Keyboard navigation & shortcuts 97 document.addEventListener('keydown', (e) => { 98 const gamePlayerModal = document.getElementById('game-player-modal-overlay'); 99 const thumbsModal = document.getElementById('carousel-thumbs-modal-overlay'); 100 const imageViewerModal = document.getElementById('gui-image-modal'); 101 if ((gamePlayerModal && gamePlayerModal.style.display !== 'none') || 102 (thumbsModal && thumbsModal.style.display !== 'none') || 103 (imageViewerModal && imageViewerModal.classList.contains('is-active'))) { 104 return; 105 } 106 107 const rect = this.container.getBoundingClientRect(); 108 const inViewport = rect.top < window.innerHeight && rect.bottom > 0; 109 if (!inViewport) return; 110 111 if (e.key === 'ArrowLeft') { 112 this.updateGallery(this.currentIndex - 1); 113 if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer(); 114 } else if (e.key === 'ArrowRight') { 115 this.updateGallery(this.currentIndex + 1); 116 if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer(); 117 } else if ((e.key === ' ' || e.code === 'Space') && e.target === document.body) { 118 if (this.thumbs.length > 1) { 119 e.preventDefault(); 120 this.toggleAutoplay(); 121 if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer(); 122 } 123 } 124 }); 125 126 // Bottom center thumbnail gallery toggle button 127 if (this.thumbsToggleBtn) { 128 this.thumbsToggleBtn.addEventListener('click', (e) => { 129 e.preventDefault(); 130 this.stopAutoplay(); 131 this.openThumbsModal(); 132 if (typeof this.resetFadeTimer === 'function') this.resetFadeTimer(); 133 }); 134 } 135 136 PhotoCarousel.bindGlobalModalEvents(); 137 this.setupAutoFade(); 138 this.updateGallery(0); 139 140 // Auto-start if container specifies data-autoplay="true" 141 if (this.container.getAttribute('data-autoplay') === 'true') { 142 this.startAutoplay(); 143 } 144 } 145 146 startAutoplay(interval) { 147 if (this.thumbs.length <= 1) return; 148 this.stopAutoplay(); 149 this.isAutoplaying = true; 150 const dur = interval || this.autoplayInterval || 3500; 151 this.updatePlayBtnUi(true); 152 this.autoplayTimer = setInterval(() => { 153 this.updateGallery(this.currentIndex + 1); 154 }, dur); 155 } 156 157 stopAutoplay() { 158 if (this.autoplayTimer) { 159 clearInterval(this.autoplayTimer); 160 this.autoplayTimer = null; 161 } 162 this.isAutoplaying = false; 163 this.updatePlayBtnUi(false); 164 } 165 166 toggleAutoplay() { 167 if (this.isAutoplaying) { 168 this.stopAutoplay(); 169 } else { 170 this.startAutoplay(); 171 } 172 } 173 174 updatePlayBtnUi(isPlaying) { 175 if (!this.playBtn) return; 176 if (isPlaying) { 177 this.playBtn.textContent = '[ ⸠PAUSE ]'; 178 this.playBtn.classList.add('retro-btn-active'); 179 this.playBtn.title = 'Pause Automatic Slideshow (Spacebar)'; 180 } else { 181 this.playBtn.textContent = '[ ⵠPLAY ]'; 182 this.playBtn.classList.remove('retro-btn-active'); 183 this.playBtn.title = 'Start Automatic Slideshow (Spacebar)'; 184 } 185 } 186 187 static bindGlobalModalEvents() { 188 if (PhotoCarousel.modalBound) return; 189 PhotoCarousel.modalBound = true; 190 191 document.addEventListener('click', (e) => { 192 const thumbsBtn = e.target.closest('#gallery-thumbs-toggle-btn, .carousel-thumbs-btn'); 193 if (thumbsBtn) { 194 e.preventDefault(); 195 const wrapper = thumbsBtn.closest('.game-carousel-wrapper, .photo-carousel-wrapper, .photo-carousel-container, .terminal-window, .project-card'); 196 if (wrapper && wrapper._photoCarousel) { 197 wrapper._photoCarousel.openThumbsModal(); 198 } 199 return; 200 } 201 202 const closeBtn = e.target.closest('#carousel-thumbs-modal-close'); 203 const modalOverlay = document.getElementById('carousel-thumbs-modal-overlay'); 204 if (closeBtn || e.target === modalOverlay) { 205 PhotoCarousel.closeThumbsModal(); 206 } 207 208 const prevBtn = e.target.closest('#carousel-thumbs-prev-page'); 209 if (prevBtn && PhotoCarousel.activeInstance) { 210 e.preventDefault(); 211 if (PhotoCarousel.activeInstance.modalCurrentPage > 0) { 212 PhotoCarousel.activeInstance.modalCurrentPage--; 213 PhotoCarousel.activeInstance.renderModalPage(); 214 } 215 } 216 217 const nextBtn = e.target.closest('#carousel-thumbs-next-page'); 218 if (nextBtn && PhotoCarousel.activeInstance) { 219 e.preventDefault(); 220 const totalPages = Math.ceil(PhotoCarousel.activeInstance.thumbs.length / PhotoCarousel.activeInstance.itemsPerPage); 221 if (PhotoCarousel.activeInstance.modalCurrentPage < totalPages - 1) { 222 PhotoCarousel.activeInstance.modalCurrentPage++; 223 PhotoCarousel.activeInstance.renderModalPage(); 224 } 225 } 226 }); 227 228 document.addEventListener('keydown', (e) => { 229 const thumbsModal = document.getElementById('carousel-thumbs-modal-overlay'); 230 if (e.key === 'Escape' && thumbsModal && thumbsModal.style.display !== 'none') { 231 PhotoCarousel.closeThumbsModal(); 232 } 233 }); 234 } 235 236 updateGallery(idx) { 237 if (idx < 0) idx = this.thumbs.length - 1; 238 if (idx >= this.thumbs.length) idx = 0; 239 this.currentIndex = idx; 240 241 const activeThumb = this.thumbs[this.currentIndex]; 242 const targetSrc = activeThumb.getAttribute('data-full-src') || activeThumb.src || activeThumb.getAttribute('data-url'); 243 const mediaType = activeThumb.getAttribute('data-media-type') || this.detectMediaType(targetSrc); 244 const youtubeId = activeThumb.getAttribute('data-youtube-id') || this.extractYoutubeId(targetSrc); 245 const isExplicitNoFilter = this.container.getAttribute('data-palette-filter') === 'false' || 246 this.container.closest('.art-stage-container, [data-palette-filter="false"], .art-viewer-body') !== null || 247 activeThumb.getAttribute('data-palette-filter') === 'false'; 248 const shouldApplyDuotone = !isExplicitNoFilter; 249 250 if (this.mainFrame) { 251 // Clear all active media elements and YouTube placeholder boxes 252 const existingMedia = this.mainFrame.querySelectorAll('img, video, iframe, .carousel-youtube-box');
253 existingMedia.forEach(el => el.remove()); 254 this.mainImg = null; 255 256 if (mediaType === 'youtube' && youtubeId) { 257 const disableEmbeds = this.container.hasAttribute('data-disable-embeds') || 258 this.container.getAttribute('data-disable-embeds') === 'true' || 259 document.body.classList.contains('disable-carousel-embeds'); 260 261 if (disableEmbeds) { 262 const wrapper = document.createElement('div'); 263 wrapper.className = 'carousel-youtube-box'; 264 265 const icon = document.createElement('span'); 266 icon.className = 'youtube-box-icon'; 267 icon.style.cssText = '-webkit-mask-image: url(\'/icons/play.svg\'); mask-image: url(\'/icons/play.svg\');'; 268 wrapper.appendChild(icon); 269 270 const title = document.createElement('div'); 271 title.className = 'youtube-box-title'; 272 title.textContent = '[ YOUTUBE VIDEO DEMO ]'; 273 wrapper.appendChild(title); 274 275 const watchBtn = document.createElement('a'); 276 watchBtn.href = `https://www.youtube.com/watch?v=${youtubeId}`; 277 watchBtn.target = '_blank'; 278 watchBtn.rel = 'noopener noreferrer'; 279 watchBtn.className = 'export-btn'; 280 watchBtn.style.cssText = 'display: inline-flex; align-items: center; gap: 0.5rem;'; 281 watchBtn.innerHTML = '<span class="stat-icon" style="-webkit-mask-image: url(\'/icons/youtube.svg\'); mask-image: url(\'/icons/youtube.svg\'); width: 14px; height: 14px; display: inline-block; background-color: currentColor;"></span> <span>[ WATCH ON YOUTUBE â ]</span>'; 282 wrapper.appendChild(watchBtn); 283 284 this.mainFrame.insertBefore(wrapper, this.mainFrame.firstChild); 285 } else { 286 const iframe = document.createElement('iframe'); 287 iframe.src = `https://www.youtube-nocookie.com/embed/${youtubeId}`; 288 iframe.setAttribute('loading', 'lazy'); 289 iframe.setAttribute('credentialless', 'true'); 290 iframe.setAttribute('frameborder', '0'); 291 iframe.setAttribute('allow', 'accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture'); 292 iframe.setAttribute('allowfullscreen', 'true'); 293 iframe.style.cssText = 'width: 100%; height: 100%; border: none; display: block; z-index: 1;'; 294 this.mainFrame.insertBefore(iframe, this.mainFrame.firstChild); 295 } 296 } else if (mediaType === 'video') { 297 const video = document.createElement('video'); 298 video.src = targetSrc; 299 video.controls = true; 300 video.autoplay = false; 301 video.loop = true; 302 video.muted = true; 303 video.setAttribute('loading', 'lazy'); 304 video.style.cssText = 'width: 100%; height: 100%; object-fit: contain; display: block; z-index: 1;'; 305 this.mainFrame.insertBefore(video, this.mainFrame.firstChild); 306 } else { 307 const img = document.createElement('img'); 308 img.id = 'gallery-main-img'; 309 img.className = 'carousel-main-img'; 310 img.src = targetSrc; 311 img.setAttribute('loading', 'lazy'); 312 img.alt = 'Gallery Media'; 313 if (shouldApplyDuotone) { 314 img.setAttribute('data-palette-filter', 'true'); 315 img.removeAttribute('filter'); 316 img.removeAttribute('data-filter'); 317 img.classList.remove('no-palette-filter', 'no-monochrome'); 318 } else { 319 img.setAttribute('data-palette-filter', 'false'); 320 img.setAttribute('filter', '0'); 321 img.setAttribute('data-filter', '0'); 322 img.classList.add('no-palette-filter', 'no-monochrome'); 323 img.style.filter = 'none'; 324 img.style.webkitFilter = 'none'; 325 } 326 img.style.cssText = 'max-width: 100%; max-height: 100%; object-fit: contain; display: block; margin: 0 auto; z-index: 1;'; 327 this.mainFrame.insertBefore(img, this.mainFrame.firstChild); 328 this.mainImg = img; 329 330 if (shouldApplyDuotone && window.PaletteEngine && typeof window.PaletteEngine.initDuotoneImages === 'function') { 331 window.PaletteEngine.initDuotoneImages(this.mainFrame); 332 } 333 } 334 } 335
336 this.thumbs.forEach((t, i) => { 337 if (i === this.currentIndex) { 338 t.classList.add('active-thumb'); 339 try { 340 t.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); 341 } catch(e) {} 342 } else { 343 t.classList.remove('active-thumb'); 344 } 345 }); 346 347 if (this.counter) { 348 this.counter.textContent = '[ ' + (this.currentIndex + 1) + ' / ' + this.thumbs.length + ' ]'; 349 } 350 351 // Preload next image 352 const nextIdx = (this.currentIndex + 1) % this.thumbs.length; 353 const nextThumb = this.thumbs[nextIdx]; 354 if (nextThumb) { 355 const nSrc = nextThumb.getAttribute('data-full-src') || nextThumb.src; 356 if (nSrc && !nSrc.includes('youtube.com')) { 357 const preImg = new Image(); 358 preImg.src = nSrc; 359 } 360 } 361 } 362 363 detectMediaType(src) { 364 if (!src) return 'image'; 365 const s = src.toLowerCase(); 366 if (s.includes('youtube.com') || s.includes('youtu.be')) return 'youtube'; 367 if (s.endsWith('.mp4') || s.endsWith('.webm') || s.endsWith('.ogg')) return 'video'; 368 return 'image'; 369 } 370 371 extractYoutubeId(url) { 372 if (!url) return ''; 373 if (url.includes('v=')) return url.split('v=')[1]?.split('&')[0] || ''; 374 if (url.includes('youtu.be/')) return url.split('youtu.be/')[1]?.split('?')[0] || ''; 375 if (url.includes('embed/')) return url.split('embed/')[1]?.split('?')[0] || ''; 376 return ''; 377 } 378 379 openThumbsModal() { 380 const modalOverlay = document.getElementById('carousel-thumbs-modal-overlay'); 381 const modalGrid = document.getElementById('carousel-thumbs-modal-grid'); 382 if (!modalOverlay || !modalGrid) return; 383 384 PhotoCarousel.activeInstance = this; 385 this.modalCurrentPage = Math.floor(this.currentIndex / this.itemsPerPage); 386 this.renderModalPage(); 387 modalOverlay.style.cssText = 'display: flex !important; z-index: 999999;'; 388 document.body.style.overflow = 'hidden'; 389 } 390 391 renderModalPage() { 392 const modalGrid = document.getElementById('carousel-thumbs-modal-grid'); 393 const pageBadge = document.getElementById('carousel-thumbs-page-badge'); 394 const prevPageBtn = document.getElementById('carousel-thumbs-prev-page'); 395 const nextPageBtn = document.getElementById('carousel-thumbs-next-page'); 396 const paginationContainer = document.getElementById('carousel-thumbs-modal-pagination'); 397 398 if (!modalGrid) return; 399 400 const totalPages = Math.ceil(this.thumbs.length / this.itemsPerPage) || 1; 401 if (this.modalCurrentPage >= totalPages) this.modalCurrentPage = totalPages - 1; 402 if (this.modalCurrentPage < 0) this.modalCurrentPage = 0; 403 404 const startIdx = this.modalCurrentPage * this.itemsPerPage; 405 const endIdx = Math.min(startIdx + this.itemsPerPage, this.thumbs.length); 406 407 modalGrid.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 0.5rem; margin-bottom: 1rem; align-items: center; justify-items: center;'; 408 modalGrid.innerHTML = ''; 409 410 for (let idx = startIdx; idx < endIdx; idx++) { 411 const t = this.thumbs[idx]; 412 const fullSrc = t.getAttribute('data-full-src') || t.src || ''; 413 const thumbSrc = t.src || t.getAttribute('data-thumbnail') || fullSrc; 414 const mediaType = t.getAttribute('data-media-type') || this.detectMediaType(fullSrc); 415 416 const card = document.createElement('div'); 417 card.style.cssText = 'position: relative; width: 100%; aspect-ratio: 16/9; max-height: 130px; border: 2px solid var(--border-color); cursor: pointer; border-radius: 3px; overflow: hidden; display: flex; align-items: center; justify-content: center; background: var(--bg-window); box-sizing: border-box; transition: border-color 0.2s ease, transform 0.15s ease;'; 418 419 if (idx === this.currentIndex) { 420 card.style.borderColor = 'var(--accent-color)'; 421 } 422 423 const thumbImg = document.createElement('img'); 424 thumbImg.src = thumbSrc; 425 thumbImg.alt = `Thumbnail ${idx + 1}`; 426 thumbImg.setAttribute('loading', 'lazy'); 427 thumbImg.style.cssText = 'width: 100%; height: 100%; object-fit: cover; object-position: center; display: block; border: none; margin: 0; padding: 0;'; 428 429 card.appendChild(thumbImg); 430 431 // Add Video / YouTube Badge Overlay 432 if (mediaType === 'youtube' || mediaType === 'video') { 433 const badge = document.createElement('div'); 434 badge.style.cssText = 'position: absolute; bottom: 6px; right: 6px; background: rgba(0, 0, 0, 0.85); color: var(--accent-color); font-size: 0.7rem; font-weight: bold; padding: 2px 6px; border: 1px solid var(--accent-color); border-radius: 2px; text-transform: uppercase; z-index: 2; display: flex; align-items: center; gap: 4px;'; 435 const iconFile = mediaType === 'youtube' ? 'youtube.svg' : 'video.svg'; 436 const iconHtml = `<span class="stat-icon" style="-webkit-mask-image: url('/icons/${iconFile}'); mask-image: url('/icons/${iconFile}'); width: 12px; height: 12px; display: inline-block; background-color: currentColor;"></span>`;
437 badge.innerHTML = `${iconHtml} <span>${mediaType === 'youtube' ? 'YOUTUBE' : 'VIDEO'}</span>`; 438 card.appendChild(badge); 439 } 440 441 card.addEventListener('mouseenter', () => { 442 card.style.transform = 'scale(1.03)'; 443 }); 444 card.addEventListener('mouseleave', () => { 445 card.style.transform = 'scale(1.0)'; 446 }); 447 448 card.addEventListener('click', () => { 449 this.updateGallery(idx); 450 PhotoCarousel.closeThumbsModal(); 451 if (window.ImageViewer && window.ImageViewer.instance && window.ImageViewer.instance.modal && window.ImageViewer.instance.modal.classList.contains('is-active')) { 452 window.ImageViewer.instance.updateFromCarousel(this); 453 } 454 }); 455 456 modalGrid.appendChild(card); 457 } 458 459 if (pageBadge) { 460 pageBadge.textContent = `[ PAGE ${this.modalCurrentPage + 1} / ${totalPages} ]`; 461 } 462 463 if (prevPageBtn) { 464 prevPageBtn.style.opacity = this.modalCurrentPage > 0 ? '1' : '0.4'; 465 prevPageBtn.style.pointerEvents = this.modalCurrentPage > 0 ? 'auto' : 'none'; 466 } 467 468 if (nextPageBtn) { 469 nextPageBtn.style.opacity = this.modalCurrentPage < totalPages - 1 ? '1' : '0.4'; 470 nextPageBtn.style.pointerEvents = this.modalCurrentPage < totalPages - 1 ? 'auto' : 'none'; 471 } 472 473 if (paginationContainer) { 474 paginationContainer.style.display = totalPages > 1 ? 'flex' : 'none'; 475 } 476 } 477 478 static closeThumbsModal() { 479 const modalOverlay = document.getElementById('carousel-thumbs-modal-overlay'); 480 if (!modalOverlay) return; 481 modalOverlay.style.cssText = 'display: none !important;'; 482 const isImageViewerActive = window.ImageViewer && window.ImageViewer.instance && window.ImageViewer.instance.modal && window.ImageViewer.instance.modal.classList.contains('is-active'); 483 if (!isImageViewerActive) { 484 document.body.style.overflow = ''; 485 } 486 } 487 488 setupAutoFade() { 489 if (!this.fadeControls || this.fadeControls.length === 0) return; 490 491 const showControls = () => { 492 this.fadeControls.forEach(ctrl => { 493 ctrl.style.opacity = '1'; 494 ctrl.style.pointerEvents = 'auto'; 495 }); 496 }; 497 498 const hideControls = () => { 499 this.fadeControls.forEach(ctrl => { 500 ctrl.style.opacity = '0'; 501 ctrl.style.pointerEvents = 'none'; 502 }); 503 }; 504 505 this.resetFadeTimer = () => { 506 showControls(); 507 if (this.fadeTimer) clearTimeout(this.fadeTimer); 508 this.fadeTimer = setTimeout(() => { 509 hideControls(); 510 }, 3000); 511 }; 512 513 this.fadeControls.forEach(ctrl => { 514 ctrl.style.transition = 'opacity 0.4s ease'; 515 }); 516 517 this.container.addEventListener('mousemove', this.resetFadeTimer); 518 this.container.addEventListener('mouseenter', this.resetFadeTimer); 519 this.container.addEventListener('focusin', this.resetFadeTimer); 520 this.container.addEventListener('mouseleave', () => { 521 if (this.fadeTimer) clearTimeout(this.fadeTimer); 522 this.fadeTimer = setTimeout(hideControls, 1000); 523 }); 524 525 this.resetFadeTimer(); 526 } 527 } 528 529 window.PhotoCarousel = PhotoCarousel; 530 531 function initCarousels() { 532 document.querySelectorAll('.game-carousel-wrapper, .photo-carousel-wrapper, .photo-carousel-container').forEach(wrapper => { 533 if (!wrapper._photoCarousel) { 534 new PhotoCarousel(wrapper); 535 } 536 }); 537 } 538 539 if (document.readyState === 'loading') { 540 document.addEventListener('DOMContentLoaded', initCarousels); 541 } else {
542 initCarousels(); 543 } 544})();
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.