PageSourceSearch

https://cdn.clarku.edu/wp-content/plugins/clarku-component-library…/view.js?ver=5fc397d58c3801f32ef3

js clarku.edu collected 2026-09-24 06:20:38 UTC 16,170 bytes, 377 lines download raw bytes

1/******/ (() => { // webpackBootstrap
2/******/ 	var __webpack_modules__ = ({
3
4/***/ "./src/youtube-lite/lite-yt-embed.js"
5/*!*******************************************!*\
6  !*** ./src/youtube-lite/lite-yt-embed.js ***!
7  \*******************************************/
8() {
9
10/**
11 * A lightweight youtube embed. Still should feel the same to the user, just MUCH faster to initialize and paint.
12 *
13 * Thx to these as the inspiration
14 *   https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
15 *   https://autoplay-youtube-player.glitch.me/
16 *
17 * Once built it, I also found these:
18 *   https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube (👍👍)
19 *   https://github.com/Daugilas/lazyYT
20 *   https://github.com/vb/lazyframe
21 */
22class LiteYTEmbed extends HTMLElement {
23  constructor() {
24    super();
25    this.currentContainer = null;
26    let ltYtClose = document.querySelectorAll('.modal button.close');
27    ltYtClose.forEach(close => {
28      close.addEventListener('click', e => this.closeStopYTPlayer(e.target.parentElement), false);
29    });
30  }
31  connectedCallback() {
32    this.videoId = this.getAttribute('videoid');
33    let playBtnEl = this.querySelector('.lyt-playbtn,.lty-playbtn');
34    // A label for the button takes priority over a [playlabel] attribute on the custom-element
35    this.playLabel = playBtnEl && playBtnEl.textContent.trim() || this.getAttribute('playlabel') || 'Play';
36    this.dataset.title = this.getAttribute('title') || "";
37
38    /**
39     * Lo, the youtube poster image!  (aka the thumbnail, image placeholder, etc)
40     *
41     * See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md
42     */
43    if (!this.style.backgroundImage) {
44      this.style.backgroundImage = `url("https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg")`;
45      this.upgradePosterImage();
46    }
47
48    // Set up play button, and its visually hidden label
49    if (!playBtnEl) {
50      playBtnEl = document.createElement('button');
51      playBtnEl.type = 'button';
52      // Include the mispelled 'lty-' in case it's still being used. https://github.com/paulirish/lite-youtube-embed/issues/65
53      playBtnEl.classList.add('lyt-playbtn', 'lty-playbtn');
54      this.append(playBtnEl);
55    }
56    if (!playBtnEl.textContent) {
57      const playBtnLabelEl = document.createElement('span');
58      playBtnLabelEl.className = 'lyt-visually-hidden';
59      playBtnLabelEl.textContent = this.playLabel;
60      playBtnEl.append(playBtnLabelEl);
61    }
62    this.addNoscriptIframe();
63
64    // for the PE pattern, change anchor's semantics to button
65    if (playBtnEl.nodeName === 'A') {
66      playBtnEl.removeAttribute('href');
67      playBtnEl.setAttribute('tabindex', '0');
68      playBtnEl.setAttribute('role', 'button');
69      // fake button needs keyboard help
70      playBtnEl.addEventListener('keydown', e => {
71        if (e.key === 'Enter' || e.key === ' ') {
72          e.preventDefault();
73          this.activate();
74        }
75      });
76    }
77
78    // On hover (or tap), warm up the TCP connections we're (likely) about to use.
79    this.addEventListener('pointerover', LiteYTEmbed.warmConnections, {
80      once: true
81    });
82    this.addEventListener('focusin', LiteYTEmbed.warmConnections, {
83      once: true
84    });
85
86    // Once the user clicks, add the real iframe and drop our play button
87    // TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time
88    //   We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003
89    this.addEventListener('click', this.activate);
90
91    // Chrome & Edge desktop have no problem with the basic YouTube Embed with ?autoplay=1
92    // However Safari desktop and most/all mobile browsers do not successfully track the user gesture of clicking through the creation/loading of the iframe,
93    // so they don't autoplay automatically. Instead we must load an additional 2 sequential JS files (1KB + 165KB) (un-br) for the YT Player API
94    // TODO: Try loading the the YT API in parallel with our iframe and then attaching/playing it. #82
95    this.needsYTApi = this.hasAttribute("js-api") || navigator.vendor.includes('Apple') || navigator.userAgent.includes('Mobi');
96  }
97
98  /**
99   * Add a <link rel={preload | preconnect} ...> to the head
100   */
101  static addPrefetch(kind, url, as) {
102    const linkEl = document.createElement('link');
103    linkEl.rel = kind;
104    linkEl.href = url;
105    if (as) {
106      linkEl.as = as;
107    }
108    document.head.append(linkEl);
109  }
110
111  /**
112   * Begin pre-connecting to warm up the iframe load
113   * Since the embed's network requests load within its iframe,
114   *   preload/prefetch'ing them outside the iframe will only cause double-downloads.
115   * So, the best we can do is warm up a few connections to origins that are in the critical path.
116   *
117   * Maybe `<link rel=preload as=document>` would work, but it's unsupported: http://crbug.com/593267
118   * But TBH, I don't think it'll happen soon with Site Isolation and split caches adding serious complexity.
119   */
120  static warmConnections() {
121    if (LiteYTEmbed.preconnected) return;
122
123    // The iframe document and most of its subresources come right off youtube.com
124    LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com');
125    // The botguard script is fetched off from google.com
126    LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com');
127
128    // Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling.
129    LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net');
130    LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net');
131    LiteYTEmbed.preconnected = true;
132  }
133  fetchYTPlayerApi() {
134    if (window.YT || window.YT && window.YT.Player) return;
135    this.ytApiPromise = new Promise((res, rej) => {
136      var el = document.createElement('script');
137      el.src = 'https://www.youtube.com/iframe_api';
138      el.async = true;
139      el.onload = _ => {
140        YT.ready(res);
141      };
142      el.onerror = rej;
143      this.append(el);
144    });
145  }
146
147  /** Return the YT Player API instance. (Public L-YT-E API) */
148  async getYTPlayer() {
149    if (!this.playerPromise) {
150      await this.activate();
151    }
152    return this.playerPromise;
153  }
154  async addYTPlayerIframe() {
155    this.fetchYTPlayerApi();
156    await this.ytApiPromise;
157    const videoPlaceholderEl = document.createElement('div');
158    this.append(videoPlaceholderEl);
159    const paramsObj = Object.fromEntries(this.getParams().entries());
160    this.playerPromise = new Promise(resolve => {
161      let player = new YT.Player(videoPlaceholderEl, {
162        width: '100%',
163        videoId: this.videoId,
164        playerVars: paramsObj,
165        events: {
166          'onReady': event => {
167            event.target.playVideo();
168            resolve(player);
169          }
170        }
171      });
172    });
173  }
174
175  // Add the iframe within <noscript> for indexability discoverability. See https://github.com/paulirish/lite-youtube-embed/issues/105
176  addNoscriptIframe() {
177    const iframeEl = this.createBasicIframe();
178    const noscriptEl = document.createElement('noscript');
179    // Appending into noscript isn't equivalant for mysterious reasons: https://html.spec.whatwg.org/multipage/scripting.html#the-noscript-element
180    noscriptEl.innerHTML = iframeEl.outerHTML;
181    this.append(noscriptEl);
182  }
183  getParams() {
184    const params = new URLSearchParams(this.getAttribute('params') || []);
185    params.append('autoplay', '1');
186    params.append('playsinline', '1');
187    return params;
188  }
189  async activate() {
190    if (this.classList.contains('lyt-activated')) return;
191    if (this.classList.contains('lyt-closing')) return;
192    let modal = this.parentElement;
193    if (modal.classList.contains('modal')) {
194      this.currentContainer = this.parentElement.parentElement; //set current container to reference later, part of safari 16.5 fix
195      let p = modal.style;
196      document.documentElement.style.overflow = 'hidden'; //Prevent Scrolling so return animation doesn't break
197      p.width = this.parentElement.getBoundingClientRect().width + 'px'; //
198      p.height = this.parentElement.getBoundingClientRect().height + 'px'; //    Calculate Video Origin for animation starting spot
199      p.top = this.parentElement.getBoundingClientRect().top + 'px'; //
200      p.left = this.parentElement.getBoundingClientRect().left + 'px'; //
201      modal.parentElement.style.height = modal.parentElement.getBoundingClientRect().height + 'px';
202      p.position = 'fixed';
203      document.documentElement.append(this.parentElement); //move out of body for safari 16.5 fix
204      await new Promise(r => setTimeout(r, 0)); //Forces browser to wait for previous function to finish
205      this.classList.add('lyt-activated');
206      await new Promise(r => setTimeout(r, 150)); // Wait for animation before adding iframe, prevents glitchy animation
207      modal.addEventListener('click', e => {
208        this.closeStopYTPlayer();
209      }, {
210        once: true
211      });
212    } else {
213      this.classList.add('lyt-activated');
214    }
215    if (this.needsYTApi) {
216      return this.addYTPlayerIframe(this.getParams());
217    }
218    const iframeEl = this.createBasicIframe();
219    this.append(iframeEl);
220
221    // Set focus for a11y
222    iframeEl.focus();
223  }
224  async closeStopYTPlayer() {
225    let el = this;
226    let player = await this.getYTPlayer();
227    player.stopVideo();
228    el.classList.add('lyt-closing');
229    el.classList.remove('lyt-activated');
230    try {
231      el.removeChild(el.querySelector('iframe'));
232    } catch (e) {} //prevent errors from user double clicking showing in console
233    await new Promise(r => setTimeout(r, 150)); //throttle activation for 1 second for animation
234    el.currentContainer.append(el.parentElement); //safari 16.5 fix
235    el.parentElement.style.removeProperty('position');
236    el.classList.remove('lyt-closing');
237    ['height', 'width', 'top', 'left'].forEach(prop => el.parentElement.style.removeProperty(prop));
238    el.parentElement.parentElement.style.removeProperty('height');
239    document.documentElement.style.removeProperty('overflow');
240  }
241  createBasicIframe() {
242    const iframeEl = document.createElement('iframe');
243    iframeEl.width = 560;
244    iframeEl.height = 315;
245    // No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include
246    iframeEl.title = this.playLabel;
247    iframeEl.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture';
248    iframeEl.allowFullscreen = true;
249    // AFAIK, the encoding here isn't necessary for XSS, but we'll do it only because this is a URL
250    // https://stackoverflow.com/q/64959723/89484
251    iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${this.getParams().toString()}`;
252    return iframeEl;
253  }
254
255  /**
256   * In the spirit of the `lowsrc` attribute and progressive JPEGs, we'll upgrade the reliable
257   * poster image to a higher resolution one, if it's available.
258   * Interestingly this sddefault webp is often smaller in filesize, but we will still attempt it second
259   * because getting _an_ image in front of the user if our first priority.
260   *
261   * See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md for more details
262   */
263  upgradePosterImage() {
264    // Defer to reduce network contention.
265    setTimeout(() => {
266      const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/sddefault.webp`;
267      const img = new Image();
268      img.fetchPriority = 'low'; // low priority to reduce network contention
269      img.referrerpolicy = 'origin'; // Not 100% sure it's needed, but https://github.com/ampproject/amphtml/pull/3940
270      img.src = webpUrl;
271      img.onload = e => {
272        // A pretty ugly hack since onerror won't fire on YouTube image 404. This is (probably) due to
273        // Youtube's style of returning data even with a 404 status. That data is a 120x90 placeholder image.
274        // … per "annoying yt 404 behavior" in the .md
275        const noAvailablePoster = e.target.naturalHeight == 90 && e.target.naturalWidth == 120;
276        if (noAvailablePoster) return;
277        this.style.backgroundImage = `url("${webpUrl}")`;
278      };
279    }, 100);
280  }
281}
282// Register custom element
283customElements.define('lite-youtube', LiteYTEmbed);
284
285/***/ }
286
287/******/ 	});
288/************************************************************************/
289/******/ 	// The module cache
290/******/ 	var __webpack_module_cache__ = {};
291/******/ 	
292/******/ 	// The require function
293/******/ 	function __webpack_require__(moduleId) {
294/******/ 		// Check if module is in cache
295/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
296/******/ 		if (cachedModule !== undefined) {
297/******/ 			return cachedModule.exports;
298/******/ 		}
299/******/ 		// Check if module exists (development only)
300/******/ 		if (__webpack_modules__[moduleId] === undefined) {
301/******/ 			var e = new Error("Cannot find module '" + moduleId + "'");
302/******/ 			e.code = 'MODULE_NOT_FOUND';
303/******/ 			throw e;
304/******/ 		}
305/******/ 		// Create a new module (and put it into the cache)
306/******/ 		var module = __webpack_module_cache__[moduleId] = {
307/******/ 			// no module.id needed
308/******/ 			// no module.loaded needed
309/******/ 			exports: {}
310/******/ 		};
311/******/ 	
312/******/ 		// Execute the module function
313/******/ 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
314/******/ 	
315/******/ 		// Return the exports of the module
316/******/ 		return module.exports;
317/******/ 	}
318/******/ 	
319/************************************************************************/
320/******/ 	/* webpack/runtime/compat get default export */
321/******/ 	(() => {
322/******/ 		// getDefaultExport function for compatibility with non-harmony modules
323/******/ 		__webpack_require__.n = (module) => {
vendor: 2,193 bytes, lines 324-377
324/******/ 			var getter = module && module.__esModule ?
325/******/ 				() => (module['default']) :
326/******/ 				() => (module);
327/******/ 			__webpack_require__.d(getter, { a: getter });
328/******/ 			return getter;
329/******/ 		};
330/******/ 	})();
331/******/ 	
332/******/ 	/* webpack/runtime/define property getters */
333/******/ 	(() => {
334/******/ 		// define getter functions for harmony exports
335/******/ 		__webpack_require__.d = (exports, definition) => {
336/******/ 			for(var key in definition) {
337/******/ 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
338/******/ 					Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
339/******/ 				}
340/******/ 			}
341/******/ 		};
342/******/ 	})();
343/******/ 	
344/******/ 	/* webpack/runtime/hasOwnProperty shorthand */
345/******/ 	(() => {
346/******/ 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
347/******/ 	})();
348/******/ 	
349/******/ 	/* webpack/runtime/make namespace object */
350/******/ 	(() => {
351/******/ 		// define __esModule on exports
352/******/ 		__webpack_require__.r = (exports) => {
353/******/ 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
354/******/ 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
355/******/ 			}
356/******/ 			Object.defineProperty(exports, '__esModule', { value: true });
357/******/ 		};
358/******/ 	})();
359/******/ 	
360/************************************************************************/
361var __webpack_exports__ = {};
362// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
363(() => {
364"use strict";
365/*!**********************************!*\
366  !*** ./src/youtube-lite/view.js ***!
367  \**********************************/
368__webpack_require__.r(__webpack_exports__);
369/* harmony import */ var _lite_yt_embed_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./lite-yt-embed.js */ "./src/youtube-lite/lite-yt-embed.js");
370/* harmony import */ var _lite_yt_embed_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_lite_yt_embed_js__WEBPACK_IMPORTED_MODULE_0__);
371
372//import './clark-yt-embed.js'
373})();
374
375/******/ })()
376;
377//# sourceMappingURL=view.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.