mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(skin): add audio skins for HTML and React presets (#772)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
038ad8b4f5
commit
d751fdabea
@@ -1,11 +1 @@
|
||||
.media-minimal-skin {
|
||||
color: var(--media-color, red);
|
||||
}
|
||||
|
||||
audio-player {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
audio-minimal-skin {
|
||||
display: contents;
|
||||
}
|
||||
@import "@videojs/skins/minimal/css/audio.css";
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render/minimal';
|
||||
import { playbackRate } from '@videojs/skins/default/tailwind/audio.tailwind';
|
||||
import {
|
||||
button,
|
||||
buttonGroup,
|
||||
controls,
|
||||
icon,
|
||||
iconContainer,
|
||||
iconFlipped,
|
||||
iconState,
|
||||
popup,
|
||||
root,
|
||||
seek,
|
||||
slider,
|
||||
time,
|
||||
} from '@videojs/skins/minimal/tailwind/audio.tailwind';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { SkinMixin } from '../skin-mixin';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
import '../ui/mute-button';
|
||||
import '../ui/play-button';
|
||||
import '../ui/playback-rate-button';
|
||||
import '../ui/popover';
|
||||
import '../ui/seek-button';
|
||||
import '../ui/time';
|
||||
import '../ui/time-slider';
|
||||
import '../ui/volume-slider';
|
||||
|
||||
const SEEK_TIME = 10;
|
||||
|
||||
function getTemplateHTML() {
|
||||
return /*html*/ `
|
||||
<media-container class="${root}">
|
||||
<slot name="media"></slot>
|
||||
|
||||
<div class="${controls}">
|
||||
<span class="${buttonGroup}">
|
||||
<media-play-button class="${cn(button.base, button.icon, iconState.play.button)}">
|
||||
${renderIcon('restart', { class: cn(icon, iconState.play.restart) })}
|
||||
${renderIcon('play', { class: cn(icon, iconState.play.play) })}
|
||||
${renderIcon('pause', { class: cn(icon, iconState.play.pause) })}
|
||||
</media-play-button>
|
||||
|
||||
<media-seek-button seconds="${-SEEK_TIME}" class="${cn(button.base, button.icon, seek.button)}">
|
||||
<span class="${iconContainer}">
|
||||
${renderIcon('seek', { class: cn(icon, iconFlipped) })}
|
||||
<span class="${cn(seek.label, seek.labelBackward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
|
||||
<media-seek-button seconds="${SEEK_TIME}" class="${cn(button.base, button.icon, seek.button)}">
|
||||
<span class="${iconContainer}">
|
||||
${renderIcon('seek', { class: icon })}
|
||||
<span class="${cn(seek.label, seek.labelForward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
</span>
|
||||
|
||||
<span class="${time.controls}">
|
||||
<media-time-group class="${time.group}">
|
||||
<media-time type="current" class="${time.current}"></media-time>
|
||||
<media-time-separator class="${time.separator}"></media-time-separator>
|
||||
<media-time type="duration" class="${time.duration}"></media-time>
|
||||
</media-time-group>
|
||||
|
||||
<media-time-slider class="${slider.root}">
|
||||
<media-slider-track class="${slider.track}">
|
||||
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
|
||||
<media-slider-buffer class="${cn(slider.fill.base, slider.fill.buffer)}"></media-slider-buffer>
|
||||
</media-slider-track>
|
||||
<media-slider-thumb class="${cn(slider.thumb.base, slider.thumb.interactive)}"></media-slider-thumb>
|
||||
</media-time-slider>
|
||||
</span>
|
||||
|
||||
<span class="${buttonGroup}">
|
||||
<media-playback-rate-button class="${cn(button.base, button.icon, playbackRate.button)}">
|
||||
</media-playback-rate-button>
|
||||
|
||||
<media-mute-button commandfor="audio-volume-popover" class="${cn(button.base, button.icon, iconState.mute.button)}">
|
||||
${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })}
|
||||
${renderIcon('volume-low', { class: cn(icon, iconState.mute.volumeLow) })}
|
||||
${renderIcon('volume-high', { class: cn(icon, iconState.mute.volumeHigh) })}
|
||||
</media-mute-button>
|
||||
|
||||
<media-popover id="audio-volume-popover" open-on-hover delay="200" close-delay="100" side="left" class="${cn(popup.base, popup.volume)}">
|
||||
<media-volume-slider class="${slider.root}" orientation="horizontal" thumb-alignment="edge">
|
||||
<media-slider-track class="${slider.track}">
|
||||
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
|
||||
</media-slider-track>
|
||||
<media-slider-thumb class="${slider.thumb.base}"></media-slider-thumb>
|
||||
</media-volume-slider>
|
||||
</media-popover>
|
||||
</span>
|
||||
</div>
|
||||
</media-container>
|
||||
`;
|
||||
}
|
||||
|
||||
export class MinimalAudioSkinTailwindElement extends SkinMixin(ReactiveElement) {
|
||||
static readonly tagName = 'audio-minimal-skin-tailwind';
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
}
|
||||
|
||||
customElements.define(MinimalAudioSkinTailwindElement.tagName, MinimalAudioSkinTailwindElement);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[MinimalAudioSkinTailwindElement.tagName]: MinimalAudioSkinTailwindElement;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,93 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render/minimal';
|
||||
import { createStyles, SkinMixin } from '../skin-mixin';
|
||||
import styles from './minimal-skin.css?inline';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
import '../ui/mute-button';
|
||||
import '../ui/play-button';
|
||||
import '../ui/playback-rate-button';
|
||||
import '../ui/popover';
|
||||
import '../ui/seek-button';
|
||||
import '../ui/time';
|
||||
import '../ui/time-slider';
|
||||
import '../ui/volume-slider';
|
||||
|
||||
const SEEK_TIME = 10;
|
||||
|
||||
function getTemplateHTML() {
|
||||
return /*html*/ `<div></div>`;
|
||||
return /*html*/ `
|
||||
<media-container class="media-minimal-skin media-minimal-skin--audio">
|
||||
<slot name="media"></slot>
|
||||
|
||||
<div class="media-controls">
|
||||
<span class="media-button-group">
|
||||
<media-play-button class="media-button media-button--icon media-button--play">
|
||||
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
|
||||
${renderIcon('play', { class: 'media-icon media-icon--play' })}
|
||||
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
|
||||
</media-play-button>
|
||||
|
||||
<media-seek-button seconds="${-SEEK_TIME}" class="media-button media-button--icon media-button--seek">
|
||||
<span class="media-icon__container">
|
||||
${renderIcon('seek', { class: 'media-icon media-icon--seek media-icon--flipped' })}
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
|
||||
<media-seek-button seconds="${SEEK_TIME}" class="media-button media-button--icon media-button--seek">
|
||||
<span class="media-icon__container">
|
||||
${renderIcon('seek', { class: 'media-icon media-icon--seek' })}
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
</span>
|
||||
|
||||
<span class="media-time-controls">
|
||||
<media-time-group class="media-time">
|
||||
<media-time type="current" class="media-time__value media-time__value--current"></media-time>
|
||||
<media-time-separator class="media-time__separator"></media-time-separator>
|
||||
<media-time type="duration" class="media-time__value media-time__value--duration"></media-time>
|
||||
</media-time-group>
|
||||
|
||||
<media-time-slider class="media-slider">
|
||||
<media-slider-track class="media-slider__track">
|
||||
<media-slider-fill class="media-slider__fill"></media-slider-fill>
|
||||
<media-slider-buffer class="media-slider__buffer"></media-slider-buffer>
|
||||
</media-slider-track>
|
||||
<media-slider-thumb class="media-slider__thumb"></media-slider-thumb>
|
||||
</media-time-slider>
|
||||
</span>
|
||||
|
||||
<span class="media-button-group">
|
||||
<media-playback-rate-button class="media-button media-button--icon media-button--playback-rate">
|
||||
</media-playback-rate-button>
|
||||
|
||||
<media-mute-button commandfor="audio-volume-popover" class="media-button media-button--icon media-button--mute">
|
||||
${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })}
|
||||
${renderIcon('volume-low', { class: 'media-icon media-icon--volume-low' })}
|
||||
${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })}
|
||||
</media-mute-button>
|
||||
|
||||
<media-popover id="audio-volume-popover" open-on-hover delay="200" close-delay="100" side="left" class="media-surface media-popup media-popup--volume media-popup-animation">
|
||||
<media-volume-slider class="media-slider" orientation="horizontal" thumb-alignment="edge">
|
||||
<media-slider-track class="media-slider__track">
|
||||
<media-slider-fill class="media-slider__fill"></media-slider-fill>
|
||||
</media-slider-track>
|
||||
<media-slider-thumb class="media-slider__thumb media-slider__thumb--persistent"></media-slider-thumb>
|
||||
</media-volume-slider>
|
||||
</media-popover>
|
||||
</span>
|
||||
</div>
|
||||
</media-container>
|
||||
`;
|
||||
}
|
||||
|
||||
export class MinimalAudioSkinElement extends ReactiveElement {
|
||||
export class MinimalAudioSkinElement extends SkinMixin(ReactiveElement) {
|
||||
static readonly tagName = 'audio-minimal-skin';
|
||||
static styles = createStyles(styles);
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const children = [...this.childNodes];
|
||||
this.innerHTML = getTemplateHTML();
|
||||
const container = this.firstElementChild;
|
||||
if (container) for (const child of children) container.append(child);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(MinimalAudioSkinElement.tagName, MinimalAudioSkinElement);
|
||||
|
||||
@@ -1,9 +1 @@
|
||||
@import "@videojs/skins/audio/default.css";
|
||||
|
||||
audio-player {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
audio-skin {
|
||||
display: contents;
|
||||
}
|
||||
@import "@videojs/skins/default/css/audio.css";
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render';
|
||||
import {
|
||||
button,
|
||||
controls,
|
||||
icon,
|
||||
iconContainer,
|
||||
iconFlipped,
|
||||
iconState,
|
||||
playbackRate,
|
||||
popup,
|
||||
root,
|
||||
seek,
|
||||
slider,
|
||||
time,
|
||||
} from '@videojs/skins/default/tailwind/audio.tailwind';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { SkinMixin } from '../skin-mixin';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
import '../ui/mute-button';
|
||||
import '../ui/play-button';
|
||||
import '../ui/playback-rate-button';
|
||||
import '../ui/popover';
|
||||
import '../ui/seek-button';
|
||||
import '../ui/time';
|
||||
import '../ui/time-slider';
|
||||
import '../ui/volume-slider';
|
||||
|
||||
const SEEK_TIME = 10;
|
||||
|
||||
function getTemplateHTML() {
|
||||
return /*html*/ `
|
||||
<media-container class="${root}">
|
||||
<slot name="media"></slot>
|
||||
|
||||
<div class="${controls}">
|
||||
<media-play-button class="${cn(button.base, button.icon, iconState.play.button)}">
|
||||
${renderIcon('restart', { class: cn(icon, iconState.play.restart) })}
|
||||
${renderIcon('play', { class: cn(icon, iconState.play.play) })}
|
||||
${renderIcon('pause', { class: cn(icon, iconState.play.pause) })}
|
||||
</media-play-button>
|
||||
|
||||
<media-seek-button seconds="${-SEEK_TIME}" class="${cn(button.base, button.icon, seek.button)}">
|
||||
<span class="${iconContainer}">
|
||||
${renderIcon('seek', { class: cn(icon, iconFlipped) })}
|
||||
<span class="${cn(seek.label, seek.labelBackward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
|
||||
<media-seek-button seconds="${SEEK_TIME}" class="${cn(button.base, button.icon, seek.button)}">
|
||||
<span class="${iconContainer}">
|
||||
${renderIcon('seek', { class: icon })}
|
||||
<span class="${cn(seek.label, seek.labelForward)}">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
|
||||
<media-time-group class="${time.group}">
|
||||
<media-time type="current" class="${time.current}"></media-time>
|
||||
<media-time-slider class="${slider.root}">
|
||||
<media-slider-track class="${slider.track}">
|
||||
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
|
||||
<media-slider-buffer class="${cn(slider.fill.base, slider.fill.buffer)}"></media-slider-buffer>
|
||||
</media-slider-track>
|
||||
<media-slider-thumb class="${cn(slider.thumb.base, slider.thumb.interactive)}"></media-slider-thumb>
|
||||
</media-time-slider>
|
||||
<media-time type="duration" class="${time.duration}"></media-time>
|
||||
</media-time-group>
|
||||
|
||||
<media-playback-rate-button class="${cn(button.base, button.icon, playbackRate.button)}">
|
||||
</media-playback-rate-button>
|
||||
|
||||
<media-mute-button commandfor="audio-volume-popover" class="${cn(button.base, button.icon, iconState.mute.button)}">
|
||||
${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })}
|
||||
${renderIcon('volume-low', { class: cn(icon, iconState.mute.volumeLow) })}
|
||||
${renderIcon('volume-high', { class: cn(icon, iconState.mute.volumeHigh) })}
|
||||
</media-mute-button>
|
||||
|
||||
<media-popover id="audio-volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="${cn(popup.base, popup.volume)}">
|
||||
<media-volume-slider class="${slider.root}" orientation="vertical" thumb-alignment="edge">
|
||||
<media-slider-track class="${slider.track}">
|
||||
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
|
||||
</media-slider-track>
|
||||
<media-slider-thumb class="${cn(slider.thumb.base, slider.thumb.persistent)}"></media-slider-thumb>
|
||||
</media-volume-slider>
|
||||
</media-popover>
|
||||
</div>
|
||||
</media-container>
|
||||
`;
|
||||
}
|
||||
|
||||
export class AudioSkinTailwindElement extends SkinMixin(ReactiveElement) {
|
||||
static readonly tagName = 'audio-skin-tailwind';
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
}
|
||||
|
||||
customElements.define(AudioSkinTailwindElement.tagName, AudioSkinTailwindElement);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AudioSkinTailwindElement.tagName]: AudioSkinTailwindElement;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,85 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render';
|
||||
import { createStyles, SkinMixin } from '../skin-mixin';
|
||||
import styles from './skin.css?inline';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
import '../ui/mute-button';
|
||||
import '../ui/play-button';
|
||||
import '../ui/playback-rate-button';
|
||||
import '../ui/popover';
|
||||
import '../ui/seek-button';
|
||||
import '../ui/time';
|
||||
import '../ui/time-slider';
|
||||
import '../ui/volume-slider';
|
||||
|
||||
const SEEK_TIME = 10;
|
||||
|
||||
function getTemplateHTML() {
|
||||
return /*html*/ `<div></div>`;
|
||||
return /*html*/ `
|
||||
<media-container class="media-default-skin media-default-skin--audio">
|
||||
<slot name="media"></slot>
|
||||
|
||||
<div class="media-surface media-controls">
|
||||
<media-play-button class="media-button media-button--icon media-button--play">
|
||||
${renderIcon('restart', { class: 'media-icon media-icon--restart' })}
|
||||
${renderIcon('play', { class: 'media-icon media-icon--play' })}
|
||||
${renderIcon('pause', { class: 'media-icon media-icon--pause' })}
|
||||
</media-play-button>
|
||||
|
||||
<media-seek-button seconds="${-SEEK_TIME}" class="media-button media-button--icon media-button--seek">
|
||||
<span class="media-icon__container">
|
||||
${renderIcon('seek', { class: 'media-icon media-icon--seek media-icon--flipped' })}
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
|
||||
<media-seek-button seconds="${SEEK_TIME}" class="media-button media-button--icon media-button--seek">
|
||||
<span class="media-icon__container">
|
||||
${renderIcon('seek', { class: 'media-icon media-icon--seek' })}
|
||||
<span class="media-icon__label">${SEEK_TIME}</span>
|
||||
</span>
|
||||
</media-seek-button>
|
||||
|
||||
<media-time-group class="media-time">
|
||||
<media-time type="current" class="media-time__value"></media-time>
|
||||
<media-time-slider class="media-slider">
|
||||
<media-slider-track class="media-slider__track">
|
||||
<media-slider-fill class="media-slider__fill"></media-slider-fill>
|
||||
<media-slider-buffer class="media-slider__buffer"></media-slider-buffer>
|
||||
</media-slider-track>
|
||||
<media-slider-thumb class="media-slider__thumb"></media-slider-thumb>
|
||||
</media-time-slider>
|
||||
<media-time type="duration" class="media-time__value"></media-time>
|
||||
</media-time-group>
|
||||
|
||||
<media-playback-rate-button class="media-button media-button--icon media-button--playback-rate">
|
||||
</media-playback-rate-button>
|
||||
|
||||
<media-mute-button commandfor="audio-volume-popover" class="media-button media-button--icon media-button--mute">
|
||||
${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })}
|
||||
${renderIcon('volume-low', { class: 'media-icon media-icon--volume-low' })}
|
||||
${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })}
|
||||
</media-mute-button>
|
||||
|
||||
<media-popover id="audio-volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="media-surface media-popup media-popup--volume media-popup-animation">
|
||||
<media-volume-slider class="media-slider" orientation="vertical" thumb-alignment="edge">
|
||||
<media-slider-track class="media-slider__track">
|
||||
<media-slider-fill class="media-slider__fill"></media-slider-fill>
|
||||
</media-slider-track>
|
||||
<media-slider-thumb class="media-slider__thumb media-slider__thumb--persistent"></media-slider-thumb>
|
||||
</media-volume-slider>
|
||||
</media-popover>
|
||||
</div>
|
||||
</media-container>
|
||||
`;
|
||||
}
|
||||
|
||||
export class AudioSkinElement extends ReactiveElement {
|
||||
export class AudioSkinElement extends SkinMixin(ReactiveElement) {
|
||||
static readonly tagName = 'audio-skin';
|
||||
static styles = createStyles(styles);
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const children = [...this.childNodes];
|
||||
this.innerHTML = getTemplateHTML();
|
||||
const container = this.firstElementChild;
|
||||
if (container) for (const child of children) container.append(child);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(AudioSkinElement.tagName, AudioSkinElement);
|
||||
|
||||
@@ -2,45 +2,81 @@ import type { ReactiveElement } from '@videojs/element';
|
||||
import type { Constructor } from '@videojs/utils/types';
|
||||
|
||||
/**
|
||||
* Mixin for skin elements that renders the template from a static
|
||||
* `getTemplateHTML` method and resolves `<slot name="media">` placeholders
|
||||
* by replacing them with the actual media element children.
|
||||
* Light DOM stylesheet that bridges CSS custom properties set by skins
|
||||
* to native WebKit text track pseudo-elements on the slotted `<video>`.
|
||||
*
|
||||
* `::slotted(video)::-webkit-media-text-track-container` is invalid per
|
||||
* CSS spec, so this bridge lives in light DOM where it can directly
|
||||
* target the video element's pseudo-elements.
|
||||
*/
|
||||
export function SkinMixin<Base extends Constructor<ReactiveElement>>(BaseClass: Base): Base {
|
||||
const NATIVE_CAPTION_BRIDGE_CSS = /* css */ `
|
||||
.media-skin > video::-webkit-media-text-track-container {
|
||||
transition: transform var(--media-caption-track-duration, 150ms) ease-out;
|
||||
transition-delay: var(--media-caption-track-delay, 600ms);
|
||||
transform: translateY(var(--media-caption-track-y, 0)) scale(0.98);
|
||||
z-index: var(--media-caption-track-z, 1);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.media-skin > video::-webkit-media-text-track-container {
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const BRIDGE_ID = 'media-caption-bridge';
|
||||
|
||||
function ensureCaptionBridge(): void {
|
||||
if (document.getElementById(BRIDGE_ID)) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = BRIDGE_ID;
|
||||
style.textContent = NATIVE_CAPTION_BRIDGE_CSS;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?: CSSStyleSheet } {
|
||||
class SkinElement extends (BaseClass as Constructor<ReactiveElement>) {
|
||||
static shadowRootOptions: ShadowRootInit = { mode: 'open' };
|
||||
static styles?: CSSStyleSheet;
|
||||
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
|
||||
const ctor = this.constructor as { getTemplateHTML?: () => string };
|
||||
this.classList.add('media-skin');
|
||||
ensureCaptionBridge();
|
||||
|
||||
if (ctor.getTemplateHTML) {
|
||||
const children = [...this.childNodes];
|
||||
this.innerHTML = ctor.getTemplateHTML();
|
||||
this.#resolveSlots(children);
|
||||
if (!this.shadowRoot) {
|
||||
const ctor = this.constructor as typeof SkinElement & { getTemplateHTML?: () => string };
|
||||
this.attachShadow(ctor.shadowRootOptions);
|
||||
|
||||
if (ctor.styles) {
|
||||
this.shadowRoot!.adoptedStyleSheets = [ctor.styles];
|
||||
}
|
||||
|
||||
if (ctor.getTemplateHTML) {
|
||||
this.shadowRoot!.innerHTML = ctor.getTemplateHTML();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
// During innerHTML parsing, children aren't available in the constructor.
|
||||
// Resolve any remaining slotted elements before the container connects.
|
||||
this.#resolveSlots();
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
#resolveSlots(nodes?: ChildNode[]): void {
|
||||
const slot = this.querySelector('slot[name="media"]');
|
||||
if (!slot) return;
|
||||
|
||||
// Collect media from either the provided node list (constructor) or
|
||||
// from direct children that haven't been placed yet (connectedCallback).
|
||||
const media = nodes
|
||||
? nodes.filter((n): n is HTMLElement => n instanceof HTMLElement && n.getAttribute('slot') === 'media')
|
||||
: [...this.querySelectorAll<HTMLElement>(':scope > [slot="media"]')];
|
||||
|
||||
for (const el of media) slot.before(el);
|
||||
slot.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return SkinElement as unknown as Base;
|
||||
return SkinElement as unknown as Base & { shadowRootOptions: ShadowRootInit; styles?: CSSStyleSheet };
|
||||
}
|
||||
|
||||
/** Create a shared `CSSStyleSheet` from a CSS string. */
|
||||
export function createStyles(css: string): CSSStyleSheet {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(css);
|
||||
return sheet;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
@import "@videojs/skins/video/minimal.css";
|
||||
@import "@videojs/skins/minimal/css/video.css";
|
||||
|
||||
@@ -16,13 +16,14 @@ import {
|
||||
seek,
|
||||
slider,
|
||||
time,
|
||||
} from '@videojs/skins/video/minimal.tailwind';
|
||||
} from '@videojs/skins/minimal/tailwind/video.tailwind';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { SkinMixin } from '../skin-mixin';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
import '../ui/buffering-indicator';
|
||||
import '../ui/captions-button';
|
||||
import '../ui/controls';
|
||||
import '../ui/fullscreen-button';
|
||||
import '../ui/mute-button';
|
||||
@@ -34,18 +35,20 @@ import '../ui/seek-button';
|
||||
import '../ui/time';
|
||||
import '../ui/time-slider';
|
||||
import '../ui/volume-slider';
|
||||
import { playbackRate } from '@videojs/skins/video/default.tailwind';
|
||||
import { playbackRate } from '@videojs/skins/default/tailwind/video.tailwind';
|
||||
|
||||
const SEEK_TIME = 10;
|
||||
|
||||
function getTemplateHTML() {
|
||||
return /*html*/ `
|
||||
<media-container class="${root}">
|
||||
<media-container class="${root(true)}">
|
||||
<slot name="media"></slot>
|
||||
|
||||
<media-buffering-indicator class="${bufferingIndicator}">
|
||||
${renderIcon('spinner')}
|
||||
</media-buffering-indicator>
|
||||
|
||||
<div class="${error.root}" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
|
||||
<!--<div class="${error.root}" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
|
||||
<div class="${error.dialog}">
|
||||
<div class="${error.content}">
|
||||
<p id="media-error-title" class="${error.title}">Something went wrong.</p>
|
||||
@@ -55,7 +58,7 @@ function getTemplateHTML() {
|
||||
<button type="button" class="${cn(button.base, button.default)}">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
<media-controls data-controls="" class="${controls}">
|
||||
<span class="${buttonGroup}">
|
||||
@@ -100,13 +103,13 @@ function getTemplateHTML() {
|
||||
<media-playback-rate-button class="${cn(button.base, button.icon, playbackRate.button)}">
|
||||
</media-playback-rate-button>
|
||||
|
||||
<media-mute-button commandfor="volume-popover" class="${cn(button.base, button.icon, iconState.mute.button)}">
|
||||
<media-mute-button commandfor="video-volume-popover" class="${cn(button.base, button.icon, iconState.mute.button)}">
|
||||
${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })}
|
||||
${renderIcon('volume-low', { class: cn(icon, iconState.mute.volumeLow) })}
|
||||
${renderIcon('volume-high', { class: cn(icon, iconState.mute.volumeHigh) })}
|
||||
</media-mute-button>
|
||||
|
||||
<media-popover id="volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="${cn(popup.base, popup.volume)}">
|
||||
<media-popover id="video-volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="${cn(popup.base, popup.volume)}">
|
||||
<media-volume-slider class="${slider.root}" orientation="vertical" thumb-alignment="edge">
|
||||
<media-slider-track class="${slider.track}">
|
||||
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
|
||||
@@ -115,10 +118,10 @@ function getTemplateHTML() {
|
||||
</media-volume-slider>
|
||||
</media-popover>
|
||||
|
||||
<!--<button type="button" class="${cn(button.base, button.icon)}" aria-label="Captions">
|
||||
${renderIcon('captions-off', { class: icon })}
|
||||
${renderIcon('captions-on', { class: icon })}
|
||||
</button>-->
|
||||
<media-captions-button class="${cn(button.base, button.icon, iconState.captions.button)}">
|
||||
${renderIcon('captions-off', { class: cn(icon, iconState.captions.off) })}
|
||||
${renderIcon('captions-on', { class: cn(icon, iconState.captions.on) })}
|
||||
</media-captions-button>
|
||||
|
||||
<media-pip-button class="${cn(button.base, button.icon)}">
|
||||
${renderIcon('pip', { class: icon })}
|
||||
@@ -132,8 +135,6 @@ function getTemplateHTML() {
|
||||
</media-controls>
|
||||
|
||||
<div class="${overlay}"></div>
|
||||
|
||||
<slot name="media"></slot>
|
||||
</media-container>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render/minimal';
|
||||
import { SkinMixin } from '../skin-mixin';
|
||||
import { createStyles, SkinMixin } from '../skin-mixin';
|
||||
import styles from './minimal-skin.css?inline';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
@@ -21,12 +22,14 @@ const SEEK_TIME = 10;
|
||||
|
||||
function getTemplateHTML() {
|
||||
return /*html*/ `
|
||||
<media-container class="media-minimal-skin">
|
||||
<media-container class="media-minimal-skin media-minimal-skin--video">
|
||||
<slot name="media"></slot>
|
||||
|
||||
<media-buffering-indicator class="media-buffering-indicator">
|
||||
${renderIcon('spinner', { class: 'media-icon' })}
|
||||
</media-buffering-indicator>
|
||||
|
||||
<div class="media-error" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
|
||||
<!--<div class="media-error" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
|
||||
<div class="media-error__dialog">
|
||||
<div class="media-error__content">
|
||||
<p id="media-error-title" class="media-error__title">Something went wrong.</p>
|
||||
@@ -36,7 +39,7 @@ function getTemplateHTML() {
|
||||
<button type="button" class="media-button">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
<media-controls class="media-controls">
|
||||
<span class="media-button-group">
|
||||
@@ -81,13 +84,13 @@ function getTemplateHTML() {
|
||||
<media-playback-rate-button class="media-button media-button--icon media-button--playback-rate">
|
||||
</media-playback-rate-button>
|
||||
|
||||
<media-mute-button commandfor="volume-popover" class="media-button media-button--icon media-button--mute">
|
||||
<media-mute-button commandfor="video-volume-popover" class="media-button media-button--icon media-button--mute">
|
||||
${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })}
|
||||
${renderIcon('volume-low', { class: 'media-icon media-icon--volume-low' })}
|
||||
${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })}
|
||||
</media-mute-button>
|
||||
|
||||
<media-popover id="volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="media-surface media-popup media-popup--volume media-popup-animation">
|
||||
<media-popover id="video-volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="media-surface media-popup media-popup--volume media-popup-animation">
|
||||
<media-volume-slider class="media-slider" orientation="vertical" thumb-alignment="edge">
|
||||
<media-slider-track class="media-slider__track">
|
||||
<media-slider-fill class="media-slider__fill"></media-slider-fill>
|
||||
@@ -113,14 +116,13 @@ function getTemplateHTML() {
|
||||
</media-controls>
|
||||
|
||||
<div class="media-overlay"></div>
|
||||
|
||||
<slot name="media"></slot>
|
||||
</media-container>
|
||||
`;
|
||||
}
|
||||
|
||||
export class MinimalVideoSkinElement extends SkinMixin(ReactiveElement) {
|
||||
static readonly tagName = 'video-minimal-skin';
|
||||
static styles = createStyles(styles);
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
@import "@videojs/skins/video/default.css";
|
||||
@import "@videojs/skins/default/css/video.css";
|
||||
|
||||
@@ -16,13 +16,14 @@ import {
|
||||
seek,
|
||||
slider,
|
||||
time,
|
||||
} from '@videojs/skins/video/default.tailwind';
|
||||
} from '@videojs/skins/default/tailwind/video.tailwind';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { SkinMixin } from '../skin-mixin';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
import '../ui/buffering-indicator';
|
||||
import '../ui/captions-button';
|
||||
import '../ui/controls';
|
||||
import '../ui/fullscreen-button';
|
||||
import '../ui/mute-button';
|
||||
@@ -39,14 +40,16 @@ const SEEK_TIME = 10;
|
||||
|
||||
function getTemplateHTML() {
|
||||
return /*html*/ `
|
||||
<media-container class="${root}">
|
||||
<media-container class="${root(true)}">
|
||||
<slot name="media"></slot>
|
||||
|
||||
<media-buffering-indicator class="${bufferingIndicator.root}">
|
||||
<div class="${bufferingIndicator.container}">
|
||||
${renderIcon('spinner')}
|
||||
</div>
|
||||
</media-buffering-indicator>
|
||||
|
||||
<div class="${error.root}" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
|
||||
<!--<div class="${error.root}" role="alertdialog" aria-labelledby="media-error-title" aria-describedby="media-error-description">
|
||||
<div class="${error.dialog}">
|
||||
<div class="${error.content}">
|
||||
<p id="media-error-title" class="${error.title}">Something went wrong.</p>
|
||||
@@ -56,7 +59,7 @@ function getTemplateHTML() {
|
||||
<button type="button" class="${cn(button.base, button.default)}">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
<media-controls data-controls="" class="${controls}">
|
||||
<media-play-button class="${cn(button.base, button.icon, iconState.play.button)}">
|
||||
@@ -94,13 +97,13 @@ function getTemplateHTML() {
|
||||
<media-playback-rate-button class="${cn(button.base, button.icon, playbackRate.button)}">
|
||||
</media-playback-rate-button>
|
||||
|
||||
<media-mute-button commandfor="volume-popover" class="${cn(button.base, button.icon, iconState.mute.button)}">
|
||||
<media-mute-button commandfor="video-volume-popover" class="${cn(button.base, button.icon, iconState.mute.button)}">
|
||||
${renderIcon('volume-off', { class: cn(icon, iconState.mute.volumeOff) })}
|
||||
${renderIcon('volume-low', { class: cn(icon, iconState.mute.volumeLow) })}
|
||||
${renderIcon('volume-high', { class: cn(icon, iconState.mute.volumeHigh) })}
|
||||
</media-mute-button>
|
||||
|
||||
<media-popover id="volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="${cn(popup.base, popup.volume)}">
|
||||
<media-popover id="video-volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="${cn(popup.base, popup.volume)}">
|
||||
<media-volume-slider class="${slider.root}" orientation="vertical" thumb-alignment="edge">
|
||||
<media-slider-track class="${slider.track}">
|
||||
<media-slider-fill class="${cn(slider.fill.base, slider.fill.fill)}"></media-slider-fill>
|
||||
@@ -109,10 +112,10 @@ function getTemplateHTML() {
|
||||
</media-volume-slider>
|
||||
</media-popover>
|
||||
|
||||
<!--<button type="button" class="${cn(button.base, button.icon)}" aria-label="Captions">
|
||||
${renderIcon('captions-off', { class: icon })}
|
||||
${renderIcon('captions-on', { class: icon })}
|
||||
</button>-->
|
||||
<media-captions-button class="${cn(button.base, button.icon, iconState.captions.button)}">
|
||||
${renderIcon('captions-off', { class: cn(icon, iconState.captions.off) })}
|
||||
${renderIcon('captions-on', { class: cn(icon, iconState.captions.on) })}
|
||||
</media-captions-button>
|
||||
|
||||
<media-pip-button class="${cn(button.base, button.icon)}">
|
||||
${renderIcon('pip', { class: icon })}
|
||||
@@ -125,8 +128,6 @@ function getTemplateHTML() {
|
||||
</media-controls>
|
||||
|
||||
<div class="${overlay}"></div>
|
||||
|
||||
<slot name="media"></slot>
|
||||
</media-container>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ReactiveElement } from '@videojs/element';
|
||||
import { renderIcon } from '@videojs/icons/render';
|
||||
import { SkinMixin } from '../skin-mixin';
|
||||
import { createStyles, SkinMixin } from '../skin-mixin';
|
||||
import styles from './skin.css?inline';
|
||||
|
||||
// Side-effect imports: register all custom elements used in the template.
|
||||
import '../media/container';
|
||||
import '../ui/buffering-indicator';
|
||||
import '../ui/captions-button';
|
||||
import '../ui/controls';
|
||||
import '../ui/fullscreen-button';
|
||||
import '../ui/mute-button';
|
||||
@@ -21,7 +23,9 @@ const SEEK_TIME = 10;
|
||||
|
||||
function getTemplateHTML() {
|
||||
return /*html*/ `
|
||||
<media-container class="media-default-skin">
|
||||
<media-container class="media-default-skin media-default-skin--video">
|
||||
<slot name="media"></slot>
|
||||
|
||||
<media-buffering-indicator class="media-buffering-indicator">
|
||||
<div class="media-surface">
|
||||
${renderIcon('spinner', { class: 'media-icon' })}
|
||||
@@ -76,13 +80,13 @@ function getTemplateHTML() {
|
||||
<media-playback-rate-button class="media-button media-button--icon media-button--playback-rate">
|
||||
</media-playback-rate-button>
|
||||
|
||||
<media-mute-button commandfor="volume-popover" class="media-button media-button--icon media-button--mute">
|
||||
<media-mute-button commandfor="video-volume-popover" class="media-button media-button--icon media-button--mute">
|
||||
${renderIcon('volume-off', { class: 'media-icon media-icon--volume-off' })}
|
||||
${renderIcon('volume-low', { class: 'media-icon media-icon--volume-low' })}
|
||||
${renderIcon('volume-high', { class: 'media-icon media-icon--volume-high' })}
|
||||
</media-mute-button>
|
||||
|
||||
<media-popover id="volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="media-surface media-popup media-popup--volume media-popup-animation">
|
||||
<media-popover id="video-volume-popover" open-on-hover delay="200" close-delay="100" side="top" class="media-surface media-popup media-popup--volume media-popup-animation">
|
||||
<media-volume-slider class="media-slider" orientation="vertical" thumb-alignment="edge">
|
||||
<media-slider-track class="media-slider__track">
|
||||
<media-slider-fill class="media-slider__fill"></media-slider-fill>
|
||||
@@ -91,10 +95,10 @@ function getTemplateHTML() {
|
||||
</media-volume-slider>
|
||||
</media-popover>
|
||||
|
||||
<!--<button type="button" class="media-button media-button--icon media-button--captions" aria-label="Captions">
|
||||
<media-captions-button class="media-button media-button--icon media-button--captions">
|
||||
${renderIcon('captions-off', { class: 'media-icon media-icon--captions-off' })}
|
||||
${renderIcon('captions-on', { class: 'media-icon media-icon--captions-on' })}
|
||||
</button>-->
|
||||
</media-captions-button>
|
||||
|
||||
<media-pip-button class="media-button media-button--icon">
|
||||
${renderIcon('pip', { class: 'media-icon' })}
|
||||
@@ -107,14 +111,13 @@ function getTemplateHTML() {
|
||||
</media-controls>
|
||||
|
||||
<div class="media-overlay"></div>
|
||||
|
||||
<slot name="media"></slot>
|
||||
</media-container>
|
||||
`;
|
||||
}
|
||||
|
||||
export class VideoSkinElement extends SkinMixin(ReactiveElement) {
|
||||
static readonly tagName = 'video-skin';
|
||||
static styles = createStyles(styles);
|
||||
static getTemplateHTML = getTemplateHTML;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user