PageSourceSearch

https://www.madisonpipe.com/static/lite-youtube.js

js madisonpipe.com collected 2026-10-04 21:25:06 UTC 10,659 bytes, 303 lines download raw bytes

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.