PageSourceSearch

https://onos.gov.in/assets/js/slider.js

js onos.gov.in collected 2026-09-24 10:27:23 UTC 30,079 bytes, 687 lines download raw bytes

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.