Files
v10/packages/html/src/components/media-volume-slider.ts
T

310 lines
8.9 KiB
TypeScript

import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
import { VolumeSlider as CoreVolumeSlider } from '@videojs/core';
import { volumeSliderStateDefinition } from '@videojs/core/store';
import { setAttributes } from '@videojs/utils/dom';
import { toConnectedHTMLComponent } from '../utils/component-factory';
/**
* VolumeSlider Root props hook - equivalent to React's useVolumeSliderRootProps
* Handles element attributes and properties based on state
*/
export const getVolumeSliderRootProps: PropsHook<{
volume: number;
muted: boolean;
volumeLevel: string;
requestVolumeChange: (volume: number) => void;
core: CoreVolumeSlider | null;
}> = (state, element) => {
const volumeText = `${Math.round(state.muted ? 0 : state.volume * 100)}%`;
const baseProps: Record<string, any> = {
role: 'slider',
tabindex: element.getAttribute('tabindex') ?? '0',
'data-muted': state.muted.toString(),
'data-volume-level': state.volumeLevel,
'data-orientation': (element as any).orientation || 'horizontal',
'aria-label': 'Volume',
'aria-valuemin': '0',
'aria-valuemax': '100',
'aria-valuetext': volumeText,
'aria-orientation': (element as any).orientation || 'horizontal',
};
return baseProps;
};
/**
* VolumeSlider Root component - Main container with pointer event handling
*/
interface VolumeSliderRootState {
volume: number;
muted: boolean;
volumeLevel: string;
requestVolumeChange: (volume: number) => void;
core: CoreVolumeSlider | null;
}
export class VolumeSliderRootBase extends HTMLElement {
static readonly observedAttributes: readonly string[] = ['orientation'];
_state: VolumeSliderRootState | undefined;
_core: CoreVolumeSlider | null = null;
get volume(): number | undefined {
return this._state?.volume;
}
get muted(): boolean {
return this._state?.muted ?? false;
}
get volumeLevel(): string {
return this._state?.volumeLevel ?? 'high';
}
get orientation(): 'horizontal' | 'vertical' {
return (this.getAttribute('orientation') as 'horizontal' | 'vertical') || 'horizontal';
}
attributeChangedCallback(name: string, _oldValue: string | null, _newValue: string | null): void {
if (name === 'orientation' && this._state) {
this._render(getVolumeSliderRootProps(this._state, this), this._state);
}
}
_update(_props: any, state: any): void {
this._state = state;
if (state && !this._core) {
this._core = new CoreVolumeSlider();
this._core.subscribe(() => this._render(getVolumeSliderRootProps(state, this), state));
this._core.attach(this);
state.core = this._core;
}
this._core?.setState(state);
}
_render(props: any, state: any): void {
const coreState = state?.core?.getState();
if (!coreState) return;
this.style.setProperty('--slider-fill', `${coreState._fillWidth.toFixed(3)}%`);
this.style.setProperty('--slider-pointer', `${coreState._pointerWidth.toFixed(3)}%`);
props['aria-valuenow'] = coreState._fillWidth.toString();
setAttributes(this, props);
}
}
/**
* VolumeSlider Track component - Track element that captures pointer events
*/
export class VolumeSliderTrackBase extends HTMLElement {
constructor() {
super();
}
connectedCallback(): void {
// Set this element as the track element in the core VolumeSlider
const rootElement = this.closest('media-volume-slider-root') as any;
if (rootElement?._state?.core) {
rootElement._state.core.setState({ _trackElement: this });
}
}
_update(props: any, _state: any): void {
setAttributes(this, props);
if (props['data-orientation'] === 'horizontal') {
this.style.width = '100%';
this.style.removeProperty('height');
} else {
this.style.height = '100%';
this.style.removeProperty('width');
}
}
}
/**
* VolumeSlider Progress component - Shows current progress
*/
export class VolumeSliderProgressBase extends HTMLElement {
constructor() {
super();
this.style.position = 'absolute';
this.style.width = 'var(--slider-fill, 0%)';
this.style.height = '100%';
}
_update(props: any, _state: any): void {
setAttributes(this, props);
if (props['data-orientation'] === 'horizontal') {
this.style.width = 'var(--slider-fill, 0%)';
this.style.height = '100%';
this.style.top = '0';
this.style.removeProperty('bottom');
} else {
this.style.height = 'var(--slider-fill, 0%)';
this.style.width = '100%';
this.style.bottom = '0';
this.style.removeProperty('top');
}
}
}
/**
* VolumeSlider Thumb component - Draggable thumb element
*/
export class VolumeSliderThumbBase extends HTMLElement {
constructor() {
super();
this.style.position = 'absolute';
}
_update(props: any, _state: any): void {
setAttributes(this, props);
// Set appropriate positioning based on orientation
if (props['data-orientation'] === 'horizontal') {
this.style.left = 'var(--slider-fill, 0%)';
this.style.top = '50%';
this.style.translate = '-50% -50%';
} else {
this.style.bottom = 'var(--slider-fill, 0%)';
this.style.left = '50%';
this.style.translate = '-50% 50%';
}
}
}
/**
* VolumeSlider Root state hook - equivalent to React's useVolumeSliderRootState
* Handles media store state subscription and transformation
*/
export const useVolumeSliderRootState: StateHook<{
volume: number;
muted: boolean;
volumeLevel: string;
requestVolumeChange: (volume: number) => void;
core: CoreVolumeSlider | null;
}> = {
keys: volumeSliderStateDefinition.keys,
transform: (rawState, mediaStore) => ({
...volumeSliderStateDefinition.stateTransform(rawState),
...volumeSliderStateDefinition.createRequestMethods(mediaStore.dispatch),
core: null,
}),
};
/**
* VolumeSlider Track props hook
*/
export const getVolumeSliderTrackProps: PropsHook<Record<string, never>> = (_state, element) => {
const rootElement = element.closest('media-volume-slider-root') as any;
return {
'data-orientation': rootElement?.orientation || 'horizontal',
};
};
/**
* VolumeSlider Progress props hook
*/
export const getVolumeSliderProgressProps: PropsHook<Record<string, never>> = (_state, element) => {
const rootElement = element.closest('media-volume-slider-root') as any;
return {
'data-orientation': rootElement?.orientation || 'horizontal',
};
};
/**
* VolumeSlider Thumb props hook
*/
export const getVolumeSliderThumbProps: PropsHook<Record<string, never>> = (_state, element) => {
const rootElement = element.closest('media-volume-slider-root') as any;
return {
'data-orientation': rootElement?.orientation || 'horizontal',
};
};
/**
* Connected VolumeSlider Root component using hook-style architecture
*/
export const VolumeSliderRoot: ConnectedComponentConstructor<{
volume: number;
muted: boolean;
volumeLevel: string;
requestVolumeChange: (volume: number) => void;
core: CoreVolumeSlider | null;
}> = toConnectedHTMLComponent(VolumeSliderRootBase, useVolumeSliderRootState, getVolumeSliderRootProps, 'VolumeSliderRoot');
/**
* Connected VolumeSlider Track component
*/
export const VolumeSliderTrack: ConnectedComponentConstructor<any> = toConnectedHTMLComponent(
VolumeSliderTrackBase,
{ keys: [], transform: () => ({}) },
getVolumeSliderTrackProps,
'VolumeSliderTrack',
);
/**
* Connected VolumeSlider Progress component
*/
export const VolumeSliderProgress: ConnectedComponentConstructor<any> = toConnectedHTMLComponent(
VolumeSliderProgressBase,
{ keys: [], transform: () => ({}) },
getVolumeSliderProgressProps,
'VolumeSliderProgress',
);
/**
* Connected VolumeSlider Thumb component
*/
export const VolumeSliderThumb: ConnectedComponentConstructor<any> = toConnectedHTMLComponent(
VolumeSliderThumbBase,
{ keys: [], transform: () => ({}) },
getVolumeSliderThumbProps,
'VolumeSliderThumb',
);
/**
* Compound VolumeSlider component object
*/
export const VolumeSlider = Object.assign(
{},
{
Root: VolumeSliderRoot,
Track: VolumeSliderTrack,
Progress: VolumeSliderProgress,
Thumb: VolumeSliderThumb,
},
) as {
Root: typeof VolumeSliderRoot;
Track: typeof VolumeSliderTrack;
Progress: typeof VolumeSliderProgress;
Thumb: typeof VolumeSliderThumb;
};
if (!globalThis.customElements.get('media-volume-slider-root')) {
globalThis.customElements.define('media-volume-slider-root', VolumeSliderRoot);
}
if (!globalThis.customElements.get('media-volume-slider-track')) {
globalThis.customElements.define('media-volume-slider-track', VolumeSliderTrack);
}
if (!globalThis.customElements.get('media-volume-slider-progress')) {
globalThis.customElements.define('media-volume-slider-progress', VolumeSliderProgress);
}
if (!globalThis.customElements.get('media-volume-slider-thumb')) {
globalThis.customElements.define('media-volume-slider-thumb', VolumeSliderThumb);
}
export default VolumeSlider;