refactor(html): SkinMixin -> SkinElement (#1159)

Co-authored-by: Rahim <rahim.alwer@gmail.com>
This commit is contained in:
Sam Potts
2026-03-31 22:22:45 -07:00
committed by GitHub
co-authored by Rahim
parent 63a82c0f92
commit 4cda8fb94f
16 changed files with 303 additions and 142 deletions
@@ -1,4 +1,3 @@
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render/minimal';
import { playbackRate } from '@videojs/skins/default/tailwind/audio.tailwind';
import {
@@ -17,9 +16,10 @@ import {
time,
tooltipState,
} from '@videojs/skins/minimal/tailwind/audio.tailwind';
import { createTemplate } from '@videojs/utils/dom';
import { cn } from '@videojs/utils/style';
import { safeDefine } from '../safe-define';
import { SkinMixin } from '../skin-mixin';
import { SkinElement } from '../skin-element';
// Side-effect imports: register all custom elements used in the template.
import '../media/container';
@@ -137,9 +137,9 @@ function getTemplateHTML() {
`;
}
export class MinimalAudioSkinTailwindElement extends SkinMixin(ReactiveElement) {
export class MinimalAudioSkinTailwindElement extends SkinElement {
static readonly tagName = 'audio-minimal-skin-tailwind';
static getTemplateHTML = getTemplateHTML;
static template = createTemplate(getTemplateHTML());
}
safeDefine(MinimalAudioSkinTailwindElement);
@@ -1,7 +1,7 @@
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render/minimal';
import { createShadowStyle, createTemplate } from '@videojs/utils/dom';
import { safeDefine } from '../safe-define';
import { createStyles, SkinMixin } from '../skin-mixin';
import { SkinElement } from '../skin-element';
import styles from './minimal-skin.css?inline';
// Side-effect imports: register all custom elements used in the template.
@@ -118,10 +118,10 @@ function getTemplateHTML() {
`;
}
export class MinimalAudioSkinElement extends SkinMixin(ReactiveElement) {
export class MinimalAudioSkinElement extends SkinElement {
static readonly tagName = 'audio-minimal-skin';
static styles = createStyles(styles);
static getTemplateHTML = getTemplateHTML;
static styles = createShadowStyle(styles);
static template = createTemplate(getTemplateHTML());
}
safeDefine(MinimalAudioSkinElement);
@@ -1,4 +1,3 @@
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render';
import {
button,
@@ -17,9 +16,10 @@ import {
time,
tooltipState,
} from '@videojs/skins/default/tailwind/audio.tailwind';
import { createTemplate } from '@videojs/utils/dom';
import { cn } from '@videojs/utils/style';
import { safeDefine } from '../safe-define';
import { SkinMixin } from '../skin-mixin';
import { SkinElement } from '../skin-element';
// Side-effect imports: register all custom elements used in the template.
import '../media/container';
@@ -132,9 +132,9 @@ function getTemplateHTML() {
`;
}
export class AudioSkinTailwindElement extends SkinMixin(ReactiveElement) {
export class AudioSkinTailwindElement extends SkinElement {
static readonly tagName = 'audio-skin-tailwind';
static getTemplateHTML = getTemplateHTML;
static template = createTemplate(getTemplateHTML());
}
safeDefine(AudioSkinTailwindElement);
+5 -5
View File
@@ -1,7 +1,7 @@
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render';
import { createShadowStyle, createTemplate } from '@videojs/utils/dom';
import { safeDefine } from '../safe-define';
import { createStyles, SkinMixin } from '../skin-mixin';
import { SkinElement } from '../skin-element';
import styles from './skin.css?inline';
// Side-effect imports: register all custom elements used in the template.
@@ -113,10 +113,10 @@ function getTemplateHTML() {
`;
}
export class AudioSkinElement extends SkinMixin(ReactiveElement) {
export class AudioSkinElement extends SkinElement {
static readonly tagName = 'audio-skin';
static styles = createStyles(styles);
static getTemplateHTML = getTemplateHTML;
static styles = createShadowStyle(styles);
static template = createTemplate(getTemplateHTML());
}
safeDefine(AudioSkinElement);
+2 -10
View File
@@ -1,18 +1,10 @@
import { ReactiveElement } from '@videojs/element';
import { namedNodeMapToObject } from '@videojs/utils/dom';
import { ensureGlobalStyle, namedNodeMapToObject } from '@videojs/utils/dom';
import { safeDefine } from '../safe-define';
import styles from './skin.css?inline';
const STYLES_ID = '__media-background-styles';
function ensureBackgroundStyles(): void {
if (document.getElementById(STYLES_ID)) return;
const style = document.createElement('style');
style.id = STYLES_ID;
style.textContent = styles;
document.head.appendChild(style);
}
function getTemplateHTML(_attrs: Record<string, string>) {
return /*html*/ `
<media-container>
@@ -31,7 +23,7 @@ export class BackgroundVideoSkinElement extends ReactiveElement {
constructor() {
super();
ensureBackgroundStyles();
ensureGlobalStyle(STYLES_ID, styles);
if (!this.shadowRoot) {
this.attachShadow((this.constructor as typeof BackgroundVideoSkinElement).shadowRootOptions);
+45
View File
@@ -0,0 +1,45 @@
import { ReactiveElement } from '@videojs/element';
import {
applyShadowStyles,
createShadowStyle,
ensureGlobalStyle,
renderTemplate,
type ShadowStyle,
} from '@videojs/utils/dom';
import rootStyles from './base.css?inline';
import sharedStyles from './shared.css?inline';
const STYLES_ID = '__media-styles';
const sharedSheet = createShadowStyle(sharedStyles);
/**
* Base element for skin definitions. Attaches a shadow root, clones
* `static template` into it, and applies shared + per-skin styles
* via `adoptedStyleSheets` (or `<style>` fallback).
*/
export class SkinElement extends ReactiveElement {
static shadowRootOptions: ShadowRootInit = { mode: 'open' };
static styles?: ShadowStyle;
static template?: HTMLTemplateElement | null;
constructor() {
super();
ensureGlobalStyle(STYLES_ID, rootStyles);
if (!this.shadowRoot) {
const ctor = this.constructor as typeof SkinElement;
this.attachShadow(ctor.shadowRootOptions);
if (ctor.template) {
renderTemplate(this.shadowRoot!, ctor.template);
}
const sheets: ShadowStyle[] = [sharedSheet];
if (ctor.styles) {
sheets.push(ctor.styles);
}
applyShadowStyles(this.shadowRoot!, sheets);
}
}
}
-96
View File
@@ -1,96 +0,0 @@
import type { ReactiveElement } from '@videojs/element';
import type { Constructor } from '@videojs/utils/types';
import rootStyles from './base.css?inline';
import sharedStyles from './shared.css?inline';
const STYLES_ID = '__media-styles';
type SkinStyles = CSSStyleSheet | string;
function ensureRootStyles(): void {
const doc = globalThis.document;
if (!doc || doc.getElementById(STYLES_ID)) return;
const style = doc.createElement('style');
style.id = STYLES_ID;
style.textContent = rootStyles;
doc.head.appendChild(style);
}
function isConstructableStyleSheet(value: SkinStyles): value is CSSStyleSheet {
return typeof globalThis.CSSStyleSheet !== 'undefined' && value instanceof globalThis.CSSStyleSheet;
}
function getStyleText(style: SkinStyles): string {
if (typeof style === 'string') return style;
return Array.from(style.cssRules)
.map((rule) => rule.cssText)
.join('\n');
}
function applyShadowStyles(shadowRoot: ShadowRoot, styles: SkinStyles[]): void {
if (styles.every(isConstructableStyleSheet) && 'adoptedStyleSheets' in shadowRoot) {
shadowRoot.adoptedStyleSheets = styles;
return;
}
const doc = shadowRoot.ownerDocument;
for (const styleText of styles.map(getStyleText)) {
const style = doc.createElement('style');
style.textContent = styleText;
shadowRoot.appendChild(style);
}
}
const sharedSheet = createStyles(sharedStyles);
/**
* Mixin for skin elements that renders the template from a static
* `getTemplateHTML` method into a shadow root. Native `<slot>` elements
* handle light DOM projection automatically.
*
* When `static styles` is set, the stylesheet is adopted into the
* shadow root via `adoptedStyleSheets`.
*/
export function SkinMixin<Base extends Constructor<ReactiveElement>>(
BaseClass: Base
): Base & { shadowRootOptions: ShadowRootInit; styles?: SkinStyles } {
class SkinElement extends (BaseClass as Constructor<ReactiveElement>) {
static shadowRootOptions: ShadowRootInit = { mode: 'open' };
static styles?: SkinStyles;
constructor(...args: any[]) {
super(...args);
ensureRootStyles();
if (!this.shadowRoot) {
const ctor = this.constructor as typeof SkinElement & { getTemplateHTML?: () => string };
this.attachShadow(ctor.shadowRootOptions);
if (ctor.getTemplateHTML) {
this.shadowRoot!.innerHTML = ctor.getTemplateHTML();
}
const sheets: SkinStyles[] = [sharedSheet];
if (ctor.styles) {
sheets.push(ctor.styles);
}
applyShadowStyles(this.shadowRoot!, sheets);
}
}
}
return SkinElement as unknown as Base & { shadowRootOptions: ShadowRootInit; styles?: SkinStyles };
}
/** Create a constructable stylesheet when available, otherwise return raw CSS. */
export function createStyles(css: string): SkinStyles {
if (typeof globalThis.CSSStyleSheet === 'undefined') {
return css;
}
const sheet = new globalThis.CSSStyleSheet();
sheet.replaceSync(css);
return sheet;
}
@@ -1,4 +1,3 @@
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render/minimal';
import {
bufferingIndicator,
@@ -22,9 +21,10 @@ import {
time,
tooltipState,
} from '@videojs/skins/minimal/tailwind/video.tailwind';
import { createTemplate } from '@videojs/utils/dom';
import { cn } from '@videojs/utils/style';
import { safeDefine } from '../safe-define';
import { SkinMixin } from '../skin-mixin';
import { SkinElement } from '../skin-element';
// Side-effect imports: register all custom elements used in the template.
import '../media/container';
@@ -199,9 +199,9 @@ function getTemplateHTML() {
`;
}
export class MinimalVideoSkinTailwindElement extends SkinMixin(ReactiveElement) {
export class MinimalVideoSkinTailwindElement extends SkinElement {
static readonly tagName = 'video-minimal-skin-tailwind';
static getTemplateHTML = getTemplateHTML;
static template = createTemplate(getTemplateHTML());
}
safeDefine(MinimalVideoSkinTailwindElement);
@@ -1,7 +1,7 @@
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render/minimal';
import { createShadowStyle, createTemplate } from '@videojs/utils/dom';
import { safeDefine } from '../safe-define';
import { createStyles, SkinMixin } from '../skin-mixin';
import { SkinElement } from '../skin-element';
import styles from './minimal-skin.css?inline';
// Side-effect imports: register all custom elements used in the template.
@@ -166,10 +166,10 @@ function getTemplateHTML() {
`;
}
export class MinimalVideoSkinElement extends SkinMixin(ReactiveElement) {
export class MinimalVideoSkinElement extends SkinElement {
static readonly tagName = 'video-minimal-skin';
static styles = createStyles(styles);
static getTemplateHTML = getTemplateHTML;
static styles = createShadowStyle(styles);
static template = createTemplate(getTemplateHTML());
}
safeDefine(MinimalVideoSkinElement);
@@ -1,4 +1,3 @@
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render';
import {
bufferingIndicator,
@@ -22,9 +21,10 @@ import {
time,
tooltipState,
} from '@videojs/skins/default/tailwind/video.tailwind';
import { createTemplate } from '@videojs/utils/dom';
import { cn } from '@videojs/utils/style';
import { safeDefine } from '../safe-define';
import { SkinMixin } from '../skin-mixin';
import { SkinElement } from '../skin-element';
// Side-effect imports: register all custom elements used in the template.
import '../media/container';
@@ -194,9 +194,9 @@ function getTemplateHTML() {
`;
}
export class VideoSkinTailwindElement extends SkinMixin(ReactiveElement) {
export class VideoSkinTailwindElement extends SkinElement {
static readonly tagName = 'video-skin-tailwind';
static getTemplateHTML = getTemplateHTML;
static template = createTemplate(getTemplateHTML());
}
safeDefine(VideoSkinTailwindElement);
+5 -5
View File
@@ -1,7 +1,7 @@
import { ReactiveElement } from '@videojs/element';
import { renderIcon } from '@videojs/icons/render';
import { createShadowStyle, createTemplate } from '@videojs/utils/dom';
import { safeDefine } from '../safe-define';
import { createStyles, SkinMixin } from '../skin-mixin';
import { SkinElement } from '../skin-element';
import styles from './skin.css?inline';
// Side-effect imports: register all custom elements used in the template.
@@ -164,10 +164,10 @@ function getTemplateHTML() {
`;
}
export class VideoSkinElement extends SkinMixin(ReactiveElement) {
export class VideoSkinElement extends SkinElement {
static readonly tagName = 'video-skin';
static styles = createStyles(styles);
static getTemplateHTML = getTemplateHTML;
static styles = createShadowStyle(styles);
static template = createTemplate(getTemplateHTML());
}
safeDefine(VideoSkinElement);