mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(packages): dry up core, html, and react UI architecture (#699)
This commit is contained in:
@@ -17,8 +17,10 @@ export { BufferingIndicatorElement } from './ui/buffering-indicator/buffering-in
|
||||
export { ControlsElement } from './ui/controls/controls-element';
|
||||
export { ControlsGroupElement } from './ui/controls/controls-group-element';
|
||||
export { FullscreenButtonElement } from './ui/fullscreen-button/fullscreen-button-element';
|
||||
export { MediaButtonElement } from './ui/media-button-element';
|
||||
// Primitives
|
||||
export * from './ui/media-element';
|
||||
export { MediaUIElement } from './ui/media-ui-element';
|
||||
export { MuteButtonElement } from './ui/mute-button/mute-button-element';
|
||||
export { PiPButtonElement } from './ui/pip-button/pip-button-element';
|
||||
export { PlayButtonElement } from './ui/play-button/play-button-element';
|
||||
@@ -26,8 +28,8 @@ export { PlaybackRateButtonElement } from './ui/playback-rate-button/playback-ra
|
||||
export { PopoverElement } from './ui/popover/popover-element';
|
||||
export { PosterElement } from './ui/poster/poster-element';
|
||||
export { SeekButtonElement } from './ui/seek-button/seek-button-element';
|
||||
export { type SliderContextValue, sliderContext } from './ui/slider/context';
|
||||
export { SliderBufferElement } from './ui/slider/slider-buffer-element';
|
||||
export { type SliderContextValue, sliderContext } from './ui/slider/slider-context';
|
||||
export { SliderElement } from './ui/slider/slider-element';
|
||||
export type { SliderEventMap, SliderValueEventDetail } from './ui/slider/slider-events';
|
||||
export { SliderFillElement } from './ui/slider/slider-fill-element';
|
||||
|
||||
@@ -83,6 +83,10 @@ export class PlayerController<Store extends PlayerStore, Result = Store> impleme
|
||||
return this.#store?.value;
|
||||
}
|
||||
|
||||
get displayName(): string | undefined {
|
||||
return this.#selector?.displayName;
|
||||
}
|
||||
|
||||
hostConnected(): void {
|
||||
const store = this.#consumer.value;
|
||||
if (store) this.#connect(store);
|
||||
|
||||
@@ -30,7 +30,7 @@ export class BufferingIndicatorElement extends MediaElement {
|
||||
});
|
||||
|
||||
if (__DEV__ && !this.#state.value) {
|
||||
logMissingFeature(BufferingIndicatorElement.tagName, 'playback');
|
||||
logMissingFeature(this.localName, this.#state.displayName!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { StateAttrMap } from '@videojs/core';
|
||||
import { applyStateDataAttrs } from '@videojs/core/dom';
|
||||
import type { PropertyValues } from '@videojs/element';
|
||||
|
||||
import { MediaElement } from './media-element';
|
||||
|
||||
/** Shape that compound context values must satisfy for parts to consume. */
|
||||
export interface PartContextValue<State extends object> {
|
||||
state: State;
|
||||
stateAttrMap: StateAttrMap<State>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base for compound-component part elements that consume a parent
|
||||
* context and apply data attributes from `ctx.state` + `ctx.stateAttrMap`.
|
||||
*
|
||||
* Subclasses only need to declare the `consumer` property:
|
||||
*
|
||||
* ```ts
|
||||
* export class SliderTrackElement extends ContextPartElement<SliderState> {
|
||||
* static readonly tagName = 'media-slider-track';
|
||||
* protected readonly consumer = new ContextConsumer(this, { context: sliderContext, subscribe: true });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export abstract class ContextPartElement<State extends object> extends MediaElement {
|
||||
protected abstract readonly consumer: { value?: PartContextValue<State> | undefined };
|
||||
|
||||
protected override update(_changed: PropertyValues): void {
|
||||
super.update(_changed);
|
||||
const ctx = this.consumer.value;
|
||||
if (ctx) applyStateDataAttrs(this, ctx.state, ctx.stateAttrMap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { ControlsState, StateAttrMap } from '@videojs/core';
|
||||
import { createContext } from '@videojs/element/context';
|
||||
|
||||
export interface ControlsContextValue {
|
||||
state: ControlsState;
|
||||
stateAttrMap: StateAttrMap<ControlsState>;
|
||||
}
|
||||
|
||||
const CONTROLS_CONTEXT_KEY = Symbol('@videojs/controls');
|
||||
|
||||
export const controlsContext = createContext<ControlsContextValue>(CONTROLS_CONTEXT_KEY);
|
||||
@@ -1,34 +1,42 @@
|
||||
import { ControlsCore, ControlsDataAttrs } from '@videojs/core';
|
||||
import { applyStateDataAttrs, logMissingFeature, selectControls } from '@videojs/core/dom';
|
||||
import type { PropertyValues } from '@videojs/element';
|
||||
import { ContextProvider } from '@videojs/element/context';
|
||||
|
||||
import { playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
import { controlsContext } from './context';
|
||||
|
||||
export class ControlsElement extends MediaElement {
|
||||
static readonly tagName = 'media-controls';
|
||||
|
||||
readonly #core = new ControlsCore();
|
||||
readonly #state = new PlayerController(this, playerContext, selectControls);
|
||||
readonly #mediaState = new PlayerController(this, playerContext, selectControls);
|
||||
readonly #provider = new ContextProvider(this, { context: controlsContext });
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
if (__DEV__ && !this.#state.value) {
|
||||
logMissingFeature(ControlsElement.tagName, 'controls');
|
||||
if (__DEV__ && !this.#mediaState.value && this.#mediaState.displayName) {
|
||||
logMissingFeature(this.localName, this.#mediaState.displayName);
|
||||
}
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
protected override update(_changed: PropertyValues): void {
|
||||
super.update(_changed);
|
||||
|
||||
const controls = this.#state.value;
|
||||
const media = this.#mediaState.value;
|
||||
if (!media) return;
|
||||
|
||||
if (!controls) {
|
||||
return;
|
||||
}
|
||||
this.#core.setMedia(media);
|
||||
const state = this.#core.getState();
|
||||
|
||||
applyStateDataAttrs(this, this.#core.getState(controls), ControlsDataAttrs);
|
||||
applyStateDataAttrs(this, state, ControlsDataAttrs);
|
||||
|
||||
this.#provider.setValue({
|
||||
state,
|
||||
stateAttrMap: ControlsDataAttrs,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { MediaElement } from '../media-element';
|
||||
import type { ControlsState } from '@videojs/core';
|
||||
import { ContextConsumer } from '@videojs/element/context';
|
||||
|
||||
export class ControlsGroupElement extends MediaElement {
|
||||
import { ContextPartElement } from '../context-part-element';
|
||||
import { controlsContext } from './context';
|
||||
|
||||
export class ControlsGroupElement extends ContextPartElement<ControlsState> {
|
||||
static readonly tagName = 'media-controls-group';
|
||||
|
||||
protected readonly consumer = new ContextConsumer(this, { context: controlsContext, subscribe: true });
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
|
||||
@@ -1,70 +1,18 @@
|
||||
import { FullscreenButtonCore, FullscreenButtonDataAttrs } from '@videojs/core';
|
||||
import {
|
||||
applyElementProps,
|
||||
applyStateDataAttrs,
|
||||
createButton,
|
||||
logMissingFeature,
|
||||
selectFullscreen,
|
||||
} from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
import { FullscreenButtonCore, FullscreenButtonDataAttrs, type MediaFullscreenState } from '@videojs/core';
|
||||
import { selectFullscreen } from '@videojs/core/dom';
|
||||
|
||||
import { playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
import { MediaButtonElement } from '../media-button-element';
|
||||
|
||||
export class FullscreenButtonElement extends MediaElement {
|
||||
export class FullscreenButtonElement extends MediaButtonElement<FullscreenButtonCore> {
|
||||
static readonly tagName = 'media-fullscreen-button';
|
||||
|
||||
static override properties = {
|
||||
label: { type: String },
|
||||
disabled: { type: Boolean },
|
||||
} satisfies PropertyDeclarationMap<keyof FullscreenButtonCore.Props>;
|
||||
protected readonly core = new FullscreenButtonCore();
|
||||
protected readonly stateAttrMap = FullscreenButtonDataAttrs;
|
||||
protected readonly mediaState = new PlayerController(this, playerContext, selectFullscreen);
|
||||
|
||||
label = FullscreenButtonCore.defaultProps.label;
|
||||
disabled = FullscreenButtonCore.defaultProps.disabled;
|
||||
|
||||
readonly #core = new FullscreenButtonCore();
|
||||
readonly #state = new PlayerController(this, playerContext, selectFullscreen);
|
||||
|
||||
#disconnect: AbortController | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#disconnect = new AbortController();
|
||||
|
||||
const buttonProps = createButton({
|
||||
onActivate: () => this.#core.toggle(this.#state.value!),
|
||||
isDisabled: () => this.disabled || !this.#state.value,
|
||||
});
|
||||
|
||||
applyElementProps(this, buttonProps, this.#disconnect.signal);
|
||||
|
||||
if (__DEV__ && !this.#state.value) {
|
||||
logMissingFeature(FullscreenButtonElement.tagName, 'fullscreen');
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = null;
|
||||
}
|
||||
|
||||
protected override willUpdate(changed: PropertyValues): void {
|
||||
super.willUpdate(changed);
|
||||
this.#core.setProps(this);
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
|
||||
const media = this.#state.value;
|
||||
|
||||
if (!media) return;
|
||||
|
||||
const state = this.#core.getState(media);
|
||||
applyElementProps(this, this.#core.getAttrs(state));
|
||||
applyStateDataAttrs(this, state, FullscreenButtonDataAttrs);
|
||||
protected activate(state: MediaFullscreenState): void {
|
||||
this.core.toggle(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { InferComponentState, InferMediaState, MediaUIComponent, StateAttrMap } from '@videojs/core';
|
||||
import { applyElementProps, applyStateDataAttrs, createButton, logMissingFeature } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
|
||||
import type { PlayerController } from '../player/player-controller';
|
||||
import { MediaElement } from './media-element';
|
||||
|
||||
/** Abstract base for HTML custom elements that render a media-control button. */
|
||||
export abstract class MediaButtonElement<Core extends MediaUIComponent> extends MediaElement {
|
||||
static override properties: PropertyDeclarationMap = {
|
||||
label: { type: String },
|
||||
disabled: { type: Boolean },
|
||||
};
|
||||
|
||||
disabled = false;
|
||||
label = '';
|
||||
|
||||
protected abstract readonly core: Core;
|
||||
protected abstract readonly stateAttrMap: StateAttrMap<InferComponentState<Core>>;
|
||||
protected abstract readonly mediaState: PlayerController<any, InferMediaState<Core> | undefined>;
|
||||
|
||||
protected abstract activate(state: InferMediaState<Core>): void;
|
||||
|
||||
#disconnect: AbortController | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#disconnect = new AbortController();
|
||||
|
||||
const buttonProps = createButton({
|
||||
onActivate: () => this.activate(this.mediaState.value!),
|
||||
isDisabled: () => this.disabled || !this.mediaState.value,
|
||||
});
|
||||
|
||||
applyElementProps(this, buttonProps, { signal: this.#disconnect.signal });
|
||||
|
||||
if (__DEV__ && !this.mediaState.value && this.mediaState.displayName) {
|
||||
logMissingFeature(this.localName, this.mediaState.displayName);
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = null;
|
||||
}
|
||||
|
||||
protected override willUpdate(changed: PropertyValues): void {
|
||||
super.willUpdate(changed);
|
||||
this.core.setProps?.(this);
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
|
||||
const media = this.mediaState.value;
|
||||
|
||||
if (!media) return;
|
||||
|
||||
this.core.setMedia(media);
|
||||
const state = this.core.getState();
|
||||
applyElementProps(this, this.core.getAttrs?.(state) ?? {});
|
||||
applyStateDataAttrs(this, state, this.stateAttrMap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { InferComponentState, InferMediaState, MediaUIComponent, StateAttrMap } from '@videojs/core';
|
||||
import { applyStateDataAttrs, logMissingFeature } from '@videojs/core/dom';
|
||||
import type { PropertyValues } from '@videojs/element';
|
||||
|
||||
import type { PlayerController } from '../player/player-controller';
|
||||
import { MediaElement } from './media-element';
|
||||
|
||||
/** Abstract base for HTML custom elements that display media state with data attributes. */
|
||||
export abstract class MediaUIElement<Core extends MediaUIComponent> extends MediaElement {
|
||||
protected abstract readonly core: Core;
|
||||
protected abstract readonly stateAttrMap: StateAttrMap<InferComponentState<Core>>;
|
||||
protected abstract readonly mediaState: PlayerController<any, InferMediaState<Core> | undefined>;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
if (__DEV__ && !this.mediaState.value && this.mediaState.displayName) {
|
||||
logMissingFeature(this.localName, this.mediaState.displayName);
|
||||
}
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
|
||||
const media = this.mediaState.value;
|
||||
|
||||
if (!media) return;
|
||||
|
||||
this.core.setMedia(media);
|
||||
const state = this.core.getState();
|
||||
applyStateDataAttrs(this, state, this.stateAttrMap);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,18 @@
|
||||
import { MuteButtonCore, MuteButtonDataAttrs } from '@videojs/core';
|
||||
import {
|
||||
applyElementProps,
|
||||
applyStateDataAttrs,
|
||||
createButton,
|
||||
logMissingFeature,
|
||||
selectVolume,
|
||||
} from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
import { type MediaVolumeState, MuteButtonCore, MuteButtonDataAttrs } from '@videojs/core';
|
||||
import { selectVolume } from '@videojs/core/dom';
|
||||
|
||||
import { playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
import { MediaButtonElement } from '../media-button-element';
|
||||
|
||||
export class MuteButtonElement extends MediaElement {
|
||||
export class MuteButtonElement extends MediaButtonElement<MuteButtonCore> {
|
||||
static readonly tagName = 'media-mute-button';
|
||||
|
||||
static override properties = {
|
||||
label: { type: String },
|
||||
disabled: { type: Boolean },
|
||||
} satisfies PropertyDeclarationMap<keyof MuteButtonCore.Props>;
|
||||
protected readonly core = new MuteButtonCore();
|
||||
protected readonly stateAttrMap = MuteButtonDataAttrs;
|
||||
protected readonly mediaState = new PlayerController(this, playerContext, selectVolume);
|
||||
|
||||
label = MuteButtonCore.defaultProps.label;
|
||||
disabled = MuteButtonCore.defaultProps.disabled;
|
||||
|
||||
readonly #core = new MuteButtonCore();
|
||||
readonly #state = new PlayerController(this, playerContext, selectVolume);
|
||||
|
||||
#disconnect: AbortController | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#disconnect = new AbortController();
|
||||
|
||||
const buttonProps = createButton({
|
||||
onActivate: () => this.#core.toggle(this.#state.value!),
|
||||
isDisabled: () => this.disabled || !this.#state.value,
|
||||
});
|
||||
|
||||
applyElementProps(this, buttonProps, this.#disconnect.signal);
|
||||
|
||||
if (__DEV__ && !this.#state.value) {
|
||||
logMissingFeature(MuteButtonElement.tagName, 'volume');
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = null;
|
||||
}
|
||||
|
||||
protected override willUpdate(changed: PropertyValues): void {
|
||||
super.willUpdate(changed);
|
||||
this.#core.setProps(this);
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
|
||||
const media = this.#state.value;
|
||||
|
||||
if (!media) return;
|
||||
|
||||
const state = this.#core.getState(media);
|
||||
applyElementProps(this, this.#core.getAttrs(state));
|
||||
applyStateDataAttrs(this, state, MuteButtonDataAttrs);
|
||||
protected activate(state: MediaVolumeState): void {
|
||||
this.core.toggle(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,18 @@
|
||||
import { PiPButtonCore, PiPButtonDataAttrs } from '@videojs/core';
|
||||
import { applyElementProps, applyStateDataAttrs, createButton, logMissingFeature, selectPiP } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
import { type MediaPictureInPictureState, PiPButtonCore, PiPButtonDataAttrs } from '@videojs/core';
|
||||
import { selectPiP } from '@videojs/core/dom';
|
||||
|
||||
import { playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
import { MediaButtonElement } from '../media-button-element';
|
||||
|
||||
export class PiPButtonElement extends MediaElement {
|
||||
export class PiPButtonElement extends MediaButtonElement<PiPButtonCore> {
|
||||
static readonly tagName = 'media-pip-button';
|
||||
|
||||
static override properties = {
|
||||
label: { type: String },
|
||||
disabled: { type: Boolean },
|
||||
} satisfies PropertyDeclarationMap<keyof PiPButtonCore.Props>;
|
||||
protected readonly core = new PiPButtonCore();
|
||||
protected readonly stateAttrMap = PiPButtonDataAttrs;
|
||||
protected readonly mediaState = new PlayerController(this, playerContext, selectPiP);
|
||||
|
||||
label = PiPButtonCore.defaultProps.label;
|
||||
disabled = PiPButtonCore.defaultProps.disabled;
|
||||
|
||||
readonly #core = new PiPButtonCore();
|
||||
readonly #state = new PlayerController(this, playerContext, selectPiP);
|
||||
|
||||
#disconnect: AbortController | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#disconnect = new AbortController();
|
||||
|
||||
const buttonProps = createButton({
|
||||
onActivate: () => this.#core.toggle(this.#state.value!),
|
||||
isDisabled: () => this.disabled || !this.#state.value,
|
||||
});
|
||||
|
||||
applyElementProps(this, buttonProps, this.#disconnect.signal);
|
||||
|
||||
if (__DEV__ && !this.#state.value) {
|
||||
logMissingFeature(PiPButtonElement.tagName, 'pip');
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = null;
|
||||
}
|
||||
|
||||
protected override willUpdate(changed: PropertyValues): void {
|
||||
super.willUpdate(changed);
|
||||
this.#core.setProps(this);
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
|
||||
const media = this.#state.value;
|
||||
|
||||
if (!media) return;
|
||||
|
||||
const state = this.#core.getState(media);
|
||||
applyElementProps(this, this.#core.getAttrs(state));
|
||||
applyStateDataAttrs(this, state, PiPButtonDataAttrs);
|
||||
protected activate(state: MediaPictureInPictureState): void {
|
||||
this.core.toggle(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,18 @@
|
||||
import { PlayButtonCore, PlayButtonDataAttrs } from '@videojs/core';
|
||||
import {
|
||||
applyElementProps,
|
||||
applyStateDataAttrs,
|
||||
createButton,
|
||||
logMissingFeature,
|
||||
selectPlayback,
|
||||
} from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
import { type MediaPlaybackState, PlayButtonCore, PlayButtonDataAttrs } from '@videojs/core';
|
||||
import { selectPlayback } from '@videojs/core/dom';
|
||||
|
||||
import { playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
import { MediaButtonElement } from '../media-button-element';
|
||||
|
||||
export class PlayButtonElement extends MediaElement {
|
||||
export class PlayButtonElement extends MediaButtonElement<PlayButtonCore> {
|
||||
static readonly tagName = 'media-play-button';
|
||||
|
||||
static override properties = {
|
||||
label: { type: String },
|
||||
disabled: { type: Boolean },
|
||||
} satisfies PropertyDeclarationMap<keyof PlayButtonCore.Props>;
|
||||
protected readonly core = new PlayButtonCore();
|
||||
protected readonly stateAttrMap = PlayButtonDataAttrs;
|
||||
protected readonly mediaState = new PlayerController(this, playerContext, selectPlayback);
|
||||
|
||||
label = PlayButtonCore.defaultProps.label;
|
||||
disabled = PlayButtonCore.defaultProps.disabled;
|
||||
|
||||
readonly #core = new PlayButtonCore();
|
||||
readonly #state = new PlayerController(this, playerContext, selectPlayback);
|
||||
|
||||
#disconnect: AbortController | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#disconnect = new AbortController();
|
||||
|
||||
const buttonProps = createButton({
|
||||
onActivate: () => this.#core.toggle(this.#state.value!),
|
||||
isDisabled: () => this.disabled || !this.#state.value,
|
||||
});
|
||||
|
||||
applyElementProps(this, buttonProps, this.#disconnect.signal);
|
||||
|
||||
if (__DEV__ && !this.#state.value) {
|
||||
logMissingFeature(PlayButtonElement.tagName, 'playback');
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = null;
|
||||
}
|
||||
|
||||
protected override willUpdate(changed: PropertyValues): void {
|
||||
super.willUpdate(changed);
|
||||
this.#core.setProps(this);
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
|
||||
const media = this.#state.value;
|
||||
|
||||
if (!media) return;
|
||||
|
||||
const state = this.#core.getState(media);
|
||||
applyElementProps(this, this.#core.getAttrs(state));
|
||||
applyStateDataAttrs(this, state, PlayButtonDataAttrs);
|
||||
protected activate(state: MediaPlaybackState): void {
|
||||
this.core.toggle(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,18 @@
|
||||
import { PlaybackRateButtonCore, PlaybackRateButtonDataAttrs } from '@videojs/core';
|
||||
import {
|
||||
applyElementProps,
|
||||
applyStateDataAttrs,
|
||||
createButton,
|
||||
logMissingFeature,
|
||||
selectPlaybackRate,
|
||||
} from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
import { type MediaPlaybackRateState, PlaybackRateButtonCore, PlaybackRateButtonDataAttrs } from '@videojs/core';
|
||||
import { selectPlaybackRate } from '@videojs/core/dom';
|
||||
|
||||
import { playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
import { MediaButtonElement } from '../media-button-element';
|
||||
|
||||
export class PlaybackRateButtonElement extends MediaElement {
|
||||
export class PlaybackRateButtonElement extends MediaButtonElement<PlaybackRateButtonCore> {
|
||||
static readonly tagName = 'media-playback-rate-button';
|
||||
|
||||
static override properties = {
|
||||
label: { type: String },
|
||||
disabled: { type: Boolean },
|
||||
} satisfies PropertyDeclarationMap<keyof PlaybackRateButtonCore.Props>;
|
||||
protected readonly core = new PlaybackRateButtonCore();
|
||||
protected readonly stateAttrMap = PlaybackRateButtonDataAttrs;
|
||||
protected readonly mediaState = new PlayerController(this, playerContext, selectPlaybackRate);
|
||||
|
||||
label = PlaybackRateButtonCore.defaultProps.label;
|
||||
disabled = PlaybackRateButtonCore.defaultProps.disabled;
|
||||
|
||||
readonly #core = new PlaybackRateButtonCore();
|
||||
readonly #state = new PlayerController(this, playerContext, selectPlaybackRate);
|
||||
|
||||
#disconnect: AbortController | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#disconnect = new AbortController();
|
||||
|
||||
const buttonProps = createButton({
|
||||
onActivate: () => this.#core.cycle(this.#state.value!),
|
||||
isDisabled: () => this.disabled || !this.#state.value,
|
||||
});
|
||||
|
||||
applyElementProps(this, buttonProps, this.#disconnect.signal);
|
||||
|
||||
if (__DEV__ && !this.#state.value) {
|
||||
logMissingFeature(PlaybackRateButtonElement.tagName, 'playbackRate');
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = null;
|
||||
}
|
||||
|
||||
protected override willUpdate(changed: PropertyValues): void {
|
||||
super.willUpdate(changed);
|
||||
this.#core.setProps(this);
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
|
||||
const media = this.#state.value;
|
||||
|
||||
if (!media) return;
|
||||
|
||||
const state = this.#core.getState(media);
|
||||
applyElementProps(this, this.#core.getAttrs(state));
|
||||
applyStateDataAttrs(this, state, PlaybackRateButtonDataAttrs);
|
||||
protected activate(state: MediaPlaybackRateState): void {
|
||||
this.core.cycle(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { PopoverCore, PopoverDataAttrs, type PopoverInteraction, type PopoverProps } from '@videojs/core';
|
||||
import { PopoverCore, PopoverDataAttrs, type PopoverInput, type PopoverProps } from '@videojs/core';
|
||||
import {
|
||||
applyElementProps,
|
||||
applyStateDataAttrs,
|
||||
createPopover,
|
||||
createTransitionHandler,
|
||||
createTransition,
|
||||
getAnchorNameStyle,
|
||||
getAnchorPositionStyle,
|
||||
type PopoverApi,
|
||||
type PopoverChangeDetails,
|
||||
type PopoverHandle,
|
||||
resolveOffsets,
|
||||
} from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
@@ -44,12 +44,12 @@ export class PopoverElement extends MediaElement {
|
||||
closeDelay = PopoverCore.defaultProps.closeDelay;
|
||||
|
||||
readonly #core = new PopoverCore();
|
||||
#popover: PopoverHandle | null = null;
|
||||
#snapshot: SnapshotController<PopoverInteraction> | null = null;
|
||||
#popover: PopoverApi | null = null;
|
||||
#snapshot: SnapshotController<PopoverInput> | null = null;
|
||||
|
||||
// Cleanup controllers
|
||||
#disconnect: AbortController | null = null;
|
||||
#triggerAc: AbortController | null = null;
|
||||
#triggerAbort: AbortController | null = null;
|
||||
#currentTrigger: HTMLElement | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
@@ -57,7 +57,7 @@ export class PopoverElement extends MediaElement {
|
||||
this.#disconnect = new AbortController();
|
||||
|
||||
this.#popover = createPopover({
|
||||
transition: createTransitionHandler(),
|
||||
transition: createTransition(),
|
||||
onOpenChange: (nextOpen: boolean, details: PopoverChangeDetails) => {
|
||||
this.open = nextOpen;
|
||||
this.dispatchEvent(new CustomEvent('open-change', { detail: { open: nextOpen, ...details } }));
|
||||
@@ -73,15 +73,15 @@ export class PopoverElement extends MediaElement {
|
||||
this.#popover.setPopupElement(this);
|
||||
|
||||
// Apply popup event handlers (pointerenter/leave, focusout) to self.
|
||||
applyElementProps(this, this.#popover.popupProps, this.#disconnect.signal);
|
||||
applyElementProps(this, this.#popover.popupProps, { signal: this.#disconnect.signal });
|
||||
|
||||
// Subscribe to interaction state for reactive updates.
|
||||
// Reuse the controller across connect/disconnect cycles to avoid
|
||||
// leaking stale controllers in the host's controller set.
|
||||
if (this.#snapshot) {
|
||||
this.#snapshot.track(this.#popover.interaction);
|
||||
this.#snapshot.track(this.#popover.input);
|
||||
} else {
|
||||
this.#snapshot = new SnapshotController(this, this.#popover.interaction);
|
||||
this.#snapshot = new SnapshotController(this, this.#popover.input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ export class PopoverElement extends MediaElement {
|
||||
|
||||
// Sync controlled open state
|
||||
if (this.#popover && changed.has('open')) {
|
||||
const { active: interactionOpen } = this.#popover.interaction.current;
|
||||
const { active: interactionOpen } = this.#popover.input.current;
|
||||
if (this.open !== interactionOpen) {
|
||||
if (this.open) {
|
||||
this.#popover.open();
|
||||
@@ -129,9 +129,10 @@ export class PopoverElement extends MediaElement {
|
||||
const triggerEl = this.#findTrigger();
|
||||
this.#syncTrigger(triggerEl);
|
||||
|
||||
// Derive state from core + interaction.
|
||||
const interaction = this.#popover.interaction.current;
|
||||
const state = this.#core.getState(interaction);
|
||||
// Derive state from core + input.
|
||||
const input = this.#popover.input.current;
|
||||
this.#core.setInput(input);
|
||||
const state = this.#core.getState();
|
||||
|
||||
// Apply popup ARIA and data attributes to self.
|
||||
applyElementProps(this, this.#core.getPopupAttrs(state));
|
||||
@@ -178,8 +179,8 @@ export class PopoverElement extends MediaElement {
|
||||
this.#popover?.setTriggerElement(triggerEl);
|
||||
|
||||
if (triggerEl && this.#popover) {
|
||||
this.#triggerAc = new AbortController();
|
||||
applyElementProps(triggerEl, this.#popover.triggerProps, this.#triggerAc.signal);
|
||||
this.#triggerAbort = new AbortController();
|
||||
applyElementProps(triggerEl, this.#popover.triggerProps, { signal: this.#triggerAbort.signal });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +195,8 @@ export class PopoverElement extends MediaElement {
|
||||
this.#currentTrigger.style.removeProperty('anchor-name');
|
||||
}
|
||||
|
||||
this.#triggerAc?.abort();
|
||||
this.#triggerAc = null;
|
||||
this.#triggerAbort?.abort();
|
||||
this.#triggerAbort = null;
|
||||
this.#currentTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,14 @@
|
||||
import { PosterCore, PosterDataAttrs } from '@videojs/core';
|
||||
import { applyStateDataAttrs, logMissingFeature, selectPlayback } from '@videojs/core/dom';
|
||||
import type { PropertyValues } from '@videojs/element';
|
||||
import { selectPlayback } from '@videojs/core/dom';
|
||||
|
||||
import { playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
import { MediaUIElement } from '../media-ui-element';
|
||||
|
||||
export class PosterElement extends MediaElement {
|
||||
export class PosterElement extends MediaUIElement<PosterCore> {
|
||||
static readonly tagName = 'media-poster';
|
||||
|
||||
readonly #core = new PosterCore();
|
||||
readonly #state = new PlayerController(this, playerContext, selectPlayback);
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
if (__DEV__ && !this.#state.value) {
|
||||
logMissingFeature(PosterElement.tagName, 'playback');
|
||||
}
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
|
||||
const media = this.#state.value;
|
||||
|
||||
if (!media) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyStateDataAttrs(this, this.#core.getState(media), PosterDataAttrs);
|
||||
}
|
||||
protected readonly core = new PosterCore();
|
||||
protected readonly stateAttrMap = PosterDataAttrs;
|
||||
protected readonly mediaState = new PlayerController(this, playerContext, selectPlayback);
|
||||
}
|
||||
|
||||
@@ -1,66 +1,26 @@
|
||||
import { SeekButtonCore, SeekButtonDataAttrs } from '@videojs/core';
|
||||
import { applyElementProps, applyStateDataAttrs, createButton, logMissingFeature, selectTime } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
import { type MediaTimeState, SeekButtonCore, SeekButtonDataAttrs } from '@videojs/core';
|
||||
import { selectTime } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap } from '@videojs/element';
|
||||
|
||||
import { playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
import { MediaButtonElement } from '../media-button-element';
|
||||
|
||||
export class SeekButtonElement extends MediaElement {
|
||||
export class SeekButtonElement extends MediaButtonElement<SeekButtonCore> {
|
||||
static readonly tagName = 'media-seek-button';
|
||||
|
||||
static override properties = {
|
||||
static override properties: PropertyDeclarationMap = {
|
||||
...MediaButtonElement.properties,
|
||||
seconds: { type: Number },
|
||||
label: { type: String },
|
||||
disabled: { type: Boolean },
|
||||
} satisfies PropertyDeclarationMap<keyof SeekButtonCore.Props>;
|
||||
};
|
||||
|
||||
seconds = SeekButtonCore.defaultProps.seconds;
|
||||
label = SeekButtonCore.defaultProps.label;
|
||||
disabled = SeekButtonCore.defaultProps.disabled;
|
||||
|
||||
readonly #core = new SeekButtonCore();
|
||||
readonly #state = new PlayerController(this, playerContext, selectTime);
|
||||
protected readonly core = new SeekButtonCore();
|
||||
protected readonly stateAttrMap = SeekButtonDataAttrs;
|
||||
protected readonly mediaState = new PlayerController(this, playerContext, selectTime);
|
||||
|
||||
#disconnect: AbortController | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#disconnect = new AbortController();
|
||||
|
||||
const buttonProps = createButton({
|
||||
onActivate: () => this.#core.seek(this.#state.value!),
|
||||
isDisabled: () => this.disabled || !this.#state.value,
|
||||
});
|
||||
|
||||
applyElementProps(this, buttonProps, this.#disconnect.signal);
|
||||
|
||||
if (__DEV__ && !this.#state.value) {
|
||||
logMissingFeature(SeekButtonElement.tagName, 'time');
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = null;
|
||||
}
|
||||
|
||||
protected override willUpdate(changed: PropertyValues): void {
|
||||
super.willUpdate(changed);
|
||||
this.#core.setProps(this);
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
|
||||
const media = this.#state.value;
|
||||
|
||||
if (!media) return;
|
||||
|
||||
const state = this.#core.getState(media);
|
||||
applyElementProps(this, this.#core.getAttrs(state));
|
||||
applyStateDataAttrs(this, state, SeekButtonDataAttrs);
|
||||
protected activate(state: MediaTimeState): void {
|
||||
this.core.seek(state);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,10 +1,12 @@
|
||||
import type { SliderState } from '@videojs/core';
|
||||
import type { SliderState, StateAttrMap } from '@videojs/core';
|
||||
import type { SliderThumbProps } from '@videojs/core/dom';
|
||||
import { createContext } from '@videojs/element/context';
|
||||
|
||||
export interface SliderContextValue {
|
||||
/** Base slider state — children use this for data attributes and value display. */
|
||||
state: SliderState;
|
||||
/** Maps state keys to `data-*` attribute names for part elements. */
|
||||
stateAttrMap: StateAttrMap<SliderState>;
|
||||
/** Domain value at the current pointer position (e.g., seconds for time, percent for volume). */
|
||||
pointerValue: number;
|
||||
/** ARIA attributes for the thumb element (role, tabindex, aria-value*, etc.). */
|
||||
@@ -1,22 +1,11 @@
|
||||
import { SliderDataAttrs } from '@videojs/core';
|
||||
import { applyStateDataAttrs } from '@videojs/core/dom';
|
||||
import type { PropertyValues } from '@videojs/element';
|
||||
import type { SliderState } from '@videojs/core';
|
||||
import { ContextConsumer } from '@videojs/element/context';
|
||||
|
||||
import { MediaElement } from '../media-element';
|
||||
import { sliderContext } from './slider-context';
|
||||
import { ContextPartElement } from '../context-part-element';
|
||||
import { sliderContext } from './context';
|
||||
|
||||
export class SliderBufferElement extends MediaElement {
|
||||
export class SliderBufferElement extends ContextPartElement<SliderState> {
|
||||
static readonly tagName = 'media-slider-buffer';
|
||||
|
||||
readonly #ctx = new ContextConsumer(this, {
|
||||
context: sliderContext,
|
||||
subscribe: true,
|
||||
});
|
||||
|
||||
protected override update(_changed: PropertyValues): void {
|
||||
super.update(_changed);
|
||||
const ctx = this.#ctx.value;
|
||||
if (ctx) applyStateDataAttrs(this, ctx.state, SliderDataAttrs);
|
||||
}
|
||||
protected readonly consumer = new ContextConsumer(this, { context: sliderContext, subscribe: true });
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { SliderCore, SliderDataAttrs } from '@videojs/core';
|
||||
import { applyStateDataAttrs, createSlider, getSliderCSSVars, type SliderHandle } from '@videojs/core/dom';
|
||||
import { applyStateDataAttrs, createSlider, getSliderCSSVars, type SliderApi } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
import { ContextProvider } from '@videojs/element/context';
|
||||
import { applyStyles, isRTL } from '@videojs/utils/dom';
|
||||
|
||||
import { MediaElement } from '../media-element';
|
||||
import { sliderContext } from './slider-context';
|
||||
import { sliderContext } from './context';
|
||||
|
||||
export class SliderElement extends MediaElement {
|
||||
static readonly tagName = 'media-slider';
|
||||
@@ -35,7 +35,7 @@ export class SliderElement extends MediaElement {
|
||||
readonly #core = new SliderCore();
|
||||
readonly #provider = new ContextProvider(this, { context: sliderContext });
|
||||
|
||||
#slider: SliderHandle | null = null;
|
||||
#slider: SliderApi | null = null;
|
||||
#disconnect: AbortController | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
@@ -51,16 +51,8 @@ export class SliderElement extends MediaElement {
|
||||
isRTL: () => isRTL(this),
|
||||
isDisabled: () => this.disabled,
|
||||
getPercent: () => this.#core.percentFromValue(this.value),
|
||||
getStepPercent: () => {
|
||||
const { step, min, max } = this.#core.props;
|
||||
const range = max - min;
|
||||
return range > 0 ? (step / range) * 100 : 0;
|
||||
},
|
||||
getLargeStepPercent: () => {
|
||||
const { largeStep, min, max } = this.#core.props;
|
||||
const range = max - min;
|
||||
return range > 0 ? (largeStep / range) * 100 : 0;
|
||||
},
|
||||
getStepPercent: () => this.#core.getStepPercent(),
|
||||
getLargeStepPercent: () => this.#core.getLargeStepPercent(),
|
||||
onValueChange: (percent) => {
|
||||
this.value = this.#core.valueFromPercent(percent);
|
||||
this.dispatchEvent(new CustomEvent('value-change', { detail: { value: this.value }, bubbles: true }));
|
||||
@@ -77,7 +69,7 @@ export class SliderElement extends MediaElement {
|
||||
},
|
||||
});
|
||||
|
||||
this.#slider.interaction.subscribe(() => this.requestUpdate(), { signal });
|
||||
this.#slider.input.subscribe(() => this.requestUpdate(), { signal });
|
||||
|
||||
// Prevent default touch gestures and text selection during interaction.
|
||||
this.style.touchAction = 'none';
|
||||
@@ -101,8 +93,8 @@ export class SliderElement extends MediaElement {
|
||||
super.update(_changed);
|
||||
if (!this.#slider) return;
|
||||
|
||||
const interaction = this.#slider.interaction.current;
|
||||
const state = this.#core.getState(interaction, this.value);
|
||||
this.#core.setInput(this.#slider.input.current);
|
||||
const state = this.#core.getSliderState(this.value);
|
||||
const cssVars = getSliderCSSVars(state);
|
||||
|
||||
applyStyles(this, cssVars);
|
||||
@@ -113,6 +105,7 @@ export class SliderElement extends MediaElement {
|
||||
// Provide context to child elements (thumb, value, track, etc.).
|
||||
this.#provider.setValue({
|
||||
state,
|
||||
stateAttrMap: SliderDataAttrs,
|
||||
pointerValue: this.#core.valueFromPercent(state.pointerPercent),
|
||||
thumbAttrs: this.#core.getAttrs(state),
|
||||
thumbProps: this.#slider.thumbProps,
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
import { SliderDataAttrs } from '@videojs/core';
|
||||
import { applyStateDataAttrs } from '@videojs/core/dom';
|
||||
import type { PropertyValues } from '@videojs/element';
|
||||
import type { SliderState } from '@videojs/core';
|
||||
import { ContextConsumer } from '@videojs/element/context';
|
||||
|
||||
import { MediaElement } from '../media-element';
|
||||
import { sliderContext } from './slider-context';
|
||||
import { ContextPartElement } from '../context-part-element';
|
||||
import { sliderContext } from './context';
|
||||
|
||||
export class SliderFillElement extends MediaElement {
|
||||
export class SliderFillElement extends ContextPartElement<SliderState> {
|
||||
static readonly tagName = 'media-slider-fill';
|
||||
|
||||
readonly #ctx = new ContextConsumer(this, {
|
||||
context: sliderContext,
|
||||
subscribe: true,
|
||||
});
|
||||
|
||||
protected override update(_changed: PropertyValues): void {
|
||||
super.update(_changed);
|
||||
const ctx = this.#ctx.value;
|
||||
if (ctx) applyStateDataAttrs(this, ctx.state, SliderDataAttrs);
|
||||
}
|
||||
protected readonly consumer = new ContextConsumer(this, { context: sliderContext, subscribe: true });
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { SliderDataAttrs } from '@videojs/core';
|
||||
import { applyElementProps, applyStateDataAttrs } from '@videojs/core/dom';
|
||||
import type { PropertyValues } from '@videojs/element';
|
||||
import { ContextConsumer } from '@videojs/element/context';
|
||||
|
||||
import { MediaElement } from '../media-element';
|
||||
import { sliderContext } from './slider-context';
|
||||
import { sliderContext } from './context';
|
||||
|
||||
export class SliderThumbElement extends MediaElement {
|
||||
static readonly tagName = 'media-slider-thumb';
|
||||
@@ -38,7 +37,7 @@ export class SliderThumbElement extends MediaElement {
|
||||
|
||||
// Apply keyboard and focus handlers once — they don't change per slider instance.
|
||||
if (!this.#thumbPropsApplied && this.#disconnect) {
|
||||
applyElementProps(this, ctx.thumbProps, this.#disconnect.signal);
|
||||
applyElementProps(this, ctx.thumbProps, { signal: this.#disconnect.signal });
|
||||
this.#thumbPropsApplied = true;
|
||||
}
|
||||
|
||||
@@ -46,6 +45,6 @@ export class SliderThumbElement extends MediaElement {
|
||||
applyElementProps(this, ctx.thumbAttrs);
|
||||
|
||||
// Apply state data attributes.
|
||||
applyStateDataAttrs(this, ctx.state, SliderDataAttrs);
|
||||
applyStateDataAttrs(this, ctx.state, ctx.stateAttrMap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
import { SliderDataAttrs } from '@videojs/core';
|
||||
import { applyStateDataAttrs } from '@videojs/core/dom';
|
||||
import type { PropertyValues } from '@videojs/element';
|
||||
import type { SliderState } from '@videojs/core';
|
||||
import { ContextConsumer } from '@videojs/element/context';
|
||||
|
||||
import { MediaElement } from '../media-element';
|
||||
import { sliderContext } from './slider-context';
|
||||
import { ContextPartElement } from '../context-part-element';
|
||||
import { sliderContext } from './context';
|
||||
|
||||
export class SliderTrackElement extends MediaElement {
|
||||
export class SliderTrackElement extends ContextPartElement<SliderState> {
|
||||
static readonly tagName = 'media-slider-track';
|
||||
|
||||
readonly #ctx = new ContextConsumer(this, {
|
||||
context: sliderContext,
|
||||
subscribe: true,
|
||||
});
|
||||
|
||||
protected override update(_changed: PropertyValues): void {
|
||||
super.update(_changed);
|
||||
const ctx = this.#ctx.value;
|
||||
if (ctx) applyStateDataAttrs(this, ctx.state, SliderDataAttrs);
|
||||
}
|
||||
protected readonly consumer = new ContextConsumer(this, { context: sliderContext, subscribe: true });
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { SliderDataAttrs } from '@videojs/core';
|
||||
import { applyStateDataAttrs } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
import { ContextConsumer } from '@videojs/element/context';
|
||||
|
||||
import { MediaElement } from '../media-element';
|
||||
import { sliderContext } from './slider-context';
|
||||
import { sliderContext } from './context';
|
||||
|
||||
export class SliderValueElement extends MediaElement {
|
||||
static readonly tagName = 'media-slider-value';
|
||||
@@ -35,6 +34,6 @@ export class SliderValueElement extends MediaElement {
|
||||
|
||||
this.textContent = ctx.formatValue ? ctx.formatValue(value, this.type) : String(Math.round(value));
|
||||
|
||||
applyStateDataAttrs(this, ctx.state, SliderDataAttrs);
|
||||
applyStateDataAttrs(this, ctx.state, ctx.stateAttrMap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type ThumbnailImage,
|
||||
type ThumbnailResizeResult,
|
||||
} from '@videojs/core';
|
||||
import type { ThumbnailHandle } from '@videojs/core/dom';
|
||||
import type { ThumbnailApi } from '@videojs/core/dom';
|
||||
import { applyElementProps, applyStateDataAttrs, createThumbnail, selectTextTrack } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
|
||||
@@ -45,7 +45,7 @@ export class ThumbnailElement extends MediaElement {
|
||||
#thumbnails: ThumbnailImage[] = [];
|
||||
#externalThumbnails: ThumbnailImage[] | undefined;
|
||||
#lastTextTrack: MediaTextTrackState | undefined;
|
||||
#handle: ThumbnailHandle | null = null;
|
||||
#api: ThumbnailApi | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -79,7 +79,7 @@ export class ThumbnailElement extends MediaElement {
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#handle = createThumbnail({
|
||||
this.#api = createThumbnail({
|
||||
getContainer: () => this,
|
||||
getImg: () => this.#img,
|
||||
onStateChange: () => this.requestUpdate(),
|
||||
@@ -88,8 +88,8 @@ export class ThumbnailElement extends MediaElement {
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#handle?.destroy();
|
||||
this.#handle = null;
|
||||
this.#api?.destroy();
|
||||
this.#api = null;
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
@@ -119,8 +119,8 @@ export class ThumbnailElement extends MediaElement {
|
||||
fetchpriority: this.fetchPriority,
|
||||
});
|
||||
|
||||
// Track src changes via the handle.
|
||||
this.#handle?.updateSrc(thumbnail?.url);
|
||||
// Track src changes via the thumbnail API.
|
||||
this.#api?.updateSrc(thumbnail?.url);
|
||||
|
||||
if (!thumbnail) {
|
||||
this.#img.removeAttribute('src');
|
||||
@@ -137,14 +137,14 @@ export class ThumbnailElement extends MediaElement {
|
||||
this.#img.src = thumbnail.url;
|
||||
}
|
||||
|
||||
const handle = this.#handle;
|
||||
const state = this.#core.getState(handle?.loading ?? false, handle?.error ?? false, thumbnail);
|
||||
const api = this.#api;
|
||||
const state = this.#core.getState(api?.loading ?? false, api?.error ?? false, thumbnail);
|
||||
applyElementProps(this, this.#core.getAttrs(state));
|
||||
applyStateDataAttrs(this, state, ThumbnailDataAttrs);
|
||||
|
||||
if (handle?.naturalWidth && handle.naturalHeight) {
|
||||
const constraints = handle.readConstraints();
|
||||
const result = this.#core.resize(thumbnail, handle.naturalWidth, handle.naturalHeight, constraints);
|
||||
if (api?.naturalWidth && api.naturalHeight) {
|
||||
const constraints = api.readConstraints();
|
||||
const result = this.#core.resize(thumbnail, api.naturalWidth, api.naturalHeight, constraints);
|
||||
|
||||
if (result) {
|
||||
this.#applyResize(result);
|
||||
|
||||
@@ -4,18 +4,19 @@ import {
|
||||
createSlider,
|
||||
getTimeSliderCSSVars,
|
||||
logMissingFeature,
|
||||
type SliderHandle,
|
||||
type SliderApi,
|
||||
selectBuffer,
|
||||
selectTime,
|
||||
} from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
import { ContextProvider } from '@videojs/element/context';
|
||||
import { applyStyles, isRTL } from '@videojs/utils/dom';
|
||||
import { formatTime } from '@videojs/utils/time';
|
||||
|
||||
import { playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
import { sliderContext } from '../slider/slider-context';
|
||||
import { sliderContext } from '../slider/context';
|
||||
|
||||
export class TimeSliderElement extends MediaElement {
|
||||
static readonly tagName = 'media-time-slider';
|
||||
@@ -28,7 +29,7 @@ export class TimeSliderElement extends MediaElement {
|
||||
orientation: { type: String },
|
||||
disabled: { type: Boolean },
|
||||
thumbAlignment: { type: String, attribute: 'thumb-alignment' },
|
||||
} satisfies PropertyDeclarationMap<keyof TimeSliderCore.Props>;
|
||||
} satisfies PropertyDeclarationMap<Exclude<keyof TimeSliderCore.Props, 'value' | 'min' | 'max'>>;
|
||||
|
||||
label = TimeSliderCore.defaultProps.label;
|
||||
commitThrottle = TimeSliderCore.defaultProps.commitThrottle;
|
||||
@@ -43,7 +44,7 @@ export class TimeSliderElement extends MediaElement {
|
||||
readonly #timeState = new PlayerController(this, playerContext, selectTime);
|
||||
readonly #bufferState = new PlayerController(this, playerContext, selectBuffer);
|
||||
|
||||
#slider: SliderHandle | null = null;
|
||||
#slider: SliderApi | null = null;
|
||||
#disconnect: AbortController | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
@@ -63,21 +64,11 @@ export class TimeSliderElement extends MediaElement {
|
||||
if (!media) return 0;
|
||||
return this.#core.percentFromValue(media.currentTime);
|
||||
},
|
||||
getStepPercent: () => {
|
||||
const { step, min, max } = this.#core.props;
|
||||
const range = max - min;
|
||||
return range > 0 ? (step / range) * 100 : 0;
|
||||
},
|
||||
getLargeStepPercent: () => {
|
||||
const { largeStep, min, max } = this.#core.props;
|
||||
const range = max - min;
|
||||
return range > 0 ? (largeStep / range) * 100 : 0;
|
||||
},
|
||||
onValueChange: () => {
|
||||
// Visual update only — CSS vars are refreshed in update().
|
||||
},
|
||||
getStepPercent: () => this.#core.getStepPercent(),
|
||||
getLargeStepPercent: () => this.#core.getLargeStepPercent(),
|
||||
onValueCommit: (percent) => {
|
||||
this.#seek(percent);
|
||||
const media = this.#timeState.value;
|
||||
if (media) media.seek(this.#core.valueFromPercent(percent));
|
||||
},
|
||||
commitThrottle: this.commitThrottle,
|
||||
onDragStart: () => {
|
||||
@@ -88,14 +79,14 @@ export class TimeSliderElement extends MediaElement {
|
||||
},
|
||||
});
|
||||
|
||||
this.#slider.interaction.subscribe(() => this.requestUpdate(), { signal });
|
||||
this.#slider.input.subscribe(() => this.requestUpdate(), { signal });
|
||||
|
||||
// Prevent default touch gestures and text selection during interaction.
|
||||
this.style.touchAction = 'none';
|
||||
this.style.userSelect = 'none';
|
||||
|
||||
if (__DEV__ && !this.#timeState.value) {
|
||||
logMissingFeature(TimeSliderElement.tagName, 'time');
|
||||
logMissingFeature(this.localName, this.#timeState.displayName!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +111,10 @@ export class TimeSliderElement extends MediaElement {
|
||||
const buffer = this.#bufferState.value;
|
||||
if (!time) return;
|
||||
|
||||
const interaction = this.#slider.interaction.current;
|
||||
this.#core.setInput(this.#slider.input.current);
|
||||
const media = { ...time, ...(buffer ?? { buffered: [], seekable: [] }) };
|
||||
const state = this.#core.getTimeState(media, interaction);
|
||||
this.#core.setMedia(media);
|
||||
const state = this.#core.getState();
|
||||
const cssVars = getTimeSliderCSSVars(state);
|
||||
|
||||
applyStyles(this, cssVars);
|
||||
@@ -133,24 +125,11 @@ export class TimeSliderElement extends MediaElement {
|
||||
// Provide context to child elements with base slider data attrs.
|
||||
this.#provider.setValue({
|
||||
state,
|
||||
stateAttrMap: TimeSliderDataAttrs,
|
||||
pointerValue: this.#core.valueFromPercent(state.pointerPercent),
|
||||
thumbAttrs: this.#core.getAttrs(state),
|
||||
thumbProps: this.#slider.thumbProps,
|
||||
formatValue: (value) => formatTime(value),
|
||||
formatValue: (value) => formatTime(value, state.duration),
|
||||
});
|
||||
}
|
||||
|
||||
#seek(percent: number): void {
|
||||
const media = this.#timeState.value;
|
||||
if (!media) return;
|
||||
const time = this.#core.valueFromPercent(percent);
|
||||
media.seek(time);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
const total = Math.round(seconds);
|
||||
const minutes = Math.floor(total / 60);
|
||||
const secs = total % 60;
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class TimeElement extends MediaElement {
|
||||
super.connectedCallback();
|
||||
|
||||
if (__DEV__ && !this.#state.value) {
|
||||
logMissingFeature(TimeElement.tagName, 'time');
|
||||
logMissingFeature(this.localName, this.#state.displayName!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@ export class TimeElement extends MediaElement {
|
||||
|
||||
if (!media) return;
|
||||
|
||||
const state = this.#core.getState(media);
|
||||
this.#core.setMedia(media);
|
||||
const state = this.#core.getState();
|
||||
|
||||
this.#signSpan.hidden = !state.negative;
|
||||
this.#signSpan.textContent = state.negative ? this.negativeSign : '';
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
createSlider,
|
||||
getSliderCSSVars,
|
||||
logMissingFeature,
|
||||
type SliderHandle,
|
||||
type SliderApi,
|
||||
selectVolume,
|
||||
} from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
@@ -14,7 +14,7 @@ import { applyStyles, isRTL } from '@videojs/utils/dom';
|
||||
import { playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
import { sliderContext } from '../slider/slider-context';
|
||||
import { sliderContext } from '../slider/context';
|
||||
|
||||
export class VolumeSliderElement extends MediaElement {
|
||||
static readonly tagName = 'media-volume-slider';
|
||||
@@ -26,7 +26,7 @@ export class VolumeSliderElement extends MediaElement {
|
||||
orientation: { type: String },
|
||||
disabled: { type: Boolean },
|
||||
thumbAlignment: { type: String, attribute: 'thumb-alignment' },
|
||||
} satisfies PropertyDeclarationMap<keyof VolumeSliderCore.Props>;
|
||||
} satisfies PropertyDeclarationMap<Exclude<keyof VolumeSliderCore.Props, 'value' | 'min' | 'max'>>;
|
||||
|
||||
label = VolumeSliderCore.defaultProps.label;
|
||||
step = VolumeSliderCore.defaultProps.step;
|
||||
@@ -39,7 +39,7 @@ export class VolumeSliderElement extends MediaElement {
|
||||
readonly #provider = new ContextProvider(this, { context: sliderContext });
|
||||
readonly #volumeState = new PlayerController(this, playerContext, selectVolume);
|
||||
|
||||
#slider: SliderHandle | null = null;
|
||||
#slider: SliderApi | null = null;
|
||||
#disconnect: AbortController | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
@@ -59,16 +59,8 @@ export class VolumeSliderElement extends MediaElement {
|
||||
if (!media) return 0;
|
||||
return media.volume * 100;
|
||||
},
|
||||
getStepPercent: () => {
|
||||
const { step, min, max } = this.#core.props;
|
||||
const range = max - min;
|
||||
return range > 0 ? (step / range) * 100 : 0;
|
||||
},
|
||||
getLargeStepPercent: () => {
|
||||
const { largeStep, min, max } = this.#core.props;
|
||||
const range = max - min;
|
||||
return range > 0 ? (largeStep / range) * 100 : 0;
|
||||
},
|
||||
getStepPercent: () => this.#core.getStepPercent(),
|
||||
getLargeStepPercent: () => this.#core.getLargeStepPercent(),
|
||||
onValueChange: (percent) => {
|
||||
this.#setVolume(percent);
|
||||
},
|
||||
@@ -83,14 +75,14 @@ export class VolumeSliderElement extends MediaElement {
|
||||
},
|
||||
});
|
||||
|
||||
this.#slider.interaction.subscribe(() => this.requestUpdate(), { signal });
|
||||
this.#slider.input.subscribe(() => this.requestUpdate(), { signal });
|
||||
|
||||
// Prevent default touch gestures and text selection during interaction.
|
||||
this.style.touchAction = 'none';
|
||||
this.style.userSelect = 'none';
|
||||
|
||||
if (__DEV__ && !this.#volumeState.value) {
|
||||
logMissingFeature(VolumeSliderElement.tagName, 'volume');
|
||||
logMissingFeature(this.localName, this.#volumeState.displayName!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,8 +106,9 @@ export class VolumeSliderElement extends MediaElement {
|
||||
const media = this.#volumeState.value;
|
||||
if (!media) return;
|
||||
|
||||
const interaction = this.#slider.interaction.current;
|
||||
const state = this.#core.getVolumeState(media, interaction);
|
||||
this.#core.setInput(this.#slider.input.current);
|
||||
this.#core.setMedia(media);
|
||||
const state = this.#core.getState();
|
||||
const cssVars = getSliderCSSVars(state);
|
||||
|
||||
applyStyles(this, cssVars);
|
||||
@@ -126,6 +119,7 @@ export class VolumeSliderElement extends MediaElement {
|
||||
// Provide context to child elements.
|
||||
this.#provider.setValue({
|
||||
state,
|
||||
stateAttrMap: SliderDataAttrs,
|
||||
pointerValue: this.#core.valueFromPercent(state.pointerPercent),
|
||||
thumbAttrs: this.#core.getAttrs(state),
|
||||
thumbProps: this.#slider.thumbProps,
|
||||
|
||||
Reference in New Issue
Block a user