- Preview
- Code
ProgressBar.js
import { newEvent } from "@/lib/newEvent";
import { BwPlayerConsumer } from "@/registry/html/PlayerProvider";
export class BwProgressBar extends BwPlayerConsumer {
_background = null;
_fill = null;
_listener = null;
_scrubbing = false;
constructor() {
super();
// A custom element's constructor must not touch the host (no attributes,
// styles, or children) — only build detached nodes and bind handlers here.
this._background = document.createElement("div");
this._background.style.position = "absolute";
this._background.style.inset = "0";
this._background.style.cursor = "pointer";
this._background.style.backgroundColor = "rgba(0, 0, 0, 0.8)";
this._background.style.opacity = "0.15";
this._fill = document.createElement("div");
this._fill.style.height = "0.5rem";
this._fill.style.width = "0%";
this._fill.style.borderRadius = "0.25rem";
this._fill.style.backgroundColor = "rgba(0, 0, 0, 0.8)";
this._fill.style.pointerEvents = "none";
this._onPress = this._onPress.bind(this);
this._onMove = this._onMove.bind(this);
this._onRelease = this._onRelease.bind(this);
}
connectedCallback() {
super.connectedCallback();
this.style.position = "relative";
this.style.display = "block";
this.style.isolation = "isolate";
this.style.height = "0.5rem";
this.style.flexGrow = "1";
this.style.overflow = "hidden";
this.style.borderRadius = "0.25rem";
this.style.cursor = "pointer";
this.appendChild(this._background);
this.appendChild(this._fill);
this.addEventListener("mousedown", this._onPress);
this.addEventListener("touchstart", this._onPress);
// Track drags on the window so scrubbing continues outside the bar.
window.addEventListener("mousemove", this._onMove);
window.addEventListener("mouseup", this._onRelease);
window.addEventListener("touchmove", this._onMove);
window.addEventListener("touchend", this._onRelease);
this._listener = this._player?.addEventListener("<any>", () => this._update());
this._update();
}
_ratio(event) {
const clientX = event.clientX ?? event.touches?.[0]?.clientX ?? 0;
const { x, width } = this.getBoundingClientRect();
return Math.max(0, Math.min(1, (clientX - x) / width));
}
_onPress(event) {
if (!this._player) return;
this._scrubbing = true;
this._player.onEvent(
newEvent({
type: "PressedProgressBar",
description: "The progress bar was pressed at some ratio.",
initiatedBy: "user",
ratio: this._ratio(event),
}),
);
}
_onMove(event) {
if (!this._scrubbing || !this._player) return;
this._player.onEvent(
newEvent({
type: "ScrubbedProgressBar",
description: "The user pressed on the progress bar then dragged.",
initiatedBy: "user",
ratio: this._ratio(event),
}),
);
}
_onRelease() {
if (!this._scrubbing || !this._player) return;
this._scrubbing = false;
this._player.onEvent(
newEvent({
type: "FinishedScrubbingProgressBar",
description: "The user let go after scrubbing the progress bar.",
initiatedBy: "user",
}),
);
}
_update() {
const currentTime = this._player?.currentTime ?? 0;
const duration = this._player?.duration ?? 0;
const progress = duration > 0 ? Math.max(0, Math.min(currentTime / duration, 1)) : 0;
this._fill.style.width = `${progress * 100}%`;
}
disconnectedCallback() {
this.removeEventListener("mousedown", this._onPress);
this.removeEventListener("touchstart", this._onPress);
window.removeEventListener("mousemove", this._onMove);
window.removeEventListener("mouseup", this._onRelease);
window.removeEventListener("touchmove", this._onMove);
window.removeEventListener("touchend", this._onRelease);
this._player?.removeEventListener("<any>", this._listener);
this._listener = null;
this._scrubbing = false;
super.disconnectedCallback();
}
}
customElements.define("bw-progress-bar", BwProgressBar);
Installation
npx @beyondwords/cli components add progress-bar --html
pnpm dlx @beyondwords/cli components add progress-bar --html
yarn dlx @beyondwords/cli components add progress-bar --html
bunx --bun @beyondwords/cli components add progress-bar --html
Usage
Import the modules (each callscustomElements.define), then use the elements. Nest the bar inside a <bw-player-provider>.
<bw-player-provider
data-project-id="9504"
data-content-id="ba00ef51-03ac-4a85-8b98-e45c957e8ef8"
data-show-user-interface="false"
>
<bw-progress-bar></bw-progress-bar>
</bw-player-provider>
<script type="module">
import "@/registry/html/PlayerProvider";
import "@/registry/html/ProgressBar";
</script>
<bw-progress-bar> finds its nearest
<bw-player-provider> ancestor. The bar fills to
currentTime / duration. Pressing and dragging emits a sequence of events
built with newEvent. Each one carries the pressed
ratio (0–1):
PressedProgressBar: on press, to seek to a position.ScrubbedProgressBar: while dragging.FinishedScrubbingProgressBar: on release.
window (mouse and touch), so scrubbing keeps
working even when the pointer leaves the bar. The element grows to fill its
container (flex-grow: 1). Place it in a flex row alongside the other
controls.
These components expect the BeyondWords player to be available as a global
BeyondWords (the player UMD script). Load it before the provider connects.Reference
| Attributes | None |
| Player state read | currentTime, duration |
| Events emitted | PressedProgressBar, ScrubbedProgressBar, FinishedScrubbingProgressBar (each carries a ratio, 0–1) |
| Requires | a <bw-player-provider> ancestor and the player UMD global loaded |