mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: add media-popover, cleanup html demo (#34)
This commit is contained in:
@@ -2,11 +2,11 @@ import { defineVjsPlayer } from '@vjs-10/html';
|
||||
|
||||
defineVjsPlayer();
|
||||
|
||||
document.body.innerHTML = `
|
||||
document.body.innerHTML = /* html */ `
|
||||
<media-provider>
|
||||
<div style="width: 100%; max-width: 800px; margin: 0 auto;">
|
||||
<media-skin-default>
|
||||
<video slot="media" src="https://stream.mux.com/a4nOgmxGWg6gULfcBbAa00gXyfcwPnAFldF8RdsNyk8M/high.mp4" muted></video>
|
||||
<video slot="media" src="https://stream.mux.com/A3VXy02VoUinw01pwyomEO3bHnG4P32xzV7u1j1FSzjNg/high.mp4" muted></video>
|
||||
</media-skin-default>
|
||||
</div>
|
||||
</media-skin-default>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@open-wc/context-protocol": "^0.0.9",
|
||||
"@vjs-10/core": "workspace:*",
|
||||
"@vjs-10/html-icons": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import type { Placement } from '@floating-ui/dom';
|
||||
|
||||
import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
|
||||
|
||||
export class MediaPopoverRoot extends HTMLElement {
|
||||
#open = false;
|
||||
#hoverTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
#cleanup: (() => void) | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener('mouseenter', this.#handleMouseEnter.bind(this));
|
||||
this.addEventListener('mouseleave', this.#handleMouseLeave.bind(this));
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.#updateVisibility();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.#clearHoverTimeout();
|
||||
this.#cleanup?.();
|
||||
}
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
return ['openonhover', 'delay', 'closedelay'];
|
||||
}
|
||||
|
||||
get openOnHover(): boolean {
|
||||
return this.hasAttribute('openonhover');
|
||||
}
|
||||
|
||||
get delay(): number {
|
||||
return parseInt(this.getAttribute('delay') ?? '0', 10);
|
||||
}
|
||||
|
||||
get closeDelay(): number {
|
||||
return parseInt(this.getAttribute('closedelay') ?? '0', 10);
|
||||
}
|
||||
|
||||
get #triggerElement(): MediaPopoverTrigger | null {
|
||||
return this.querySelector('media-popover-trigger') as MediaPopoverTrigger | null;
|
||||
}
|
||||
|
||||
get #positionerElement(): MediaPopoverPositioner | null {
|
||||
return this.querySelector('media-popover-positioner') as MediaPopoverPositioner | null;
|
||||
}
|
||||
|
||||
get #popupElement(): MediaPopoverPopup | null {
|
||||
return this.querySelector('media-popover-popup') as MediaPopoverPopup | null;
|
||||
}
|
||||
|
||||
#setOpen(open: boolean): void {
|
||||
if (this.#open === open) return;
|
||||
|
||||
this.#open = open;
|
||||
this.#updateVisibility();
|
||||
|
||||
if (open) {
|
||||
this.#setupFloating();
|
||||
} else {
|
||||
this.#cleanup?.();
|
||||
this.#cleanup = null;
|
||||
}
|
||||
}
|
||||
|
||||
#updateVisibility(): void {
|
||||
this.toggleAttribute('data-open', this.#open);
|
||||
this.style.display = 'contents';
|
||||
|
||||
if (this.#popupElement) {
|
||||
this.#popupElement.style.display = this.#open ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
#setupFloating(): void {
|
||||
if (!this.#triggerElement || !this.#popupElement) return;
|
||||
|
||||
const trigger = this.#triggerElement.firstElementChild as HTMLElement;
|
||||
const popup = this.#popupElement;
|
||||
|
||||
if (!trigger || !popup) return;
|
||||
|
||||
const placement = this.#positionerElement?.side ?? 'top';
|
||||
const sideOffset = this.#positionerElement?.sideOffset;
|
||||
|
||||
const updatePosition = () => {
|
||||
computePosition(trigger, popup, {
|
||||
placement,
|
||||
middleware: [offset(sideOffset), flip(), shift()],
|
||||
}).then(({ x, y }: { x: number; y: number }) => {
|
||||
Object.assign(popup.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
this.#cleanup = autoUpdate(trigger, popup, updatePosition);
|
||||
}
|
||||
|
||||
#clearHoverTimeout(): void {
|
||||
if (this.#hoverTimeout) {
|
||||
clearTimeout(this.#hoverTimeout);
|
||||
this.#hoverTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
#handleMouseEnter(): void {
|
||||
if (!this.openOnHover) return;
|
||||
|
||||
this.#clearHoverTimeout();
|
||||
this.#hoverTimeout = globalThis.setTimeout(() => {
|
||||
this.#setOpen(true);
|
||||
}, this.delay);
|
||||
}
|
||||
|
||||
#handleMouseLeave(): void {
|
||||
if (!this.openOnHover) return;
|
||||
|
||||
this.#clearHoverTimeout();
|
||||
this.#hoverTimeout = globalThis.setTimeout(() => {
|
||||
this.#setOpen(false);
|
||||
}, this.closeDelay);
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaPopoverTrigger extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
this.style.display = 'contents';
|
||||
|
||||
const triggerElement = this.firstElementChild as HTMLElement;
|
||||
if (triggerElement) {
|
||||
triggerElement.setAttribute('aria-haspopup', 'true');
|
||||
triggerElement.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaPopoverPositioner extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
this.style.display = 'contents';
|
||||
|
||||
const popup = this.firstElementChild as HTMLElement;
|
||||
if (popup) {
|
||||
Object.assign(popup.style, {
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
zIndex: '1000',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get side(): Placement {
|
||||
return this.getAttribute('side') as Placement;
|
||||
}
|
||||
|
||||
get sideOffset(): number {
|
||||
return parseInt(this.getAttribute('sideoffset') ?? '0', 10);
|
||||
}
|
||||
}
|
||||
|
||||
export class MediaPopoverPopup extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
this.setAttribute('role', 'dialog');
|
||||
this.setAttribute('aria-modal', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-popover-root')) {
|
||||
globalThis.customElements.define('media-popover-root', MediaPopoverRoot);
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-popover-trigger')) {
|
||||
globalThis.customElements.define('media-popover-trigger', MediaPopoverTrigger);
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-popover-positioner')) {
|
||||
globalThis.customElements.define('media-popover-positioner', MediaPopoverPositioner);
|
||||
}
|
||||
|
||||
if (!globalThis.customElements.get('media-popover-popup')) {
|
||||
globalThis.customElements.define('media-popover-popup', MediaPopoverPopup);
|
||||
}
|
||||
|
||||
export const Popover: {
|
||||
Root: typeof MediaPopoverRoot;
|
||||
Trigger: typeof MediaPopoverTrigger;
|
||||
Positioner: typeof MediaPopoverPositioner;
|
||||
Popup: typeof MediaPopoverPopup;
|
||||
} = {
|
||||
Root: MediaPopoverRoot,
|
||||
Trigger: MediaPopoverTrigger,
|
||||
Positioner: MediaPopoverPositioner,
|
||||
Popup: MediaPopoverPopup,
|
||||
};
|
||||
|
||||
export default Popover;
|
||||
@@ -8,6 +8,7 @@ import '../components/media-time-range';
|
||||
import '../components/media-fullscreen-button';
|
||||
import '../components/media-duration-display';
|
||||
import '../components/media-current-time-display';
|
||||
import '../components/media-popover';
|
||||
import '@vjs-10/html-icons';
|
||||
|
||||
export function getTemplateHTML() {
|
||||
@@ -24,6 +25,7 @@ export function getTemplateHTML() {
|
||||
}
|
||||
|
||||
media-container > ::slotted([slot=media]) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -45,7 +47,6 @@ export function getTemplateHTML() {
|
||||
/* Time Display Styling */
|
||||
media-current-time-display,
|
||||
media-duration-display {
|
||||
background: rgb(20 20 30 / .7);
|
||||
padding: 4px 8px;
|
||||
color: rgb(238 238 238);
|
||||
font-family: monospace;
|
||||
@@ -59,28 +60,32 @@ export function getTemplateHTML() {
|
||||
/* Generic Media Button Styling */
|
||||
.button {
|
||||
border: none;
|
||||
background: rgb(20 20 30 / .7);
|
||||
padding: 4px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
color: rgb(238 238 238);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.button .icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Media Play Button UI/Styles */
|
||||
media-play-button:not([data-paused]) .pause-icon,
|
||||
media-play-button[data-paused] .play-icon {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Media Fullscreen Button UI/Styles */
|
||||
media-fullscreen-button:not([data-fullscreen]) .fullscreen-enter-icon,
|
||||
media-fullscreen-button[data-fullscreen] .fullscreen-exit-icon {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* One way to define the "default visible" icon (CJP) */
|
||||
@@ -89,11 +94,12 @@ export function getTemplateHTML() {
|
||||
media-mute-button[data-volume-level=low] .volume-low-icon,
|
||||
media-mute-button[data-volume-level=medium] .volume-low-icon,
|
||||
media-mute-button[data-volume-level=off] .volume-off-icon {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Media Control Bar UI/Styles */
|
||||
.control-bar {
|
||||
background: rgb(20 20 30 / .7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
@@ -170,6 +176,11 @@ export function getTemplateHTML() {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
media-popover-popup {
|
||||
background: rgb(20 20 30 / .7);
|
||||
padding: 14px 0;
|
||||
}
|
||||
|
||||
/* VolumeRange Component Styles */
|
||||
media-volume-range-root {
|
||||
display: flex;
|
||||
@@ -251,17 +262,25 @@ export function getTemplateHTML() {
|
||||
<media-time-range-thumb></media-time-range-thumb>
|
||||
</media-time-range-root>
|
||||
<media-duration-display></media-duration-display>
|
||||
<media-mute-button class="button">
|
||||
<media-volume-high-icon class="icon volume-high-icon"></media-volume-high-icon>
|
||||
<media-volume-low-icon class="icon volume-low-icon"></media-volume-low-icon>
|
||||
<media-volume-off-icon class="icon volume-off-icon"></media-volume-off-icon>
|
||||
</media-mute-button>
|
||||
<media-volume-range-root orientation="vertical">
|
||||
<media-volume-range-track>
|
||||
<media-volume-range-progress></media-volume-range-progress>
|
||||
</media-volume-range-track>
|
||||
<media-volume-range-thumb></media-volume-range-thumb>
|
||||
</media-volume-range-root>
|
||||
<media-popover-root openonhover delay="200" closedelay="100">
|
||||
<media-popover-trigger>
|
||||
<media-mute-button class="button">
|
||||
<media-volume-high-icon class="icon volume-high-icon"></media-volume-high-icon>
|
||||
<media-volume-low-icon class="icon volume-low-icon"></media-volume-low-icon>
|
||||
<media-volume-off-icon class="icon volume-off-icon"></media-volume-off-icon>
|
||||
</media-mute-button>
|
||||
</media-popover-trigger>
|
||||
<media-popover-positioner side="top" sideOffset="0">
|
||||
<media-popover-popup>
|
||||
<media-volume-range-root orientation="vertical">
|
||||
<media-volume-range-track>
|
||||
<media-volume-range-progress></media-volume-range-progress>
|
||||
</media-volume-range-track>
|
||||
<media-volume-range-thumb></media-volume-range-thumb>
|
||||
</media-volume-range-root>
|
||||
</media-popover-popup>
|
||||
</media-popover-positioner>
|
||||
</media-popover-root>
|
||||
<media-fullscreen-button class="button">
|
||||
<media-fullscreen-enter-icon class="icon fullscreen-enter-icon"></media-fullscreen-enter-icon>
|
||||
<media-fullscreen-exit-icon class="icon fullscreen-exit-icon"></media-fullscreen-exit-icon>
|
||||
|
||||
Generated
+3
@@ -173,6 +173,9 @@ importers:
|
||||
|
||||
packages/html/html:
|
||||
dependencies:
|
||||
'@floating-ui/dom':
|
||||
specifier: ^1.6.13
|
||||
version: 1.7.4
|
||||
'@open-wc/context-protocol':
|
||||
specifier: ^0.0.9
|
||||
version: 0.0.9
|
||||
|
||||
Reference in New Issue
Block a user