From 9c8d9cd8ed1196d8f876d8197d7280fb1b21238a Mon Sep 17 00:00:00 2001 From: rahim Date: Mon, 2 Feb 2026 22:52:02 +1100 Subject: [PATCH] feat(html): add `PlayerElement` to `createPlayer` (#376) --- packages/html/src/index.ts | 4 +-- packages/html/src/player/create-player.ts | 35 +++++++++---------- packages/html/src/player/player-controller.ts | 10 +++--- packages/html/src/player/player-mixin.ts | 19 +++++----- .../src/player/tests/create-player.test.ts | 34 ++++++++++++++++++ packages/html/src/store/container-mixin.ts | 16 ++++----- packages/html/src/store/provider-mixin.ts | 16 ++++----- packages/html/src/store/types.ts | 26 ++++++++++++-- packages/html/src/ui/media-element.ts | 3 ++ 9 files changed, 105 insertions(+), 58 deletions(-) create mode 100644 packages/html/src/player/tests/create-player.test.ts diff --git a/packages/html/src/index.ts b/packages/html/src/index.ts index fc94a957..57d4fda9 100644 --- a/packages/html/src/index.ts +++ b/packages/html/src/index.ts @@ -4,6 +4,7 @@ export * from '@videojs/core/dom'; // Store export type { Comparator, Selector } from '@videojs/store'; export { createSelector, shallowEqual } from '@videojs/store'; + // Player export * from './player/context'; export * from './player/create-player'; @@ -11,8 +12,7 @@ export * from './player/player-controller'; export * from './player/player-mixin'; export * from './store/container-mixin'; export * from './store/provider-mixin'; -// Store bindings export * from './store/types'; // Primitives -export { MediaElement } from './ui/media-element'; +export * from './ui/media-element'; diff --git a/packages/html/src/player/create-player.ts b/packages/html/src/player/create-player.ts index c6cba892..9176c726 100644 --- a/packages/html/src/player/create-player.ts +++ b/packages/html/src/player/create-player.ts @@ -3,6 +3,8 @@ import { combine, createStore } from '@videojs/store'; import { type ContainerMixin, createContainerMixin } from '../store/container-mixin'; import { createProviderMixin, type ProviderMixin } from '../store/provider-mixin'; +import type { PlayerElementConstructor } from '../store/types'; +import { MediaElement } from '../ui/media-element'; import { type PlayerContext, playerContext } from './context'; import { PlayerController } from './player-controller'; import { createPlayerMixin, type PlayerMixin } from './player-mixin'; @@ -21,6 +23,9 @@ export interface CreatePlayerResult { /** Player controller bound to this player's context. */ PlayerController: PlayerController.Constructor; + /** Pre-composed player element ready for customElements.define(). */ + PlayerElement: PlayerElementConstructor; + /** Mixin for a complete player element (provider + container). */ PlayerMixin: PlayerMixin; @@ -37,28 +42,21 @@ export interface CreatePlayerResult { * @example * ```ts * import { features } from '@videojs/core/dom'; + * import { createPlayer, MediaElement } from '@videojs/html'; * - * const { - * context, - * create, - * PlayerController, - * PlayerMixin, - * ProviderMixin, - * ContainerMixin, - * } = createPlayer({ + * const { PlayerElement, PlayerController, context } = createPlayer({ * features: [...features.video], * }); * - * // Complete player element - * class VideoPlayer extends PlayerMixin(ReactiveElement) {} + * // Simple: register pre-composed PlayerElement + * customElements.define('video-player', PlayerElement); * - * // Or separate provider/container - * class MyPlayer extends ProviderMixin(ReactiveElement) {} - * class MyContainer extends ContainerMixin(ReactiveElement) {} + * // Custom: extend with PlayerMixin + * class MyPlayer extends PlayerMixin(MediaElement) {} * - * // Control element - * class PlayButton extends ReactiveElement { - * #playback = new PlayerController(this, selectPlayback); + * // Control element with selector + * class PlayButton extends MediaElement { + * #playback = new PlayerController(this, context, selectPlayback); * } * ``` */ @@ -76,14 +74,15 @@ export function createPlayer( const ctx = playerContext as PlayerContext; const PlayerMixin = createPlayerMixin(ctx, create); + const PlayerElement = PlayerMixin(MediaElement); const ProviderMixin = createProviderMixin(ctx, create); const ContainerMixin = createContainerMixin(ctx); - const BoundPlayerController = PlayerController as unknown as PlayerController.Constructor; return { context: ctx, create, - PlayerController: BoundPlayerController, + PlayerController, + PlayerElement, PlayerMixin, ProviderMixin, ContainerMixin, diff --git a/packages/html/src/player/player-controller.ts b/packages/html/src/player/player-controller.ts index a2fdc42f..1f75f376 100644 --- a/packages/html/src/player/player-controller.ts +++ b/packages/html/src/player/player-controller.ts @@ -3,7 +3,6 @@ import type { ReactiveController, ReactiveControllerHost } from '@lit/reactive-e import type { PlayerStore } from '@videojs/core/dom'; import type { InferStoreState, Selector } from '@videojs/store'; import { StoreController } from '@videojs/store/lit'; -import type { Constructor as Ctor } from '@videojs/utils/types'; import type { PlayerContext, PlayerContextValue } from './context'; @@ -18,7 +17,7 @@ export type PlayerControllerHost = ReactiveControllerHost & HTMLElement; * @example * ```ts * // Store access (no subscription) - * class Controls extends ReactiveElement { + * class Controls extends MediaElement { * #player = new PlayerController(this, playerContext); * * handleClick() { @@ -27,7 +26,7 @@ export type PlayerControllerHost = ReactiveControllerHost & HTMLElement; * } * * // Selector-based subscription - * class PlayButton extends ReactiveElement { + * class PlayButton extends MediaElement { * #playback = new PlayerController(this, playerContext, selectPlayback); * } * ``` @@ -95,7 +94,8 @@ export class PlayerController impleme export namespace PlayerController { export type Host = PlayerControllerHost; - export type Constructor = Ctor< - typeof PlayerController + export type Constructor = typeof PlayerController< + Store, + Result >; } diff --git a/packages/html/src/player/player-mixin.ts b/packages/html/src/player/player-mixin.ts index a9408885..dbd609b6 100644 --- a/packages/html/src/player/player-mixin.ts +++ b/packages/html/src/player/player-mixin.ts @@ -1,19 +1,16 @@ -import type { ReactiveElement } from '@lit/reactive-element'; import type { PlayerStore } from '@videojs/core/dom'; -import type { Constructor } from '@videojs/utils/types'; - +import type { MediaElementConstructor } from '@/ui/media-element'; import { createContainerMixin } from '../store/container-mixin'; import { createProviderMixin } from '../store/provider-mixin'; -import type { PlayerProvider } from '../store/types'; +import type { PlayerProviderConstructor } from '../store/types'; import type { PlayerContext } from './context'; -export interface PlayerElement extends PlayerProvider {} +type Result = Class & + PlayerProviderConstructor; -type Base = Constructor; - -type Result = Class & PlayerElement; - -export type PlayerMixin = (BaseClass: Class) => Result; +export type PlayerMixin = ( + BaseClass: Class +) => Result; /** * Creates a mixin that combines provider and container functionality. @@ -27,7 +24,7 @@ export function createPlayerMixin( const ProviderMixin = createProviderMixin(context, factory); const ContainerMixin = createContainerMixin(context); - return (BaseClass: Class) => { + return (BaseClass: Class) => { return ContainerMixin(ProviderMixin(BaseClass)) as unknown as Result; }; } diff --git a/packages/html/src/player/tests/create-player.test.ts b/packages/html/src/player/tests/create-player.test.ts new file mode 100644 index 00000000..2acb64fb --- /dev/null +++ b/packages/html/src/player/tests/create-player.test.ts @@ -0,0 +1,34 @@ +import { features } from '@videojs/core/dom'; +import { describe, expect, it } from 'vitest'; + +import { createPlayer } from '../create-player'; + +describe('createPlayer', () => { + it('returns expected exports', () => { + const result = createPlayer({ features: [...features.video] }); + + expect(result.context).toBeDefined(); + expect(result.create).toBeInstanceOf(Function); + expect(result.PlayerController).toBeDefined(); + expect(result.PlayerElement).toBeDefined(); + expect(result.PlayerMixin).toBeInstanceOf(Function); + expect(result.ProviderMixin).toBeInstanceOf(Function); + expect(result.ContainerMixin).toBeInstanceOf(Function); + }); + + it('create() returns a store instance', () => { + const { create } = createPlayer({ features: [...features.video] }); + const store = create(); + + expect(store.attach).toBeInstanceOf(Function); + expect(store.subscribe).toBeInstanceOf(Function); + expect(store.destroy).toBeInstanceOf(Function); + }); + + it('PlayerElement is a valid custom element class', () => { + const { PlayerElement } = createPlayer({ features: [...features.video] }); + + expect(typeof PlayerElement).toBe('function'); + expect(PlayerElement.prototype).toBeDefined(); + }); +}); diff --git a/packages/html/src/store/container-mixin.ts b/packages/html/src/store/container-mixin.ts index 0592a37e..d28336d5 100644 --- a/packages/html/src/store/container-mixin.ts +++ b/packages/html/src/store/container-mixin.ts @@ -1,22 +1,18 @@ import { ContextConsumer } from '@lit/context'; -import type { ReactiveElement } from '@lit/reactive-element'; import type { MediaContainer, PlayerStore, PlayerTarget } from '@videojs/core/dom'; import { listen, querySlot } from '@videojs/utils/dom'; import { Disposer } from '@videojs/utils/events'; import { noop } from '@videojs/utils/function'; -import type { Constructor } from '@videojs/utils/types'; - +import type { MediaElementConstructor } from '@/ui/media-element'; import type { PlayerContext } from '../player/context'; -import type { PlayerConsumer } from './types'; +import type { PlayerConsumer, PlayerConsumerConstructor } from './types'; -type Base = Constructor; - -type Result = Class & Constructor>; - -export type ContainerMixin = (BaseClass: Class) => Result; +export type ContainerMixin = ( + BaseClass: Class +) => Class & PlayerConsumerConstructor; export function createContainerMixin(context: PlayerContext): ContainerMixin { - return (BaseClass: Class) => { + return (BaseClass: Class) => { class PlayerContainerElement extends BaseClass implements PlayerConsumer, MediaContainer { #detach = noop; #disposer = new Disposer(); diff --git a/packages/html/src/store/provider-mixin.ts b/packages/html/src/store/provider-mixin.ts index 9ded5742..b7d4347d 100644 --- a/packages/html/src/store/provider-mixin.ts +++ b/packages/html/src/store/provider-mixin.ts @@ -1,23 +1,19 @@ import { ContextProvider } from '@lit/context'; -import type { ReactiveElement } from '@lit/reactive-element'; import type { Media, PlayerStore } from '@videojs/core/dom'; import { isNull } from '@videojs/utils/predicate'; -import type { Constructor } from '@videojs/utils/types'; - +import type { MediaElementConstructor } from '@/ui/media-element'; import type { PlayerContext } from '../player/context'; -import type { PlayerProvider } from './types'; +import type { PlayerProvider, PlayerProviderConstructor } from './types'; -type Base = Constructor; - -type Result = Class & Constructor>; - -export type ProviderMixin = (BaseClass: Class) => Result; +export type ProviderMixin = ( + BaseClass: Class +) => Class & PlayerProviderConstructor; export function createProviderMixin( context: PlayerContext, factory: () => Store ): ProviderMixin { - return (BaseClass: Class) => { + return (BaseClass: Class) => { class PlayerProviderElement extends BaseClass implements PlayerProvider { #store: Store | null = null; #media: Media | null = null; diff --git a/packages/html/src/store/types.ts b/packages/html/src/store/types.ts index d155f39f..59c59fb5 100644 --- a/packages/html/src/store/types.ts +++ b/packages/html/src/store/types.ts @@ -1,10 +1,32 @@ import type { Media, PlayerStore } from '@videojs/core/dom'; +import type { Constructor } from '@videojs/utils/types'; +import type { MediaElement } from '@/ui/media-element'; -export interface PlayerProvider { +// ---------------------------------------- +// PlayerElement +// ---------------------------------------- + +export interface PlayerElement extends MediaElement, PlayerProvider {} + +export interface PlayerElementConstructor extends Constructor> {} + +// ---------------------------------------- +// PlayerProvider +// ---------------------------------------- + +export interface PlayerProvider extends MediaElement { readonly store: Store; media: Media | null; } -export interface PlayerConsumer { +export interface PlayerProviderConstructor extends Constructor> {} + +// ---------------------------------------- +// PlayerConsumer +// ---------------------------------------- + +export interface PlayerConsumer extends MediaElement { readonly store: Store | null; } + +export interface PlayerConsumerConstructor extends Constructor> {} diff --git a/packages/html/src/ui/media-element.ts b/packages/html/src/ui/media-element.ts index 866862d9..9f2763ed 100644 --- a/packages/html/src/ui/media-element.ts +++ b/packages/html/src/ui/media-element.ts @@ -1,3 +1,6 @@ import { ReactiveElement } from '@lit/reactive-element'; +import type { Constructor } from '@videojs/utils/types'; export class MediaElement extends ReactiveElement {} + +export interface MediaElementConstructor extends Constructor {}