@7sage/vidstack
Version:
UI component library for building high-quality, accessible video and audio experiences on the web.
318 lines (307 loc) • 11.3 kB
JavaScript
import { computed, signal, effect, camelToKebabCase, Host } from '../../chunks/vidstack-Bu2kfzUd.js';
import { DefaultLayout, useDefaultLayoutContext, DefaultPlayButton, DefaultAnnouncer, DefaultCaptions, DefaultAirPlayButton, DefaultGoogleCastButton, DefaultControlsSpacer, DefaultCaptionButton, DefaultDownloadButton, DefaultVolumePopup, DefaultTimeInfo, DefaultTitle, DefaultFullscreenButton, DefaultTimeSlider, DefaultPIPButton, DefaultChaptersMenu, DefaultSettingsMenu, setLayoutName, createMenuContainer, DefaultLayoutIconsLoader } from '../../chunks/vidstack-DBy9lvwr.js';
import { useMediaContext, useMediaState } from '../../chunks/vidstack-DFImIcIL.js';
import { $signal, SlotManager } from '../../chunks/vidstack-Bcmx8pmK.js';
import { LitElement } from '../../chunks/vidstack-CwTj4H1w.js';
import { html } from 'lit-html';
import { keyed } from 'lit-html/directives/keyed.js';
import { createSlot } from '../../chunks/vidstack-C_l97D5j.js';
import '../../chunks/vidstack-28cU2iGK.js';
import '../../chunks/vidstack-BTM4ERc7.js';
import 'lit-html/directives/if-defined.js';
import 'lit-html/directives/ref.js';
import '../../chunks/vidstack-zG6PIeGg.js';
import '../../chunks/vidstack-CjhKISI0.js';
import 'lit-html/directives/unsafe-svg.js';
import 'lit-html/async-directive.js';
import '@floating-ui/dom';
class DefaultVideoLayout extends DefaultLayout {
static props = {
...super.props,
when: ({ viewType }) => viewType === "video",
smallWhen: ({ width, height }) => width < 576 || height < 380
};
}
function DefaultKeyboardDisplay() {
return $signal(() => {
const media = useMediaContext(), { noKeyboardAnimations, userPrefersKeyboardAnimations } = useDefaultLayoutContext(), $disabled = computed(() => noKeyboardAnimations() || !userPrefersKeyboardAnimations());
if ($disabled()) {
return null;
}
const visible = signal(false), { lastKeyboardAction } = media.$state;
effect(() => {
visible.set(!!lastKeyboardAction());
const id = setTimeout(() => visible.set(false), 500);
return () => {
visible.set(false);
window.clearTimeout(id);
};
});
const $actionDataAttr = computed(() => {
const action = lastKeyboardAction()?.action;
return action && visible() ? camelToKebabCase(action) : null;
});
const $classList = computed(() => `vds-kb-action${!visible() ? " hidden" : ""}`), $text = computed(getText), $iconSlot = computed(() => {
const name = getIconName();
return name ? createSlot(name) : null;
});
function Icon() {
const $slot = $iconSlot();
if (!$slot) return null;
return html`
<div class="vds-kb-bezel">
<div class="vds-kb-icon">${$slot}</div>
</div>
`;
}
return html`
<div class=${$signal($classList)} data-action=${$signal($actionDataAttr)}>
<div class="vds-kb-text-wrapper">
<div class="vds-kb-text">${$signal($text)}</div>
</div>
${$signal(() => keyed(lastKeyboardAction(), Icon()))}
</div>
`;
});
}
function getText() {
const { $state } = useMediaContext(), action = $state.lastKeyboardAction()?.action, audioGain = $state.audioGain() ?? 1;
switch (action) {
case "toggleMuted":
return $state.muted() ? "0%" : getVolumeText($state.volume(), audioGain);
case "volumeUp":
case "volumeDown":
return getVolumeText($state.volume(), audioGain);
default:
return "";
}
}
function getVolumeText(volume, gain) {
return `${Math.round(volume * gain * 100)}%`;
}
function getIconName() {
const { $state } = useMediaContext(), action = $state.lastKeyboardAction()?.action;
switch (action) {
case "togglePaused":
return !$state.paused() ? "kb-play-icon" : "kb-pause-icon";
case "toggleMuted":
return $state.muted() || $state.volume() === 0 ? "kb-mute-icon" : $state.volume() >= 0.5 ? "kb-volume-up-icon" : "kb-volume-down-icon";
case "toggleFullscreen":
return `kb-fs-${$state.fullscreen() ? "enter" : "exit"}-icon`;
case "togglePictureInPicture":
return `kb-pip-${$state.pictureInPicture() ? "enter" : "exit"}-icon`;
case "toggleCaptions":
return $state.hasCaptions() ? `kb-cc-${$state.textTrack() ? "on" : "off"}-icon` : null;
case "volumeUp":
return "kb-volume-up-icon";
case "volumeDown":
return "kb-volume-down-icon";
case "seekForward":
return "kb-seek-forward-icon";
case "seekBackward":
return "kb-seek-backward-icon";
default:
return null;
}
}
function DefaultVideoLayoutLarge() {
return [
DefaultAnnouncer(),
DefaultVideoGestures(),
DefaultBufferingIndicator(),
DefaultKeyboardDisplay(),
DefaultCaptions(),
html`<div class="vds-scrim"></div>`,
html`
<media-controls class="vds-controls">
${[
DefaultControlsGroupTop(),
DefaultControlsSpacer(),
html`<media-controls-group class="vds-controls-group"></media-controls-group>`,
DefaultControlsSpacer(),
html`
<media-controls-group class="vds-controls-group">
${DefaultTimeSlider()}
</media-controls-group>
`,
html`
<media-controls-group class="vds-controls-group">
${[
DefaultPlayButton({ tooltip: "top start" }),
DefaultVolumePopup({ orientation: "horizontal", tooltip: "top" }),
DefaultTimeInfo(),
DefaultTitle(),
DefaultCaptionButton({ tooltip: "top" }),
DefaultBottomMenuGroup(),
DefaultAirPlayButton({ tooltip: "top" }),
DefaultGoogleCastButton({ tooltip: "top" }),
DefaultDownloadButton(),
DefaultPIPButton(),
DefaultFullscreenButton({ tooltip: "top end" })
]}
</media-controls-group>
`
]}
</media-controls>
`
];
}
function DefaultBottomMenuGroup() {
return $signal(() => {
const { menuGroup } = useDefaultLayoutContext();
return menuGroup() === "bottom" ? DefaultVideoMenus() : null;
});
}
function DefaultControlsGroupTop() {
return html`
<media-controls-group class="vds-controls-group">
${$signal(() => {
const { menuGroup } = useDefaultLayoutContext();
return menuGroup() === "top" ? [DefaultControlsSpacer(), DefaultVideoMenus()] : null;
})}
</media-controls-group>
`;
}
function DefaultVideoLayoutSmall() {
return [
DefaultAnnouncer(),
DefaultVideoGestures(),
DefaultBufferingIndicator(),
DefaultCaptions(),
DefaultKeyboardDisplay(),
html`<div class="vds-scrim"></div>`,
html`
<media-controls class="vds-controls">
<media-controls-group class="vds-controls-group">
${[
DefaultAirPlayButton({ tooltip: "top start" }),
DefaultGoogleCastButton({ tooltip: "bottom start" }),
DefaultControlsSpacer(),
DefaultCaptionButton({ tooltip: "bottom" }),
DefaultDownloadButton(),
DefaultVideoMenus(),
DefaultVolumePopup({ orientation: "vertical", tooltip: "bottom end" })
]}
</media-controls-group>
${DefaultControlsSpacer()}
<media-controls-group class="vds-controls-group" style="pointer-events: none;">
${[
DefaultControlsSpacer(),
DefaultPlayButton({ tooltip: "top" }),
DefaultControlsSpacer()
]}
</media-controls-group>
${DefaultControlsSpacer()}
<media-controls-group class="vds-controls-group">
${[DefaultTimeInfo(), DefaultTitle(), DefaultFullscreenButton({ tooltip: "top end" })]}
</media-controls-group>
<media-controls-group class="vds-controls-group">
${DefaultTimeSlider()}
</media-controls-group>
</media-controls>
`,
StartDuration()
];
}
function DefaultVideoLoadLayout() {
return html`
<div class="vds-load-container">
${[DefaultBufferingIndicator(), DefaultPlayButton({ tooltip: "top" })]}
</div>
`;
}
function StartDuration() {
return $signal(() => {
const { duration } = useMediaState();
if (duration() === 0) return null;
return html`
<div class="vds-start-duration">
<media-time class="vds-time" type="duration"></media-time>
</div>
`;
});
}
function DefaultBufferingIndicator() {
return html`
<div class="vds-buffering-indicator">
<media-spinner class="vds-buffering-spinner"></media-spinner>
</div>
`;
}
function DefaultVideoMenus() {
const { menuGroup, smallWhen: smWhen } = useDefaultLayoutContext(), $side = () => menuGroup() === "top" || smWhen() ? "bottom" : "top", $tooltip = computed(() => `${$side()} ${menuGroup() === "top" ? "end" : "center"}`), $placement = computed(() => `${$side()} end`);
return [
DefaultChaptersMenu({ tooltip: $tooltip, placement: $placement, portal: true }),
DefaultSettingsMenu({ tooltip: $tooltip, placement: $placement, portal: true })
];
}
function DefaultVideoGestures() {
return $signal(() => {
const { noGestures } = useDefaultLayoutContext();
if (noGestures()) return null;
return html`
<div class="vds-gestures">
<media-gesture class="vds-gesture" event="pointerup" action="toggle:paused"></media-gesture>
<media-gesture
class="vds-gesture"
event="pointerup"
action="toggle:controls"
></media-gesture>
<media-gesture
class="vds-gesture"
event="dblpointerup"
action="toggle:fullscreen"
></media-gesture>
<media-gesture class="vds-gesture" event="dblpointerup" action="seek:-10"></media-gesture>
<media-gesture class="vds-gesture" event="dblpointerup" action="seek:10"></media-gesture>
</div>
`;
});
}
class MediaVideoLayoutElement extends Host(LitElement, DefaultVideoLayout) {
static tagName = "media-video-layout";
static attrs = {
smallWhen: {
converter(value) {
return value !== "never" && !!value;
}
}
};
#media;
onSetup() {
this.forwardKeepAlive = false;
this.#media = useMediaContext();
this.classList.add("vds-video-layout");
}
onConnect() {
setLayoutName("video", () => this.isMatch);
this.#setupMenuContainer();
}
render() {
return $signal(this.#render.bind(this));
}
#setupMenuContainer() {
const { menuPortal } = useDefaultLayoutContext();
effect(() => {
if (!this.isMatch) return;
const container = createMenuContainer(
this,
this.menuContainer,
"vds-video-layout",
() => this.isSmallLayout
), roots = container ? [this, container] : [this];
const iconsManager = this.$props.customIcons() ? new SlotManager(roots) : new DefaultLayoutIconsLoader(roots);
iconsManager.connect();
menuPortal.set(container);
return () => {
container.remove();
menuPortal.set(null);
};
});
}
#render() {
const { load } = this.#media.$props, { canLoad, streamType, nativeControls } = this.#media.$state;
return !nativeControls() && this.isMatch ? load() === "play" && !canLoad() ? DefaultVideoLoadLayout() : streamType() === "unknown" ? DefaultBufferingIndicator() : this.isSmallLayout ? DefaultVideoLayoutSmall() : DefaultVideoLayoutLarge() : null;
}
}
export { MediaVideoLayoutElement };