1/* ========================================================= 2 ONOS â Sliders: Hero Banner + Testimonials Carousel 3 ========================================================= */ 4(function () { 5 /* ---------------- Hero Banner Slider ---------------- */ 6 function initHeroSlider() { 7 const slides = document.querySelectorAll(".hero-slide"); 8 const dotsWrap = document.getElementById("heroDots"); 9 const prevBtn = document.getElementById("heroPrev"); 10 const nextBtn = document.getElementById("heroNext"); 11 if (!slides.length) 12 return; 13 14 let current = 0; 15 let timer = null; 16 17 slides.forEach((_, i) => { 18 const dot = document.createElement("span"); 19 dot.className = "dot" + (i === 0 ? " active" : ""); 20 dot.addEventListener("click", () => goTo(i)); 21 dotsWrap.appendChild(dot); 22 }); 23 const dots = dotsWrap.querySelectorAll(".dot"); 24 25 function goTo(index) { 26 slides[current].classList.remove("active"); 27 dots[current].classList.remove("active"); 28 current = (index + slides.length) % slides.length; 29 slides[current].classList.add("active"); 30 dots[current].classList.add("active"); 31 restartAutoplay(); 32 } 33 34 function next() { 35 goTo(current + 1); 36 } 37 function prev() { 38 goTo(current - 1); 39 } 40 41 function restartAutoplay() { 42 clearInterval(timer); 43 timer = setInterval(next, 6000); 44 } 45 46 prevBtn && prevBtn.addEventListener("click", prev); 47 nextBtn && nextBtn.addEventListener("click", next); 48 49 const heroSection = document.getElementById("hero"); 50 heroSection.addEventListener("mouseenter", () => clearInterval(timer)); 51 heroSection.addEventListener("mouseleave", restartAutoplay); 52 53 restartAutoplay(); 54 } 55 56 /* ---------------- Testimonials Carousel ---------------- */ 57 const TESTIMONIALS = [ 58 { 59 name: "Prof Shashikant S Udikeri", role: "Vice Chancellor", org: "Gulbarga University, Kalaburagi, Karnataka", rating: 5, color1: "#123265", color2: "#1E4A8A", 60 image: "assets/images/Shashikant S.jpg", 61 text: "eJournals under ONOS are essential and help our students and research scholars."}, 62 63 { 64 name: "Prof R.L Raibagkar", role: "Senior Professor, Dept of Applied Electronics", org: "Gulbarga University, Kalaburagi, Karnataka", rating: 5, color1: "#E8A33D", color2: "#C9862A", 65 image: "assets/images/R.L Raibagkar.jpg", 66 text: "I am thrilled to optimize e.resources underONOS and helps in strengthening research quality and advancement of knowledge."}, 67 68 { 69 name: "Dr Suresh Jange", role: "University Librarian & Director IQAC", org: "Gulbarga University, Kalaburagi, Karnataka", rating: 5, color1: "#1F8A5C", color2: "#2FAE79", 70 image: "assets/images/Suresh Jange.jpg", 71 text: "Under ONOS, academic Journals of international repute serve as authoritative platform and great boon to our academic community in enhancing their visibility and professional credibility."}, 72 73 { 74 name: "Dr Savitri Krishna", role: "Senior Librarian & PM USHA Coordinator", org: "Govt First Grade College, Afzalpur, Kalaburagi, Karnataka", rating: 5, color1: "#B5442E", color2: "#D46A4E", 75 image: "assets/images/Savitri Krishna.jpg", 76 text: "ONOS encourages research and innovation. Students can work on mini-projects, final-year projects, and research in network security, traffic engineering, and smart networking solutions."}, 77 78 { 79 name: "Dr. Shivaprasad Kumbar", role: "Associate Professor", org: "Department of Pharmacology Koppal Institute of Medical Sciences, Koppal, Karnataka", rating: 4, color1: "#123265", color2: "#1E4A8A", 80 image: "assets/images/user.png", 81 text: "ONOS is providing articles from the publishers whom we once 'dreamt' of, but it has 'empowered' at least me. I try to make justice to the resources you provided."}, 82 83 { 84 name: "Dr. Purnima Kumari", role: "In-Charge Librarian", org: "Pt. Sundarlal Sharma Library, Pt. Ravishankar Shukla University, Raipur, Chhattisgarh", rating: 5, color1: "#E8A33D", color2: "#C9862A", 85 image: "assets/images/Purnima Kumari.jpg", 86 text: "ONOS is available through the IP addresses of the institutions, making it easy and secure for the users to access. With this facility, when students, teachers, or researchers access the internet from their college, university, or registered institutionâs network, they will automatically get access to the subscribed journals and research platforms without the need for any additional usernames and passwords."}, 87 88 { 89 name: "Jitendra Kumar Patel", role: "Librarian", org: "Chandrapal Dadsena Govt.College Pithora Dist. Mahasamund, Chhattisgarh", rating: 5, color1: "#1F8A5C", color2: "#2FAE79", 90 image: "assets/images/Jitendra Kumar Patel.jpg", 91 text: "The entire process of ONOS is digital; students (through their institutions) can log in to the ONOS portal and search, download, and read journals online without physical barriers or paperwork."}, 92 93 { 94 name: "Gangaram", role: "Librarian", org: "Shaskiya Vedram Mahavidyalaya, Janjgir, Champa, Chhattisgarh", rating: 4, color1: "#B5442E", color2: "#D46A4E", 95 image: "assets/images/Gangaram.jpg", 96 text: "महाविदà¥à¤¯à¤¾à¤²à¤¯ मà¥à¤ ठà¤à¥ तठà¤à¤¾à¤¤à¥à¤°/à¤à¤¾à
96¤¤à¥à¤°à¤¾à¤à¤/ठधà¥à¤¯à¤¾à¤ªà¤à¥à¤ दà¥à¤µà¤¾à¤°à¤¾ 1015 पà¥à¤¸à¥à¤¤à¤à¥à¤ à¤à¤¾ पà¤à¤à¥à¤¯à¤¨ सफलतापà¥à¤°à¥à¤µà¤ à¤à¤¿à¤¯à¤¾ à¤à¤¾ à¤à¥à¤à¤¾ हॠà¤à¤¿à¤¸à¤¸à¥ सà¤à¥ यà¥à¤à¤°à¥à¤¸ दà¥à¤µà¤¾à¤°à¤¾ मà¥à¤°à¥ à¤à¥à¤²à¥à¤ मà¥à¤ पà¥à¤°à¥à¤«à¤¾à¤à¤² बनाना à¤à¤¸à¤¾à¤¨ हॠà¤à¤¯à¤¾à¥¤ सà¥à¤®à¤¿à¤¨à¤¾à¤° पà¥à¤°à¤¿à¤à¤ à¤à¤°à¤¨à¤¾ à¤à¥à¤¸à¥ à¤à¤¦à¤¿ à¤à¤¾à¤°à¥à¤¯à¥à¤ à¤à¤¾ लाठलॠरहॠहà¥à¤à¥¤"}, 97 98 { 99 name: "Dr. Mahendra Kumar Patel", role: "Guest Assistant Librarian", org: "Pt. Sundarlal Sharma Library, Pt. Ravishankar Shukla University, Raipur,Chhattisgarh", rating: 4, color1: "#B5442E", color2: "#D46A4E", 100 image: "assets/images/Mahendra Kumar Patel.jpg", 101 text: "ONOS will enable researchers to publish their research in the prestigious open access journals without any personal or institutional cost, thereby directly increasing the visibility and global impact of India's scientific production."}, 102 103 { 104 name: "Meghraj Patel", role: "Librarian", org: "Govt. Degree, College Dabhara Dist-Sakti, Chhattisgarh", rating: 4, color1: "#B5442E", color2: "#D46A4E", 105 image: "assets/images/Meghraj Patel.jpg", 106 text: "ONOS bridge the knowledge gap between urban and rural institutions. Students and teachers in rural colleges often lack access to expensive journals due to limited funding. ONOS ensures equal access of learning resources for all."} 107 ]; 108 109 // Expose text testimonials so other pages (e.g. all-testimonials.html) 110 // can reuse the same dataset without duplicating it. 111 window.ONOS_TESTIMONIALS = TESTIMONIALS; 112 113 function starString(n) { 114 return '<i class="fa-solid fa-star"></i>'.repeat(n) + '<i class="fa-regular fa-star"></i>'.repeat(5 - n); 115 } 116 117 function initTestimonials() { 118 const track = document.getElementById("testTrack"); 119 const viewport = document.getElementById("testViewport"); 120 const dotsWrap = document.getElementById("testDots"); 121 const prevBtn = document.getElementById("testPrev"); 122 const nextBtn = document.getElementById("testNext"); 123 if (!track || !viewport) 124 return; 125 126 function renderCardsMarkup(items) { 127 return items.map((t) => ` 128 <div class="testimonial-card"> 129 <div class="test-avatar"> 130 <img src="${t.image || 'assets/images/user.png'}" 131 alt="${t.name}" 132 loading="lazy" 133 class="testimonial-image"> 134 </div> 135 <p class="test-text">"${t.text}"</p> 136 <div class="test-name">${t.name}</div> 137 <div class="test-role">${t.role}, ${t.org}</div> 138 </div> 139 `).join(""); 140 } 141 142 function attachImageFallback() { 143 document.querySelectorAll('.testimonial-image').forEach(img => { 144 img.addEventListener('error', function handleError() { 145 this.removeEventListener('error', handleError); 146 this.src = 'assets/images/user.png'; 147 }); 148 }); 149 } 150 151 function setTrackLayout() { 152 const viewportWidth = Math.max(1, viewport.clientWidth || window.innerWidth); 153 const slides = Array.from(track.children); 154 155 track.style.setProperty("--slide-width", `${viewportWidth}px`); 156 track.style.setProperty("--slide-count", slides.length); 157 158 viewport.scrollTo({ 159 left: page * viewportWidth, 160 behavior: "smooth" 161 }); 162 } 163 164 function renderTrack() { 165 const pages = []; 166 for (let i = 0; i < TESTIMONIALS.length; i += perView) { 167 const slice = TESTIMONIALS.slice(i, i + perView); 168 pages.push(`<div class="testimonial-page testimonial-page--${perView}">${renderCardsMarkup(slice)}</div>`); 169 } 170 track.innerHTML = pages.join(""); 171 attachImageFallback(); 172 setTrackLayout(); 173 } 174 175 function getPerView() { 176 const w = window.innerWidth; 177 if (w <= 767) 178 return 1; 179 if (w <= 1199) 180 return 2; 181 return 3; 182 } 183 184 let perView = getPerView(); 185 let page = 0; 186 let timer = null; 187 188 function buildDots() {
189 dotsWrap.innerHTML = ""; 190 const totalPages = Math.ceil(TESTIMONIALS.length / perView); 191 for (let i = 0; i < totalPages; i++) { 192 const dot = document.createElement("span"); 193 dot.className = "dot" + (i === 0 ? " active" : ""); 194 dot.addEventListener("click", () => goToPage(i)); 195 dotsWrap.appendChild(dot); 196 } 197 } 198 199 function goToPage(index) { 200 const totalPages = Math.ceil(TESTIMONIALS.length / perView); 201 page = (index + totalPages) % totalPages; 202 setTrackLayout(); 203 [...dotsWrap.children].forEach((d, i) => d.classList.toggle("active", i === page)); 204 restartAutoplay(); 205 } 206 207 viewport.addEventListener("scroll", () => { 208 const totalPages = Math.ceil(TESTIMONIALS.length / perView); 209 const nextPage = Math.round(viewport.scrollLeft / Math.max(1, viewport.clientWidth)); 210 if (nextPage >= 0 && nextPage < totalPages && nextPage !== page) { 211 page = nextPage; 212 [...dotsWrap.children].forEach((d, i) => d.classList.toggle("active", i === page)); 213 } 214 }); 215 216 function next() { 217 const totalPages = Math.ceil(TESTIMONIALS.length / perView); 218 goToPage((page + 1) % totalPages); 219 } 220 function prev() { 221 const totalPages = Math.ceil(TESTIMONIALS.length / perView); 222 goToPage((page - 1 + totalPages) % totalPages); 223 } 224 225 function restartAutoplay() { 226 clearInterval(timer); 227 timer = setInterval(next, 5000); 228 } 229 230 prevBtn && prevBtn.addEventListener("click", prev); 231 nextBtn && nextBtn.addEventListener("click", next); 232 233 const section = document.getElementById("testimonials"); 234 section.addEventListener("mouseenter", () => clearInterval(timer)); 235 section.addEventListener("mouseleave", restartAutoplay); 236 237 window.addEventListener("resize", () => { 238 const newPerView = getPerView(); 239 if (newPerView !== perView) { 240 perView = newPerView; 241 page = 0; 242 renderTrack(); 243 buildDots(); 244 goToPage(0); 245 } else { 246 setTrackLayout(); 247 } 248 }); 249 250 renderTrack(); 251 buildDots(); 252 requestAnimationFrame(() => { 253 setTrackLayout(); 254 goToPage(0); 255 }); 256 restartAutoplay(); 257 } 258 259 document.addEventListener("DOMContentLoaded", () => { 260 initHeroSlider(); 261 initTestimonials(); 262 }); 263})(); 264 265 266/* ========================================================== 267 ONOS â Video Testimonials 268 Carousel behaviour (split from single-file build) 269 ========================================================== */ 270(function () { 271 272 // Run only after the DOM (and the #otTrack / #otViewport markup) is 273 // guaranteed to exist. Previously this whole block ran immediately at 274 // script-parse time; on a slow/cold load that raced page layout and, 275 // more importantly, its one-time width measurement below could run 276 // before layout had settled, silently leaving stepPx wrong and the 277 // track positioned off-screen until a refresh (warm cache) "fixed" it. 278 document.addEventListener("DOMContentLoaded", initVideoTestimonials); 279 280 function initVideoTestimonials() { 281 282 // --------------------------------------------------------------- 283 // 1. DATA â YouTube video links. Poster thumbnails are fetched 284 // automatically from YouTube (no local poster images needed). 285 // --------------------------------------------------------------- 286 const testimonials = [ 287 { 288 duration: "2:14", 289 video: "https://www.youtube.com/watch?v=AIuGNSd8HEk" 290 }, 291 { 292 duration: "1:48", 293 video: "https://www.youtube.com/watch?v=Ez8Hu8_u0Vo&list=PLItyJh0Uywh4&index=8" 294 }, 295 { 296 duration: "3:02", 297 video: "https://www.youtube.com/watch?v=LytZU_RHpEM&list=PLItyJh0Uywh4&index=4" 298 }, 299 { 300 duration: "2:31", 301 video: "https://www.youtube.com/watch?v=8X3VjWRbclc&list=PLItyJh0Uywh4&index=6" 302 }, 303 { 304 duration: "1:59", 305 video: "https://www.youtube.com/watch?v=1YTtS46P7mU&list=PLItyJh0Uywh4&index=3" 306 }, 307 { 308 duration: "2:47", 309 video: "https://www.youtube.com/watch?v=1XreTHuOM8I&list=PLItyJh0Uywh4&index=1" 310 }, 311 { 312 duration: "2:05", 313 video: "https://www.youtube.com/watch?v=5RCLpOY3Vwc&list=PLItyJh0Uywh4&index=7" 314 }, 315 { 316 duration: "1:36", 317 video: "https://www.youtube.com/watch?v=KlanT6H6Aso&list=PLItyJh0Uywh4&index=5" 318 } 319 ]; 320 321 // Expose so other pages (e.g. all-testimonials.html) can reuse the 322 // same dataset without duplicating it. 323 window.ONOS_VIDEO_TESTIMONIALS = testimonials; 324 325 // --------------------------------------------------------------- 326 // 2. ICONS 327 // --------------------------------------------------------------- 328 const playIcon = '<svg viewBox="0 0 24 24">
328<path d="M8 5v14l11-7z"/></svg>'; 329 const verifiedIcon = '<svg viewBox="0 0 24 24"><path d="M12 2 3.5 5.5v6.1C3.5 16.7 7.1 21 12 22c4.9-1 8.5-5.3 8.5-10.4V5.5L12 2Zm-1.1 12.9-3.4-3.4 1.4-1.4 2 2 4.6-4.6 1.4 1.4-6 6Z"/></svg>'; 330 const starIcon = '<svg viewBox="0 0 24 24"><path d="M12 2.5l2.9 6.3 6.9.7-5.2 4.7 1.5 6.8L12 17.6 5.9 21l1.5-6.8-5.2-4.7 6.9-.7L12 2.5Z"/></svg>'; 331 332 // --------------------------------------------------------------- 333 // 2b. YOUTUBE HELPERS 334 // Extract the 11-char video ID from any standard YouTube URL 335 // (watch?v=, youtu.be/, embed/, shorts/, with or without extra 336 // query params like a playlist "list"/"index") and build the 337 // auto-generated thumbnail URLs. 338 // --------------------------------------------------------------- 339 function getYouTubeId(url) { 340 if (!url) 341 return null; 342 const m = url.match(/(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|shorts\/))([a-zA-Z0-9_-]{11})/); 343 return m ? m[1] : null; 344 } 345 346 function youtubePosterUrls(ytId) { 347 return { 348 hi: `https://img.youtube.com/vi/${ytId}/maxresdefault.jpg`, 349 fallback: `https://img.youtube.com/vi/${ytId}/hqdefault.jpg` 350 }; 351 } 352 353 // --------------------------------------------------------------- 354 // 3. BUILD CARD MARKUP 355 // --------------------------------------------------------------- 356 function cardTemplate(item, index) { 357 const ytId = getYouTubeId(item.video); 358 const posters = youtubePosterUrls(ytId); 359 360 return ` 361 <div class="ot-card" data-index="${index}" data-yt-id="${ytId}"> 362 <div class="ot-video-frame"> 363 <img 364 class="ot-yt-poster" 365 src="${posters.hi}" 366 data-fallback="${posters.fallback}" 367 loading="lazy" 368 alt="Video testimonial thumbnail"> 369 370 <span class="ot-badge ot-badge-verified"> 371 ${verifiedIcon} Verified Testimonial 372 </span> 373 374 <span class="ot-badge ot-badge-duration"> 375 ${item.duration} 376 </span> 377 378 <button 379 type="button" 380 class="ot-thumb-overlay" 381 aria-label="Play testimonial video"> 382 <span class="ot-play-btn"> 383 ${playIcon} 384 </span> 385 </button> 386 </div> 387 </div> 388 `; 389 } 390 391 // --------------------------------------------------------------- 392 // 4. CAROUSEL STATE 393 // --------------------------------------------------------------- 394 const track = document.getElementById('otTrack'); 395 const viewport = document.getElementById('otViewport'); 396 const dotsWrap = document.getElementById('otDots'); 397 const prevBtn = document.getElementById('otPrev'); 398 const nextBtn = document.getElementById('otNext'); 399 400 if (!track || !viewport || !dotsWrap || !prevBtn || !nextBtn) 401 return; 402 403 const REAL = testimonials.length; // 8 404 const CLONES = 3; // matches max cards-per-view 405 406 // extended list: [last 3 clones] + [real 8] + [first 3 clones] 407 const prependSlice = testimonials.slice(REAL - CLONES); 408 const appendSlice = testimonials.slice(0, CLONES); 409 const extended = [...prependSlice, ...testimonials, ...appendSlice]; 410 411 track.innerHTML = extended.map((item, i) => cardTemplate(item, i)).join(''); 412 413 // Attach image fallback (CSP-safe) 414 track.querySelectorAll(".ot-yt-poster").forEach((img) => { 415 img.addEventListener("error", function handleError() { 416 this.removeEventListener("error", handleError); 417 418 const fallback = this.dataset.fallback; 419 if (fallback && this.src !== fallback) { 420 this.src = fallback; 421 } 422 }); 423 }); 424 425 let cards = Array.from(track.children); 426 let index = CLONES; // start pointing at real card 0 427 let cardsPerView = getCardsPerView(); 428 let stepPx = 0; 429 let isHovering = false;
430 let isVideoPlaying = false; 431 let autoplayTimer = null; 432 433 // Breakpoints match the CSS: >=768px desktop-ish shows more cards, 434 // 600-767px and below show a single card per view. 435 function getCardsPerView() { 436 const w = window.innerWidth; 437 if (w < 768) 438 return 1; 439 if (w < 1024) 440 return 2; 441 return 3; 442 } 443 444 function measure() { 445 cardsPerView = getCardsPerView(); 446 track.style.setProperty('--cpv', cardsPerView); 447 // Recalc after layout settles. A single requestAnimationFrame can still 448 // fire before the browser has finished applying the --cpv-based width 449 // on a slow/cold load, which previously left stepPx at 0 (or stale) and 450 // the track positioned off-screen â invisible until a refresh warmed 451 // the cache and layout happened to be ready in time. Double-rAF plus a 452 // width check with a short retry removes that race entirely. 453 requestAnimationFrame(() => { 454 requestAnimationFrame(() => { 455 const first = cards[0]; 456 if (!first) 457 return; 458 const gap = parseFloat(getComputedStyle(track).gap) || 0; 459 const width = first.getBoundingClientRect().width; 460 if (width > 0) { 461 stepPx = width + gap; 462 setPosition(false); 463 } else { 464 // Layout still not ready (e.g. fonts/images still loading) â try again shortly. 465 setTimeout(measure, 100); 466 } 467 }); 468 }); 469 } 470 471 function setPosition(animate) { 472 track.classList.toggle('animate', animate); 473 track.classList.toggle('no-transition', !animate); 474 track.style.transform = `translateX(${-index * stepPx}px)`; 475 } 476 477 function goTo(newIndex, animate = true) { 478 index = newIndex; 479 setPosition(animate); 480 } 481 482 function next() { 483 goTo(index + 1, true); 484 } 485 function prev() { 486 goTo(index - 1, true); 487 } 488 489 track.addEventListener('transitionend', () => { 490 if (index >= CLONES + REAL) { 491 goTo(index - REAL, false); 492 } else if (index < CLONES) { 493 goTo(index + REAL, false); 494 } 495 updateDots(); 496 }); 497 498 function updateDots() { 499 const realIndex = ((index - CLONES) % REAL + REAL) % REAL; 500 Array.from(dotsWrap.children).forEach((d, i) => { 501 d.classList.toggle('is-active', i === realIndex); 502 d.setAttribute('aria-current', i === realIndex ? 'true' : 'false'); 503 }); 504 } 505 506 // --------------------------------------------------------------- 507 // 5. DOTS 508 // --------------------------------------------------------------- 509 dotsWrap.innerHTML = testimonials.map((_, i) => 510 `<button class="ot-dot${i === 0 ? ' is-active' : ''}" role="tab" aria-label="Go to testimonial ${i + 1}"></button>` 511 ).join(''); 512 513 Array.from(dotsWrap.children).forEach((dot, i) => { 514 dot.addEventListener('click', () => { 515 goTo(CLONES + i, true); 516 stopPlaybackAndResume(); 517 }); 518 }); 519 520 // --------------------------------------------------------------- 521 // 6. NAV BUTTONS 522 // --------------------------------------------------------------- 523 prevBtn.addEventListener('click', () => { 524 prev(); 525 stopPlaybackAndResume(); 526 }); 527 nextBtn.addEventListener('click', () => { 528 next(); 529 stopPlaybackAndResume(); 530 }); 531 532 // --------------------------------------------------------------- 533 // 7. KEYBOARD ACCESSIBILITY 534 // --------------------------------------------------------------- 535 viewport.addEventListener('keydown', (e) => { 536 if (e.key === 'ArrowRight') { 537 e.preventDefault(); 538 next(); 539 stopPlaybackAndResume(); 540 } 541 if (e.key === 'ArrowLeft') { 542 e.preventDefault(); 543 prev(); 544 stopPlaybackAndResume(); 545 } 546 }); 547 548 // --------------------------------------------------------------- 549 // 8. TOUCH SWIPE SUPPORT 550 // --------------------------------------------------------------- 551 let touchStartX = 0; 552 viewport.addEventListener('touchstart', (e) => { 553 touchStartX = e.touches[0].clientX; 554 }, {passive: true}); 555 556 viewport.addEventListener('touchend', (e) => { 557 const dx = e.changedTouches[0].clientX - touchStartX; 558 if (Math.abs(dx) > 50) { 559 dx < 0 ? next() : prev(); 560 stopPlaybackAndResume(); 561 } 562 }, {passive: true}); 563 564 // --------------------------------------------------------------- 565 // 9. HOVER PAUSE 566 // --------------------------------------------------------------- 567 viewport.addEventListener('mouseenter', () => { 568 isHovering = true; 569 stopAutoplay(); 570 }); 571 viewport.addEventListener('mouseleave', () => { 572 isHovering = false;
573 if (!isVideoPlaying) 574 startAutoplay(); 575 }); 576 577 // --------------------------------------------------------------- 578 // 10. AUTOPLAY 579 // --------------------------------------------------------------- 580 function startAutoplay() { 581 stopAutoplay(); 582 autoplayTimer = setInterval(() => { 583 if (!isHovering && !isVideoPlaying) 584 next(); 585 }, 5000); 586 } 587 function stopAutoplay() { 588 if (autoplayTimer) { 589 clearInterval(autoplayTimer); 590 autoplayTimer = null; 591 } 592 } 593 function restartAutoplay() { 594 if (!isHovering && !isVideoPlaying) 595 startAutoplay(); 596 } 597 598 // --------------------------------------------------------------- 599 // 11. VIDEO PLAYBACK (YouTube, lazy-loaded iframe embed, single 600 // active player at a time). The poster/thumbnail is fetched 601 // automatically from YouTube, so nothing is embedded until the 602 // user actually presses play. 603 // --------------------------------------------------------------- 604 function pauseAllVideos(exceptCard) { 605 cards.forEach(card => { 606 if (card === exceptCard) 607 return; 608 const iframe = card.querySelector('.ot-yt-embed'); 609 if (iframe) { 610 iframe.remove(); 611 const poster = card.querySelector('.ot-yt-poster'); 612 if (poster) 613 poster.style.display = ''; 614 card.classList.remove('is-playing'); 615 } 616 }); 617 } 618 619 cards.forEach(card => { 620 const overlay = card.querySelector('.ot-thumb-overlay'); 621 const frame = card.querySelector('.ot-video-frame'); 622 const ytId = card.dataset.ytId; 623 624 overlay.addEventListener('click', () => { 625 if (!ytId) 626 return; 627 628 pauseAllVideos(card); 629 630 const poster = card.querySelector('.ot-yt-poster'); 631 if (poster) 632 poster.style.display = 'none'; 633 634 const iframe = document.createElement('iframe'); 635 iframe.className = 'ot-yt-embed'; 636 iframe.src = `https://www.youtube-nocookie.com/embed/${ytId}?autoplay=1&rel=0&modestbranding=1&playsinline=1`; 637 iframe.title = 'YouTube video player'; 638 iframe.setAttribute('frameborder', '0'); 639 iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'; 640 iframe.allowFullscreen = true; 641 frame.insertBefore(iframe, overlay); 642 643 card.classList.add('is-playing'); 644 isVideoPlaying = true; 645 stopAutoplay(); 646 }); 647 }); 648 649 // A YouTube embed inside a cross-origin iframe can't report "ended" 650 // back to us, so once the user manually navigates the carousel we 651 // stop whatever is playing and let autoplay resume normally. 652 function stopPlaybackAndResume() { 653 if (isVideoPlaying) { 654 pauseAllVideos(null); 655 isVideoPlaying = false; 656 } 657 restartAutoplay(); 658 } 659 660 // --------------------------------------------------------------- 661 // 12. RESIZE 662 // --------------------------------------------------------------- 663 let resizeTimer; 664 window.addEventListener('resize', () => { 665 clearTimeout(resizeTimer); 666 resizeTimer = setTimeout(measure, 150); 667 }); 668 669 // --------------------------------------------------------------- 670 // 13. RE-MEASURE ON FULL LOAD 671 // --------------------------------------------------------------- 672 // Web fonts and poster images can still shift card widths after 673 // DOMContentLoaded. Re-measuring once everything has finished loading 674 // guarantees the track is correctly positioned even if the very first 675 // measurement happened to run before layout was fully settled. 676 window.addEventListener('load', () => measure()); 677 678 // --------------------------------------------------------------- 679 // INIT 680 // --------------------------------------------------------------- 681 measure(); 682 updateDots(); 683 startAutoplay(); 684 685 } // end initVideoTestimonials 686 687})();
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.