UNPKG

@vime/core

Version:

Customizable, extensible, accessible and framework agnostic media player.

369 lines (368 loc) 11.8 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { Component, Element, h, Prop, State, Watch } from '@stencil/core'; import { Disposal } from '../../../../utils/Disposal'; import { listen } from '../../../../utils/dom'; import { formatTime } from '../../../../utils/formatters'; import { isUndefined } from '../../../../utils/unit'; import { findPlayer } from '../../../core/player/findPlayer'; import { createDispatcher, } from '../../../core/player/PlayerDispatcher'; import { withComponentRegistry } from '../../../core/player/withComponentRegistry'; import { withPlayerContext } from '../../../core/player/withPlayerContext'; /** * A control that displays the progression of playback and the amount buffered on a horizontal * timeline. The timeline is a slider (`input[type="range"]`) that can be used to change the * current playback time. * * If the player is buffering, the scrubber will display an animated candystripe in the porition * of the timeline that has not buffered. * * ## Visual * * <img * src="https://raw.githubusercontent.com/vime-js/vime/master/packages/core/src/components/ui/controls/scrubber-control/scrubber-control.png" * alt="Vime scrubber control component" * /> */ export class ScrubberControl { constructor() { this.keyboardDisposal = new Disposal(); this.timestamp = ''; this.endTime = 0; /** * Whether the timestamp in the tooltip should show the hours unit, even if the time is less than * 1 hour (eg: `20:35` -> `00:20:35`). */ this.alwaysShowHours = false; /** * Whether the tooltip should not be displayed. */ this.hideTooltip = false; /** @internal */ this.currentTime = 0; /** @internal */ this.duration = -1; /** * Prevents seeking forward/backward by using the Left/Right arrow keys. */ this.noKeyboard = false; /** @internal */ this.buffering = false; /** @internal */ this.buffered = 0; /** @internal */ this.i18n = {}; withComponentRegistry(this); withPlayerContext(this, [ 'i18n', 'currentTime', 'duration', 'buffering', 'buffered', ]); } onNoKeyboardChange() { return __awaiter(this, void 0, void 0, function* () { this.keyboardDisposal.empty(); if (this.noKeyboard) return; const player = yield findPlayer(this); if (isUndefined(player)) return; const onKeyDown = (event) => { if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return; event.preventDefault(); const isLeftArrow = event.key === 'ArrowLeft'; const seekTo = isLeftArrow ? Math.max(0, this.currentTime - 5) : Math.min(this.duration, this.currentTime + 5); this.dispatch('currentTime', seekTo); }; this.keyboardDisposal.add(listen(player, 'keydown', onKeyDown)); }); } onDurationChange() { // Avoid -1. this.endTime = Math.max(0, this.duration); } connectedCallback() { this.dispatch = createDispatcher(this); this.timestamp = formatTime(this.currentTime, this.alwaysShowHours); this.onNoKeyboardChange(); } disconnectedCallback() { this.keyboardDisposal.empty(); } setTooltipPosition(value) { var _a, _b; const tooltipRect = (_b = (_a = this.tooltip.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.tooltip')) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect(); const bounds = this.slider.getBoundingClientRect(); const thumbWidth = parseFloat(window .getComputedStyle(this.slider) .getPropertyValue('--vm-slider-thumb-width')); const leftLimit = tooltipRect.width / 2 - thumbWidth / 2; const rightLimit = bounds.width - tooltipRect.width / 2 - thumbWidth / 2; const xPos = Math.max(leftLimit, Math.min(value, rightLimit)); this.tooltip.style = `--vm-tooltip-left: ${xPos}px`; } onSeek(event) { this.dispatch('currentTime', event.detail); } onSeeking(event) { if (this.duration < 0 || this.tooltip.hidden) return; if (event.type === 'mouseleave') { this.getSliderInput().blur(); this.tooltip.active = false; return; } const rect = this.host.getBoundingClientRect(); const percent = Math.max(0, Math.min(100, (100 / rect.width) * (event.pageX - rect.left))); this.timestamp = formatTime((this.duration / 100) * percent, this.alwaysShowHours); this.setTooltipPosition((percent / 100) * rect.width); if (!this.tooltip.active) { this.getSliderInput().focus(); this.tooltip.active = true; } } getSliderInput() { var _a; return (_a = this.slider.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('input'); } render() { const sliderValueText = this.i18n.scrubberLabel .replace(/{currentTime}/, formatTime(this.currentTime)) .replace(/{duration}/, formatTime(this.endTime)); return (h("div", { class: "scrubber", onMouseEnter: this.onSeeking.bind(this), onMouseLeave: this.onSeeking.bind(this), onMouseMove: this.onSeeking.bind(this), onTouchMove: () => { this.getSliderInput().focus(); }, onTouchEnd: () => { this.getSliderInput().blur(); } }, h("vm-slider", { step: 0.01, max: this.endTime, value: this.currentTime, label: this.i18n.scrubber, valueText: sliderValueText, onVmValueChange: this.onSeek.bind(this), ref: (el) => { this.slider = el; } }), h("progress", { class: { loading: this.buffering, }, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore min: 0, max: this.endTime, value: this.buffered, "aria-label": this.i18n.buffered, "aria-valuemin": "0", "aria-valuemax": this.endTime, "aria-valuenow": this.buffered, "aria-valuetext": `${(this.endTime > 0 ? this.buffered / this.endTime : 0).toFixed(0)}%` }, "% buffered"), h("vm-tooltip", { hidden: this.hideTooltip, ref: (el) => { this.tooltip = el; } }, this.timestamp))); } static get is() { return "vm-scrubber-control"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["scrubber-control.css"] }; } static get styleUrls() { return { "$": ["scrubber-control.css"] }; } static get properties() { return { "alwaysShowHours": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Whether the timestamp in the tooltip should show the hours unit, even if the time is less than\n1 hour (eg: `20:35` -> `00:20:35`)." }, "attribute": "always-show-hours", "reflect": false, "defaultValue": "false" }, "hideTooltip": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Whether the tooltip should not be displayed." }, "attribute": "hide-tooltip", "reflect": false, "defaultValue": "false" }, "currentTime": { "type": "number", "mutable": false, "complexType": { "original": "PlayerProps['currentTime']", "resolved": "number", "references": { "PlayerProps": { "location": "import", "path": "../../../core/player/PlayerProps" } } }, "required": false, "optional": false, "docs": { "tags": [{ "text": undefined, "name": "internal" }], "text": "" }, "attribute": "current-time", "reflect": false, "defaultValue": "0" }, "duration": { "type": "number", "mutable": false, "complexType": { "original": "PlayerProps['duration']", "resolved": "number", "references": { "PlayerProps": { "location": "import", "path": "../../../core/player/PlayerProps" } } }, "required": false, "optional": false, "docs": { "tags": [{ "text": undefined, "name": "internal" }], "text": "" }, "attribute": "duration", "reflect": false, "defaultValue": "-1" }, "noKeyboard": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Prevents seeking forward/backward by using the Left/Right arrow keys." }, "attribute": "no-keyboard", "reflect": false, "defaultValue": "false" }, "buffering": { "type": "boolean", "mutable": false, "complexType": { "original": "PlayerProps['buffering']", "resolved": "boolean", "references": { "PlayerProps": { "location": "import", "path": "../../../core/player/PlayerProps" } } }, "required": false, "optional": false, "docs": { "tags": [{ "text": undefined, "name": "internal" }], "text": "" }, "attribute": "buffering", "reflect": false, "defaultValue": "false" }, "buffered": { "type": "number", "mutable": false, "complexType": { "original": "PlayerProps['buffered']", "resolved": "number", "references": { "PlayerProps": { "location": "import", "path": "../../../core/player/PlayerProps" } } }, "required": false, "optional": false, "docs": { "tags": [{ "text": undefined, "name": "internal" }], "text": "" }, "attribute": "buffered", "reflect": false, "defaultValue": "0" }, "i18n": { "type": "unknown", "mutable": false, "complexType": { "original": "PlayerProps['i18n']", "resolved": "Translation | { [x: string]: string; }", "references": { "PlayerProps": { "location": "import", "path": "../../../core/player/PlayerProps" } } }, "required": false, "optional": false, "docs": { "tags": [{ "text": undefined, "name": "internal" }], "text": "" }, "defaultValue": "{}" } }; } static get states() { return { "timestamp": {}, "endTime": {} }; } static get elementRef() { return "host"; } static get watchers() { return [{ "propName": "noKeyboard", "methodName": "onNoKeyboardChange" }, { "propName": "duration", "methodName": "onDurationChange" }]; } }