forked from justinribeiro/lite-youtube
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(shadowdom): lets shadow dom this thing
- Loading branch information
1 parent
54dd92d
commit 036a100
Showing
7 changed files
with
1,275 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
{ | ||
"extends": ["eslint:recommended", "google"], | ||
"parserOptions": { | ||
"ecmaVersion": 2018, | ||
"sourceType": "module" | ||
}, | ||
"env": { | ||
"browser": true, | ||
"es6": true, | ||
"node": true | ||
}, | ||
"plugins": [ | ||
"html" | ||
], | ||
"rules": { | ||
"max-len": ["error", { | ||
"ignoreTemplateLiterals": true, | ||
"ignoreStrings": true, | ||
"ignoreRegExpLiterals": true | ||
}], | ||
"no-var": "error", | ||
"require-jsdoc": "off", | ||
"arrow-parens": "off", | ||
"no-console": "off", | ||
"new-cap": "off", | ||
"indent": [2, 2], | ||
"brace-style": [2, "1tbs"], | ||
"no-loop-func": "error", | ||
"no-await-in-loop": "error", | ||
"no-useless-call": "error", | ||
"padded-blocks": ["error", { | ||
"blocks": "never", | ||
"classes": "never", | ||
"switches": "never" | ||
}], | ||
"space-in-parens": "error", | ||
"singleQuote": true | ||
}, | ||
"globals": { | ||
"customElements": false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,60 @@ | ||
# lite-youtube | ||
The shadowDom web component version of Paul's lite-youtube-embed. | ||
[![npm version](https://badge.fury.io/js/%40justinribeiro%2Flite-youtube.svg)](https://badge.fury.io/js/%40justinribeiro%2Flite-youtube) | ||
|
||
# \<lite-youtube\> | ||
|
||
> A web component that displays render YouTube embeds faster. The shadowDom web component version of Paul's [lite-youtube-embed](https://github.com/paulirish/lite-youtube-embed). | ||
## Features | ||
|
||
* No dependencies; it's just a vanilla web component. | ||
* It's fast yo. | ||
* It's shadowDom encapsulated! | ||
* It's responsive (just style it like you normally would with height and width and things) | ||
|
||
## Install | ||
|
||
This web component is built with ES modules in mind and is | ||
available on NPM: | ||
|
||
Install code-block: | ||
|
||
```sh | ||
npm i @justinribeiro/lite-youtube | ||
# or | ||
yarn add @justinribeiro/lite-youtube | ||
``` | ||
|
||
After install, import into your project: | ||
|
||
```js | ||
import '@justinribeiro/lite-youtube'; | ||
``` | ||
|
||
Finally, use as required: | ||
|
||
```html | ||
<lite-youtube videoid="guJLfqTFfIw"></lite-youtube> | ||
``` | ||
|
||
## Install with CDN | ||
|
||
If you want the paste-and-go version, you can simply load it via CDN: | ||
|
||
```html | ||
<script type="module" src="https://cdn.jsdelivr.net/npm/@justinribeiro/lite-youtube@0.1.0/lite-youtube.js"> | ||
``` | ||
Finally, use as required: | ||
```html | ||
<lite-youtube videoid="guJLfqTFfIw"></lite-youtube> | ||
``` | ||
## Attributes | ||
The web component allows certain attributes to be give a little additional | ||
flexibility. | ||
| Name | Description | Default | | ||
| --- | --- | --- | | ||
| `videoid` | The YouTube videoid | `guJLfqTFfIw` | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes"> | ||
<title>lite-youtube demo</title> | ||
<script type="module" src="../lite-youtube.js"></script> | ||
</head> | ||
<body> | ||
<h3>Basic Usage</h3> | ||
<pre> | ||
<lite-youtube videoid="guJLfqTFfIw"></lite-youtube> | ||
</pre> | ||
<lite-youtube videoid="guJLfqTFfIw"></lite-youtube> | ||
|
||
<h3>Style It</h3> | ||
<p>Add a class or just style it directly. Height and Width are responsive in the container (there is a min-height requirement of 315px to make the basic embed work easier).</p> | ||
<pre> | ||
<lite-youtube videoid="guJLfqTFfIw" style="width: 400px; height: 400px"></lite-youtube> | ||
</pre> | ||
<lite-youtube videoid="guJLfqTFfIw" style="width: 400px; height: 400px"></lite-youtube> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
/** | ||
* | ||
* The shadowDom version of Paul's concept: https://github.com/paulirish/lite-youtube-embed | ||
* | ||
* A lightweight youtube embed. Still should feel the same to the user, just MUCH faster to initialize and paint. | ||
* | ||
* Thx to these as the inspiration | ||
* https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html | ||
* https://autoplay-youtube-player.glitch.me/ | ||
* | ||
* Once built it, I also found these: | ||
* https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube (👍👍) | ||
* https://github.com/Daugilas/lazyYT | ||
* https://github.com/vb/lazyframe | ||
*/ | ||
class LiteYTEmbed extends HTMLElement { | ||
static get observedAttributes() { | ||
return ['videoid']; | ||
} | ||
|
||
connectedCallback() { | ||
// On hover (or tap), warm up the TCP connections we're (likely) about to use. | ||
this.addEventListener('pointerover', LiteYTEmbed.warmConnections, { | ||
once: true, | ||
}); | ||
// Once the user clicks, add the real iframe and drop our play button | ||
// TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time | ||
// We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003 | ||
this.addEventListener('click', e => this.addIframe()); | ||
this.setupDom(); | ||
this.setupComponent(); | ||
} | ||
|
||
setupDom() { | ||
const shadowDom = this.attachShadow({mode: 'open'}); | ||
shadowDom.innerHTML = ` | ||
<style> | ||
:host { | ||
contain: strict; | ||
display: block; | ||
position: relative; | ||
width: 100%; | ||
min-height: 315px; | ||
} | ||
#frame { | ||
width: 100%; | ||
height: 100%; | ||
background-color: #000; | ||
background-position: center center; | ||
background-size: cover; | ||
cursor: pointer; | ||
position: fixed; | ||
} | ||
iframe { | ||
position: fixed; | ||
width: 100%; | ||
height: 100%; | ||
} | ||
/* gradient */ | ||
#frame::before { | ||
content: ''; | ||
display: block; | ||
position: absolute; | ||
top: 0; | ||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==); | ||
background-position: top; | ||
background-repeat: repeat-x; | ||
height: 60px; | ||
padding-bottom: 50px; | ||
width: 100%; | ||
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); | ||
} | ||
/* play button */ | ||
.lty-playbtn { | ||
width: 70px; | ||
height: 46px; | ||
background-color: #212121; | ||
z-index: 1; | ||
opacity: 0.8; | ||
border-radius: 14%; /* TODO: Consider replacing this with YT's actual svg. Eh. */ | ||
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); | ||
} | ||
#frame:hover .lty-playbtn { | ||
background-color: #f00; | ||
opacity: 1; | ||
} | ||
/* play button triangle */ | ||
.lty-playbtn:before { | ||
content: ''; | ||
border-style: solid; | ||
border-width: 11px 0 11px 19px; | ||
border-color: transparent transparent transparent #fff; | ||
} | ||
.lty-playbtn, | ||
.lty-playbtn:before { | ||
position: absolute; | ||
top: 50%; | ||
left: 50%; | ||
transform: translate3d(-50%, -50%, 0); | ||
} | ||
/* Post-click styles */ | ||
.lyt-activated { | ||
cursor: unset; | ||
} | ||
.lyt-activated::before, | ||
.lyt-activated .lty-playbtn { | ||
display: none; | ||
} | ||
</style> | ||
<div id="frame"> | ||
<div class="lty-playbtn"></div> | ||
</div> | ||
`; | ||
this.__domRefFrame = this.shadowRoot.querySelector('#frame'); | ||
} | ||
|
||
setupComponent() { | ||
// Gotta encode the untrusted value | ||
// https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-2---attribute-escape-before-inserting-untrusted-data-into-html-common-attributes | ||
this.videoId = encodeURIComponent(this.getAttribute('videoid')); | ||
|
||
/** | ||
* Lo, the youtube placeholder image! (aka the thumbnail, poster image, etc) | ||
* There is much internet debate on the reliability of thumbnail URLs. Weak consensus is that you | ||
* cannot rely on anything and have to use the YouTube Data API. | ||
* | ||
* amp-youtube also eschews using the API, so they just try sddefault with a hqdefault fallback: | ||
* https://github.com/ampproject/amphtml/blob/6039a6317325a8589586e72e4f98c047dbcbf7ba/extensions/amp-youtube/0.1/amp-youtube.js#L498-L537 | ||
* For now I'm gonna go with this confident (lol) assersion: https://stackoverflow.com/a/20542029, though I'll use `i.ytimg` to optimize for origin reuse. | ||
* | ||
* Worth noting that sddefault is _higher_ resolution than hqdefault. Naming is hard. ;) | ||
* From my own testing, it appears that hqdefault is ALWAYS there sddefault is missing for ~10% of videos | ||
* | ||
* TODO: Do the sddefault->hqdefault fallback | ||
* - When doing this, apply referrerpolicy (https://github.com/ampproject/amphtml/pull/3940) | ||
* TODO: Consider using webp if supported, falling back to jpg | ||
*/ | ||
this.posterUrl = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`; | ||
// Warm the connection for the poster image | ||
LiteYTEmbed.addPrefetch('preload', this.posterUrl, 'image'); | ||
// TODO: support dynamically setting the attribute via attributeChangedCallback | ||
this.__domRefFrame.style.backgroundImage = `url("${this.posterUrl}")`; | ||
} | ||
|
||
/** | ||
* Lifecycle method that we use to listen for attribute changes to period | ||
* @param {*} name | ||
* @param {*} oldVal | ||
* @param {*} newVal | ||
*/ | ||
attributeChangedCallback(name, oldVal, newVal) { | ||
switch (name) { | ||
case 'videoid': { | ||
if (oldVal !== newVal) { | ||
this.setupComponent(); | ||
this.__domRefFrame.classList.remove('lyt-activated'); | ||
this.shadowRoot.querySelector('iframe').remove(); | ||
} | ||
break; | ||
} | ||
default: | ||
break; | ||
} | ||
} | ||
|
||
/** | ||
* Add a <link rel={preload | preconnect} ...> to the head | ||
*/ | ||
static addPrefetch(kind, url, as) { | ||
const linkElem = document.createElement('link'); | ||
linkElem.rel = kind; | ||
linkElem.href = url; | ||
if (as) { | ||
linkElem.as = as; | ||
} | ||
linkElem.crossorigin = true; | ||
document.head.append(linkElem); | ||
} | ||
|
||
/** | ||
* Begin preconnecting to warm up the iframe load | ||
* Since the embed's netwok requests load within its iframe, | ||
* preload/prefetch'ing them outside the iframe will only cause double-downloads. | ||
* So, the best we can do is warm up a few connections to origins that are in the critical path. | ||
* | ||
* Maybe `<link rel=preload as=document>` would work, but it's unsupported: http://crbug.com/593267 | ||
* But TBH, I don't think it'll happen soon with Site Isolation and split caches adding serious complexity. | ||
*/ | ||
static warmConnections() { | ||
if (LiteYTEmbed.preconnected) return; | ||
// The iframe document and most of its subresources come right off youtube.com | ||
LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com'); | ||
// The botguard script is fetched off from google.com | ||
LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com'); | ||
// Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling. | ||
LiteYTEmbed.addPrefetch( | ||
'preconnect', | ||
'https://googleads.g.doubleclick.net', | ||
); | ||
LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net'); | ||
LiteYTEmbed.preconnected = true; | ||
} | ||
|
||
addIframe() { | ||
// https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-2---attribute-escape-before-inserting-untrusted-data-into-html-common-attributes | ||
const escapedVideoId = encodeURIComponent(this.videoId); | ||
const iframeHTML = ` | ||
<iframe width="560" height="315" frameborder="0" | ||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen | ||
src="https://www.youtube.com/embed/${escapedVideoId}?autoplay=1" | ||
></iframe>`; | ||
this.__domRefFrame.insertAdjacentHTML('beforeend', iframeHTML); | ||
this.__domRefFrame.classList.add('lyt-activated'); | ||
} | ||
} | ||
// Register custom element | ||
customElements.define('lite-youtube', LiteYTEmbed); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"name": "@justinribeiro/lite-youtube", | ||
"description": "A web component that loads YouTube embed iframes faster. ShadowDom based version of Paul Irish' concept.", | ||
"author": "Justin Ribeiro <justin@justinribeiro.com>", | ||
"repository": { | ||
"type": "git", | ||
"url": "git@github.com:justinribeiro/lite-youtube.git" | ||
}, | ||
"license": "MIT", | ||
"version": "0.1.0", | ||
"main": "lite-youtube.js", | ||
"module": "lite-youtube.js", | ||
"keywords": [ | ||
"web components", | ||
"youtube" | ||
], | ||
"devDependencies": { | ||
"eslint": "^6.6.0", | ||
"eslint-config-google": "^0.14.0", | ||
"eslint-plugin-html": "^6.0.0" | ||
} | ||
} |
Oops, something went wrong.