mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: add html preview time display (#58)
This commit is contained in:
@@ -165,16 +165,23 @@ export class MediaPopoverPortal extends HTMLElement {
|
||||
}
|
||||
|
||||
#setupPortal(): void {
|
||||
const portalId = this.getAttribute('root-id');
|
||||
const portalId = this.getAttribute('root-id') ?? '@default_portal_id';
|
||||
if (!portalId) return;
|
||||
|
||||
const portalContainer = (this.getRootNode() as ShadowRoot | Document).getElementById(portalId);
|
||||
/* @TODO We need to make sure portal logic is non-brittle longer term (CJP) */
|
||||
// NOTE: Hacky solution in part to ensure styling propogates from skin to container's baked in portal (TL;DR - Shadow DOM vs. Light DOM CSS) (CJP)
|
||||
const portalContainer
|
||||
= ((this.getRootNode() as ShadowRoot | Document).getElementById(portalId)
|
||||
?? (this.getRootNode() as ShadowRoot | Document)
|
||||
.querySelector('media-container')
|
||||
?.shadowRoot
|
||||
?.getElementById(portalId))
|
||||
? (this.getRootNode() as ShadowRoot | Document).querySelector('media-container')
|
||||
: undefined;
|
||||
if (!portalContainer) return;
|
||||
|
||||
portalContainer.addEventListener('mouseenter', this);
|
||||
portalContainer.addEventListener('mouseleave', this);
|
||||
|
||||
this.#portal = document.createElement('div');
|
||||
this.#portal.slot = 'portal';
|
||||
this.#portal.id = uniqueId();
|
||||
portalContainer.append(this.#portal);
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { PreviewTimeDisplayState } from '@vjs-10/media-store';
|
||||
import type { ConnectedComponentConstructor, PropsHook, StateHook } from '../utils/component-factory';
|
||||
|
||||
import { formatDisplayTime, previewTimeDisplayStateDefinition } from '@vjs-10/media-store';
|
||||
|
||||
import { toConnectedHTMLComponent } from '../utils/component-factory';
|
||||
|
||||
export class PreviewTimeDisplayBase extends HTMLElement {
|
||||
static shadowRootOptions = {
|
||||
mode: 'open' as ShadowRootMode,
|
||||
};
|
||||
|
||||
static observedAttributes: string[] = ['show-remaining'];
|
||||
|
||||
_state:
|
||||
| {
|
||||
previewTime: number | undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (!this.shadowRoot) {
|
||||
this.attachShadow((this.constructor as typeof PreviewTimeDisplayBase).shadowRootOptions);
|
||||
}
|
||||
}
|
||||
|
||||
get previewTime(): number {
|
||||
return this._state?.previewTime ?? 0;
|
||||
}
|
||||
|
||||
get showRemaining(): boolean {
|
||||
return this.hasAttribute('show-remaining');
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _oldValue: string | null, _newValue: string | null): void {
|
||||
if (name === 'show-remaining' && this._state) {
|
||||
// Re-render with current state when show-remaining attribute changes
|
||||
this._update({}, this._state);
|
||||
}
|
||||
}
|
||||
|
||||
_update(_props: any, state: any): void {
|
||||
this._state = state;
|
||||
|
||||
/** @TODO Should this live here or elsewhere? (CJP) */
|
||||
const timeLabel = formatDisplayTime(state.previewTime);
|
||||
|
||||
if (this.shadowRoot) {
|
||||
this.shadowRoot.textContent = timeLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const usePreviewTimeDisplayState: StateHook<{
|
||||
previewTime: number | undefined;
|
||||
}> = {
|
||||
keys: [...previewTimeDisplayStateDefinition.keys],
|
||||
transform: (rawState, _mediaStore) => ({
|
||||
...previewTimeDisplayStateDefinition.stateTransform(rawState),
|
||||
// Preview time display is read-only, so no request methods needed
|
||||
}),
|
||||
};
|
||||
|
||||
export const usePreviewTimeDisplayProps: PropsHook<{
|
||||
previewTime: number | undefined;
|
||||
}> = (_state, _element) => {
|
||||
const baseProps: Record<string, any> = {};
|
||||
return baseProps;
|
||||
};
|
||||
|
||||
export const PreviewTimeDisplay: ConnectedComponentConstructor<PreviewTimeDisplayState> = toConnectedHTMLComponent(
|
||||
PreviewTimeDisplayBase,
|
||||
usePreviewTimeDisplayState,
|
||||
usePreviewTimeDisplayProps,
|
||||
'PreviewTimeDisplay',
|
||||
);
|
||||
|
||||
// Register the custom element
|
||||
if (!globalThis.customElements.get('preview-time-display')) {
|
||||
globalThis.customElements.define('preview-time-display', PreviewTimeDisplay);
|
||||
}
|
||||
|
||||
export default PreviewTimeDisplay;
|
||||
@@ -10,11 +10,13 @@ export class MediaTooltipRoot extends HTMLElement {
|
||||
#hoverTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
#cleanup: (() => void) | null = null;
|
||||
#arrowElement: HTMLElement | null = null;
|
||||
#mousePosition = { x: 0, y: 0 };
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener('mouseenter', this);
|
||||
this.addEventListener('mouseleave', this);
|
||||
this.addEventListener('mousemove', this);
|
||||
}
|
||||
|
||||
handleEvent(event: Event): void {
|
||||
@@ -22,6 +24,8 @@ export class MediaTooltipRoot extends HTMLElement {
|
||||
this.#handleMouseEnter();
|
||||
} else if (event.type === 'mouseleave') {
|
||||
this.#handleMouseLeave();
|
||||
} else if (event.type === 'mousemove') {
|
||||
this.#handleMouseMove(event as MouseEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,17 +39,22 @@ export class MediaTooltipRoot extends HTMLElement {
|
||||
}
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
return ['delay', 'close-delay'];
|
||||
return ['delay', 'close-delay', 'track-cursor-axis'];
|
||||
}
|
||||
|
||||
get delay(): number {
|
||||
return Number.parseInt(this.getAttribute('delay') ?? '600', 10);
|
||||
return Number.parseInt(this.getAttribute('delay') ?? '0', 10);
|
||||
}
|
||||
|
||||
get closeDelay(): number {
|
||||
return Number.parseInt(this.getAttribute('close-delay') ?? '0', 10);
|
||||
}
|
||||
|
||||
get trackCursorAxis(): 'x' | 'y' | 'both' | undefined {
|
||||
const value = this.getAttribute('track-cursor-axis');
|
||||
return value === 'x' || value === 'y' || value === 'both' ? value : undefined;
|
||||
}
|
||||
|
||||
get #triggerElement(): MediaTooltipTrigger | null {
|
||||
return this.querySelector('media-tooltip-trigger') as MediaTooltipTrigger | null;
|
||||
}
|
||||
@@ -121,7 +130,54 @@ export class MediaTooltipRoot extends HTMLElement {
|
||||
middleware.push(arrow({ element: this.#arrowElement }));
|
||||
}
|
||||
|
||||
computePosition(trigger, popup, {
|
||||
// Use cursor position for positioning if trackCursorAxis is enabled
|
||||
const referenceElement = this.trackCursorAxis
|
||||
? {
|
||||
getBoundingClientRect: () => {
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
|
||||
if (this.trackCursorAxis === 'x') {
|
||||
// Track horizontal cursor position, use trigger's vertical position
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: triggerRect.top,
|
||||
right: this.#mousePosition.x,
|
||||
bottom: triggerRect.bottom,
|
||||
left: this.#mousePosition.x,
|
||||
x: this.#mousePosition.x,
|
||||
y: triggerRect.top,
|
||||
};
|
||||
} else if (this.trackCursorAxis === 'y') {
|
||||
// Track vertical cursor position, use trigger's horizontal position
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: this.#mousePosition.y,
|
||||
right: triggerRect.right,
|
||||
bottom: this.#mousePosition.y,
|
||||
left: triggerRect.left,
|
||||
x: triggerRect.left,
|
||||
y: this.#mousePosition.y,
|
||||
};
|
||||
} else {
|
||||
// Track both axes (trackCursorAxis === 'both')
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: this.#mousePosition.y,
|
||||
right: this.#mousePosition.x,
|
||||
bottom: this.#mousePosition.y,
|
||||
left: this.#mousePosition.x,
|
||||
x: this.#mousePosition.x,
|
||||
y: this.#mousePosition.y,
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
: trigger;
|
||||
|
||||
computePosition(referenceElement, popup, {
|
||||
placement,
|
||||
middleware,
|
||||
}).then(({ x, y, middlewareData }: { x: number; y: number; middlewareData: any }) => {
|
||||
@@ -142,7 +198,16 @@ export class MediaTooltipRoot extends HTMLElement {
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
this.#cleanup = autoUpdate(trigger, popup, updatePosition);
|
||||
|
||||
if (!this.trackCursorAxis) {
|
||||
this.#cleanup = autoUpdate(trigger, popup, updatePosition);
|
||||
}
|
||||
}
|
||||
|
||||
#updatePosition(): void {
|
||||
if (this.#open && this.trackCursorAxis) {
|
||||
this.#setupFloating();
|
||||
}
|
||||
}
|
||||
|
||||
#clearHoverTimeout(): void {
|
||||
@@ -165,6 +230,16 @@ export class MediaTooltipRoot extends HTMLElement {
|
||||
this.#setOpen(false);
|
||||
}, this.closeDelay);
|
||||
}
|
||||
|
||||
#handleMouseMove(event: MouseEvent): void {
|
||||
if (this.trackCursorAxis) {
|
||||
this.#mousePosition = { x: event.clientX, y: event.clientY };
|
||||
// Update position if tooltip is open
|
||||
if (this.#open) {
|
||||
this.#updatePosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaTooltipTrigger extends HTMLElement {
|
||||
@@ -195,7 +270,7 @@ export class MediaTooltipTrigger extends HTMLElement {
|
||||
}
|
||||
|
||||
const attributeName = mutation.attributeName;
|
||||
if (!attributeName) {
|
||||
if (!attributeName || !attributeName.startsWith('data-')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../components/media-time-slider';
|
||||
import '../components/media-fullscreen-button';
|
||||
import '../components/media-duration-display';
|
||||
import '../components/media-current-time-display';
|
||||
import '../components/media-preview-time-display';
|
||||
import '../components/media-popover';
|
||||
import '../components/media-tooltip';
|
||||
import '@vjs-10/html-icons';
|
||||
@@ -293,13 +294,26 @@ export function getTemplateHTML() {
|
||||
</media-tooltip-root>
|
||||
<!-- Use the show-remaining attribute to show count down/remaining time -->
|
||||
<media-current-time-display show-remaining></media-current-time-display>
|
||||
<media-time-slider-root>
|
||||
<media-time-slider-track>
|
||||
<media-time-slider-progress></media-time-slider-progress>
|
||||
<media-time-slider-pointer></media-time-slider-pointer>
|
||||
</media-time-slider-track>
|
||||
<media-time-slider-thumb></media-time-slider-thumb>
|
||||
</media-time-slider-root>
|
||||
|
||||
<media-tooltip-root track-cursor-axis="x">
|
||||
<media-tooltip-trigger>
|
||||
<media-time-slider-root>
|
||||
<media-time-slider-track>
|
||||
<media-time-slider-progress></media-time-slider-progress>
|
||||
<media-time-slider-pointer></media-time-slider-pointer>
|
||||
</media-time-slider-track>
|
||||
<media-time-slider-thumb></media-time-slider-thumb>
|
||||
</media-time-slider-root>
|
||||
</media-tooltip-trigger>
|
||||
<media-tooltip-portal>
|
||||
<media-tooltip-positioner side="top" side-offset="18" collision-padding="12">
|
||||
<media-tooltip-popup>
|
||||
<preview-time-display></preview-time-display>
|
||||
</media-tooltip-popup>
|
||||
</media-tooltip-positioner>
|
||||
</media-tooltip-portal>
|
||||
</media-tooltip-root>
|
||||
|
||||
<media-duration-display></media-duration-display>
|
||||
<media-popover-root open-on-hover delay="200" close-delay="100">
|
||||
<media-popover-trigger>
|
||||
@@ -310,7 +324,7 @@ export function getTemplateHTML() {
|
||||
</media-mute-button>
|
||||
</media-popover-trigger>
|
||||
<media-popover-portal>
|
||||
<media-popover-positioner side="top" side-offset="0">
|
||||
<media-popover-positioner side="top">
|
||||
<media-popover-popup>
|
||||
<media-volume-slider-root orientation="vertical">
|
||||
<media-volume-slider-track>
|
||||
|
||||
Reference in New Issue
Block a user