super-media-element
Advanced tools
Comparing version 0.4.2 to 1.0.0
{ | ||
"name": "super-media-element", | ||
"version": "0.4.2", | ||
"version": "1.0.0", | ||
"description": "Helps you create a custom element w/ a HTMLMediaElement API.", | ||
@@ -19,12 +19,10 @@ "type": "module", | ||
"scripts": { | ||
"lint": "eslint *.js", | ||
"lint": "npx eslint *.js", | ||
"pretest": "esbuild super-media-element.js --format=iife --outdir=dist", | ||
"test": "web-test-runner --config test/web-test-runner.config.js", | ||
"dev": "npx serve ." | ||
"test": "wet run test/lazy.html test/eager.html", | ||
"dev": "wet serve --cors --redirect :example/" | ||
}, | ||
"devDependencies": { | ||
"@open-wc/testing": "^3.1.7", | ||
"@web/test-runner": "^0.15.0", | ||
"esbuild": "^0.17.3", | ||
"eslint": "^8.32.0" | ||
"wet-run": "^0.0.8" | ||
}, | ||
@@ -31,0 +29,0 @@ "eslintConfig": { |
# Super Media Element | ||
[![Version](https://img.shields.io/npm/v/super-media-element?style=flat-square)](https://www.npmjs.com/package/super-media-element) | ||
[![Badge size](https://img.badgesize.io/https://cdn.jsdelivr.net/npm/super-media-element/+esm?compression=gzip&label=gzip&style=flat-square)](https://cdn.jsdelivr.net/npm/super-media-element/+esm) | ||
A custom element that helps save alienated player API's to bring back their true inner [HTMLMediaElement API](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement), or to extend a native media element like `<audio>` or `<video>`. | ||
@@ -4,0 +7,0 @@ |
@@ -11,2 +11,34 @@ /** | ||
// The onevent like props are weirdly set on the HTMLElement prototype with other | ||
// generic events making it impossible to pick these specific to HTMLMediaElement. | ||
const Events = [ | ||
'abort', | ||
'canplay', | ||
'canplaythrough', | ||
'durationchange', | ||
'emptied', | ||
'encrypted', | ||
'ended', | ||
'error', | ||
'loadeddata', | ||
'loadedmetadata', | ||
'loadstart', | ||
'pause', | ||
'play', | ||
'playing', | ||
'progress', | ||
'ratechange', | ||
'seeked', | ||
'seeking', | ||
'stalled', | ||
'suspend', | ||
'timeupdate', | ||
'volumechange', | ||
'waiting', | ||
'waitingforkey', | ||
'resize', | ||
'enterpictureinpicture', | ||
'leavepictureinpicture', | ||
]; | ||
const styles = ` | ||
@@ -41,49 +73,34 @@ :host { | ||
export const SuperMediaMixin = (superclass, { tag, is }) => { | ||
// Can't check typeof directly on element prototypes without | ||
// throwing Illegal Invocation errors, so creating an element | ||
// to check on instead. | ||
const nativeElTest = document.createElement(tag, { is }); | ||
const nativeElProps = getNativeElProps(nativeElTest); | ||
// Most of the media events are set on the HTMLElement prototype. | ||
const AllEvents = [ | ||
...nativeElProps, | ||
...Object.getOwnPropertyNames(HTMLElement.prototype), | ||
] | ||
.filter((name) => name.startsWith('on')) | ||
.map((name) => name.slice(2)); | ||
return class SuperMedia extends superclass { | ||
static template = template; | ||
static Events = Events; | ||
static #isDefined; | ||
// observedAttributes is required to trigger attributeChangedCallback | ||
// for any attributes on the custom element. | ||
// Attributes need to be the lowercase word, e.g. crossorigin, not crossOrigin | ||
static get observedAttributes() { | ||
SuperMedia.#define(); | ||
// Instead of manually creating a list of all observed attributes, | ||
// observe any getter/setter prop name (lowercase) | ||
let attrs = []; | ||
Object.getOwnPropertyNames(this.prototype).forEach((propName) => { | ||
// Non-func properties throw errors because it's not an instance | ||
let isFunc = false; | ||
try { | ||
if (typeof this.prototype[propName] === 'function') isFunc = true; | ||
} catch (e) { | ||
// | ||
} | ||
// Exclude functions and constants | ||
if (!isFunc && propName !== propName.toUpperCase()) { | ||
attrs.push(propName.toLowerCase()); | ||
} | ||
}); | ||
// Include any attributes from the super class (recursive) | ||
const supAttrs = Object.getPrototypeOf(this).observedAttributes; | ||
// Include any attributes from the custom built-in. | ||
const natAttrs = Object.getPrototypeOf(nativeElTest).observedAttributes; | ||
return [...(natAttrs ?? []), ...attrs, ...(supAttrs ?? [])]; | ||
const attrs = [ | ||
...(natAttrs ?? []), | ||
'autopictureinpicture', | ||
'disablepictureinpicture', | ||
'disableremoteplayback', | ||
'autoplay', | ||
'controls', | ||
'controlslist', | ||
'crossorigin', | ||
'loop', | ||
'muted', | ||
'playsinline', | ||
'poster', | ||
'preload', | ||
'src', | ||
]; | ||
return attrs; | ||
} | ||
@@ -120,7 +137,3 @@ | ||
this.#init(); | ||
return ( | ||
this.get?.(prop) ?? | ||
this.nativeEl?.[prop] ?? | ||
this.#standinEl[prop] | ||
); | ||
return this.get?.(prop) ?? this.nativeEl?.[prop] ?? this.#standinEl[prop]; | ||
}, | ||
@@ -191,27 +204,2 @@ }; | ||
#initStandinEl() { | ||
const dummyEl = document.createElement(tag, { is }); | ||
[...this.attributes].forEach(({ name, value }) => { | ||
dummyEl.setAttribute(name, value); | ||
}); | ||
this.#standinEl = {}; | ||
getNativeElProps(dummyEl).forEach((name) => { | ||
this.#standinEl[name] = dummyEl[name]; | ||
}); | ||
// unload dummy video element | ||
dummyEl.removeAttribute('src'); | ||
dummyEl.load(); | ||
} | ||
async #initNativeEl() { | ||
if (this.loadComplete && !this.isLoaded) await this.loadComplete; | ||
// If there is no nativeEl by now, create it our bloody selves. | ||
if (!this.nativeEl) { | ||
this.shadowRoot.append(document.createElement(tag, { is })); | ||
} | ||
} | ||
async #init() { | ||
@@ -251,96 +239,59 @@ if (this.#isInit) return; | ||
// This makes it possible to add event listeners before the element is upgraded. | ||
AllEvents.forEach((type) => { | ||
this.shadowRoot.addEventListener?.( | ||
type, | ||
(evt) => { | ||
if (evt.target !== this.nativeEl) { | ||
return; | ||
} | ||
// Filter out non-media events. | ||
if ( | ||
!['Event', 'CustomEvent', 'PictureInPictureEvent'].includes( | ||
evt.constructor.name | ||
) | ||
) { | ||
return; | ||
} | ||
this.dispatchEvent( | ||
new CustomEvent(evt.type, { detail: evt.detail }) | ||
); | ||
}, | ||
true | ||
); | ||
Events.forEach((type) => { | ||
this.shadowRoot.addEventListener?.(type, (evt) => { | ||
if (evt.target !== this.nativeEl) return; | ||
this.dispatchEvent(new CustomEvent(evt.type, { detail: evt.detail })); | ||
}, true); | ||
}); | ||
} | ||
// Initialize all the attribute properties | ||
// This is required before attributeChangedCallback is called after construction | ||
// so the initial state of all the attributes are forwarded to the native element. | ||
// Don't call attributeChangedCallback directly here because the extending class | ||
// could have overridden attributeChangedCallback leading to unexpected results. | ||
[...this.attributes].forEach((attrNode) => { | ||
this.#forwardAttribute(attrNode.name, null, attrNode.value); | ||
#initStandinEl() { | ||
const dummyEl = document.createElement(tag, { is }); | ||
[...this.attributes].forEach(({ name, value }) => { | ||
dummyEl.setAttribute(name, value); | ||
}); | ||
// Neither Chrome or Firefox support setting the muted attribute | ||
// after using document.createElement. | ||
// One way to get around this would be to build the native tag as a string. | ||
// But just fixing it manually for now. | ||
// Apparently this may also be an issue with <input checked> for buttons | ||
this.#standinEl = {}; | ||
getNativeElProps(dummyEl).forEach((name) => { | ||
this.#standinEl[name] = dummyEl[name]; | ||
}); | ||
// unload dummy video element | ||
dummyEl.removeAttribute('src'); | ||
dummyEl.load(); | ||
} | ||
async #initNativeEl() { | ||
if (this.loadComplete && !this.isLoaded) await this.loadComplete; | ||
if (this.nativeEl.defaultMuted) { | ||
this.muted = true; | ||
// If there is no nativeEl by now, create it our bloody selves. | ||
if (!this.nativeEl) { | ||
// Neither Chrome or Firefox support setting the muted attribute | ||
// after using document.createElement. | ||
// Get around this by building the native tag as a string. | ||
const muted = this.hasAttribute('muted') ? ' muted' : ''; | ||
const tpl = document.createElement('template'); | ||
tpl.innerHTML = `<${tag}${is ? ` is="${is}"` : ''}${muted}></${tag}>`; | ||
this.shadowRoot.append(tpl.content); | ||
} | ||
} | ||
async attributeChangedCallback(attrName, oldValue, newValue) { | ||
attributeChangedCallback(attrName, oldValue, newValue) { | ||
// Initialize right after construction when the attributes become available. | ||
if (!this.#isInit) { | ||
await this.#init(); | ||
} | ||
this.#init(); | ||
this.#forwardAttribute(attrName, oldValue, newValue); | ||
} | ||
// We need to handle sub-class custom attributes differently from | ||
// attrs meant to be passed to the internal native el. | ||
async #forwardAttribute(attrName, oldValue, newValue) { | ||
if (this.loadComplete && !this.isLoaded) await this.loadComplete; | ||
// Find the matching prop for custom attributes | ||
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(this)); | ||
const propName = ownProps.find( | ||
(name) => name.toLowerCase() === attrName.toLowerCase() | ||
); | ||
// Check if this is the original custom native element or a subclass | ||
const isBaseElement = | ||
Object.getPrototypeOf(this.constructor).name === 'HTMLElement'; | ||
// If this is a subclass custom attribute we want to set the | ||
// matching property on the subclass | ||
if (propName && !isBaseElement) { | ||
// Boolean props should never start as null | ||
if (typeof this[propName] == 'boolean') { | ||
// null is returned when attributes are removed i.e. boolean attrs | ||
if (newValue === null) { | ||
this[propName] = false; | ||
} else { | ||
// The new value might be an empty string, which is still true | ||
// for boolean attributes | ||
this[propName] = true; | ||
} | ||
} else { | ||
this[propName] = newValue; | ||
} | ||
if (newValue === null) { | ||
this.nativeEl.removeAttribute?.(attrName); | ||
} else { | ||
// When this is the original Custom Element, or the subclass doesn't | ||
// have a matching prop, pass it through. | ||
if (newValue === null) { | ||
this.nativeEl.removeAttribute?.(attrName); | ||
} else { | ||
// Ignore a few that don't need to be passed through just in case | ||
// it creates unexpected behavior. | ||
if (!['id', 'class'].includes(attrName)) { | ||
this.nativeEl.setAttribute?.(attrName, newValue); | ||
} | ||
// Ignore a few that don't need to be passed through just in case | ||
// it creates unexpected behavior. | ||
if (!['id', 'class'].includes(attrName)) { | ||
this.nativeEl.setAttribute?.(attrName, newValue); | ||
} | ||
@@ -347,0 +298,0 @@ } |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
2
0
82
14805
4
306