Skip to content

Commit

Permalink
feat(shadowdom): lets shadow dom this thing
Browse files Browse the repository at this point in the history
  • Loading branch information
justinribeiro committed Nov 15, 2019
1 parent 54dd92d commit 036a100
Show file tree
Hide file tree
Showing 7 changed files with 1,275 additions and 2 deletions.
42 changes: 42 additions & 0 deletions .eslintrc.json
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
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
62 changes: 60 additions & 2 deletions README.md
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` |
23 changes: 23 additions & 0 deletions demo/index.html
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>
&lt;lite-youtube videoid=&quot;guJLfqTFfIw&quot;&gt;&lt;/lite-youtube&gt;
</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>
&lt;lite-youtube videoid=&quot;guJLfqTFfIw&quot; style=&quot;width: 400px; height: 400px&quot;&gt;&lt;/lite-youtube&gt;
</pre>
<lite-youtube videoid="guJLfqTFfIw" style="width: 400px; height: 400px"></lite-youtube>
</body>
</html>
220 changes: 220 additions & 0 deletions lite-youtube.js
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);
22 changes: 22 additions & 0 deletions package.json
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"
}
}
Loading

0 comments on commit 036a100

Please sign in to comment.