mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: add compound html timerange component (#14)
* fix: add context logic to component factory * feat: add compound time range html component
This commit is contained in:
@@ -4,8 +4,11 @@ defineVjsPlayer();
|
|||||||
|
|
||||||
document.body.innerHTML = `
|
document.body.innerHTML = `
|
||||||
<media-provider>
|
<media-provider>
|
||||||
<media-skin-default>
|
<div style="width: 100%; max-width: 800px; margin: 0 auto;">
|
||||||
<video slot="media" src="https://www.w3schools.com/html/mov_bbb.mp4" muted></video>
|
<media-skin-default>
|
||||||
|
<video slot="media" src="https://stream.mux.com/a4nOgmxGWg6gULfcBbAa00gXyfcwPnAFldF8RdsNyk8M/high.mp4" muted></video>
|
||||||
|
</media-skin-default>
|
||||||
|
</div>
|
||||||
</media-skin-default>
|
</media-skin-default>
|
||||||
</media-provider>
|
</media-provider>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ module.exports = [
|
|||||||
format: 'esm',
|
format: 'esm',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: (id) => {
|
external: (id) => {
|
||||||
// Don't externalize relative imports (starts with . or /)
|
// Don't externalize relative imports (starts with . or /)
|
||||||
if (id.startsWith('.') || id.startsWith('/')) return false;
|
if (id.startsWith('.') || id.startsWith('/')) return false;
|
||||||
@@ -43,6 +46,9 @@ module.exports = [
|
|||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: (id) => {
|
external: (id) => {
|
||||||
// Don't externalize relative imports (starts with . or /)
|
// Don't externalize relative imports (starts with . or /)
|
||||||
if (id.startsWith('.') || id.startsWith('/')) return false;
|
if (id.startsWith('.') || id.startsWith('/')) return false;
|
||||||
@@ -66,4 +72,4 @@ module.exports = [
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ module.exports = [
|
|||||||
format: 'esm',
|
format: 'esm',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: (id) => {
|
external: (id) => {
|
||||||
// Don't externalize relative imports (starts with . or /)
|
// Don't externalize relative imports (starts with . or /)
|
||||||
if (id.startsWith('.') || id.startsWith('/')) return false;
|
if (id.startsWith('.') || id.startsWith('/')) return false;
|
||||||
@@ -39,6 +42,9 @@ module.exports = [
|
|||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: (id) => {
|
external: (id) => {
|
||||||
// Don't externalize relative imports (starts with . or /)
|
// Don't externalize relative imports (starts with . or /)
|
||||||
if (id.startsWith('.') || id.startsWith('/')) return false;
|
if (id.startsWith('.') || id.startsWith('/')) return false;
|
||||||
@@ -59,4 +65,4 @@ module.exports = [
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ module.exports = [
|
|||||||
format: 'esm',
|
format: 'esm',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: (id) => {
|
external: (id) => {
|
||||||
// Don't externalize relative imports (starts with . or /)
|
// Don't externalize relative imports (starts with . or /)
|
||||||
if (id.startsWith('.') || id.startsWith('/')) return false;
|
if (id.startsWith('.') || id.startsWith('/')) return false;
|
||||||
@@ -39,6 +42,9 @@ module.exports = [
|
|||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: (id) => {
|
external: (id) => {
|
||||||
// Don't externalize relative imports (starts with . or /)
|
// Don't externalize relative imports (starts with . or /)
|
||||||
if (id.startsWith('.') || id.startsWith('/')) return false;
|
if (id.startsWith('.') || id.startsWith('/')) return false;
|
||||||
@@ -59,4 +65,4 @@ module.exports = [
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c && tsc --project tsconfig.build.json",
|
"build": "rollup -c && tsc --project tsconfig.build.json",
|
||||||
|
"dev": "rollup -c -w",
|
||||||
"test": "echo \"No tests yet\"",
|
"test": "echo \"No tests yet\"",
|
||||||
"clean": "rm -rf dist"
|
"clean": "rm -rf dist"
|
||||||
},
|
},
|
||||||
@@ -44,4 +45,4 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ module.exports = [
|
|||||||
format: 'esm',
|
format: 'esm',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: (id) => {
|
external: (id) => {
|
||||||
// Don't externalize relative imports (starts with . or /)
|
// Don't externalize relative imports (starts with . or /)
|
||||||
if (id.startsWith('.') || id.startsWith('/')) return false;
|
if (id.startsWith('.') || id.startsWith('/')) return false;
|
||||||
@@ -39,6 +42,9 @@ module.exports = [
|
|||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: (id) => {
|
external: (id) => {
|
||||||
// Don't externalize relative imports (starts with . or /)
|
// Don't externalize relative imports (starts with . or /)
|
||||||
if (id.startsWith('.') || id.startsWith('/')) return false;
|
if (id.startsWith('.') || id.startsWith('/')) return false;
|
||||||
@@ -59,4 +65,4 @@ module.exports = [
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,10 +5,29 @@ import {
|
|||||||
} from '../utils/component-factory';
|
} from '../utils/component-factory';
|
||||||
import { timeRangeStateDefinition } from '@vjs-10/media-store';
|
import { timeRangeStateDefinition } from '@vjs-10/media-store';
|
||||||
|
|
||||||
|
// Utility functions for pointer position and seek time calculations
|
||||||
|
const calculatePointerRatio = (clientX: number, rect: DOMRect): number => {
|
||||||
|
const x = clientX - rect.left;
|
||||||
|
return Math.max(0, Math.min(100, (x / rect.width) * 100));
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateSeekTimeFromRatio = (ratio: number, duration: number): number => {
|
||||||
|
return (ratio / 100) * duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateSeekTimeFromPointerEvent = (
|
||||||
|
event: PointerEvent,
|
||||||
|
duration: number
|
||||||
|
): number => {
|
||||||
|
const rect = (event.target as HTMLElement).getBoundingClientRect();
|
||||||
|
const ratio = calculatePointerRatio(event.clientX, rect);
|
||||||
|
return calculateSeekTimeFromRatio(ratio, duration);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @TODO Should we use a base "range" superclass or just duplicate shared code? (CJP)
|
* TimeRange Root component - Main container with pointer event handling
|
||||||
**/
|
*/
|
||||||
export class TimeRangeBase extends HTMLElement {
|
export class TimeRangeRootBase extends HTMLElement {
|
||||||
_state:
|
_state:
|
||||||
| {
|
| {
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
@@ -16,35 +35,87 @@ export class TimeRangeBase extends HTMLElement {
|
|||||||
requestSeek: (time: number) => void;
|
requestSeek: (time: number) => void;
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
_input: HTMLInputElement;
|
_trackElement: HTMLElement | null = null;
|
||||||
|
_pointerPosition: number | null = null;
|
||||||
|
_hovering: boolean = false;
|
||||||
|
_dragging: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
/**
|
|
||||||
* @TODO This is just a simple placeholder input to demonstrate functionality.
|
// Add pointer event listeners
|
||||||
* A full implementation will need to implement a "compound component" architecture and likely should use templates. (CJP)
|
this.addEventListener('pointerdown', this);
|
||||||
**/
|
this.addEventListener('pointermove', this);
|
||||||
this._input = document.createElement('input');
|
this.addEventListener('pointerup', this);
|
||||||
this._input.type = 'range';
|
this.addEventListener('pointerenter', this);
|
||||||
this._input.min = '0';
|
this.addEventListener('pointerleave', this);
|
||||||
this._input.max = '100';
|
|
||||||
this._input.step = '0.1';
|
|
||||||
this._input.addEventListener('input', this);
|
|
||||||
this.appendChild(this._input);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEvent(event: Event) {
|
handleEvent(event: Event) {
|
||||||
const { type } = event;
|
const { type } = event;
|
||||||
const state = this._state;
|
const state = this._state;
|
||||||
if (state) {
|
if (!state) return;
|
||||||
if (type === 'input') {
|
|
||||||
const ratio = parseFloat(this._input.value) / 100;
|
switch (type) {
|
||||||
const seekTime = ratio * state.duration;
|
case 'pointerdown':
|
||||||
state.requestSeek(seekTime);
|
this._handlePointerDown(event as PointerEvent);
|
||||||
}
|
break;
|
||||||
|
case 'pointermove':
|
||||||
|
this._handlePointerMove(event as PointerEvent);
|
||||||
|
break;
|
||||||
|
case 'pointerup':
|
||||||
|
this._handlePointerUp(event as PointerEvent);
|
||||||
|
break;
|
||||||
|
case 'pointerenter':
|
||||||
|
this._handlePointerEnter();
|
||||||
|
break;
|
||||||
|
case 'pointerleave':
|
||||||
|
this._handlePointerLeave();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handlePointerDown(event: PointerEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
this._dragging = true;
|
||||||
|
const seekTime = calculateSeekTimeFromPointerEvent(event, this._state!.duration);
|
||||||
|
this._state!.requestSeek(seekTime);
|
||||||
|
|
||||||
|
// Capture pointer events
|
||||||
|
this.setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handlePointerMove(event: PointerEvent) {
|
||||||
|
if (!this._trackElement) return;
|
||||||
|
|
||||||
|
const rect = this._trackElement.getBoundingClientRect();
|
||||||
|
const ratio = calculatePointerRatio(event.clientX, rect);
|
||||||
|
this._pointerPosition = ratio;
|
||||||
|
|
||||||
|
if (this._dragging) {
|
||||||
|
const seekTime = calculateSeekTimeFromRatio(ratio, this._state!.duration);
|
||||||
|
this._state!.requestSeek(seekTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handlePointerUp(event: PointerEvent) {
|
||||||
|
this.releasePointerCapture(event.pointerId);
|
||||||
|
|
||||||
|
if (this._dragging && this._trackElement && this._pointerPosition !== null) {
|
||||||
|
const seekTime = calculateSeekTimeFromRatio(this._pointerPosition, this._state!.duration);
|
||||||
|
this._state!.requestSeek(seekTime);
|
||||||
|
}
|
||||||
|
this._dragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handlePointerEnter() {
|
||||||
|
this._hovering = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handlePointerLeave() {
|
||||||
|
this._hovering = false;
|
||||||
|
}
|
||||||
|
|
||||||
get currentTime() {
|
get currentTime() {
|
||||||
return this._state?.currentTime;
|
return this._state?.currentTime;
|
||||||
}
|
}
|
||||||
@@ -55,25 +126,106 @@ export class TimeRangeBase extends HTMLElement {
|
|||||||
|
|
||||||
_update(props: any, state: any) {
|
_update(props: any, state: any) {
|
||||||
this._state = state;
|
this._state = state;
|
||||||
const ratio =
|
|
||||||
state.duration > 0 ? (state.currentTime / state.duration) * 100 : 0;
|
// Find track element
|
||||||
this._input.value = ratio.toString();
|
this._trackElement = this.querySelector('media-time-range-track') as HTMLElement;
|
||||||
this._input.max = '100';
|
|
||||||
this._input.setAttribute('aria-label', props['aria-label']);
|
// Calculate slider fill percentage
|
||||||
this._input.setAttribute('aria-valuetext', props['aria-valuetext']);
|
const sliderFill = this._dragging && this._pointerPosition !== null
|
||||||
this._input.disabled = props.disabled ?? false;
|
? this._pointerPosition
|
||||||
|
: state.duration > 0
|
||||||
|
? (state.currentTime / state.duration) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
// Update data attributes for styling
|
// Update CSS custom properties
|
||||||
this.setAttribute('data-current-time', props['data-current-time']);
|
this.style.setProperty('--slider-fill', `${Math.round(sliderFill)}%`);
|
||||||
this.setAttribute('data-duration', props['data-duration']);
|
this.style.setProperty('--slider-pointer',
|
||||||
|
this._hovering && this._pointerPosition !== null
|
||||||
|
? `${Math.round(this._pointerPosition)}%`
|
||||||
|
: '0%'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update ARIA attributes
|
||||||
|
this.setAttribute('role', 'slider');
|
||||||
|
this.setAttribute('aria-label', props['aria-label'] || 'Seek');
|
||||||
|
this.setAttribute('aria-valuemin', '0');
|
||||||
|
this.setAttribute('aria-valuemax', '100');
|
||||||
|
this.setAttribute('aria-valuenow', sliderFill.toString());
|
||||||
|
this.setAttribute('aria-valuetext', props['aria-valuetext'] || '');
|
||||||
|
|
||||||
|
// Update data attributes
|
||||||
|
this.setAttribute('data-current-time', state.currentTime.toString());
|
||||||
|
this.setAttribute('data-duration', state.duration.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TimeRange state hook - equivalent to React's useTimeRangeState
|
* TimeRange Track component - Track element that captures pointer events
|
||||||
|
*/
|
||||||
|
export class TimeRangeTrackBase extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
_update(_props: any, _state: any) {
|
||||||
|
// Track doesn't need much state management
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimeRange Progress component - Shows current progress
|
||||||
|
*/
|
||||||
|
export class TimeRangeProgressBase extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.style.position = 'absolute';
|
||||||
|
this.style.width = 'var(--slider-fill, 0%)';
|
||||||
|
this.style.height = '100%';
|
||||||
|
}
|
||||||
|
|
||||||
|
_update(_props: any, _state: any) {
|
||||||
|
// Progress updates are handled by CSS custom properties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimeRange Pointer component - Shows hover position
|
||||||
|
*/
|
||||||
|
export class TimeRangePointerBase extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.style.position = 'absolute';
|
||||||
|
this.style.width = 'var(--slider-pointer, 0%)';
|
||||||
|
this.style.height = '100%';
|
||||||
|
}
|
||||||
|
|
||||||
|
_update(_props: any, _state: any) {
|
||||||
|
// Pointer updates are handled by CSS custom properties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimeRange Thumb component - Draggable thumb element
|
||||||
|
*/
|
||||||
|
export class TimeRangeThumbBase extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.style.position = 'absolute';
|
||||||
|
this.style.top = '50%';
|
||||||
|
this.style.left = 'var(--slider-fill, 0%)';
|
||||||
|
this.style.transform = 'translate(-50%, -50%)';
|
||||||
|
}
|
||||||
|
|
||||||
|
_update(_props: any, _state: any) {
|
||||||
|
// Thumb updates are handled by CSS custom properties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimeRange Root state hook - equivalent to React's useTimeRangeRootState
|
||||||
* Handles media store state subscription and transformation
|
* Handles media store state subscription and transformation
|
||||||
*/
|
*/
|
||||||
export const useTimeRangeState: StateHook<{
|
export const useTimeRangeRootState: StateHook<{
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
}> = {
|
}> = {
|
||||||
@@ -85,10 +237,10 @@ export const useTimeRangeState: StateHook<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TimeRange props hook - equivalent to React's useTimeRangeProps
|
* TimeRange Root props hook - equivalent to React's useTimeRangeRootProps
|
||||||
* Handles element attributes and properties based on state
|
* Handles element attributes and properties based on state
|
||||||
*/
|
*/
|
||||||
export const useTimeRangeProps: PropsHook<{
|
export const useTimeRangeRootProps: PropsHook<{
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
}> = (state, _element) => {
|
}> = (state, _element) => {
|
||||||
@@ -108,31 +260,133 @@ export const useTimeRangeProps: PropsHook<{
|
|||||||
/** aria attributes/props */
|
/** aria attributes/props */
|
||||||
['aria-label']: 'Seek',
|
['aria-label']: 'Seek',
|
||||||
['aria-valuetext']: `${currentTimeText} of ${durationText}`,
|
['aria-valuetext']: `${currentTimeText} of ${durationText}`,
|
||||||
/** input props */
|
|
||||||
disabled: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return baseProps;
|
return baseProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @TODO When implementing compound components, this function may need to be swapped out, modified, or augmented in some way or another. (CJP)
|
* TimeRange Track props hook
|
||||||
*/
|
*/
|
||||||
|
export const useTimeRangeTrackProps: PropsHook<{}> = (_state, _element) => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connected TimeRange component using hook-style architecture
|
* TimeRange Progress props hook
|
||||||
* Equivalent to React's TimeRange = toConnectedComponent(...)
|
|
||||||
*/
|
*/
|
||||||
export const TimeRange = toConnectedHTMLComponent(
|
export const useTimeRangeProgressProps: PropsHook<{}> = (_state, _element) => {
|
||||||
TimeRangeBase,
|
return {};
|
||||||
useTimeRangeState,
|
};
|
||||||
useTimeRangeProps,
|
|
||||||
'TimeRange',
|
/**
|
||||||
|
* TimeRange Pointer props hook
|
||||||
|
*/
|
||||||
|
export const useTimeRangePointerProps: PropsHook<{}> = (_state, _element) => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimeRange Thumb props hook
|
||||||
|
*/
|
||||||
|
export const useTimeRangeThumbProps: PropsHook<{}> = (_state, _element) => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connected TimeRange Root component using hook-style architecture
|
||||||
|
*/
|
||||||
|
export const TimeRangeRoot = toConnectedHTMLComponent(
|
||||||
|
TimeRangeRootBase,
|
||||||
|
useTimeRangeRootState,
|
||||||
|
useTimeRangeRootProps,
|
||||||
|
'TimeRangeRoot',
|
||||||
);
|
);
|
||||||
|
|
||||||
// NOTE: In this architecture it will be important to decouple component class definitions from their registration in the CustomElementsRegistry. (CJP)
|
/**
|
||||||
if (!globalThis.customElements.get('media-time-range')) {
|
* Connected TimeRange Track component
|
||||||
|
*/
|
||||||
|
export const TimeRangeTrack = toConnectedHTMLComponent(
|
||||||
|
TimeRangeTrackBase,
|
||||||
|
{ keys: [], transform: () => ({}) },
|
||||||
|
useTimeRangeTrackProps,
|
||||||
|
'TimeRangeTrack',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connected TimeRange Progress component
|
||||||
|
*/
|
||||||
|
export const TimeRangeProgress = toConnectedHTMLComponent(
|
||||||
|
TimeRangeProgressBase,
|
||||||
|
{ keys: [], transform: () => ({}) },
|
||||||
|
useTimeRangeProgressProps,
|
||||||
|
'TimeRangeProgress',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connected TimeRange Pointer component
|
||||||
|
*/
|
||||||
|
export const TimeRangePointer = toConnectedHTMLComponent(
|
||||||
|
TimeRangePointerBase,
|
||||||
|
{ keys: [], transform: () => ({}) },
|
||||||
|
useTimeRangePointerProps,
|
||||||
|
'TimeRangePointer',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connected TimeRange Thumb component
|
||||||
|
*/
|
||||||
|
export const TimeRangeThumb = toConnectedHTMLComponent(
|
||||||
|
TimeRangeThumbBase,
|
||||||
|
{ keys: [], transform: () => ({}) },
|
||||||
|
useTimeRangeThumbProps,
|
||||||
|
'TimeRangeThumb',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compound TimeRange component object
|
||||||
|
*/
|
||||||
|
export const TimeRange = Object.assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
Root: TimeRangeRoot,
|
||||||
|
Track: TimeRangeTrack,
|
||||||
|
Progress: TimeRangeProgress,
|
||||||
|
Pointer: TimeRangePointer,
|
||||||
|
Thumb: TimeRangeThumb,
|
||||||
|
}
|
||||||
|
) as {
|
||||||
|
Root: typeof TimeRangeRoot;
|
||||||
|
Track: typeof TimeRangeTrack;
|
||||||
|
Progress: typeof TimeRangeProgress;
|
||||||
|
Pointer: typeof TimeRangePointer;
|
||||||
|
Thumb: typeof TimeRangeThumb;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register custom elements
|
||||||
|
if (!globalThis.customElements.get('media-time-range-root')) {
|
||||||
// @ts-ignore - Custom element constructor compatibility
|
// @ts-ignore - Custom element constructor compatibility
|
||||||
globalThis.customElements.define('media-time-range', TimeRange);
|
globalThis.customElements.define('media-time-range-root', TimeRangeRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.customElements.get('media-time-range-track')) {
|
||||||
|
// @ts-ignore - Custom element constructor compatibility
|
||||||
|
globalThis.customElements.define('media-time-range-track', TimeRangeTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.customElements.get('media-time-range-progress')) {
|
||||||
|
// @ts-ignore - Custom element constructor compatibility
|
||||||
|
globalThis.customElements.define('media-time-range-progress', TimeRangeProgress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.customElements.get('media-time-range-pointer')) {
|
||||||
|
// @ts-ignore - Custom element constructor compatibility
|
||||||
|
globalThis.customElements.define('media-time-range-pointer', TimeRangePointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!globalThis.customElements.get('media-time-range-thumb')) {
|
||||||
|
// @ts-ignore - Custom element constructor compatibility
|
||||||
|
globalThis.customElements.define('media-time-range-thumb', TimeRangeThumb);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TimeRange;
|
export default TimeRange;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function getTemplateHTML() {
|
|||||||
color: rgb(238 238 238);
|
color: rgb(238 238 238);
|
||||||
}
|
}
|
||||||
|
|
||||||
media-container > [slot=media] {
|
media-container > ::slotted([slot=media]) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -103,6 +103,45 @@ export function getTemplateHTML() {
|
|||||||
.spacer {
|
.spacer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TimeRange Component Styles */
|
||||||
|
media-time-range-root {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
min-width: 100px;
|
||||||
|
width: 100%;
|
||||||
|
padding-block: .75rem;
|
||||||
|
margin: 0 .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-time-range-track {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: .375rem;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: .25rem;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-time-range-thumb {
|
||||||
|
width: .75rem;
|
||||||
|
height: .75rem;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-time-range-pointer {
|
||||||
|
background-color: rgba(255, 255, 255, .5);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-time-range-progress {
|
||||||
|
background-color: #007bff;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<media-container>
|
<media-container>
|
||||||
<slot name="media" slot="media"></slot>
|
<slot name="media" slot="media"></slot>
|
||||||
@@ -116,7 +155,13 @@ export function getTemplateHTML() {
|
|||||||
</media-play-button>
|
</media-play-button>
|
||||||
<!-- Use the show-remaining attribute to show count down/remaining time -->
|
<!-- Use the show-remaining attribute to show count down/remaining time -->
|
||||||
<media-current-time-display show-remaining></media-current-time-display>
|
<media-current-time-display show-remaining></media-current-time-display>
|
||||||
<media-time-range></media-time-range>
|
<media-time-range-root>
|
||||||
|
<media-time-range-track>
|
||||||
|
<media-time-range-progress></media-time-range-progress>
|
||||||
|
<media-time-range-pointer></media-time-range-pointer>
|
||||||
|
</media-time-range-track>
|
||||||
|
<media-time-range-thumb></media-time-range-thumb>
|
||||||
|
</media-time-range-root>
|
||||||
<media-duration-display></media-duration-display>
|
<media-duration-display></media-duration-display>
|
||||||
<media-mute-button class="button">
|
<media-mute-button class="button">
|
||||||
<media-volume-high-icon class="icon volume-high-icon"></media-volume-high-icon>
|
<media-volume-high-icon class="icon volume-high-icon"></media-volume-high-icon>
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ module.exports = [
|
|||||||
format: 'esm',
|
format: 'esm',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: (id) => {
|
external: (id) => {
|
||||||
// Don't externalize relative imports (starts with . or /)
|
// Don't externalize relative imports (starts with . or /)
|
||||||
if (id.startsWith('.') || id.startsWith('/')) return false;
|
if (id.startsWith('.') || id.startsWith('/')) return false;
|
||||||
@@ -39,6 +42,9 @@ module.exports = [
|
|||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: (id) => {
|
external: (id) => {
|
||||||
// Don't externalize relative imports (starts with . or /)
|
// Don't externalize relative imports (starts with . or /)
|
||||||
if (id.startsWith('.') || id.startsWith('/')) return false;
|
if (id.startsWith('.') || id.startsWith('/')) return false;
|
||||||
@@ -59,4 +65,4 @@ module.exports = [
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ module.exports = [
|
|||||||
format: 'esm',
|
format: 'esm',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: [
|
external: [
|
||||||
'react',
|
'react',
|
||||||
'@vjs-10/react-icons',
|
'@vjs-10/react-icons',
|
||||||
@@ -46,6 +49,9 @@ module.exports = [
|
|||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
},
|
||||||
external: [
|
external: [
|
||||||
'react',
|
'react',
|
||||||
'@vjs-10/react-icons',
|
'@vjs-10/react-icons',
|
||||||
@@ -72,4 +78,4 @@ module.exports = [
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -33,29 +33,6 @@ const calculateSeekTimeFromPointerEvent = (
|
|||||||
// ROOT COMPONENT
|
// ROOT COMPONENT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
interface TimeRangeRootContextValue {
|
|
||||||
currentTime: number;
|
|
||||||
duration: number;
|
|
||||||
requestSeek: (time: number) => void;
|
|
||||||
pointerPosition: number | null;
|
|
||||||
setPointerPosition: (position: number | null) => void;
|
|
||||||
hovering: boolean;
|
|
||||||
setHovering: (hovering: boolean) => void;
|
|
||||||
dragging: boolean;
|
|
||||||
setDragging: (dragging: boolean) => void;
|
|
||||||
setTrackRef: (ref: HTMLDivElement | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TimeRangeRootContext = React.createContext<TimeRangeRootContextValue | null>(null);
|
|
||||||
|
|
||||||
export const useTimeRangeRootContext = (): TimeRangeRootContextValue => {
|
|
||||||
const context = React.useContext(TimeRangeRootContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useTimeRangeRootContext must be used within a TimeRange.Root component');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useTimeRangeRootState = (_props: any) => {
|
export const useTimeRangeRootState = (_props: any) => {
|
||||||
const mediaStore = useMediaStore();
|
const mediaStore = useMediaStore();
|
||||||
const mediaState = useMediaSelector(timeRangeStateDefinition.stateTransform, shallowEqual);
|
const mediaState = useMediaSelector(timeRangeStateDefinition.stateTransform, shallowEqual);
|
||||||
@@ -186,43 +163,11 @@ export const useTimeRangeRootProps = (
|
|||||||
|
|
||||||
type useTimeRangeRootState = typeof useTimeRangeRootState;
|
type useTimeRangeRootState = typeof useTimeRangeRootState;
|
||||||
type useTimeRangeRootProps = typeof useTimeRangeRootProps;
|
type useTimeRangeRootProps = typeof useTimeRangeRootProps;
|
||||||
type TimeRangeRootState = ReturnType<useTimeRangeRootState>;
|
|
||||||
type TimeRangeRootProps = ReturnType<useTimeRangeRootProps>;
|
type TimeRangeRootProps = ReturnType<useTimeRangeRootProps>;
|
||||||
|
|
||||||
export const renderTimeRangeRoot = (props: TimeRangeRootProps, state: TimeRangeRootState) => {
|
export const renderTimeRangeRoot = (props: TimeRangeRootProps) => {
|
||||||
const contextValue: TimeRangeRootContextValue = React.useMemo(
|
|
||||||
() => ({
|
|
||||||
currentTime: state.currentTime,
|
|
||||||
duration: state.duration,
|
|
||||||
requestSeek: state.requestSeek,
|
|
||||||
pointerPosition: state.pointerPosition,
|
|
||||||
setPointerPosition: state.setPointerPosition,
|
|
||||||
hovering: state.hovering,
|
|
||||||
setHovering: state.setHovering,
|
|
||||||
dragging: state.dragging,
|
|
||||||
setDragging: state.setDragging,
|
|
||||||
setTrackRef: state.setTrackRef,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
state.currentTime,
|
|
||||||
state.duration,
|
|
||||||
state.requestSeek,
|
|
||||||
state.pointerPosition,
|
|
||||||
state.setPointerPosition,
|
|
||||||
state.hovering,
|
|
||||||
state.setHovering,
|
|
||||||
state.dragging,
|
|
||||||
state.setDragging,
|
|
||||||
state.setTrackRef,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimeRangeRootContext.Provider value={contextValue}>
|
<div {...props} />
|
||||||
<div style={props.style} {...props}>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</TimeRangeRootContext.Provider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,8 +182,8 @@ const TimeRangeRoot = toConnectedComponent(
|
|||||||
// TRACK COMPONENT
|
// TRACK COMPONENT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export const useTimeRangeTrackProps = (props: React.PropsWithChildren<{ [k: string]: any }>) => {
|
export const useTimeRangeTrackProps = (props: React.PropsWithChildren<{ [k: string]: any }>, context: any) => {
|
||||||
const { setTrackRef } = useTimeRangeRootContext();
|
const { setTrackRef } = context;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ref: setTrackRef,
|
ref: setTrackRef,
|
||||||
|
|||||||
@@ -7,18 +7,20 @@ export type StateHookFn<TProps = any, TState = any> = (props: TProps) => TState;
|
|||||||
|
|
||||||
export type PropsHookFn<TProps = any, TState = any, TResultProps = any> = (
|
export type PropsHookFn<TProps = any, TState = any, TResultProps = any> = (
|
||||||
props: TProps,
|
props: TProps,
|
||||||
state: TState,
|
state: TState
|
||||||
) => TResultProps;
|
) => TResultProps;
|
||||||
|
|
||||||
export type RenderFn<TProps = any, TState = any> = (
|
export type RenderFn<TProps = any, TState = any> = (
|
||||||
props: TProps,
|
props: TProps,
|
||||||
state: TState,
|
state: TState
|
||||||
) => React.ReactElement;
|
) => React.ReactElement;
|
||||||
|
|
||||||
|
const Context = React.createContext<any | null>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic factory function to create connected components following the hooks pattern
|
* Generic factory function to create connected components following the hooks pattern
|
||||||
* inspired by Adobe React Spectrum and Base UI architectures.
|
* inspired by Adobe React Spectrum and Base UI architectures.
|
||||||
*
|
*
|
||||||
* @param useStateHook - Hook that provides component state
|
* @param useStateHook - Hook that provides component state
|
||||||
* @param usePropsHook - Hook that enhances props with state-derived values
|
* @param usePropsHook - Hook that enhances props with state-derived values
|
||||||
* @param defaultRender - Default render function for the component
|
* @param defaultRender - Default render function for the component
|
||||||
@@ -29,12 +31,12 @@ export const toConnectedComponent = <
|
|||||||
TProps extends Record<string, any>,
|
TProps extends Record<string, any>,
|
||||||
TState,
|
TState,
|
||||||
TResultProps extends Record<string, any>,
|
TResultProps extends Record<string, any>,
|
||||||
TRenderFn extends RenderFn<TResultProps, TState>
|
TRenderFn extends RenderFn<TResultProps, TState>,
|
||||||
>(
|
>(
|
||||||
useStateHook: StateHookFn<TProps, TState>,
|
useStateHook: StateHookFn<TProps, TState>,
|
||||||
usePropsHook: PropsHookFn<TProps, TState, TResultProps>,
|
usePropsHook: PropsHookFn<TProps, TState, TResultProps>,
|
||||||
defaultRender: TRenderFn,
|
defaultRender: TRenderFn,
|
||||||
displayName: string,
|
displayName: string
|
||||||
) => {
|
) => {
|
||||||
const ConnectedComponent = ({
|
const ConnectedComponent = ({
|
||||||
render = defaultRender,
|
render = defaultRender,
|
||||||
@@ -42,7 +44,11 @@ export const toConnectedComponent = <
|
|||||||
}: TProps & { render?: TRenderFn }) => {
|
}: TProps & { render?: TRenderFn }) => {
|
||||||
const connectedState = useStateHook(props as TProps);
|
const connectedState = useStateHook(props as TProps);
|
||||||
const connectedProps = usePropsHook(props as TProps, connectedState);
|
const connectedProps = usePropsHook(props as TProps, connectedState);
|
||||||
return render(connectedProps, connectedState);
|
return (
|
||||||
|
<Context.Provider value={connectedState}>
|
||||||
|
{render(connectedProps, connectedState)}
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ConnectedComponent.displayName = displayName;
|
ConnectedComponent.displayName = displayName;
|
||||||
@@ -54,13 +60,13 @@ export const toConnectedComponent = <
|
|||||||
*/
|
*/
|
||||||
export type ConnectedComponent<
|
export type ConnectedComponent<
|
||||||
TProps extends Record<string, any>,
|
TProps extends Record<string, any>,
|
||||||
TRenderFn extends RenderFn<any, any>
|
TRenderFn extends RenderFn<any, any>,
|
||||||
> = React.FC<TProps & { render?: TRenderFn }>;
|
> = React.FC<TProps & { render?: TRenderFn }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory function to create context-based components that don't use toConnectedComponent
|
* Factory function to create context-based components that don't use toConnectedComponent
|
||||||
* These components rely on context provided by a parent component.
|
* These components rely on context provided by a parent component.
|
||||||
*
|
*
|
||||||
* @param usePropsHook - Hook that enhances props with context-derived values
|
* @param usePropsHook - Hook that enhances props with context-derived values
|
||||||
* @param defaultRender - Default render function for the component
|
* @param defaultRender - Default render function for the component
|
||||||
* @param displayName - Display name for React DevTools
|
* @param displayName - Display name for React DevTools
|
||||||
@@ -69,18 +75,19 @@ export type ConnectedComponent<
|
|||||||
export const toContextComponent = <
|
export const toContextComponent = <
|
||||||
TProps extends Record<string, any>,
|
TProps extends Record<string, any>,
|
||||||
TResultProps extends Record<string, any>,
|
TResultProps extends Record<string, any>,
|
||||||
TRenderFn extends (props: TResultProps) => React.ReactElement
|
TRenderFn extends (props: TResultProps, context: any) => React.ReactElement,
|
||||||
>(
|
>(
|
||||||
usePropsHook: (props: TProps) => TResultProps,
|
usePropsHook: (props: TProps, context: any) => TResultProps,
|
||||||
defaultRender: TRenderFn,
|
defaultRender: TRenderFn,
|
||||||
displayName: string,
|
displayName: string
|
||||||
) => {
|
) => {
|
||||||
const ContextComponent = ({
|
const ContextComponent = ({
|
||||||
render = defaultRender,
|
render = defaultRender,
|
||||||
...props
|
...props
|
||||||
}: TProps & { render?: TRenderFn }) => {
|
}: TProps & { render?: TRenderFn }) => {
|
||||||
const contextProps = usePropsHook(props as TProps);
|
const context = React.useContext(Context);
|
||||||
return render(contextProps);
|
const contextProps = usePropsHook(props as TProps, context);
|
||||||
|
return render(contextProps, context);
|
||||||
};
|
};
|
||||||
|
|
||||||
ContextComponent.displayName = displayName;
|
ContextComponent.displayName = displayName;
|
||||||
@@ -92,5 +99,5 @@ export const toContextComponent = <
|
|||||||
*/
|
*/
|
||||||
export type ContextComponent<
|
export type ContextComponent<
|
||||||
TProps extends Record<string, any>,
|
TProps extends Record<string, any>,
|
||||||
TRenderFn extends (props: any) => React.ReactElement
|
TRenderFn extends (props: any, context: any) => React.ReactElement,
|
||||||
> = React.FC<TProps & { render?: TRenderFn }>;
|
> = React.FC<TProps & { render?: TRenderFn }>;
|
||||||
|
|||||||
Reference in New Issue
Block a user