1// ============================================ 2// YouTube Facade - click-to-load embeds 3// ADD TO: global site scripts (alongside click-to-zoom.js) 4// Server renders <div class="yt-facade" data-id="..."> in place of 5// eager iframes; this injects the real player only on interaction. 6// Delegated handlers, so it works for any number of facades and for 7// content added after load. No init call required. 8// 9// ORIENTATION (new) 10// Vertical videos (Shorts) get blurred side bars when the player sits in 11// a 16:9 box. Each facade is checked once on load; videos taller than 12// 16:9 are tagged so the stylesheet can size the box to match: 13// 14// .yt-facade gets .yt-facade-tall or .yt-facade-wide 15// .video-container gets .video-container-tall (ratio > 16:9) 16// and .video-container-portrait (taller than wide) 17// plus --video-ratio = height / width (e.g. 1.7778) 18// 19// Detection order: 20// 1. data-ratio="1.7778" or data-orientation="portrait|landscape" on 21// the facade. The server can set these at save time (YouTube's 22// oEmbed reports the real size when queried with the /shorts/ URL 23// form, for any video ID). No probe is made in this case. 24// 2. Otherwise the "original aspect ratio" thumbnail 25// https://i.ytimg.com/vi/<id>/oardefault.jpg is loaded off-screen and 26// its natural dimensions are read. No API key, no CORS issue
27// (naturalWidth/Height are readable cross-origin). Anything that 28// fails to load, or comes back as ytimg's 120x90 placeholder, is 29// treated as landscape - i.e. today's behaviour. 30// 31// For tall videos the 4:3 hqdefault poster (black bars on Shorts) is 32// swapped for the true-ratio thumbnail as well. 33// 34// Facades added after load: window.YTFacade.scan(rootElement) 35// ============================================ 36 37(function () { 38 'use strict'; 39 40 var ID_RE = /^[A-Za-z0-9_-]{5,20}$/; 41 var LANDSCAPE = 9 / 16; // height / width of a 16:9 box 42 var TALL_THRESHOLD = 0.6; // taller than 16:9, with a little slack 43 var PLACEHOLDER_MAX_WIDTH = 120; // i.ytimg.com serves a 120x90 grey tile for missing thumbs 44 45 // ---------- orientation ---------- 46 47 function oarUrl(id) { 48 return 'https://i.ytimg.com/vi/' + id + '/oardefault.jpg'; 49 } 50 51 // Loads the original-aspect-ratio thumbnail; cb(img) on success, cb(null) if 52 // it is missing or is the placeholder tile. 53 function loadOarThumb(id, cb) { 54 var probe = new Image(); 55 probe.onload = function () { 56 var ok = probe.naturalWidth > PLACEHOLDER_MAX_WIDTH && probe.naturalHeight > 0; 57 cb(ok ? probe : null); 58 }; 59 probe.onerror = function () { cb(null); }; 60 probe.src = oarUrl(id); 61 } 62 63 function setPoster(facade, src) { 64 var img = facade.querySelector('img'); 65 if (img && img.src !== src) img.src = src; 66 } 67 68 function applyRatio(facade, ratio) { 69 var tall = ratio > TALL_THRESHOLD; 70 var portrait = ratio > 1; 71 72 facade.classList.add(tall ? 'yt-facade-tall' : 'yt-facade-wide'); 73 facade.dataset.ratio = ratio.toFixed(4); 74 75 var container = facade.closest('.video-container'); 76 if (container) { 77 container.classList.toggle('video-container-tall', tall); 78 container.classList.toggle('video-container-portrait', portrait); 79 if (tall) container.style.setProperty('--video-ratio', ratio.toFixed(4)); 80 } 81 return tall; 82 } 83 84 function detectOrientation(facade) { 85 // Already done (or done server-side and applied on a previous scan) 86 if (facade.classList.contains('yt-facade-tall') || 87 facade.classList.contains('yt-facade-wide')) return; 88 89 var id = facade.dataset.id; 90 if (!id || !ID_RE.test(id)) { applyRatio(facade, LANDSCAPE); return; } 91 92 // 1. Explicit value from the server / editor wins - no network probe 93 var explicit = parseFloat(facade.dataset.ratio); 94 if (!(isFinite(explicit) && explicit > 0)) { 95 if (facade.dataset.orientation === 'portrait') explicit = 16 / 9; 96 else if (facade.dataset.orientation === 'landscape') explicit = LANDSCAPE; 97 } 98 if (explicit) { 99 if (applyRatio(facade, explicit)) { 100 // Still want the true-ratio poster instead of the 4:3 one 101 loadOarThumb(id, function (img) { if (img) setPoster(facade, img.src); }); 102 } 103 return; 104 } 105 106 // 2. Probe the original-aspect-ratio thumbnail 107 loadOarThumb(id, function (img) { 108 if (!img) { applyRatio(facade, LANDSCAPE); return; } 109 var ratio = img.naturalHeight / img.naturalWidth; 110 if (applyRatio(facade, ratio)) setPoster(facade, img.src); 111 }); 112 } 113 114 function scan(root) { 115 var facades = (root || document).querySelectorAll('.yt-facade'); 116 for (var i = 0; i < facades.length; i++) detectOrientation(facades[i]); 117 } 118 119 if (document.readyState === 'loading') { 120 document.addEventListener('DOMContentLoaded', function () { scan(); }); 121 } else { 122 scan(); 123 } 124 125 // Public hook for facades injected after load (mirrors VideoResizer.refresh) 126 window.YTFacade = { scan: scan }; 127 128 // ---------- activation (unchanged) ---------- 129 130 function activate(facade) { 131 if (facade.querySelector('iframe')) return; // already activated 132 133 var id = facade.dataset.id; 134 if (!id || !ID_RE.test(id)) return; 135 136 var params = 'autoplay=1&rel=0'; 137 // Only accept a plain non-negative integer for the start offset 138 if (facade.dataset.start && /^\d{1,6}$/.test(facade.dataset.start)) { 139 params += '&start=' + facade.dataset.start; 140 } 141 142 var titleEl = facade.querySelector('.yt-facade-title'); 143 var videoTitle = titleEl ? titleEl.textContent : 'YouTube video player'; 144 145 var iframe = document.createElement('iframe'); 146 iframe.src = 'https://www.youtube-nocookie.com/embed/' + id + '?' + params; 147 iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'); 148 iframe.setAttribute('allowfullscreen', ''); 149 iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin'); 150 iframe.setAttribute('title', videoTitle); 151 iframe.setAttribute('frameborder', '0'); 152 153 facade.replaceChildren(iframe); 154 facade.removeAttribute('role'); 155 facade.removeAttribute('tabindex'); 156 facade.classList.add('yt-facade-active'); 157 } 158 159 document.addEventListener('click', function (e) { 160 var facade = e.target.closest && e.target.closest('.yt-facade'); 161 if (facade) { 162 e.preventDefault(); 163 activate(facade); 164 } 165 }); 166 167 document.addEventListener('keydown', function (e) { 168 if (e.key !== 'Enter' && e.key !== ' ') return; 169 var facade = e.target.closest && e.target.closest('.yt-facade'); 170 if (facade) { 171 e.preventDefault(); 172 activate(facade); 173 } 174 }); 175})();
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.