mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: use anchor API for html elements (#174)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import './media-container';
|
||||
// be sure to import video-provider first for proper context initialization
|
||||
import './video-provider';
|
||||
import './media-container';
|
||||
import './media-play-button';
|
||||
import './media-mute-button';
|
||||
import './media-volume-slider';
|
||||
|
||||
@@ -27,6 +27,8 @@ export class ButtonElement extends HTMLElement {
|
||||
|
||||
static getTemplateHTML: typeof getTemplateHTML = getTemplateHTML;
|
||||
|
||||
static observedAttributes: string[] = ['commandfor'];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -46,6 +48,12 @@ export class ButtonElement extends HTMLElement {
|
||||
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') {
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import type { ComputePositionReturn } from '@floating-ui/core';
|
||||
import type { Placement } from '@floating-ui/dom';
|
||||
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
|
||||
import { contains, getDocument, getDocumentOrShadowRoot, safePolygon } from '@videojs/utils/dom';
|
||||
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
};
|
||||
|
||||
type FloatingContext = Prettify<ComputePositionReturn> & {
|
||||
elements: {
|
||||
domReference: HTMLElement;
|
||||
floating: HTMLElement;
|
||||
};
|
||||
};
|
||||
type Placement = 'top' | 'bottom' | 'left' | 'right';
|
||||
|
||||
export class PopoverElement extends HTMLElement {
|
||||
static get observedAttributes(): string[] {
|
||||
return ['id', 'open-on-hover', 'delay', 'close-delay', 'side', 'side-offset'];
|
||||
}
|
||||
|
||||
#open = false;
|
||||
#transitionStatus: 'initial' | 'open' | 'close' | 'unmounted' = 'initial';
|
||||
#hoverTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
#cleanup: (() => void) | null = null;
|
||||
#abortController: AbortController | null = null;
|
||||
#floatingContext: FloatingContext | null = null;
|
||||
|
||||
attributeChangedCallback(name: string, _oldValue: string, newValue: string): void {
|
||||
if (name === 'id') {
|
||||
this.style.setProperty('position-anchor', `--${newValue}`);
|
||||
}
|
||||
|
||||
this.style.setProperty('top', `calc(anchor(${this.side}) - ${this.sideOffset}px)`);
|
||||
this.style.setProperty('translate', `0 -100%`);
|
||||
this.style.setProperty('justify-self', 'anchor-center');
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.#abortController ??= new AbortController();
|
||||
@@ -71,10 +72,6 @@ export class PopoverElement extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
return ['open-on-hover', 'delay', 'close-delay', 'side', 'side-offset'];
|
||||
}
|
||||
|
||||
get openOnHover(): boolean {
|
||||
return this.hasAttribute('open-on-hover');
|
||||
}
|
||||
@@ -105,8 +102,6 @@ export class PopoverElement extends HTMLElement {
|
||||
this.#open = open;
|
||||
|
||||
if (open) {
|
||||
this.#setupFloating();
|
||||
|
||||
this.#transitionStatus = 'initial';
|
||||
this.#updateVisibility();
|
||||
|
||||
@@ -141,38 +136,6 @@ export class PopoverElement extends HTMLElement {
|
||||
this.toggleAttribute('data-closed', this.#transitionStatus === 'close' || this.#transitionStatus === 'unmounted');
|
||||
}
|
||||
|
||||
#setupFloating(): void {
|
||||
const trigger = this.#triggerElement as HTMLElement;
|
||||
if (!trigger) return;
|
||||
|
||||
const placement = this.side ?? 'top';
|
||||
const sideOffset = this.sideOffset;
|
||||
|
||||
const updatePosition = () => {
|
||||
computePosition(trigger, this, {
|
||||
placement,
|
||||
middleware: [offset(sideOffset), flip(), shift()],
|
||||
strategy: 'fixed',
|
||||
}).then((data: ComputePositionReturn) => {
|
||||
this.#floatingContext = {
|
||||
...data,
|
||||
elements: {
|
||||
domReference: trigger,
|
||||
floating: this,
|
||||
},
|
||||
};
|
||||
|
||||
Object.assign(this.style, {
|
||||
left: `${data.x}px`,
|
||||
top: `${data.y}px`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
this.#cleanup = autoUpdate(trigger, this, updatePosition);
|
||||
}
|
||||
|
||||
#clearHoverTimeout(): void {
|
||||
if (this.#hoverTimeout) {
|
||||
globalThis.clearTimeout(this.#hoverTimeout);
|
||||
@@ -210,10 +173,14 @@ export class PopoverElement extends HTMLElement {
|
||||
}
|
||||
|
||||
#handlePointerMove(event: PointerEvent): void {
|
||||
if (!this.openOnHover || !this.#floatingContext) return;
|
||||
if (!this.openOnHover || !this.#triggerElement) return;
|
||||
|
||||
const close = safePolygon({ blockPointerEvents: true })({
|
||||
...this.#floatingContext,
|
||||
placement: this.side,
|
||||
elements: {
|
||||
domReference: this.#triggerElement,
|
||||
floating: this,
|
||||
},
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
onClose: () => {
|
||||
|
||||
@@ -50,7 +50,7 @@ export const getTimeSliderRootProps: PropsHook<{
|
||||
};
|
||||
|
||||
export class TimeSliderRoot extends HTMLElement {
|
||||
static readonly observedAttributes: readonly string[] = ['orientation'];
|
||||
static readonly observedAttributes: readonly string[] = ['commandfor', 'orientation'];
|
||||
|
||||
_state: TimeSliderRootState | undefined;
|
||||
_core: CoreTimeSlider | null = null;
|
||||
@@ -67,9 +67,11 @@ export class TimeSliderRoot extends HTMLElement {
|
||||
return (this.getAttribute('orientation') as 'horizontal' | 'vertical') || 'horizontal';
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _oldValue: string | null, _newValue: string | null): void {
|
||||
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void {
|
||||
if (name === 'orientation' && this._state) {
|
||||
this._render(getTimeSliderRootProps(this._state, this), this._state);
|
||||
} else if (name === 'commandfor') {
|
||||
this.style.setProperty('anchor-name', `--${newValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,46 @@
|
||||
import type { Placement } from '@floating-ui/dom';
|
||||
import type { MediaContainerElement } from '@/media/media-container';
|
||||
|
||||
import { arrow, autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
|
||||
import { getDocumentOrShadowRoot } from '@videojs/utils/dom';
|
||||
import {
|
||||
getBoundingClientRectWithoutTransform,
|
||||
getDocumentOrShadowRoot,
|
||||
getInBoundsAdjustments,
|
||||
} from '@videojs/utils/dom';
|
||||
|
||||
type Placement = 'top' | 'bottom' | 'left' | 'right';
|
||||
|
||||
export class TooltipElement extends HTMLElement {
|
||||
static get observedAttributes(): string[] {
|
||||
return ['id', 'delay', 'close-delay', 'track-cursor-axis', 'side', 'side-offset', 'collision-padding'];
|
||||
}
|
||||
|
||||
#open = false;
|
||||
#hoverTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
#cleanup: (() => void) | null = null;
|
||||
#arrowElement: HTMLElement | null = null;
|
||||
#pointerPosition = { x: 0, y: 0 };
|
||||
#transitionStatus: 'initial' | 'open' | 'close' | 'unmounted' = 'initial';
|
||||
#abortController: AbortController | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => this.#checkCollision());
|
||||
resizeObserver.observe(this);
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _oldValue: string, newValue: string): void {
|
||||
if (name === 'id') {
|
||||
this.style.setProperty('position-anchor', `--${newValue}`);
|
||||
}
|
||||
|
||||
this.style.setProperty('top', `calc(anchor(${this.side}) - ${this.sideOffset}px)`);
|
||||
|
||||
if (this.trackCursorAxis) {
|
||||
this.style.setProperty('translate', `-50% -100%`);
|
||||
} else {
|
||||
this.style.setProperty('translate', `0 -100%`);
|
||||
this.style.setProperty('justify-self', 'anchor-center');
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.setAttribute('role', 'tooltip');
|
||||
|
||||
@@ -33,7 +61,6 @@ export class TooltipElement extends HTMLElement {
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.#clearHoverTimeout();
|
||||
this.#cleanup?.();
|
||||
this.#abortController?.abort();
|
||||
this.#abortController = null;
|
||||
|
||||
@@ -57,10 +84,6 @@ export class TooltipElement extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
return ['delay', 'close-delay', 'track-cursor-axis', 'side', 'side-offset', 'collision-padding'];
|
||||
}
|
||||
|
||||
get delay(): number {
|
||||
return Number.parseInt(this.getAttribute('delay') ?? '0', 10);
|
||||
}
|
||||
@@ -96,8 +119,6 @@ export class TooltipElement extends HTMLElement {
|
||||
this.#open = open;
|
||||
|
||||
if (open) {
|
||||
this.#setupFloating();
|
||||
|
||||
this.#transitionStatus = 'initial';
|
||||
this.#updateVisibility();
|
||||
|
||||
@@ -106,6 +127,7 @@ export class TooltipElement extends HTMLElement {
|
||||
requestAnimationFrame(() => {
|
||||
this.#transitionStatus = 'open';
|
||||
this.#updateVisibility();
|
||||
this.#checkCollision();
|
||||
});
|
||||
} else {
|
||||
this.#transitionStatus = 'close';
|
||||
@@ -119,16 +141,16 @@ export class TooltipElement extends HTMLElement {
|
||||
} else {
|
||||
this.hidePopover();
|
||||
}
|
||||
|
||||
this.#cleanup?.();
|
||||
this.#cleanup = null;
|
||||
}
|
||||
}
|
||||
|
||||
#updateVisibility(): void {
|
||||
this.toggleAttribute('data-starting-style', this.#transitionStatus === 'initial');
|
||||
this.toggleAttribute('data-open', this.#transitionStatus === 'initial' || this.#transitionStatus === 'open');
|
||||
this.toggleAttribute('data-ending-style', this.#transitionStatus === 'close' || this.#transitionStatus === 'unmounted');
|
||||
this.toggleAttribute(
|
||||
'data-ending-style',
|
||||
this.#transitionStatus === 'close' || this.#transitionStatus === 'unmounted',
|
||||
);
|
||||
this.toggleAttribute('data-closed', this.#transitionStatus === 'close' || this.#transitionStatus === 'unmounted');
|
||||
|
||||
const triggerElement = this.#triggerElement as HTMLElement;
|
||||
@@ -137,105 +159,34 @@ export class TooltipElement extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
#setupFloating(): void {
|
||||
const trigger = this.#triggerElement as HTMLElement;
|
||||
if (!trigger) return;
|
||||
|
||||
const placement = this.side;
|
||||
const sideOffset = this.sideOffset;
|
||||
const collisionPadding = this.collisionPadding;
|
||||
const mediaContainer = this.closest('media-container') as MediaContainerElement;
|
||||
|
||||
this.#arrowElement = this.querySelector('media-tooltip-arrow') as HTMLElement;
|
||||
|
||||
const updatePosition = () => {
|
||||
const middleware = [
|
||||
offset(sideOffset),
|
||||
flip(),
|
||||
shift({
|
||||
boundary: mediaContainer,
|
||||
padding: collisionPadding,
|
||||
}),
|
||||
];
|
||||
|
||||
if (this.#arrowElement) {
|
||||
middleware.push(arrow({ element: this.#arrowElement }));
|
||||
}
|
||||
|
||||
const referenceElement = this.trackCursorAxis
|
||||
? {
|
||||
getBoundingClientRect: () => {
|
||||
const triggerRect = trigger.getBoundingClientRect();
|
||||
|
||||
if (this.trackCursorAxis === 'x') {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: triggerRect.top,
|
||||
right: this.#pointerPosition.x,
|
||||
bottom: triggerRect.bottom,
|
||||
left: this.#pointerPosition.x,
|
||||
x: this.#pointerPosition.x,
|
||||
y: triggerRect.top,
|
||||
};
|
||||
} else if (this.trackCursorAxis === 'y') {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: this.#pointerPosition.y,
|
||||
right: triggerRect.right,
|
||||
bottom: this.#pointerPosition.y,
|
||||
left: triggerRect.left,
|
||||
x: triggerRect.left,
|
||||
y: this.#pointerPosition.y,
|
||||
};
|
||||
} else {
|
||||
// Track both axes (trackCursorAxis === 'both')
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: this.#pointerPosition.y,
|
||||
right: this.#pointerPosition.x,
|
||||
bottom: this.#pointerPosition.y,
|
||||
left: this.#pointerPosition.x,
|
||||
x: this.#pointerPosition.x,
|
||||
y: this.#pointerPosition.y,
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
: trigger;
|
||||
|
||||
computePosition(referenceElement, this, {
|
||||
placement,
|
||||
middleware,
|
||||
strategy: 'fixed',
|
||||
}).then(({ x, y, middlewareData }: { x: number; y: number; middlewareData: any }) => {
|
||||
Object.assign(this.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
|
||||
if (this.#arrowElement && middlewareData.arrow) {
|
||||
const { x: arrowX, y: arrowY } = middlewareData.arrow;
|
||||
Object.assign(this.#arrowElement.style, {
|
||||
left: arrowX != null ? `${arrowX}px` : undefined,
|
||||
top: arrowY != null ? `${arrowY}px` : undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
|
||||
if (!this.trackCursorAxis) {
|
||||
this.#cleanup = autoUpdate(trigger, this, updatePosition);
|
||||
#updatePosition(): void {
|
||||
if (this.#open && this.trackCursorAxis) {
|
||||
this.style.setProperty('left', `${this.#pointerPosition.x}px`);
|
||||
this.#checkCollision();
|
||||
}
|
||||
}
|
||||
|
||||
#updatePosition(): void {
|
||||
if (this.#open && this.trackCursorAxis) {
|
||||
this.#setupFloating();
|
||||
#checkCollision(): void {
|
||||
const mediaContainer = this.closest('media-container') as MediaContainerElement;
|
||||
if (!mediaContainer || !this.#open) return;
|
||||
|
||||
const popupRect = getBoundingClientRectWithoutTransform(this);
|
||||
const boundsRect = getBoundingClientRectWithoutTransform(mediaContainer);
|
||||
const { x } = getInBoundsAdjustments(popupRect, boundsRect, this.collisionPadding);
|
||||
|
||||
if (x !== 0) {
|
||||
if (this.trackCursorAxis) {
|
||||
const currentLeft = Number.parseFloat(this.style.left || '0');
|
||||
this.style.setProperty('left', `${currentLeft + x}px`);
|
||||
} else {
|
||||
this.style.setProperty('translate', `${x}px -100%`);
|
||||
}
|
||||
} else {
|
||||
if (this.trackCursorAxis) {
|
||||
this.style.setProperty('translate', '-50% -100%');
|
||||
} else {
|
||||
this.style.setProperty('translate', '0 -100%');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ export class VolumeSliderTrack extends HTMLElement {
|
||||
/**
|
||||
* VolumeSlider Progress component - Shows current progress
|
||||
*/
|
||||
export class VolumeSliderIndicatorElement extends HTMLElement {
|
||||
export class VolumeSliderIndicator extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.style.position = 'absolute';
|
||||
@@ -255,8 +255,8 @@ export const VolumeSliderTrackElement: ConnectedComponentConstructor<any> = toCo
|
||||
/**
|
||||
* Connected VolumeSlider Progress component
|
||||
*/
|
||||
export const VolumeSliderProgressElement: ConnectedComponentConstructor<any> = toConnectedHTMLComponent(
|
||||
VolumeSliderIndicatorElement,
|
||||
export const VolumeSliderIndicatorElement: ConnectedComponentConstructor<any> = toConnectedHTMLComponent(
|
||||
VolumeSliderIndicator,
|
||||
{ keys: [], transform: () => ({}) },
|
||||
getVolumeSliderProgressProps,
|
||||
'VolumeSliderProgress',
|
||||
@@ -280,12 +280,12 @@ export const VolumeSliderElement = Object.assign(
|
||||
{
|
||||
Root: VolumeSliderRootElement,
|
||||
Track: VolumeSliderTrackElement,
|
||||
Progress: VolumeSliderProgressElement,
|
||||
Indicator: VolumeSliderIndicatorElement,
|
||||
Thumb: VolumeSliderThumbElement,
|
||||
},
|
||||
) as {
|
||||
Root: typeof VolumeSliderRootElement;
|
||||
Track: typeof VolumeSliderTrackElement;
|
||||
Progress: typeof VolumeSliderProgressElement;
|
||||
Indicator: typeof VolumeSliderIndicatorElement;
|
||||
Thumb: typeof VolumeSliderThumbElement;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { MediaSkinElement } from '@/media/media-skin';
|
||||
import { defineCustomElement } from '@/utils/custom-element';
|
||||
import styles from './styles.css';
|
||||
import '@/define/media-container';
|
||||
// be sure to import video-provider first for proper context initialization
|
||||
import '@/define/video-provider';
|
||||
import '@/define/media-container';
|
||||
import '@/define/media-play-button';
|
||||
import '@/define/media-mute-button';
|
||||
import '@/define/media-volume-slider';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { MediaSkinElement } from '@/media/media-skin';
|
||||
import { defineCustomElement } from '@/utils/custom-element';
|
||||
import styles from './styles.css';
|
||||
import '@/define/media-container';
|
||||
// be sure to import video-provider first for proper context initialization
|
||||
import '@/define/video-provider';
|
||||
import '@/define/media-container';
|
||||
import '@/define/media-play-button';
|
||||
import '@/define/media-mute-button';
|
||||
import '@/define/media-volume-slider';
|
||||
|
||||
Reference in New Issue
Block a user