vendor: 6,313 bytes, lines 1-196
1/** 2 * 3 * The shadowDom / Intersection Observer version of Paul's concept: 4 * https://github.com/paulirish/lite-youtube-embed 5 * 6 * A lightweight YouTube embed. Still should feel the same to the user, just 7 * MUCH faster to initialize and paint. 8 * 9 * Thx to these as the inspiration 10 * https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html 11 * https://autoplay-youtube-player.glitch.me/ 12 * 13 * Once built it, I also found these (ðð): 14 * https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube 15 * https://github.com/Daugilas/lazyYT https://github.com/vb/lazyframe 16 */ 17export class LiteYTEmbed extends HTMLElement { 18 constructor() { 19 super(); 20 this.iframeLoaded = false; 21 this.setupDom(); 22 } 23 static get observedAttributes() { 24 return ['videoid']; 25 } 26 connectedCallback() { 27 this.addEventListener('pointerover', LiteYTEmbed.warmConnections, { 28 once: true, 29 }); 30 this.addEventListener('click', () => this.addIframe()); 31 } 32 get videoId() { 33 return encodeURIComponent(this.getAttribute('videoid') || ''); 34 } 35 set videoId(id) { 36 this.setAttribute('videoid', id); 37 } 38 get videoTitle() { 39 return this.getAttribute('videotitle') || 'Video'; 40 } 41 set videoTitle(title) { 42 this.setAttribute('videotitle', title); 43 } 44 get videoPlay() { 45 return this.getAttribute('videoPlay') || 'Play'; 46 } 47 set videoPlay(name) { 48 this.setAttribute('videoPlay', name); 49 } 50 get videoStartAt() { 51 return Number(this.getAttribute('videoStartAt') || '0'); 52 } 53 set videoStartAt(time) { 54 this.setAttribute('videoStartAt', String(time)); 55 } 56 get autoLoad() { 57 return this.hasAttribute('autoload'); 58 } 59 set autoLoad(value) { 60 if (value) { 61 this.setAttribute('autoload', ''); 62 } 63 else { 64 this.removeAttribute('autoload'); 65 } 66 } 67 get params() { 68 return `start=${this.videoStartAt}&${this.getAttribute('params')}`; 69 } 70 /** 71 * Define our shadowDOM for the component 72 */ 73 setupDom() { 74 const shadowDom = this.attachShadow({ mode: 'open' }); 75 shadowDom.innerHTML = ` 76 <style> 77 :host { 78 contain: content; 79 display: block; 80 position: relative; 81 width: 100%; 82 padding-bottom: calc(100% / (16 / 9)); 83 } 84 85 #frame, #fallbackPlaceholder, iframe { 86 position: absolute; 87 width: 100%; 88 height: 100%; 89 } 90 91 #frame { 92 cursor: pointer; 93 } 94 95 #fallbackPlaceholder { 96 object-fit: cover; 97 } 98 99 #frame::before { 100 content: ''; 101 display: block; 102 position: absolute; 103 top: 0; 104 background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==); 105 background-position: top; 106 background-repeat: repeat-x; 107 height: 60px; 108 padding-bottom: 50px; 109 width: 100%; 110 transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); 111 z-index: 1; 112 } 113 /* play button */ 114 .lty-playbtn { 115 width: 70px; 116 height: 46px; 117 background-color: #212121; 118 z-index: 1; 119 opacity: 0.8; 120 border-radius: 14%; /* TODO: Consider replacing this with YT's actual svg. Eh. */ 121 transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); 122 border: 0; 123 } 124 #frame:hover .lty-playbtn { 125 background-color: #f00; 126 opacity: 1; 127 } 128 /* play button triangle */ 129 .lty-playbtn:before { 130 content: ''; 131 border-style: solid; 132 border-width: 11px 0 11px 19px; 133 border-color: transparent transparent transparent #fff; 134 } 135 .lty-playbtn, 136 .lty-playbtn:before { 137 position: absolute; 138 top: 50%; 139 left: 50%; 140 transform: translate3d(-50%, -50%, 0); 141 } 142 143 /* Post-click styles */ 144 .lyt-activated { 145 cursor: unset; 146 } 147 148 #frame.lyt-activated::before, 149 .lyt-activated .lty-playbtn { 150 display: none; 151 } 152 </style> 153 <div id="frame"> 154 <picture> 155 <source id="webpPlaceholder" type="image/webp"> 156 <source id="jpegPlaceholder" type="image/jpeg"> 157 <img id="fallbackPlaceholder" referrerpolicy="origin"> 158 </picture> 159 <button class="lty-playbtn"></button> 160 </div> 161 `; 162 this.domRefFrame = this.shadowRoot.querySelector('#frame'); 163 this.domRefImg = { 164 fallback: this.shadowRoot.querySelector('#fallbackPlaceholder'), 165 webp: this.shadowRoot.querySelector('#webpPlaceholder'), 166 jpeg: this.shadowRoot.querySelector('#jpegPlaceholder'), 167 }; 168 this.domRefPlayButton = this.shadowRoot.querySelector('.lty-playbtn'); 169 } 170 /** 171 * Parse our attributes and fire up some placeholders 172 */ 173 setupComponent() { 174 this.initImagePlaceholder(); 175 this.domRefPlayButton.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`); 176 this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`); 177 if (this.autoLoad) { 178 this.initIntersectionObserver(); 179 } 180 } 181 /** 182 * Lifecycle method that we use to listen for attribute changes to period 183 * @param {*} name 184 * @param {*} oldVal 185 * @param {*} newVal 186 */ 187 attributeChangedCallback(name, oldVal, newVal) { 188 switch (name) { 189 case 'videoid': { 190 if (oldVal !== newVal) { 191 this.setupComponent(); 192 // if we have a previous iframe, remove it and the activated class 193 if (this.domRefFrame.classList.contains('lyt-activated')) { 194 this.domRefFrame.classList.remove('lyt-activated'); 195 this.shadowRoot.querySelector('iframe').remove(); 196 this.iframeLoaded = false;
197 } 198 } 199 break; 200 } 201 default: 202 break; 203 } 204 } 205 /** 206 * Inject the iframe into the component body 207 */ 208 addIframe() { 209 if (!this.iframeLoaded) { 210 const iframeHTML = ` 211<iframe frameborder="0" 212 allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen 213 src="https://www.youtube.com/embed/${this.videoId}?autoplay=1&${this.params}" 214></iframe>`; 215 this.domRefFrame.insertAdjacentHTML('beforeend', iframeHTML); 216 this.domRefFrame.classList.add('lyt-activated'); 217 this.iframeLoaded = true; 218 } 219 } 220 /** 221 * Setup the placeholder image for the component 222 */ 223 initImagePlaceholder() { 224 // we don't know which image type to preload, so warm the connection 225 LiteYTEmbed.addPrefetch('preconnect', 'https://i.ytimg.com/'); 226 const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/hqdefault.webp`; 227 const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`; 228 this.domRefImg.webp.srcset = posterUrlWebp; 229 this.domRefImg.jpeg.srcset = posterUrlJpeg; 230 this.domRefImg.fallback.src = posterUrlJpeg; 231 this.domRefImg.fallback.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`); 232 this.domRefImg.fallback.setAttribute('alt', `${this.videoPlay}: ${this.videoTitle}`); 233 } 234 /** 235 * Setup the Intersection Observer to load the iframe when scrolled into view 236 */ 237 initIntersectionObserver() { 238 if ('IntersectionObserver' in window && 239 'IntersectionObserverEntry' in window) { 240 const options = { 241 root: null, 242 rootMargin: '0px', 243 threshold: 0, 244 }; 245 const observer = new IntersectionObserver((entries, observer) => { 246 entries.forEach(entry => { 247 if (entry.isIntersecting && !this.iframeLoaded) { 248 LiteYTEmbed.warmConnections(); 249 this.addIframe(); 250 observer.unobserve(this); 251 } 252 }); 253 }, options); 254 observer.observe(this); 255 } 256 } 257 /** 258 * Add a <link rel={preload | preconnect} ...> to the head 259 * @param {*} kind 260 * @param {*} url 261 * @param {*} as 262 */ 263 static addPrefetch(kind, url, as) { 264 const linkElem = document.createElement('link'); 265 linkElem.rel = kind; 266 linkElem.href = url; 267 if (as) { 268 linkElem.as = as; 269 } 270 linkElem.crossOrigin = 'true'; 271 document.head.append(linkElem); 272 } 273 /** 274 * Begin preconnecting to warm up the iframe load Since the embed's netwok 275 * requests load within its iframe, preload/prefetch'ing them outside the 276 * iframe will only cause double-downloads. So, the best we can do is warm up 277 * a few connections to origins that are in the critical path. 278 * 279 * Maybe `<link rel=preload as=document>` would work, but it's unsupported: 280 * http://crbug.com/593267 But TBH, I don't think it'll happen soon with Site 281 * Isolation and split caches adding serious complexity. 282 */ 283 static warmConnections() { 284 if (LiteYTEmbed.preconnected) 285 return; 286 // Host that YT uses to serve JS needed by player, per amp-youtube 287 LiteYTEmbed.addPrefetch('preconnect', 'https://s.ytimg.com'); 288 // The iframe document and most of its subresources come right off 289 // youtube.com 290 LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com'); 291 // The botguard script is fetched off from google.com 292 LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com'); 293 // TODO: Not certain if these ad related domains are in the critical path. 294 // Could verify with domain-specific throttling. 295 LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net'); 296 LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net'); 297 LiteYTEmbed.preconnected = true; 298 } 299} 300LiteYTEmbed.preconnected = false;
301// Register custom element 302customElements.define('lite-youtube', LiteYTEmbed); 303//# sourceMappingURL=lite-youtube.js.map
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.