PageSourceSearch

https://forgames.cz/wp-content/plugins/abf-custom-blocks/src/js/intro-carousel.js?ver=1790409615

js forgames.cz collected 2026-10-08 23:36:21 UTC 18,210 bytes, 333 lines download raw bytes

1/**
2 * Intro carousel (custom/block-intro-carousel) — frontend
3 *
4 * Tabs-model carousel: lišta názvů (role=tablist) + ujíždějící progress (CSS animace
5 * vázaná na --slide-duration), autoplay s pauzou na hover ovládací lišty / focus,
6 * trvalý stop tlačítkem, mixed media (obrázek / MP4 smyčka / YouTube|Vimeo — VŽDY
7 * ztlumeně a bez ovládacích prvků, ať na kliknutí nebo data-yt-autoplay=true na
8 * pozadí), respekt k prefers-reduced-motion. Markup je předrenderovaný v save();
9 * JS jen řídí stav.
10 * Vše scopované per blok (.wp-block-custom-intro-carousel) → víc instancí je nezávislých.
11 */
12document.addEventListener('DOMContentLoaded', function () {
13    var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
14
15    // JEDEN listener pro všechny bloky (per blok by se násobil). Kromě „ready" sleduje
16    // i stav přehrávače (infoDelivery.playerState: 1 = hraje, 2 = pauza) — pauzu je
17    // nutné POTVRDIT: příkaz poslaný ve špatný moment YouTube tiše zahodí a video by
18    // hrálo pod skrytým slidem (nález review).
19    window.addEventListener('message', function (e) {
20        if (e.origin !== 'https://www.youtube.com') return;
21        var data;
22        try { data = JSON.parse(e.data); } catch (err) { return; }
23        if (!data || !data.event) return;
24        var wraps = Array.prototype.slice.call(document.querySelectorAll('.wp-block-custom-intro-carousel .hc-yt'));
25        for (var i = 0; i < wraps.length; i++) {
26            var iframe = wraps[i].querySelector('iframe');
27            if (iframe && iframe.contentWindow === e.source) {
28                wraps[i].__ytReady = true;
29                if (data.info && typeof data.info.playerState === 'number') {
30                    wraps[i].__ytState = data.info.playerState;
31                }
32                break;
33            }
34        }
35    });
36
37    // Jediný režim (rozhodnutí klienta 25. 8. 2026): VŽDY ztlumené video na pozadí bez
38    // ovládacích prvků. Zvuk v carouselu neexistuje — přes YouTube lištu šel dřív zapnout
39    // a přehrávač si volbu pamatoval mezi mounty, což rozbíjelo „vypnutý zvuk" z nastavení.
40    // controls=0 + disablekb ovládání schová; enablejsapi+origin otevírá postMessage kanál
41    // pro mute/pauzu. Regex musí umět stejné tvary URL jako editorový ytId (shorts/live,
42    // v= i jinde v query), jinak editor náhled ukáže, ale frontend nic nepřehraje.
43    function ytEmbedUrl(url) {
44        if (!url) return '';
45        var yt = url.match(/(?:youtube\.com\/(?:watch\?(?:[^#]*&)?v=|embed\/|v\/|shorts\/|live\/)|youtu\.be\/)([\w-]{6,})/);
46        if (yt) return 'https://www.youtube.com/embed/' + yt[1]
47            + '?autoplay=1&mute=1&playsinline=1&rel=0&loop=1&playlist=' + yt[1]
48            + '&controls=0&disablekb=1&iv_load_policy=3&fs=0&enablejsapi=1&origin=' + encodeURIComponent(window.location.origin || (window.location.protocol + '//' + window.location.host));
49        var vim = url.match(/vimeo\.com\/(?:video\/)?(\d+)(?:\/([0-9a-fA-F]+))?/);
50        if (vim) {
51            // unlisted videa mají privacy hash (vimeo.com/ID/hash nebo ?h=hash) — bez něj embed spadne
52            var h = vim[2] || (url.match(/[?&]h=([0-9a-fA-F]+)/) || [])[1] || '';
53            return 'https://player.vimeo.com/video/' + vim[1]
54                + '?background=1&autopause=0' // background = bez UI, ztlumeně, loop
55                + (h ? '&h=' + h : '');
56        }
57        return '';
58    }
59
60    function initCarousel(root) {
61        var slides = Array.prototype.slice.call(root.querySelectorAll('.hc-slide'));
62        var tabs = Array.prototype.slice.call(root.querySelectorAll('.hc-tab'));
63        var fills = tabs.map(function (t) { return t.querySelector('.hc-fill'); });
64        var prevBtn = root.querySelector('.hc-arrow--prev');
65        var nextBtn = root.querySelector('.hc-arrow--next');
66        var playBtn = root.querySelector('.hc-playpause');
67        var strip = root.querySelector('.hc-strip');
68        var live = root.querySelector('.hc-live');
69        var count = slides.length;
70        if (count === 0) return;
71
72        var idx = 0;
73        var autoOn = root.getAttribute('data-autoplay') === 'true' && !reduce && count > 1;
74        // Dva ODDĚLENÉ přechodné holdy (neruší autoplay režim). Jeden sdílený boolean nestačí:
75        // klik myší na šipku (mimo controls) by přes focusin zamrzl pauzu navěky (mouseleave
76        // controls nikdy nepřijde) a naopak mouseleave by rušil klávesnicovou focus-pauzu.
77        var hoverHeld = false; // myš na ovládací liště
78        var focusHeld = false; // klávesnicový fokus uvnitř bloku
79        var hoverSuppressed = false; // po kliknutí na play: hover-pauzu neobnovuj, dokud kurzor lištu neopustí
80
81        function running() { return autoOn && !hoverHeld && !focusHeld; }
82
83        function clearFills() {
84            fills.forEach(function (f) {
85                if (!f) return;
86                f.classList.remove('hc-fill--run', 'hc-fill--paused');
87                f.style.transform = 'scaleX(0)';
88                void f.offsetWidth; // reflow → reset animace
89            });
90        }
91
92        function applyFillPause() {
93            var f = fills[idx];
94            if (!f) return;
95            if (running()) f.classList.remove('hc-fill--paused');
96            else f.classList.add('hc-fill--paused');
97        }
98
99        function startProgress() {
100            clearFills();
101            var f = fills[idx];
102            if (!f) return;
103            if (reduce) { f.style.transform = 'scaleX(1)'; return; }
104            f.style.transform = '';
105            f.classList.add('hc-fill--run');
106            if (!running()) f.classList.add('hc-fill--paused');
107        }
108
109        function manageVideos() {
110            slides.forEach(function (s, k) {
111                var v = s.querySelector('video.hc-video');
112                if (!v) return;
113                if (k === idx) {
114                    v.muted = true;
115                    var p = v.play();
116                    if (p && p.catch) p.catch(function () {});
117                } else {
118                    try { v.pause(); } catch (e) {}
119                }
120            });
121        }
122
123        function announce() { if (live) live.textContent = 'Slide ' + (idx + 1) + ' z ' + count; }
124
125        function show(i, announceIt) {
126            var novy = ((i % count) + count) % count;
127            // Pauza MUSÍ odejít dřív, než slide dostane hidden: skrytý iframe prohlížeč
128            // zmrazí a postMessage se do něj už nedoručí (ověřeno — přehrávač přestane
129            // odpovídat úplně). Dřív se pauzovalo až po skrytí, potvrzení nikdy nepřišlo
130            // a pojistka video odmountovala → po návratu hrálo od začátku (hlášeno kolegy).
131            pauzujOdchazejici(novy);
132            idx = novy;
133            slides.forEach(function (s, k) { if (k === idx) s.removeAttribute('hidden'); else s.setAttribute('hidden', ''); });
134            tabs.forEach(function (t, k) {
135                var on = k === idx;
136                t.classList.toggle('is-active', on);
137                t.setAttribute('aria-selected', on ? 'true' : 'false');
138                t.tabIndex = on ? 0 : -1;
139            });
140            manageVideos();
141            manageYt();
142            if (announceIt) announce();
143            startProgress();
144        }
145
146        function updatePlayBtn() {
147            if (!playBtn) return;
148            playBtn.setAttribute('data-playing', autoOn ? 'true' : 'false');
149            playBtn.setAttribute('aria-label', autoOn ? 'Pozastavit automatické přehrávání' : 'Spustit automatické přehrávání');
150        }
151
152        // posun na další slide po dokončení animace progressu (jen když reálně běží)
153        fills.forEach(function (f, i) {
154            if (!f) return;
155            f.addEventListener('animationend', function () { if (i === idx && running()) show(idx + 1, false); });
156        });
157
158        // manuální navigace → skok + reset timeru; autoplay režim pokračuje (stop jen tlačítkem)
159        tabs.forEach(function (t, i) { t.addEventListener('click', function () { show(i, true); }); });
160        if (prevBtn) prevBtn.addEventListener('click', function () { show(idx - 1, true); });
161        if (nextBtn) nextBtn.addEventListener('click', function () { show(idx + 1, true); });
162
163        if (playBtn) playBtn.addEventListener('click', function () {
164            autoOn = !autoOn;
165            // Klik je EXPLICITNÍ záměr uživatele → ruší OBA přechodné holdy.
166            // hoverHeld: kurzor po kliknutí zůstává na liště.
167            // focusHeld: aktivace klávesnicí (Enter/Space) nechá fokus na tlačítku, takže
168            // focusout nikdy nepřijde a pauza by držela navěky — tlačítko pak jen přepnulo
169            // ikonu a nic se nedělo (hlášeno kolegy podruhé).
170            hoverHeld = false;
171            focusHeld = false;
172            hoverSuppressed = true;
173            updatePlayBtn();
174            if (autoOn) startProgress(); else applyFillPause();
175        });
176
177        // hover-pauza jen na ovládací liště (pásek s odpočtem + tlačítka) — na zbytku hera
178        // odpočet běží dál; trvalá pauza je na play/pause tlačítku
179        var controls = root.querySelector('.hc-controls');
180        if (controls) {
181            // Po kliknutí na play se hover-pauza obnoví až poté, co kurzor lištu opustí —
182            // jinak stačilo popojet myší nad širokou lištou a odpočet hned zase zamrzl.
183            controls.addEventListener('mouseenter', function () { if (hoverSuppressed) return; hoverHeld = true; applyFillPause(); });
184            controls.addEventListener('mouseleave', function () { hoverSuppressed = false; hoverHeld = false; applyFillPause(); });
185        }
186        // Focus-pauza jen pro KLÁVESNICOVÝ fokus (:focus-visible) — jinak by aktivní slide
187        // zmizel pod fokusem. Klik myší (šipky, taby) fokus taky dává, ale pauzu držet nesmí.
188        // Fallback musí být false: na prohlížečích bez :focus-visible (starší iOS Safari)
189        // by true znamenalo „každý klik myší = klávesnicový fokus" → trvalá pauza a mrtvé
190        // tlačítko. Ztratíme tam jen pauzu při fokusu; povinnou pauzu nese samotné tlačítko.
191        function kbFocus(el) { try { return el.matches(':focus-visible'); } catch (e) { return false; } }
192        root.addEventListener('focusin', function (e) { if (kbFocus(e.target)) { focusHeld = true; applyFillPause(); } });
193        root.addEventListener('focusout', function () { if (!root.contains(document.activeElement)) { focusHeld = false; applyFillPause(); } });
194
195        // klávesnice na tablistu (šipky / Home / End) — roving tabindex
196        if (strip) strip.addEventListener('keydown', function (e) {
197            var handled = true;
198            if (e.key === 'ArrowRight' || e.key === 'ArrowDown') show(idx + 1, true);
199            else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') show(idx - 1, true);
200            else if (e.key === 'Home') show(0, true);
201            else if (e.key === 'End') show(count - 1, true);
202            else handled = false;
203            if (handled) { e.preventDefault(); if (tabs[idx]) tabs[idx].focus(); }
204        });
205
206        // YouTube / Vimeo: mount/unmount iframe. Fasáda (poster + play) se ukládá,
207        // aby šla po odjetí ze slidu obnovit — jinak by skryté video hrálo dál (i se zvukem).
208
209        // Ztlumené video na pozadí se při přepnutí slidu jen PAUZUJE (nepřemountovává) —
210        // jinak startovalo pokaždé od nuly (hlášeno kolegy). Vyžaduje YT postMessage API:
211        // (a) enablejsapi+origin v URL, (b) handshake 'listening' opakovaně, dokud přehrávač
212        // neodpoví, (c) cílový origin youtube.com. Když se API do ~2 s neozve, wrap spadne
213        // do záložního režimu unmount/mount (staré chování) — nikdy nesmí hrát pod hidden.
214        function ytCmd(wrap, func) {
215            var iframe = wrap.querySelector('iframe');
216            if (!iframe || !iframe.contentWindow) return;
217            try {
218                iframe.contentWindow.postMessage(
219                    JSON.stringify({ event: 'command', func: func, args: [] }),
220                    'https://www.youtube.com'
221                );
222            } catch (e) { /* zavřený kanál — fallback řeší manageYt */ }
223        }
224        function ytHandshake(wrap) {
225            var pokusy = 0;
226            (function ping() {
227                var iframe = wrap.querySelector('iframe');
228                if (!iframe || wrap.__ytReady || pokusy++ >= 7) return;
229                try {
230                    iframe.contentWindow.postMessage(JSON.stringify({ event: 'listening', id: 'abf-hc' }), 'https://www.youtube.com');
231                } catch (e) { return; }
232                setTimeout(ping, 300);
233            }());
234        }
235
236        function mountYt(wrap) {
237            var embed = ytEmbedUrl(wrap.getAttribute('data-yt') || '');
238            if (!embed || wrap.querySelector('iframe')) return false;
239            if (wrap.__facade == null) wrap.__facade = wrap.innerHTML;
240            var iframe = document.createElement('iframe');
241            iframe.className = 'hc-iframe';
242            iframe.src = embed;
243            iframe.title = 'Video';
244            iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
245            iframe.setAttribute('allowfullscreen', '');
246            wrap.innerHTML = '';
247            wrap.appendChild(iframe);
248            wrap.__mutedMount = true;
249            wrap.__ytReady = false;
250            ytHandshake(wrap);
251            return true;
252        }
253        function unmountYt(wrap) {
254            if (wrap.querySelector('iframe') && wrap.__facade != null) {
255                wrap.innerHTML = wrap.__facade;
256                wrap.__ytReady = false;
257                wrap.__mutedMount = false;
258            }
259        }
260        // Volá se ještě PŘED nastavením hidden — iframe je v tu chvíli živý, takže
261        // příkaz reálně projde.
262        //
263        // POZOR na videa na pozadí (data-yt-autoplay): ta se ZÁMĚRNĚ nepauzují.
264        // Pozastavený YouTube přehrávač si sám vykreslí velká tlačítka uprostřed
265        // (předchozí / pauza / další) — a ta pak zůstanou viset přes obraz i po
266        // návratu na slide (hlášeno kolegy). Ztlumené video běžící pod skrytým
267        // slidem nic nestojí: prohlížeč vykreslování skrytého iframu sám utlumí.
268        // Ručně spuštěná videa se místo pauzy vracejí na fasádu (viz manageYt).
269        function pauzujOdchazejici(novyIdx) {
270            slides.forEach(function (s, k) {
271                if (k === novyIdx) return;
272                var wrap = s.querySelector('.hc-yt');
273                if (!wrap || !wrap.querySelector('iframe') || !wrap.__ytReady) return;
274                var pozadi = wrap.getAttribute('data-yt-autoplay') === 'true' && !reduce;
275                if (!pozadi) ytCmd(wrap, 'pauseVideo');
276            });
277        }
278
279        // aktivní slide s data-yt-autoplay → ztlumený autoplay na pozadí (odpočet běží dál);
280        // neaktivní slidy → iframe zůstává POZASTAVENÝ (naváže po návratu), a jen když
281        // se API vůbec neozvalo, spadne se na jistotu do fasády
282        function manageYt() {
283            slides.forEach(function (s, k) {
284                var wrap = s.querySelector('.hc-yt');
285                if (!wrap) return;
286                var pozadi = wrap.getAttribute('data-yt-autoplay') === 'true' && !reduce;
287                if (k === idx) {
288                    if (!pozadi) return;
289                    if (wrap.querySelector('iframe') && wrap.__mutedMount && wrap.__ytReady) {
290                        // Video běželo i pod skrytým slidem, takže tu nic nerestartujeme.
291                        // Mute jistíme znovu (YouTube si pamatuje, že si ho divák dřív
292                        // odemkl) a playVideo posíláme jen pro případ, že přehrávač sám
293                        // usnul — do běžícího videa je to no-op.
294                        ytCmd(wrap, 'mute');
295                        ytCmd(wrap, 'playVideo');
296                    } else {
297                        unmountYt(wrap);
298                        mountYt(wrap);
299                    }
300                } else if (wrap.querySelector('iframe')) {
301                    if (pozadi && wrap.__ytReady) {
302                        // Video na pozadí necháváme běžet — viz pauzujOdchazejici().
303                        // Nic nedělat je tu správně.
304                    } else if (wrap.__ytReady) {
305                        // Ručně spuštěné video: vrátit fasádu, ať přes obraz nezůstanou
306                        // viset ovládací prvky pozastaveného přehrávače.
307                        unmountYt(wrap);
308                    } else {
309                        // API se vůbec neozvalo → pauzu nemáme jak vynutit, zpět na fasádu
310                        unmountYt(wrap);
311                    }
312                }
313            });
314        }
315        // klik na play = spuštění videa (ztlumeně, bez ovládacích prvků) + stop rotace;
316        // delegace na wrap — fasáda se přes innerHTML obnovuje, listener na tlačítku by zanikl
317        Array.prototype.slice.call(root.querySelectorAll('.hc-yt')).forEac
317h(function (wrap) {
318            wrap.__facade = wrap.innerHTML;
319            wrap.addEventListener('click', function (e) {
320                if (!e.target.closest || !e.target.closest('.hc-yt-play')) return;
321                // rotaci zastavit jen když se video reálně namountovalo (neplatná URL nesmí
322                // jedním klikem na mrtvé tlačítko potichu vypnout celý carousel)
323                if (mountYt(wrap)) { autoOn = false; updatePlayBtn(); applyFillPause(); }
324            });
325        });
326
327        if (count < 2) root.classList.add('hc--single');
328        updatePlayBtn();
329        show(0, false);
330    }
331
332    document.querySelectorAll('.wp-block-custom-intro-carousel').forEach(initCarousel);
333});

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.