mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(html): discover media elements and attach store target via DOM (#481)
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"declarationDir": "../../types/dom"
|
||||
"declarationDir": "../../types/dom",
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"references": [{ "path": "../.." }],
|
||||
"include": ["./**/*.ts"]
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { type Context, createContext } from '@lit/context';
|
||||
import type { AnyPlayerStore, Media, PlayerStore } from '@videojs/core/dom';
|
||||
import type { AnyPlayerStore, PlayerStore } from '@videojs/core/dom';
|
||||
|
||||
export const PLAYER_CONTEXT_KEY = Symbol('@videojs/player');
|
||||
|
||||
export interface PlayerContextValue<Store extends PlayerStore = AnyPlayerStore> {
|
||||
store: Store;
|
||||
media: Media | null;
|
||||
}
|
||||
export type PlayerContextValue<Store extends PlayerStore = AnyPlayerStore> = Store;
|
||||
|
||||
export type PlayerContext<Store extends PlayerStore = AnyPlayerStore> = Context<
|
||||
typeof PLAYER_CONTEXT_KEY,
|
||||
|
||||
@@ -83,14 +83,13 @@ export function createPlayer(config: CreatePlayerConfig<AnyPlayerFeature[]>): Cr
|
||||
return createStore<PlayerTarget>()(slice);
|
||||
}
|
||||
|
||||
const ctx = playerContext;
|
||||
const PlayerMixin = createPlayerMixin<PlayerStore>(ctx, create);
|
||||
const PlayerMixin = createPlayerMixin<PlayerStore>(playerContext, create);
|
||||
const PlayerElement = PlayerMixin(MediaElement);
|
||||
const ProviderMixin = createProviderMixin<PlayerStore>(ctx, create);
|
||||
const ContainerMixin = createContainerMixin<PlayerStore>(ctx);
|
||||
const ProviderMixin = createProviderMixin<PlayerStore>(playerContext, create);
|
||||
const ContainerMixin = createContainerMixin<PlayerStore>(playerContext);
|
||||
|
||||
return {
|
||||
context: ctx,
|
||||
context: playerContext,
|
||||
create,
|
||||
PlayerController,
|
||||
PlayerElement,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PlayerStore } from '@videojs/core/dom';
|
||||
import type { InferStoreState, Selector } from '@videojs/store';
|
||||
import { StoreController } from '@videojs/store/lit';
|
||||
|
||||
import type { PlayerContext, PlayerContextValue } from './context';
|
||||
import type { PlayerContext } from './context';
|
||||
|
||||
export type PlayerControllerHost = ReactiveControllerHost & HTMLElement;
|
||||
|
||||
@@ -62,31 +62,28 @@ export class PlayerController<Store extends PlayerStore, Result = Store> impleme
|
||||
}
|
||||
|
||||
get value(): Result | undefined {
|
||||
const ctx = this.#consumer.value;
|
||||
if (!ctx) return undefined;
|
||||
const store = this.#consumer.value;
|
||||
if (!store) return undefined;
|
||||
|
||||
// Without selector: return store directly
|
||||
if (!this.#selector) return ctx.store as unknown as Result;
|
||||
if (!this.#selector) return store as unknown as Result;
|
||||
|
||||
// With selector: use StoreController
|
||||
return this.#store?.value;
|
||||
}
|
||||
|
||||
hostConnected(): void {
|
||||
const ctx = this.#consumer.value;
|
||||
if (ctx) this.#connect(ctx);
|
||||
const store = this.#consumer.value;
|
||||
if (store) this.#connect(store);
|
||||
}
|
||||
|
||||
hostDisconnected(): void {
|
||||
this.#store = null;
|
||||
}
|
||||
|
||||
#connect(ctx: PlayerContextValue<Store> | undefined): void {
|
||||
if (!ctx) return;
|
||||
|
||||
// Create StoreController with the store directly
|
||||
#connect(store: Store): void {
|
||||
if (!this.#store && this.#selector) {
|
||||
this.#store = new StoreController(this.#host, ctx.store, this.#selector);
|
||||
this.#store = new StoreController(this.#host, store, this.#selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ export function createPlayerMixin<Store extends PlayerStore>(
|
||||
const ContainerMixin = createContainerMixin<Store>(context);
|
||||
|
||||
return <Class extends MediaElementConstructor>(BaseClass: Class) => {
|
||||
return ContainerMixin(ProviderMixin(BaseClass)) as unknown as Result<Class, Store>;
|
||||
return ProviderMixin(ContainerMixin(BaseClass)) as unknown as Result<Class, Store>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ContextConsumer } from '@lit/context';
|
||||
import type { MediaContainer, PlayerStore, PlayerTarget } from '@videojs/core/dom';
|
||||
import { listen, querySlot } from '@videojs/utils/dom';
|
||||
import type { Media, MediaContainer, PlayerStore, PlayerTarget } from '@videojs/core/dom';
|
||||
import { noop } from '@videojs/utils/function';
|
||||
import type { MediaElementConstructor } from '@/ui/media-element';
|
||||
import type { PlayerContext } from '../player/context';
|
||||
@@ -14,7 +13,7 @@ export function createContainerMixin<Store extends PlayerStore>(context: PlayerC
|
||||
return <Class extends MediaElementConstructor>(BaseClass: Class) => {
|
||||
class PlayerContainerElement extends BaseClass implements PlayerConsumer<Store>, MediaContainer {
|
||||
#detach = noop;
|
||||
#disconnect: AbortController | null = null;
|
||||
#observer: MutationObserver | null = null;
|
||||
|
||||
#consumer = new ContextConsumer(this, {
|
||||
context,
|
||||
@@ -23,44 +22,54 @@ export function createContainerMixin<Store extends PlayerStore>(context: PlayerC
|
||||
});
|
||||
|
||||
get store(): Store | null {
|
||||
return (this.#consumer.value?.store as Store) ?? null;
|
||||
return this.#consumer.value ?? null;
|
||||
}
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = new AbortController();
|
||||
this.#observer = new MutationObserver((records) => {
|
||||
if (records.some(hasMediaNode)) this.#attachMedia();
|
||||
});
|
||||
|
||||
if (this.shadowRoot) {
|
||||
const slot = querySlot(this.shadowRoot, '');
|
||||
if (slot) listen(slot, 'slotchange', () => this.#attachMedia(), { signal: this.#disconnect.signal });
|
||||
}
|
||||
this.#observer.observe(this, { childList: true, subtree: true });
|
||||
|
||||
this.#attachMedia();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = null;
|
||||
this.#observer?.disconnect();
|
||||
this.#observer = null;
|
||||
this.#detach();
|
||||
}
|
||||
|
||||
#attachMedia() {
|
||||
const ctx = this.#consumer.value;
|
||||
if (!ctx) return;
|
||||
// Store will be overridden and set by provider mixin if consumer is empty.
|
||||
const store = this.#consumer.value ?? this.store;
|
||||
if (!store) return;
|
||||
|
||||
const { store, media } = ctx;
|
||||
const media = this.querySelector<HTMLMediaElement>('video, audio');
|
||||
|
||||
if (!media) return;
|
||||
if (!media) {
|
||||
this.#detach();
|
||||
this.#detach = noop;
|
||||
return;
|
||||
}
|
||||
|
||||
const target: PlayerTarget = {
|
||||
media,
|
||||
container: this,
|
||||
};
|
||||
|
||||
if (store.target?.media !== target.media || store.target?.container !== target.container) {
|
||||
const hasMediaChanged = store.target?.media !== target.media,
|
||||
hasContainerChanged = store.target?.container !== target.container;
|
||||
|
||||
if (hasMediaChanged || hasContainerChanged) {
|
||||
this.#detach();
|
||||
this.#detach = store.attach(target);
|
||||
}
|
||||
@@ -70,3 +79,19 @@ export function createContainerMixin<Store extends PlayerStore>(context: PlayerC
|
||||
return PlayerContainerElement;
|
||||
};
|
||||
}
|
||||
|
||||
function isMediaNode(node: Node): boolean {
|
||||
return node instanceof HTMLMediaElement;
|
||||
}
|
||||
|
||||
function hasMediaNode(record: MutationRecord): boolean {
|
||||
for (const node of record.addedNodes) {
|
||||
if (isMediaNode(node)) return true;
|
||||
}
|
||||
|
||||
for (const node of record.removedNodes) {
|
||||
if (isMediaNode(node)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ContextProvider } from '@lit/context';
|
||||
import type { Media, PlayerStore } from '@videojs/core/dom';
|
||||
import type { PlayerStore } from '@videojs/core/dom';
|
||||
import { isNull } from '@videojs/utils/predicate';
|
||||
import type { MediaElementConstructor } from '@/ui/media-element';
|
||||
import type { PlayerContext } from '../player/context';
|
||||
@@ -15,36 +15,34 @@ export function createProviderMixin<Store extends PlayerStore>(
|
||||
): ProviderMixin<Store> {
|
||||
return <Class extends MediaElementConstructor>(BaseClass: Class) => {
|
||||
class PlayerProviderElement extends BaseClass implements PlayerProvider<Store> {
|
||||
#store: Store | null = null;
|
||||
#media: Media | null = null;
|
||||
#store: Store | null = factory();
|
||||
|
||||
#provider = new ContextProvider(this, {
|
||||
context,
|
||||
initialValue: { store: this.store, media: null },
|
||||
initialValue: this.store,
|
||||
});
|
||||
|
||||
get store(): Store {
|
||||
if (isNull(this.#store)) {
|
||||
this.#store = factory();
|
||||
}
|
||||
|
||||
return this.#store;
|
||||
}
|
||||
|
||||
get media(): Media | null {
|
||||
return this.#media;
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
set media(value: Media | null) {
|
||||
this.#media = value;
|
||||
this.#provider.setValue({ store: this.store, media: value });
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.#provider.setValue(this.store);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.#store) {
|
||||
this.#store.destroy();
|
||||
this.#store = null;
|
||||
}
|
||||
this.#store?.destroy();
|
||||
this.#store = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Media, PlayerStore } from '@videojs/core/dom';
|
||||
import type { PlayerStore } from '@videojs/core/dom';
|
||||
import type { Constructor } from '@videojs/utils/types';
|
||||
import type { MediaElement } from '@/ui/media-element';
|
||||
|
||||
@@ -16,7 +16,6 @@ export interface PlayerElementConstructor<Store extends PlayerStore> extends Con
|
||||
|
||||
export interface PlayerProvider<Store extends PlayerStore> extends MediaElement {
|
||||
readonly store: Store;
|
||||
media: Media | null;
|
||||
}
|
||||
|
||||
export interface PlayerProviderConstructor<Store extends PlayerStore> extends Constructor<PlayerProvider<Store>> {}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ReactiveElement } from '@lit/reactive-element';
|
||||
import type { Constructor } from '@videojs/utils/types';
|
||||
|
||||
export class MediaElement extends ReactiveElement {}
|
||||
export class MediaElement extends ReactiveElement {
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export interface MediaElementConstructor extends Constructor<MediaElement> {}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"declarationDir": "types"
|
||||
"declarationDir": "types",
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export const Value = forwardRef(function Value(
|
||||
ref: [forwardedRef],
|
||||
props: [
|
||||
{
|
||||
datetime: state.datetime,
|
||||
dateTime: state.datetime,
|
||||
children: content,
|
||||
...core.getAttrs(state),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user