youtube-video-element
Advanced tools
Comparing version 1.0.1 to 1.1.0
{ | ||
"name": "youtube-video-element", | ||
"version": "1.0.1", | ||
"version": "1.1.0", | ||
"description": "Custom element (web component) for the YouTube player.", | ||
"type": "module", | ||
"main": "youtube-video-element.js", | ||
"files": [], | ||
"scripts": { | ||
"lint": "npx eslint@8 *.js -c ./node_modules/wet-run/.eslintrc.json", | ||
"test": "wet run", | ||
"dev": "wet serve" | ||
}, | ||
"repository": "muxinc/youtube-video-element", | ||
"author": "@muxinc", | ||
@@ -20,3 +11,25 @@ "license": "MIT", | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/muxinc/media-elements.git", | ||
"directory": "packages/youtube-video-element" | ||
}, | ||
"files": [ | ||
"dist" | ||
], | ||
"type": "module", | ||
"main": "youtube-video-element.js", | ||
"exports": { | ||
".": "./youtube-video-element.js", | ||
"./react": "./dist/react.js" | ||
}, | ||
"scripts": { | ||
"lint": "npx eslint@8 *.js", | ||
"test": "wet run", | ||
"serve": "wet serve", | ||
"build:react": "build-react", | ||
"build": "run-s build:*" | ||
}, | ||
"devDependencies": { | ||
"npm-run-all": "^4.1.5", | ||
"wet-run": "^1.2.2" | ||
@@ -23,0 +36,0 @@ }, |
@@ -10,23 +10,58 @@ // https://developers.google.com/youtube/iframe_api_reference | ||
const templateShadowDOM = globalThis.document?.createElement('template'); | ||
if (templateShadowDOM) { | ||
templateShadowDOM.innerHTML = /*html*/` | ||
<style> | ||
:host { | ||
display: inline-block; | ||
line-height: 0; | ||
position: relative; | ||
min-width: 300px; | ||
min-height: 150px; | ||
} | ||
iframe { | ||
position: absolute; | ||
top: 0; | ||
left: 0; | ||
} | ||
</style> | ||
function getTemplateHTML(attrs) { | ||
const iframeAttrs = { | ||
src: serializeIframeUrl(attrs), | ||
frameborder: 0, | ||
width: '100%', | ||
height: '100%', | ||
allow: 'accelerometer; fullscreen; autoplay; encrypted-media; gyroscope; picture-in-picture', | ||
}; | ||
return /*html*/` | ||
<style> | ||
:host { | ||
display: inline-block; | ||
line-height: 0; | ||
position: relative; | ||
min-width: 300px; | ||
min-height: 150px; | ||
} | ||
iframe { | ||
position: absolute; | ||
top: 0; | ||
left: 0; | ||
} | ||
</style> | ||
<iframe${serializeAttributes(iframeAttrs)}></iframe> | ||
`; | ||
} | ||
function serializeIframeUrl(attrs) { | ||
if (!attrs.src) return; | ||
const matches = attrs.src.match(MATCH_SRC); | ||
const srcId = matches && matches[1]; | ||
const params = { | ||
// ?controls=true is enabled by default in the iframe | ||
controls: attrs.controls === '' ? null : 0, | ||
autoplay: attrs.autoplay, | ||
loop: attrs.loop, | ||
mute: attrs.muted, | ||
playsinline: attrs.playsinline, | ||
preload: attrs.preload ?? 'metadata', | ||
// origin: globalThis.location?.origin, | ||
enablejsapi: 1, | ||
showinfo: 0, | ||
rel: 0, | ||
iv_load_policy: 3, | ||
modestbranding: 1, | ||
}; | ||
return `${EMBED_BASE}/${srcId}?${serialize(params)}`; | ||
} | ||
class YoutubeVideoElement extends (globalThis.HTMLElement ?? class {}) { | ||
static getTemplateHTML = getTemplateHTML; | ||
static shadowRootOptions = { mode: 'open' }; | ||
static observedAttributes = [ | ||
@@ -44,3 +79,5 @@ 'autoplay', | ||
#options; | ||
loadComplete = new PublicPromise(); | ||
#loadRequested; | ||
#hasLoaded; | ||
#readyState = 0; | ||
@@ -51,18 +88,19 @@ #seeking = false; | ||
constructor() { | ||
super(); | ||
async load() { | ||
if (this.#loadRequested) return; | ||
this.attachShadow({ mode: 'open' }); | ||
this.shadowRoot.append(templateShadowDOM.content.cloneNode(true)); | ||
if (!this.shadowRoot) { | ||
this.attachShadow({ mode: 'open' }); | ||
} | ||
this.loadComplete = new PublicPromise(); | ||
} | ||
async load() { | ||
if (this.hasLoaded) { | ||
if (this.#hasLoaded) { | ||
this.loadComplete = new PublicPromise(); | ||
this.isLoaded = false; | ||
} | ||
this.hasLoaded = true; | ||
this.#hasLoaded = true; | ||
// Wait 1 tick to allow other attributes to be set. | ||
await (this.#loadRequested = Promise.resolve()); | ||
this.#loadRequested = null; | ||
this.#readyState = 0; | ||
@@ -74,8 +112,5 @@ this.dispatchEvent(new Event('emptied')); | ||
// Wait 1 tick to allow other attributes to be set. | ||
await Promise.resolve(); | ||
oldApi?.destroy(); | ||
if (!this.src) { | ||
// Removes the <iframe> containing the player. | ||
oldApi?.destroy(); | ||
return; | ||
@@ -86,26 +121,8 @@ } | ||
this.#options = { | ||
autoplay: this.autoplay, | ||
controls: this.controls, | ||
loop: this.loop, | ||
mute: this.defaultMuted, | ||
playsinline: this.playsInline, | ||
preload: this.preload ?? 'metadata', | ||
origin: location.origin, | ||
enablejsapi: 1, | ||
showinfo: 0, | ||
rel: 0, | ||
iv_load_policy: 3, | ||
modestbranding: 1, | ||
}; | ||
let iframe = this.shadowRoot.querySelector('iframe'); | ||
let attrs = namedNodeMapToObject(this.attributes); | ||
const matches = this.src.match(MATCH_SRC); | ||
const metaId = matches && matches[1]; | ||
const src = `${EMBED_BASE}/${metaId}?${serialize( | ||
boolToBinary(this.#options) | ||
)}`; | ||
let iframe = this.shadowRoot.querySelector('iframe'); | ||
if (!iframe) { | ||
iframe = createEmbedIframe({ src }); | ||
this.shadowRoot.append(iframe); | ||
if (!iframe?.src || iframe.src !== serializeIframeUrl(attrs)) { | ||
this.shadowRoot.innerHTML = getTemplateHTML(attrs); | ||
iframe = this.shadowRoot.querySelector('iframe'); | ||
} | ||
@@ -222,14 +239,8 @@ | ||
async attributeChangedCallback(attrName) { | ||
async attributeChangedCallback(attrName, oldValue, newValue) { | ||
if (oldValue === newValue) return; | ||
// This is required to come before the await for resolving loadComplete. | ||
switch (attrName) { | ||
case 'src': { | ||
this.load(); | ||
return; | ||
} | ||
} | ||
await this.loadComplete; | ||
switch (attrName) { | ||
case 'src': | ||
case 'autoplay': | ||
@@ -239,6 +250,3 @@ case 'controls': | ||
case 'playsinline': { | ||
if (this.#options[attrName] !== this.hasAttribute(attrName)) { | ||
this.load(); | ||
} | ||
break; | ||
this.load(); | ||
} | ||
@@ -305,4 +313,3 @@ } | ||
if (this.autoplay == val) return; | ||
if (val) this.setAttribute('autoplay', ''); | ||
else this.removeAttribute('autoplay'); | ||
this.toggleAttribute('autoplay', Boolean(val)); | ||
} | ||
@@ -326,4 +333,3 @@ | ||
if (this.controls == val) return; | ||
if (val) this.setAttribute('controls', ''); | ||
else this.removeAttribute('controls'); | ||
this.toggleAttribute('controls', Boolean(val)); | ||
} | ||
@@ -351,4 +357,3 @@ | ||
if (this.defaultMuted == val) return; | ||
if (val) this.setAttribute('muted', ''); | ||
else this.removeAttribute('muted'); | ||
this.toggleAttribute('muted', Boolean(val)); | ||
} | ||
@@ -366,4 +371,3 @@ | ||
if (this.loop == val) return; | ||
if (val) this.setAttribute('loop', ''); | ||
else this.removeAttribute('loop'); | ||
this.toggleAttribute('loop', Boolean(val)); | ||
} | ||
@@ -400,4 +404,3 @@ | ||
if (this.playsInline == val) return; | ||
if (val) this.setAttribute('playsinline', ''); | ||
else this.removeAttribute('playsinline'); | ||
this.toggleAttribute('playsinline', Boolean(val)); | ||
} | ||
@@ -427,2 +430,35 @@ | ||
function serializeAttributes(attrs) { | ||
let html = ''; | ||
for (const key in attrs) { | ||
const value = attrs[key]; | ||
if (value === '') html += ` ${key}`; | ||
else html += ` ${key}="${value}"`; | ||
} | ||
return html; | ||
} | ||
function serialize(props) { | ||
return String(new URLSearchParams(boolToBinary(props))); | ||
} | ||
function boolToBinary(props) { | ||
let p = {}; | ||
for (let key in props) { | ||
let val = props[key]; | ||
if (val === true || val === '') p[key] = 1; | ||
else if (val === false) p[key] = 0; | ||
else if (val != null) p[key] = val; | ||
} | ||
return p; | ||
} | ||
function namedNodeMapToObject(namedNodeMap) { | ||
let obj = {}; | ||
for (let attr of namedNodeMap) { | ||
obj[attr.name] = attr.value; | ||
} | ||
return obj; | ||
} | ||
const loadScriptCache = {}; | ||
@@ -488,43 +524,2 @@ async function loadScript(src, globalName, readyFnName) { | ||
function createElement(tag, attrs = {}, ...children) { | ||
const el = document.createElement(tag); | ||
Object.keys(attrs).forEach( | ||
(name) => attrs[name] != null && el.setAttribute(name, attrs[name]) | ||
); | ||
el.append(...children); | ||
return el; | ||
} | ||
const allow = | ||
'accelerometer; autoplay; fullscreen; encrypted-media; gyroscope; picture-in-picture'; | ||
function createEmbedIframe({ src, ...props }) { | ||
return createElement('iframe', { | ||
src, | ||
width: '100%', | ||
height: '100%', | ||
allow, | ||
frameborder: 0, | ||
...props, | ||
}); | ||
} | ||
function serialize(props) { | ||
return Object.keys(props) | ||
.map((key) => { | ||
if (props[key] == null) return ''; | ||
return `${key}=${encodeURIComponent(props[key])}`; | ||
}) | ||
.join('&'); | ||
} | ||
function boolToBinary(props) { | ||
let p = { ...props }; | ||
for (let key in p) { | ||
if (p[key] === false) p[key] = 0; | ||
else if (p[key] === true) p[key] = 1; | ||
} | ||
return p; | ||
} | ||
/** | ||
@@ -531,0 +526,0 @@ * Creates a fake `TimeRanges` object. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
0
17919
2
3
471
1