mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
chore(root): prepare workspace for alpha (#276)
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { namedNodeMapToObject } from '@videojs/utils-preview/dom';
|
||||
|
||||
export function getTemplateHTML(
|
||||
this: typeof ButtonElement,
|
||||
_attrs: Record<string, string>,
|
||||
_props: Record<string, any> = {},
|
||||
): string {
|
||||
return /* html */ `
|
||||
<style>
|
||||
/*
|
||||
NOTE: Even though primitives should aim to be "unstyled" in their core definitions, we should
|
||||
still add pointer-events, as this defines functionality. (CJP)
|
||||
*/
|
||||
:host {
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
||||
<slot>
|
||||
</slot>
|
||||
`;
|
||||
}
|
||||
|
||||
export class ButtonElement extends HTMLElement {
|
||||
static shadowRootOptions = {
|
||||
mode: 'open' as ShadowRootMode,
|
||||
};
|
||||
|
||||
static getTemplateHTML: typeof getTemplateHTML = getTemplateHTML;
|
||||
|
||||
static observedAttributes: string[] = ['commandfor'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (!this.shadowRoot) {
|
||||
// Set up the Shadow DOM if not using Declarative Shadow DOM.
|
||||
this.attachShadow((this.constructor as typeof ButtonElement).shadowRootOptions);
|
||||
|
||||
const attrs = namedNodeMapToObject(this.attributes);
|
||||
const html = (this.constructor as typeof ButtonElement).getTemplateHTML(attrs);
|
||||
// From MDN: setHTMLUnsafe should be used instead of ShadowRoot.innerHTML
|
||||
// when a string of HTML may contain declarative shadow roots.
|
||||
const shadowRoot = this.shadowRoot as unknown as ShadowRoot;
|
||||
shadowRoot.setHTMLUnsafe ? shadowRoot.setHTMLUnsafe(html) : (shadowRoot.innerHTML = html);
|
||||
}
|
||||
|
||||
this.addEventListener('click', this);
|
||||
this.addEventListener('keydown', this);
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _oldValue: string, newValue: string): void {
|
||||
if (name === 'commandfor') {
|
||||
this.style.setProperty('anchor-name', `--${newValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(event: Event): void {
|
||||
const { type } = event;
|
||||
if (type === 'keydown') {
|
||||
this.#handleKeyDown(event as KeyboardEvent);
|
||||
}
|
||||
}
|
||||
|
||||
#handleKeyDown = (event: KeyboardEvent): void => {
|
||||
const { metaKey, altKey, key } = event;
|
||||
if (metaKey || altKey || !['Enter', ' '].includes(key)) {
|
||||
this.removeEventListener('keyup', this.#handleKeyUp);
|
||||
return;
|
||||
}
|
||||
this.addEventListener('keyup', this.#handleKeyUp, { once: true });
|
||||
};
|
||||
|
||||
#handleKeyUp = (_event: KeyboardEvent): void => {
|
||||
this.handleEvent({ type: 'click' } as Event);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { CurrentTimeDisplayState } from '@videojs/core-preview/store';
|
||||
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
|
||||
|
||||
import { currentTimeDisplayStateDefinition } from '@videojs/core-preview/store';
|
||||
import { formatDisplayTime } from '@videojs/utils-preview';
|
||||
import { toConnectedHTMLComponent } from '../utils/component-factory';
|
||||
|
||||
export const getCurrentTimeDisplayState: StateHook<CurrentTimeDisplay, CurrentTimeDisplayState> = (_element, mediaStore) => {
|
||||
return {
|
||||
...currentTimeDisplayStateDefinition.stateTransform(mediaStore.getState()),
|
||||
// Current time display is read-only, so no request methods needed
|
||||
};
|
||||
};
|
||||
|
||||
export const getCurrentTimeDisplayProps: PropsHook<CurrentTimeDisplay, CurrentTimeDisplayState> = (_element, _state) => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export class CurrentTimeDisplay extends HTMLElement {
|
||||
static shadowRootOptions = {
|
||||
mode: 'open' as ShadowRootMode,
|
||||
};
|
||||
|
||||
static observedAttributes: string[] = ['show-remaining'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (!this.shadowRoot) {
|
||||
this.attachShadow((this.constructor as typeof CurrentTimeDisplay).shadowRootOptions);
|
||||
}
|
||||
}
|
||||
|
||||
get showRemaining(): boolean {
|
||||
return this.hasAttribute('show-remaining');
|
||||
}
|
||||
|
||||
_update(_props: any, state: CurrentTimeDisplayState): void {
|
||||
/** @TODO Should this live here or elsewhere? (CJP) */
|
||||
const timeLabel
|
||||
= this.showRemaining && state.duration != null && state.currentTime != null
|
||||
? formatDisplayTime(-(state.duration - state.currentTime))
|
||||
: formatDisplayTime(state.currentTime);
|
||||
|
||||
if (this.shadowRoot) {
|
||||
this.shadowRoot.textContent = timeLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CurrentTimeDisplayElement: ConnectedComponentConstructor<CurrentTimeDisplay, CurrentTimeDisplayState> = toConnectedHTMLComponent(
|
||||
CurrentTimeDisplay,
|
||||
getCurrentTimeDisplayState,
|
||||
getCurrentTimeDisplayProps,
|
||||
'CurrentTimeDisplay',
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { DurationDisplayState } from '@videojs/core-preview/store';
|
||||
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
|
||||
|
||||
import { durationDisplayStateDefinition } from '@videojs/core-preview/store';
|
||||
|
||||
import { formatDisplayTime } from '@videojs/utils-preview';
|
||||
import { namedNodeMapToObject } from '@videojs/utils-preview/dom';
|
||||
import { toConnectedHTMLComponent } from '../utils/component-factory';
|
||||
|
||||
export function getTemplateHTML(
|
||||
this: typeof DurationDisplay,
|
||||
_attrs: Record<string, string>,
|
||||
_props: Record<string, any> = {},
|
||||
) {
|
||||
return /* html */ `
|
||||
<span></span>
|
||||
`;
|
||||
}
|
||||
|
||||
export class DurationDisplay extends HTMLElement {
|
||||
static shadowRootOptions = {
|
||||
mode: 'open' as ShadowRootMode,
|
||||
};
|
||||
|
||||
static getTemplateHTML: typeof getTemplateHTML = getTemplateHTML;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (!this.shadowRoot) {
|
||||
this.attachShadow((this.constructor as typeof DurationDisplay).shadowRootOptions);
|
||||
|
||||
const attrs = namedNodeMapToObject(this.attributes);
|
||||
const html = (this.constructor as typeof DurationDisplay).getTemplateHTML(attrs);
|
||||
const shadowRoot = this.shadowRoot as unknown as ShadowRoot;
|
||||
shadowRoot.setHTMLUnsafe ? shadowRoot.setHTMLUnsafe(html) : (shadowRoot.innerHTML = html);
|
||||
}
|
||||
}
|
||||
|
||||
_update(_props: any, state: DurationDisplayState): void {
|
||||
// Update the span content with formatted duration
|
||||
const spanElement = this.shadowRoot?.querySelector('span') as HTMLElement;
|
||||
if (spanElement) {
|
||||
spanElement.textContent = formatDisplayTime(state.duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getDurationDisplayState: StateHook<DurationDisplay, DurationDisplayState> = (_element, mediaStore) => {
|
||||
return {
|
||||
...durationDisplayStateDefinition.stateTransform(mediaStore.getState()),
|
||||
// Duration display is read-only, so no request methods needed
|
||||
};
|
||||
};
|
||||
|
||||
export const getDurationDisplayProps: PropsHook<DurationDisplay, DurationDisplayState> = (_element, _state) => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export const DurationDisplayElement: ConnectedComponentConstructor<DurationDisplay, DurationDisplayState> = toConnectedHTMLComponent(
|
||||
DurationDisplay,
|
||||
getDurationDisplayState,
|
||||
getDurationDisplayProps,
|
||||
'DurationDisplay',
|
||||
);
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { FullscreenButtonState } from '@videojs/core-preview/store';
|
||||
import type { Prettify } from '../types';
|
||||
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
|
||||
|
||||
import { fullscreenButtonStateDefinition } from '@videojs/core-preview/store';
|
||||
|
||||
import { memoize } from '@videojs/utils-preview';
|
||||
import { toConnectedHTMLComponent } from '../utils/component-factory';
|
||||
import { ButtonElement } from './button';
|
||||
|
||||
type FullscreenButtonStateWithMethods = Prettify<FullscreenButtonState & ReturnType<typeof fullscreenButtonStateDefinition.createRequestMethods>>;
|
||||
|
||||
const fullscreenButtonCreateRequestMethods = memoize(fullscreenButtonStateDefinition.createRequestMethods);
|
||||
|
||||
/**
|
||||
* FullscreenButton state hook - equivalent to React's useFullscreenButtonState
|
||||
* Handles media store state subscription and transformation
|
||||
*/
|
||||
export const getFullscreenButtonState: StateHook<FullscreenButton, FullscreenButtonStateWithMethods> = (_element, mediaStore) => {
|
||||
return {
|
||||
...fullscreenButtonStateDefinition.stateTransform(mediaStore.getState()),
|
||||
...fullscreenButtonCreateRequestMethods(mediaStore.dispatch),
|
||||
};
|
||||
};
|
||||
|
||||
export const getFullscreenButtonProps: PropsHook<FullscreenButton, FullscreenButtonStateWithMethods> = (_element, state) => {
|
||||
const baseProps: Record<string, any> = {
|
||||
/** data attributes/props */
|
||||
'data-fullscreen': state.fullscreen,
|
||||
/** @TODO Need another state provider in core for i18n (CJP) */
|
||||
/** aria attributes/props */
|
||||
role: 'button',
|
||||
tabindex: '0',
|
||||
'aria-label': state.fullscreen ? 'exit fullscreen' : 'enter fullscreen',
|
||||
/** tooltip */
|
||||
'data-tooltip': state.fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
|
||||
/** @TODO Figure out how we want to handle attr overrides (e.g. aria-label) (CJP) */
|
||||
/** external props spread last to allow for overriding */
|
||||
// ...props,
|
||||
};
|
||||
return baseProps;
|
||||
};
|
||||
|
||||
export class FullscreenButton extends ButtonElement {
|
||||
_state: FullscreenButtonStateWithMethods | undefined;
|
||||
|
||||
handleEvent(event: Event): void {
|
||||
super.handleEvent(event);
|
||||
|
||||
const { type } = event;
|
||||
const state = this._state;
|
||||
if (state && type === 'click') {
|
||||
if (state.fullscreen) {
|
||||
state.requestExitFullscreen();
|
||||
} else {
|
||||
state.requestEnterFullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const FullscreenButtonElement: ConnectedComponentConstructor<FullscreenButton, FullscreenButtonStateWithMethods> = toConnectedHTMLComponent(
|
||||
FullscreenButton,
|
||||
getFullscreenButtonState,
|
||||
getFullscreenButtonProps,
|
||||
'FullscreenButton',
|
||||
);
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { MuteButtonState } from '@videojs/core-preview/store';
|
||||
import type { Prettify } from '../types';
|
||||
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
|
||||
|
||||
import { muteButtonStateDefinition } from '@videojs/core-preview/store';
|
||||
|
||||
import { memoize } from '@videojs/utils-preview';
|
||||
import { toConnectedHTMLComponent } from '../utils/component-factory';
|
||||
import { ButtonElement } from './button';
|
||||
|
||||
type MuteButtonStateWithMethods = Prettify<MuteButtonState & ReturnType<typeof muteButtonStateDefinition.createRequestMethods>>;
|
||||
|
||||
const muteButtonCreateRequestMethods = memoize(muteButtonStateDefinition.createRequestMethods);
|
||||
|
||||
export const getMuteButtonState: StateHook<MuteButton, MuteButtonStateWithMethods> = (_element, mediaStore) => {
|
||||
return {
|
||||
...muteButtonStateDefinition.stateTransform(mediaStore.getState()),
|
||||
...muteButtonCreateRequestMethods(mediaStore.dispatch),
|
||||
};
|
||||
};
|
||||
|
||||
export const getMuteButtonProps: PropsHook<MuteButton, MuteButtonStateWithMethods> = (_element, state) => {
|
||||
const baseProps: Record<string, any> = {
|
||||
/** data attributes/props */
|
||||
'data-muted': state.muted,
|
||||
'data-volume-level': state.volumeLevel,
|
||||
/** @TODO Need another state provider in core for i18n (CJP) */
|
||||
/** aria attributes/props */
|
||||
role: 'button',
|
||||
tabindex: '0',
|
||||
'aria-label': state.muted ? 'unmute' : 'mute',
|
||||
/** tooltip */
|
||||
'data-tooltip': state.muted ? 'Unmute' : 'Mute',
|
||||
/** @TODO Figure out how we want to handle attr overrides (e.g. aria-label) (CJP) */
|
||||
/** external props spread last to allow for overriding */
|
||||
// ...props,
|
||||
};
|
||||
return baseProps;
|
||||
};
|
||||
|
||||
export class MuteButton extends ButtonElement {
|
||||
_state: MuteButtonStateWithMethods | undefined;
|
||||
|
||||
handleEvent(event: Event): void {
|
||||
super.handleEvent(event);
|
||||
|
||||
const { type } = event;
|
||||
const state = this._state;
|
||||
|
||||
if (state) {
|
||||
if (type === 'click') {
|
||||
if (state.volumeLevel === 'off') {
|
||||
state.requestUnmute();
|
||||
} else {
|
||||
state.requestMute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const MuteButtonElement: ConnectedComponentConstructor<MuteButton, MuteButtonStateWithMethods> = toConnectedHTMLComponent(
|
||||
MuteButton,
|
||||
getMuteButtonState,
|
||||
getMuteButtonProps,
|
||||
'MuteButton',
|
||||
);
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { PlayButtonState } from '@videojs/core-preview/store';
|
||||
import type { Prettify } from '../types';
|
||||
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
|
||||
|
||||
import { playButtonStateDefinition } from '@videojs/core-preview/store';
|
||||
import { memoize } from '@videojs/utils-preview';
|
||||
import { toConnectedHTMLComponent } from '../utils/component-factory';
|
||||
import { ButtonElement } from './button';
|
||||
|
||||
type PlayButtonStateWithMethods = Prettify<PlayButtonState & ReturnType<typeof playButtonStateDefinition.createRequestMethods>>;
|
||||
|
||||
const playButtonCreateRequestMethods = memoize(playButtonStateDefinition.createRequestMethods);
|
||||
|
||||
/**
|
||||
* PlayButton state hook - equivalent to React's usePlayButtonState
|
||||
* Handles media store state subscription and transformation
|
||||
*/
|
||||
export const getPlayButtonState: StateHook<PlayButton, PlayButtonStateWithMethods> = (_element, mediaStore) => {
|
||||
return {
|
||||
...playButtonStateDefinition.stateTransform(mediaStore.getState()),
|
||||
...playButtonCreateRequestMethods(mediaStore.dispatch),
|
||||
};
|
||||
};
|
||||
|
||||
export const getPlayButtonProps: PropsHook<PlayButton, PlayButtonStateWithMethods> = (_element, state) => {
|
||||
const baseProps: Record<string, any> = {
|
||||
/** data attributes/props */
|
||||
'data-paused': state.paused,
|
||||
/** @TODO Need another state provider in core for i18n (CJP) */
|
||||
/** aria attributes/props */
|
||||
role: 'button',
|
||||
tabindex: '0',
|
||||
'aria-label': state.paused ? 'play' : 'pause',
|
||||
/** tooltip */
|
||||
'data-tooltip': state.paused ? 'Play' : 'Pause',
|
||||
/** @TODO Figure out how we want to handle attr overrides (e.g. aria-label) (CJP) */
|
||||
/** external props spread last to allow for overriding */
|
||||
// ...props,
|
||||
};
|
||||
|
||||
return baseProps;
|
||||
};
|
||||
|
||||
export class PlayButton extends ButtonElement {
|
||||
_state: PlayButtonStateWithMethods | undefined;
|
||||
|
||||
handleEvent(event: Event): void {
|
||||
super.handleEvent(event);
|
||||
|
||||
const { type } = event;
|
||||
const state = this._state;
|
||||
if (state && type === 'click') {
|
||||
if (state.paused) {
|
||||
state.requestPlay();
|
||||
} else {
|
||||
state.requestPause();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const PlayButtonElement: ConnectedComponentConstructor<PlayButton, PlayButtonStateWithMethods> = toConnectedHTMLComponent(
|
||||
PlayButton,
|
||||
getPlayButtonState,
|
||||
getPlayButtonProps,
|
||||
'PlayButton',
|
||||
);
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { PopoverState as CorePopoverState } from '@videojs/core-preview';
|
||||
import type { Prettify } from '../types';
|
||||
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
|
||||
import { Popover as CorePopover } from '@videojs/core-preview';
|
||||
import { getDocumentOrShadowRoot } from '@videojs/utils-preview/dom';
|
||||
import { getCoreState, getPropsFromAttrs, toConnectedHTMLComponent } from '../utils/component-factory';
|
||||
|
||||
type Placement = CorePopoverState['placement'];
|
||||
|
||||
export type PopoverState = Prettify<ReturnType<CorePopover['getState']>>;
|
||||
|
||||
export const getPopoverState: StateHook<Popover, PopoverState> = (element, _mediaStore) => {
|
||||
const coreState = getCoreState(CorePopover, getPropsFromAttrs(element));
|
||||
return {
|
||||
...coreState,
|
||||
};
|
||||
};
|
||||
|
||||
export const getPopoverProps: PropsHook<Popover, PopoverState> = (element, state) => {
|
||||
if (state._popoverElement !== element) {
|
||||
state._setPopoverElement(element);
|
||||
}
|
||||
|
||||
const triggerElement = getDocumentOrShadowRoot(element)?.querySelector(
|
||||
`[commandfor="${element.id}"]`,
|
||||
) as HTMLElement | null;
|
||||
|
||||
if (state._triggerElement !== triggerElement) {
|
||||
state._setTriggerElement(triggerElement);
|
||||
}
|
||||
|
||||
const mediaContainer = element.closest(element.collisionBoundary
|
||||
? `#${element.collisionBoundary}`
|
||||
: 'media-container') as HTMLElement | null;
|
||||
|
||||
if (state._collisionBoundaryElement !== mediaContainer) {
|
||||
state._setCollisionBoundaryElement(mediaContainer);
|
||||
}
|
||||
|
||||
return {
|
||||
'data-side': state.placement,
|
||||
'data-starting-style': state._transitionStatus === 'initial',
|
||||
'data-open': state._transitionStatus === 'initial' || state._transitionStatus === 'open',
|
||||
'data-ending-style': state._transitionStatus === 'close' || state._transitionStatus === 'unmounted',
|
||||
'data-closed': state._transitionStatus === 'close' || state._transitionStatus === 'unmounted',
|
||||
style: {
|
||||
...state._popoverStyle,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export class Popover extends HTMLElement {
|
||||
static get observedAttributes(): string[] {
|
||||
return [
|
||||
'id',
|
||||
'open-on-hover',
|
||||
'delay',
|
||||
'close-delay',
|
||||
'side',
|
||||
'side-offset',
|
||||
'collision-padding',
|
||||
'collision-boundary',
|
||||
];
|
||||
}
|
||||
|
||||
get openOnHover(): boolean {
|
||||
return this.hasAttribute('open-on-hover');
|
||||
}
|
||||
|
||||
get delay(): number {
|
||||
return Number.parseInt(this.getAttribute('delay') ?? '0', 10);
|
||||
}
|
||||
|
||||
get closeDelay(): number {
|
||||
return Number.parseInt(this.getAttribute('close-delay') ?? '0', 10);
|
||||
}
|
||||
|
||||
get side(): Placement {
|
||||
return this.getAttribute('side') as Placement;
|
||||
}
|
||||
|
||||
get sideOffset(): number {
|
||||
return Number.parseInt(this.getAttribute('side-offset') ?? '0', 10);
|
||||
}
|
||||
|
||||
get collisionPadding(): number {
|
||||
return Number.parseInt(this.getAttribute('collision-padding') ?? '0', 10);
|
||||
}
|
||||
|
||||
get collisionBoundary(): string | null {
|
||||
return this.getAttribute('collision-boundary');
|
||||
}
|
||||
}
|
||||
|
||||
export const PopoverElement: ConnectedComponentConstructor<Popover, PopoverState> = toConnectedHTMLComponent(
|
||||
Popover,
|
||||
getPopoverState,
|
||||
getPopoverProps,
|
||||
'Popover',
|
||||
);
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { PreviewTimeDisplayState } from '@videojs/core-preview/store';
|
||||
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
|
||||
|
||||
import { previewTimeDisplayStateDefinition } from '@videojs/core-preview/store';
|
||||
|
||||
import { formatDisplayTime } from '@videojs/utils-preview';
|
||||
import { toConnectedHTMLComponent } from '../utils/component-factory';
|
||||
|
||||
export const getPreviewTimeDisplayState: StateHook<PreviewTimeDisplay, PreviewTimeDisplayState> = (_element, mediaStore) => {
|
||||
return {
|
||||
...previewTimeDisplayStateDefinition.stateTransform(mediaStore.getState()),
|
||||
// Preview time display is read-only, so no request methods needed
|
||||
};
|
||||
};
|
||||
|
||||
export const getPreviewTimeDisplayProps: PropsHook<PreviewTimeDisplay, PreviewTimeDisplayState> = (_element, _state) => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export class PreviewTimeDisplay extends HTMLElement {
|
||||
static shadowRootOptions = {
|
||||
mode: 'open' as ShadowRootMode,
|
||||
};
|
||||
|
||||
static observedAttributes: string[] = ['show-remaining'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (!this.shadowRoot) {
|
||||
this.attachShadow((this.constructor as typeof PreviewTimeDisplay).shadowRootOptions);
|
||||
}
|
||||
}
|
||||
|
||||
get showRemaining(): boolean {
|
||||
return this.hasAttribute('show-remaining');
|
||||
}
|
||||
|
||||
_update(_props: any, state: PreviewTimeDisplayState): void {
|
||||
/** @TODO Should this live here or elsewhere? (CJP) */
|
||||
const timeLabel = formatDisplayTime(state.previewTime);
|
||||
|
||||
if (this.shadowRoot) {
|
||||
this.shadowRoot.textContent = timeLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const PreviewTimeDisplayElement: ConnectedComponentConstructor<PreviewTimeDisplay, PreviewTimeDisplayState> = toConnectedHTMLComponent(
|
||||
PreviewTimeDisplay,
|
||||
getPreviewTimeDisplayState,
|
||||
getPreviewTimeDisplayProps,
|
||||
'PreviewTimeDisplay',
|
||||
);
|
||||
@@ -0,0 +1,242 @@
|
||||
import type { Prettify } from '../types';
|
||||
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
|
||||
|
||||
import { TimeSlider as CoreTimeSlider } from '@videojs/core-preview';
|
||||
import { timeSliderStateDefinition } from '@videojs/core-preview/store';
|
||||
import { memoize } from '@videojs/utils-preview';
|
||||
|
||||
import { getCoreState, getPropsFromAttrs, toConnectedHTMLComponent } from '../utils/component-factory';
|
||||
|
||||
// ============================================================================
|
||||
// ROOT COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
type TimeSliderState = Prettify<ReturnType<CoreTimeSlider['getState']>>;
|
||||
type TimeSliderStateWithMethods = Prettify<TimeSliderState & ReturnType<typeof timeSliderStateDefinition.createRequestMethods>>;
|
||||
|
||||
const timeSliderCreateRequestMethods = memoize(timeSliderStateDefinition.createRequestMethods);
|
||||
|
||||
export const getTimeSliderRootState: StateHook<TimeSliderRoot, TimeSliderStateWithMethods> = (element, mediaStore) => {
|
||||
const mediaState = timeSliderStateDefinition.stateTransform(mediaStore.getState());
|
||||
const mediaMethods = timeSliderCreateRequestMethods(mediaStore.dispatch);
|
||||
const coreState = getCoreState(CoreTimeSlider, {
|
||||
...getPropsFromAttrs(element),
|
||||
...mediaState,
|
||||
...mediaMethods,
|
||||
});
|
||||
return {
|
||||
...coreState,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* TimeSlider Root props hook - equivalent to React's useTimeSliderRootProps
|
||||
* Handles element attributes and properties based on state
|
||||
*/
|
||||
export const getTimeSliderRootProps: PropsHook<TimeSliderRoot, TimeSliderStateWithMethods> = (element, state) => {
|
||||
if (state._rootElement !== element) {
|
||||
state._setRootElement(element);
|
||||
}
|
||||
|
||||
return {
|
||||
role: 'slider',
|
||||
tabindex: element.getAttribute('tabindex') ?? '0',
|
||||
'data-current-time': state.currentTime.toString(),
|
||||
'data-duration': state.duration.toString(),
|
||||
'data-orientation': element.orientation || 'horizontal',
|
||||
'aria-label': 'Seek',
|
||||
'aria-valuemin': '0',
|
||||
'aria-valuemax': Math.round(state.duration).toString(),
|
||||
'aria-valuenow': Math.round(state.currentTime).toString(),
|
||||
'aria-valuetext': `${state._currentTimeText} of ${state._durationText}`,
|
||||
'aria-orientation': element.orientation || 'horizontal',
|
||||
style: {
|
||||
...(element.hasAttribute('commandfor') ? { anchorName: `--${element.getAttribute('commandfor')}` } : {}),
|
||||
'--slider-fill': `${state._fillWidth.toFixed(3)}%`,
|
||||
'--slider-pointer': `${(state._pointerWidth * 100).toFixed(3)}%`,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export class TimeSliderRoot extends HTMLElement {
|
||||
static readonly observedAttributes: readonly string[] = ['commandfor', 'orientation'];
|
||||
|
||||
_state: TimeSliderState | undefined;
|
||||
|
||||
get orientation(): 'horizontal' | 'vertical' {
|
||||
return (this.getAttribute('orientation') as 'horizontal' | 'vertical') || 'horizontal';
|
||||
}
|
||||
}
|
||||
|
||||
export const TimeSliderRootElement: ConnectedComponentConstructor<TimeSliderRoot, TimeSliderStateWithMethods> = toConnectedHTMLComponent(
|
||||
TimeSliderRoot,
|
||||
getTimeSliderRootState,
|
||||
getTimeSliderRootProps,
|
||||
'TimeSliderRoot',
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// TRACK COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
export const getTimeSliderTrackProps: PropsHook<TimeSliderTrack, undefined> = (element, _state) => {
|
||||
const rootElement = element.closest('media-time-slider') as TimeSliderRoot;
|
||||
|
||||
if (rootElement._state?._trackElement !== element) {
|
||||
rootElement._state?._setTrackElement?.(element);
|
||||
}
|
||||
|
||||
const orientation = rootElement?.orientation || 'horizontal';
|
||||
|
||||
return {
|
||||
'data-orientation': orientation,
|
||||
style: {
|
||||
width: orientation === 'horizontal' ? '100%' : undefined,
|
||||
height: orientation !== 'horizontal' ? '100%' : undefined,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export class TimeSliderTrack extends HTMLElement {}
|
||||
|
||||
export const TimeSliderTrackElement: ConnectedComponentConstructor<TimeSliderTrack, undefined> = toConnectedHTMLComponent(
|
||||
TimeSliderTrack,
|
||||
undefined,
|
||||
getTimeSliderTrackProps,
|
||||
'TimeSliderTrack',
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// PROGRESS COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
export const getTimeSliderProgressProps: PropsHook<TimeSliderProgress, undefined> = (element, _state) => {
|
||||
const rootElement = element.closest('media-time-slider') as TimeSliderRoot;
|
||||
const orientation = rootElement?.orientation || 'horizontal';
|
||||
const style = orientation === 'horizontal'
|
||||
? {
|
||||
position: 'absolute',
|
||||
width: 'var(--slider-fill, 0%)',
|
||||
height: '100%',
|
||||
top: '0',
|
||||
bottom: undefined,
|
||||
}
|
||||
: {
|
||||
position: 'absolute',
|
||||
height: 'var(--slider-fill, 0%)',
|
||||
width: '100%',
|
||||
bottom: '0',
|
||||
top: undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
'data-orientation': orientation,
|
||||
style,
|
||||
};
|
||||
};
|
||||
|
||||
export class TimeSliderProgress extends HTMLElement {}
|
||||
|
||||
export const TimeSliderProgressElement: ConnectedComponentConstructor<TimeSliderProgress, undefined> = toConnectedHTMLComponent(
|
||||
TimeSliderProgress,
|
||||
undefined,
|
||||
getTimeSliderProgressProps,
|
||||
'TimeSliderProgress',
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// POINTER COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
export const getTimeSliderPointerProps: PropsHook<TimeSliderPointer, undefined> = (element, _state) => {
|
||||
const rootElement = element.closest('media-time-slider') as TimeSliderRoot;
|
||||
const orientation = rootElement?.orientation || 'horizontal';
|
||||
const style = orientation === 'horizontal'
|
||||
? {
|
||||
position: 'absolute',
|
||||
width: 'var(--slider-pointer, 0%)',
|
||||
height: '100%',
|
||||
top: '0',
|
||||
bottom: undefined,
|
||||
}
|
||||
: {
|
||||
position: 'absolute',
|
||||
height: 'var(--slider-pointer, 0%)',
|
||||
width: '100%',
|
||||
bottom: '0',
|
||||
top: undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
'data-orientation': orientation,
|
||||
style,
|
||||
};
|
||||
};
|
||||
|
||||
export class TimeSliderPointer extends HTMLElement {}
|
||||
|
||||
export const TimeSliderPointerElement: ConnectedComponentConstructor<TimeSliderPointer, undefined> = toConnectedHTMLComponent(
|
||||
TimeSliderPointer,
|
||||
undefined,
|
||||
getTimeSliderPointerProps,
|
||||
'TimeSliderPointer',
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// THUMB COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
export const getTimeSliderThumbProps: PropsHook<TimeSliderThumb, undefined> = (element, _state) => {
|
||||
const rootElement = element.closest('media-time-slider') as TimeSliderRoot;
|
||||
const orientation = rootElement?.orientation || 'horizontal';
|
||||
const style = orientation === 'horizontal'
|
||||
? {
|
||||
position: 'absolute',
|
||||
left: 'var(--slider-fill, 0%)',
|
||||
top: '50%',
|
||||
bottom: undefined,
|
||||
translate: '-50% -50%',
|
||||
}
|
||||
: {
|
||||
position: 'absolute',
|
||||
bottom: 'var(--slider-fill, 0%)',
|
||||
left: '50%',
|
||||
top: undefined,
|
||||
translate: '-50% 50%',
|
||||
};
|
||||
|
||||
return {
|
||||
'data-orientation': orientation,
|
||||
style,
|
||||
};
|
||||
};
|
||||
|
||||
export class TimeSliderThumb extends HTMLElement {}
|
||||
|
||||
export const TimeSliderThumbElement: ConnectedComponentConstructor<TimeSliderThumb, undefined> = toConnectedHTMLComponent(
|
||||
TimeSliderThumb,
|
||||
undefined,
|
||||
getTimeSliderThumbProps,
|
||||
'TimeSliderThumb',
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export const TimeSliderElement = Object.assign(
|
||||
{},
|
||||
{
|
||||
Root: TimeSliderRootElement,
|
||||
Track: TimeSliderTrackElement,
|
||||
Progress: TimeSliderProgressElement,
|
||||
Pointer: TimeSliderPointerElement,
|
||||
Thumb: TimeSliderThumbElement,
|
||||
},
|
||||
) as {
|
||||
Root: typeof TimeSliderRootElement;
|
||||
Track: typeof TimeSliderTrackElement;
|
||||
Progress: typeof TimeSliderProgressElement;
|
||||
Pointer: typeof TimeSliderPointerElement;
|
||||
Thumb: typeof TimeSliderThumbElement;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Prettify } from '../types';
|
||||
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
|
||||
import { Tooltip as CoreTooltip } from '@videojs/core-preview';
|
||||
import { getCoreState, getPropsFromAttrs, toConnectedHTMLComponent } from '../utils/component-factory';
|
||||
import { getPopoverProps, Popover } from './popover';
|
||||
|
||||
export type TooltipState = Prettify<ReturnType<CoreTooltip['getState']>>;
|
||||
|
||||
export const getTooltipState: StateHook<Tooltip, TooltipState> = (element, _mediaStore) => {
|
||||
const coreState = getCoreState(CoreTooltip, getPropsFromAttrs(element));
|
||||
return {
|
||||
...coreState,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTooltipProps: PropsHook<Tooltip, TooltipState> = getPopoverProps;
|
||||
|
||||
export class Tooltip extends Popover {
|
||||
static get observedAttributes(): string[] {
|
||||
return [
|
||||
...super.observedAttributes,
|
||||
'track-cursor-axis',
|
||||
];
|
||||
}
|
||||
|
||||
get trackCursorAxis(): 'x' | null {
|
||||
const value = this.getAttribute('track-cursor-axis');
|
||||
return value === 'x' ? value : null;
|
||||
}
|
||||
}
|
||||
|
||||
export const TooltipElement: ConnectedComponentConstructor<Tooltip, TooltipState> = toConnectedHTMLComponent(
|
||||
Tooltip,
|
||||
getTooltipState,
|
||||
getTooltipProps,
|
||||
'Tooltip',
|
||||
);
|
||||
@@ -0,0 +1,203 @@
|
||||
import type { Prettify } from '../types';
|
||||
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
|
||||
|
||||
import { VolumeSlider as CoreVolumeSlider } from '@videojs/core-preview';
|
||||
import { volumeSliderStateDefinition } from '@videojs/core-preview/store';
|
||||
import { memoize } from '@videojs/utils-preview';
|
||||
|
||||
import { getCoreState, getPropsFromAttrs, toConnectedHTMLComponent } from '../utils/component-factory';
|
||||
|
||||
// ============================================================================
|
||||
// ROOT COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
type VolumeSliderState = Prettify<ReturnType<CoreVolumeSlider['getState']>>;
|
||||
type VolumeSliderStateWithMethods = Prettify<VolumeSliderState & ReturnType<typeof volumeSliderStateDefinition.createRequestMethods>>;
|
||||
|
||||
const volumeSliderCreateRequestMethods = memoize(volumeSliderStateDefinition.createRequestMethods);
|
||||
|
||||
export const getVolumeSliderRootState: StateHook<VolumeSliderRoot, VolumeSliderStateWithMethods> = (element, mediaStore) => {
|
||||
const mediaState = volumeSliderStateDefinition.stateTransform(mediaStore.getState());
|
||||
const mediaMethods = volumeSliderCreateRequestMethods(mediaStore.dispatch);
|
||||
const coreState = getCoreState(CoreVolumeSlider, {
|
||||
...getPropsFromAttrs(element),
|
||||
...mediaState,
|
||||
...mediaMethods,
|
||||
});
|
||||
return {
|
||||
...coreState,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* VolumeSlider Root props hook - equivalent to React's useVolumeSliderRootProps
|
||||
* Handles element attributes and properties based on state
|
||||
*/
|
||||
export const getVolumeSliderRootProps: PropsHook<VolumeSliderRoot, VolumeSliderStateWithMethods> = (element, state) => {
|
||||
if (state._rootElement !== element) {
|
||||
state._setRootElement(element);
|
||||
}
|
||||
|
||||
const volumeText = `${Math.round(state.muted ? 0 : state.volume * 100)}%`;
|
||||
|
||||
return {
|
||||
role: 'slider',
|
||||
tabindex: element.getAttribute('tabindex') ?? '0',
|
||||
'data-muted': state.muted.toString(),
|
||||
'data-volume-level': state.volumeLevel,
|
||||
'data-orientation': element.orientation || 'horizontal',
|
||||
'aria-label': 'Volume',
|
||||
'aria-valuemin': '0',
|
||||
'aria-valuemax': '100',
|
||||
'aria-valuenow': Math.round(state.muted ? 0 : state.volume * 100).toString(),
|
||||
'aria-valuetext': volumeText,
|
||||
'aria-orientation': element.orientation || 'horizontal',
|
||||
style: {
|
||||
...(element.hasAttribute('commandfor') ? { anchorName: `--${element.getAttribute('commandfor')}` } : {}),
|
||||
'--slider-fill': `${state._fillWidth.toFixed(3)}%`,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export class VolumeSliderRoot extends HTMLElement {
|
||||
static readonly observedAttributes: readonly string[] = ['commandfor', 'orientation'];
|
||||
|
||||
_state: VolumeSliderState | undefined;
|
||||
|
||||
get orientation(): 'horizontal' | 'vertical' {
|
||||
return (this.getAttribute('orientation') as 'horizontal' | 'vertical') || 'horizontal';
|
||||
}
|
||||
}
|
||||
|
||||
export const VolumeSliderRootElement: ConnectedComponentConstructor<VolumeSliderRoot, VolumeSliderStateWithMethods> = toConnectedHTMLComponent(
|
||||
VolumeSliderRoot,
|
||||
getVolumeSliderRootState,
|
||||
getVolumeSliderRootProps,
|
||||
'VolumeSliderRoot',
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// TRACK COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
export const getVolumeSliderTrackProps: PropsHook<VolumeSliderTrack, undefined> = (element, _state) => {
|
||||
const rootElement = element.closest('media-volume-slider') as VolumeSliderRoot;
|
||||
|
||||
if (rootElement._state?._trackElement !== element) {
|
||||
rootElement._state?._setTrackElement?.(element);
|
||||
}
|
||||
|
||||
const orientation = rootElement?.orientation || 'horizontal';
|
||||
|
||||
return {
|
||||
'data-orientation': orientation,
|
||||
style: {
|
||||
width: orientation === 'horizontal' ? '100%' : undefined,
|
||||
height: orientation !== 'horizontal' ? '100%' : undefined,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export class VolumeSliderTrack extends HTMLElement {}
|
||||
|
||||
export const VolumeSliderTrackElement: ConnectedComponentConstructor<VolumeSliderTrack, undefined> = toConnectedHTMLComponent(
|
||||
VolumeSliderTrack,
|
||||
undefined,
|
||||
getVolumeSliderTrackProps,
|
||||
'VolumeSliderTrack',
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// INDICATOR COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
export const getVolumeSliderIndicatorProps: PropsHook<VolumeSliderIndicator, undefined> = (element, _state) => {
|
||||
const rootElement = element.closest('media-volume-slider') as VolumeSliderRoot;
|
||||
const orientation = rootElement?.orientation || 'horizontal';
|
||||
const style = orientation === 'horizontal'
|
||||
? {
|
||||
position: 'absolute',
|
||||
width: 'var(--slider-fill, 0%)',
|
||||
height: '100%',
|
||||
top: '0',
|
||||
bottom: undefined,
|
||||
}
|
||||
: {
|
||||
position: 'absolute',
|
||||
height: 'var(--slider-fill, 0%)',
|
||||
width: '100%',
|
||||
bottom: '0',
|
||||
top: undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
'data-orientation': orientation,
|
||||
style,
|
||||
};
|
||||
};
|
||||
|
||||
export class VolumeSliderIndicator extends HTMLElement {}
|
||||
|
||||
export const VolumeSliderIndicatorElement: ConnectedComponentConstructor<VolumeSliderIndicator, undefined> = toConnectedHTMLComponent(
|
||||
VolumeSliderIndicator,
|
||||
undefined,
|
||||
getVolumeSliderIndicatorProps,
|
||||
'VolumeSliderIndicator',
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// THUMB COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
export const getVolumeSliderThumbProps: PropsHook<VolumeSliderThumb, undefined> = (element, _state) => {
|
||||
const rootElement = element.closest('media-volume-slider') as VolumeSliderRoot;
|
||||
const orientation = rootElement?.orientation || 'horizontal';
|
||||
const style = orientation === 'horizontal'
|
||||
? {
|
||||
position: 'absolute',
|
||||
left: 'var(--slider-fill, 0%)',
|
||||
top: '50%',
|
||||
bottom: undefined,
|
||||
translate: '-50% -50%',
|
||||
}
|
||||
: {
|
||||
position: 'absolute',
|
||||
bottom: 'var(--slider-fill, 0%)',
|
||||
left: '50%',
|
||||
top: undefined,
|
||||
translate: '-50% 50%',
|
||||
};
|
||||
|
||||
return {
|
||||
'data-orientation': orientation,
|
||||
style,
|
||||
};
|
||||
};
|
||||
|
||||
export class VolumeSliderThumb extends HTMLElement {}
|
||||
|
||||
export const VolumeSliderThumbElement: ConnectedComponentConstructor<VolumeSliderThumb, undefined> = toConnectedHTMLComponent(
|
||||
VolumeSliderThumb,
|
||||
undefined,
|
||||
getVolumeSliderThumbProps,
|
||||
'VolumeSliderThumb',
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export const VolumeSliderElement = Object.assign(
|
||||
{},
|
||||
{
|
||||
Root: VolumeSliderRootElement,
|
||||
Track: VolumeSliderTrackElement,
|
||||
Indicator: VolumeSliderIndicatorElement,
|
||||
Thumb: VolumeSliderThumbElement,
|
||||
},
|
||||
) as {
|
||||
Root: typeof VolumeSliderRootElement;
|
||||
Track: typeof VolumeSliderTrackElement;
|
||||
Indicator: typeof VolumeSliderIndicatorElement;
|
||||
Thumb: typeof VolumeSliderThumbElement;
|
||||
};
|
||||
Reference in New Issue
Block a user