@webwriter/interactive-video
Version:
(WIP) Enhance learning by adding interactive content in popups to videos for an engaging, interactive experience.
375 lines (333 loc) • 11.5 kB
text/typescript
import { html, css, LitElement, PropertyValues } from "lit";
import { LitElementWw } from "@webwriter/lit";
import { customElement, property, query } from "lit/decorators.js";
import {
SlIconButton,
SlRange,
SlDropdown,
SlMenu,
SlMenuItem,
SlButton,
SlIcon,
} from "@shoelace-style/shoelace";
import "@shoelace-style/shoelace/dist/themes/light.css";
import {
videoContext,
InteractiveVideoContext,
} from "../../utils/interactive-video-context";
import { consume } from "@lit/context";
//Tabler
import playerPlay from "@tabler/icons/filled/player-play.svg";
import list from "@tabler/icons/outline/list.svg";
import volumeDown from "@tabler/icons/outline/volume-2.svg";
import volumeUp from "@tabler/icons/outline/volume.svg";
import volumeMute from "@tabler/icons/outline/volume-3.svg";
import volumeOff from "@tabler/icons/outline/volume-off.svg";
import fullscreenEnter from "@tabler/icons/outline/arrows-maximize.svg";
import fullscreenExit from "@tabler/icons/outline/arrows-minimize.svg";
import brandSpeedtest from "@tabler/icons/outline/brand-speedtest.svg";
import { formatTime, parseTime } from "../../utils/timeFormatter";
import styles from "./video-controls-bar.styles";
export class VideoControlsBar extends LitElementWw {
({ context: videoContext, subscribe: true })
accessor videoContext: InteractiveVideoContext;
({ type: Object }) accessor currentChapter;
/**
* Query for the mute button.
*/
("#mute-volume-button")
accessor muteButton: SlIconButton;
/**
* Query for the volume slider.
*/
("#volume-slider")
accessor volumeSlider;
/**
* Query for the fullscreen button.
*/
("#fullscreen-button")
accessor fullscreenButton: SlIconButton;
/**
* Query for the play button
*/
("#play")
accessor playButton: SlIconButton;
/**
* Query for the videos time stamp.
*/
("#time-stamp")
accessor timeStamp;
/**
* Returns an object that maps custom element names to their corresponding classes.
* These custom elements can be used within the scope of the `webwriter-interactive-video` component.
*
* @returns An object mapping custom element names to their corresponding classes.
*/
static get scopedElements() {
return {
"sl-icon-button": SlIconButton,
"sl-range": SlRange,
"sl-dropdown": SlDropdown,
"sl-menu": SlMenu,
"sl-menu-item": SlMenuItem,
"sl-button": SlButton,
"sl-icon": SlIcon,
};
}
//import CSS
static styles = [styles];
/*
*/
firstUpdated() {}
/**
* Renders the lower controls for the webwriter interactive video widget.
*
* @remarks
* This function renders the lower controls for the interactive video widget, including the play button, time stamp, volume slider, and fullscreen button.
* If the video has chapters, it also renders a chapters button.
* If the content is editable, it renders an add button for adding interactive elements.
*/
render() {
return html` <!-- -->
<div id="controls-lower">
<div id="controls-lower-left">
<sl-icon-button
class="icon-button"
id="play"
@click=${this.handlePlayClick}
src="${playerPlay}"
></sl-icon-button>
<p id="time-stamp">00:00 / 00:00</p>
${this.isContentEditable
? html` <sl-button
id="chapters-button"
@click=${this.toggleChaptersDrawer}
>
<sl-icon
style="height: 20px; width: 20px;"
slot="prefix"
src=${list}
></sl-icon>
${this.renderCurrentChapter()}
</sl-button>`
: this.videoContext.hasChapters
? html` <sl-button
id="chapters-button"
@click=${this.toggleChaptersDrawer}
>
<sl-icon
style="height: 20px; width: 20px;"
slot="prefix"
src=${list}
></sl-icon>
${this.renderCurrentChapter()}
</sl-button>`
: null}
</div>
<!-- contains the volume slider and other controls -->
<div id="controls-lower-right">
<div
style="display: flex; flex-direction: row; gap: 6px; align-items: center; justify-content: center;"
>
<sl-icon-button
class="volume-button"
id="mute-volume-button"
@click=${this.handleMuteClick}
src="${volumeDown}"
>
</sl-icon-button>
<sl-range
id="volume-slider"
style="--thumb-size: 15px; --track-height: 5px;"
@sl-change=${this.handleVolumeChange}
></sl-range>
</div>
<sl-dropdown
placement="top-start"
id="settings-menu"
@sl-select=${this.settingSelectionHandler}
>
<sl-icon-button
class="icon-button"
id="settings-button"
src="${brandSpeedtest}"
slot="trigger"
></sl-icon-button>
<sl-menu>
<sl-menu-item>
Playback Speed
<sl-menu slot="submenu">
<sl-menu-item value="0.25">0.25x</sl-menu-item>
<sl-menu-item value="0.5">0.5x</sl-menu-item>
<sl-menu-item value="1">1x</sl-menu-item>
<sl-menu-item value="1.5">1.5x</sl-menu-item>
<sl-menu-item value="2">2x</sl-menu-item>
</sl-menu>
</sl-menu-item>
</sl-menu>
</sl-dropdown>
<!-- <sl-icon-button
class="icon-button"
id="fullscreen-button"
src="${fullscreenEnter}"
@click=${this.handleFullscreenClick}
></sl-icon-button> -->
</div>
</div>`;
}
/**
* Handles the click event when the play button is clicked.
*
* @param e - The custom event object.
*/
handlePlayClick = (e: CustomEvent) => {
this.dispatchEvent(
new CustomEvent("startstopVideo", {
bubbles: true,
composed: true,
})
);
};
/**
* Toggles the chapters drawer open or closed.
*/
toggleChaptersDrawer() {
this.dispatchEvent(
new CustomEvent("toggleChaptersDrawer", {
bubbles: true,
composed: true,
})
);
}
/**
* Handles the change event for the volume slider and sets the video volume and button icon accordingly.
*
* @param e - The custom event object.
*/
handleVolumeChange = (e: CustomEvent) => {
const volumeSlider = e.target as SlRange;
this.volumeButtonIconHelper();
this.dispatchEvent(
new CustomEvent("volumeChange", {
detail: { value: volumeSlider.value },
bubbles: true,
composed: true,
})
);
};
/**
* Updates the volume button icon based on the current state of the video and volume slider.
* If the video is muted, no changes are made to the icon.
* If the volume slider value is 0, the mute button icon is set to `volumeOff`.
* If the volume slider value is less than 50, the mute button icon is set to `volumeDown`.
* If the volume slider value is 50 or greater, the mute button icon is set to `volumeUp`.
*/
volumeButtonIconHelper() {
if (this.volumeSlider.value === 0)
this.muteButton.setAttribute("src", `${volumeOff}`);
else {
this.volumeSlider.value < 50
? this.muteButton.setAttribute("src", `${volumeDown}`)
: this.muteButton.setAttribute("src", `${volumeUp}`);
}
}
/**
* Handles the click event for the mute button.
*
* @param e - The custom event object.
*/
handleMuteClick = (e: CustomEvent) => {
if (!this.videoContext.videoLoaded) return;
this.dispatchEvent(
new CustomEvent("toggleMute", {
bubbles: true,
composed: true,
})
);
if (this.muteButton.src === volumeMute) {
this.volumeButtonIconHelper();
} else {
this.muteButton.setAttribute("src", `${volumeMute}`);
}
};
/**
* Renders the current chapter of the interactive video.
*
* @returns The HTML representation of the current chapter, or an empty string if there is no current chapter.
*/
renderCurrentChapter() {
this.dispatchEvent(
new CustomEvent("getCurrentChapter", {
bubbles: true,
composed: true,
})
);
return this.currentChapter
? html`<p style="margin: 0px; padding: 0px;">
${this.currentChapter.title}
</p>`
: "";
}
/**
* Handles the click event for the fullscreen button.
* If the document is currently in fullscreen mode, it exits fullscreen and updates the fullscreen button icon.
* If the document is not in fullscreen mode, it enters fullscreen, updates the fullscreen button icon,
* The controls should be sticky in fullscreen mode, i.e. stick to the lower part of the screen if the video is not fully in view. I didnt get around to doing this.
*/
handleFullscreenClick = () => {
if (document.fullscreenElement) {
this.fullscreenButton.setAttribute("src", `${fullscreenEnter}`);
document.exitFullscreen();
this.removeEventListener("resize", this.handleFullscreenResize);
} else {
this.fullscreenButton.setAttribute("src", `${fullscreenExit}`);
this.addEventListener("resize", this.handleFullscreenResize);
this.requestFullscreen();
// MARK: todo
if (!this.checkControlsVisible()) {
this.makeControlsSticky();
}
}
};
/**
* Event handler for selection of playback speeds from the setting menu.
* @param {CustomEvent} e - The custom event object.
*/
settingSelectionHandler = (e: CustomEvent) => {
if (!this.videoContext.videoLoaded) return;
this.dispatchEvent(
new CustomEvent("playbackRateChange", {
detail: { value: e.detail.item.value },
bubbles: true,
composed: true,
})
);
};
//TODO: implement
makeControlsSticky() {
const e = new Error("Not implemented");
}
/**
* Checks if the controls are visible based on the height of the window and the offset height of the element.
* @returns {boolean} Returns true if the controls are visible, false otherwise.
*/
checkControlsVisible(): Boolean {
if (window.innerHeight < this.offsetHeight) {
return false;
}
return true;
}
/**
* Handles the resize event when the video player enters or exits fullscreen mode.
* If the controls are not visible, makes the controls sticky.
*/
handleFullscreenResize() {
if (!this.checkControlsVisible) {
this.makeControlsSticky();
}
}
handleTimeUpdate(lastTimeupdate, videoDurationFormatted) {
this.timeStamp.innerHTML =
formatTime(lastTimeupdate) + " / " + videoDurationFormatted;
}
}