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.