@aglyn/plugins-mui 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +22 -0
- package/package.json +47 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +22 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/accordion.d.ts +96 -0
- package/src/lib/components/accordion.js +392 -0
- package/src/lib/components/accordion.js.map +1 -0
- package/src/lib/components/app-bar.d.ts +32 -0
- package/src/lib/components/app-bar.js +86 -0
- package/src/lib/components/app-bar.js.map +1 -0
- package/src/lib/components/blocks.d.ts +46 -0
- package/src/lib/components/blocks.js +1007 -0
- package/src/lib/components/blocks.js.map +1 -0
- package/src/lib/components/box.d.ts +47 -0
- package/src/lib/components/box.js +125 -0
- package/src/lib/components/box.js.map +1 -0
- package/src/lib/components/breadcrumbs.d.ts +44 -0
- package/src/lib/components/breadcrumbs.js +172 -0
- package/src/lib/components/breadcrumbs.js.map +1 -0
- package/src/lib/components/button.d.ts +89 -0
- package/src/lib/components/button.js +272 -0
- package/src/lib/components/button.js.map +1 -0
- package/src/lib/components/card.d.ts +61 -0
- package/src/lib/components/card.js +336 -0
- package/src/lib/components/card.js.map +1 -0
- package/src/lib/components/collection.d.ts +514 -0
- package/src/lib/components/collection.js +2999 -0
- package/src/lib/components/collection.js.map +1 -0
- package/src/lib/components/container.d.ts +42 -0
- package/src/lib/components/container.js +169 -0
- package/src/lib/components/container.js.map +1 -0
- package/src/lib/components/custom-html.d.ts +78 -0
- package/src/lib/components/custom-html.js +219 -0
- package/src/lib/components/custom-html.js.map +1 -0
- package/src/lib/components/data-table.d.ts +57 -0
- package/src/lib/components/data-table.js +216 -0
- package/src/lib/components/data-table.js.map +1 -0
- package/src/lib/components/document-root.d.ts +77 -0
- package/src/lib/components/document-root.js +118 -0
- package/src/lib/components/document-root.js.map +1 -0
- package/src/lib/components/drawer.d.ts +93 -0
- package/src/lib/components/drawer.js +566 -0
- package/src/lib/components/drawer.js.map +1 -0
- package/src/lib/components/function-controls.d.ts +66 -0
- package/src/lib/components/function-controls.js +207 -0
- package/src/lib/components/function-controls.js.map +1 -0
- package/src/lib/components/function-scope.d.ts +110 -0
- package/src/lib/components/function-scope.js +478 -0
- package/src/lib/components/function-scope.js.map +1 -0
- package/src/lib/components/function-widget.d.ts +75 -0
- package/src/lib/components/function-widget.js +307 -0
- package/src/lib/components/function-widget.js.map +1 -0
- package/src/lib/components/grid.d.ts +91 -0
- package/src/lib/components/grid.js +322 -0
- package/src/lib/components/grid.js.map +1 -0
- package/src/lib/components/icon.d.ts +54 -0
- package/src/lib/components/icon.js +163 -0
- package/src/lib/components/icon.js.map +1 -0
- package/src/lib/components/image-list.d.ts +58 -0
- package/src/lib/components/image-list.js +336 -0
- package/src/lib/components/image-list.js.map +1 -0
- package/src/lib/components/image.d.ts +154 -0
- package/src/lib/components/image.js +451 -0
- package/src/lib/components/image.js.map +1 -0
- package/src/lib/components/inline-text.d.ts +74 -0
- package/src/lib/components/inline-text.js +407 -0
- package/src/lib/components/inline-text.js.map +1 -0
- package/src/lib/components/language-switcher.d.ts +35 -0
- package/src/lib/components/language-switcher.js +179 -0
- package/src/lib/components/language-switcher.js.map +1 -0
- package/src/lib/components/layout-slot.d.ts +68 -0
- package/src/lib/components/layout-slot.js +192 -0
- package/src/lib/components/layout-slot.js.map +1 -0
- package/src/lib/components/link-box-accessible-name.d.ts +79 -0
- package/src/lib/components/link-box-accessible-name.js +158 -0
- package/src/lib/components/link-box-accessible-name.js.map +1 -0
- package/src/lib/components/link-box.d.ts +66 -0
- package/src/lib/components/link-box.js +212 -0
- package/src/lib/components/link-box.js.map +1 -0
- package/src/lib/components/list-item-text.d.ts +22 -0
- package/src/lib/components/list-item-text.js +86 -0
- package/src/lib/components/list-item-text.js.map +1 -0
- package/src/lib/components/list-item.d.ts +32 -0
- package/src/lib/components/list-item.js +127 -0
- package/src/lib/components/list-item.js.map +1 -0
- package/src/lib/components/list.d.ts +34 -0
- package/src/lib/components/list.js +108 -0
- package/src/lib/components/list.js.map +1 -0
- package/src/lib/components/markdown.d.ts +129 -0
- package/src/lib/components/markdown.js +853 -0
- package/src/lib/components/markdown.js.map +1 -0
- package/src/lib/components/nav-menu.d.ts +68 -0
- package/src/lib/components/nav-menu.js +683 -0
- package/src/lib/components/nav-menu.js.map +1 -0
- package/src/lib/components/pagination.d.ts +59 -0
- package/src/lib/components/pagination.js +243 -0
- package/src/lib/components/pagination.js.map +1 -0
- package/src/lib/components/paper.d.ts +47 -0
- package/src/lib/components/paper.js +202 -0
- package/src/lib/components/paper.js.map +1 -0
- package/src/lib/components/plugin-frame.d.ts +64 -0
- package/src/lib/components/plugin-frame.js +335 -0
- package/src/lib/components/plugin-frame.js.map +1 -0
- package/src/lib/components/plugin.d.ts +95 -0
- package/src/lib/components/plugin.js +304 -0
- package/src/lib/components/plugin.js.map +1 -0
- package/src/lib/components/product.d.ts +40 -0
- package/src/lib/components/product.js +287 -0
- package/src/lib/components/product.js.map +1 -0
- package/src/lib/components/reusable-instance.d.ts +57 -0
- package/src/lib/components/reusable-instance.js +126 -0
- package/src/lib/components/reusable-instance.js.map +1 -0
- package/src/lib/components/screen-link.d.ts +85 -0
- package/src/lib/components/screen-link.js +324 -0
- package/src/lib/components/screen-link.js.map +1 -0
- package/src/lib/components/search-box.d.ts +41 -0
- package/src/lib/components/search-box.js +125 -0
- package/src/lib/components/search-box.js.map +1 -0
- package/src/lib/components/section.d.ts +56 -0
- package/src/lib/components/section.js +132 -0
- package/src/lib/components/section.js.map +1 -0
- package/src/lib/components/stack.d.ts +55 -0
- package/src/lib/components/stack.js +309 -0
- package/src/lib/components/stack.js.map +1 -0
- package/src/lib/components/tabs.d.ts +288 -0
- package/src/lib/components/tabs.js +788 -0
- package/src/lib/components/tabs.js.map +1 -0
- package/src/lib/components/theme-mode-switcher.d.ts +38 -0
- package/src/lib/components/theme-mode-switcher.js +168 -0
- package/src/lib/components/theme-mode-switcher.js.map +1 -0
- package/src/lib/components/toolbar.d.ts +37 -0
- package/src/lib/components/toolbar.js +117 -0
- package/src/lib/components/toolbar.js.map +1 -0
- package/src/lib/components/typography.d.ts +33 -0
- package/src/lib/components/typography.js +433 -0
- package/src/lib/components/typography.js.map +1 -0
- package/src/lib/components/video-lightbox-controls.d.ts +131 -0
- package/src/lib/components/video-lightbox-controls.js +536 -0
- package/src/lib/components/video-lightbox-controls.js.map +1 -0
- package/src/lib/components/video-lightbox.d.ts +60 -0
- package/src/lib/components/video-lightbox.js +247 -0
- package/src/lib/components/video-lightbox.js.map +1 -0
- package/src/lib/components/video-playback-beacon.d.ts +80 -0
- package/src/lib/components/video-playback-beacon.js +128 -0
- package/src/lib/components/video-playback-beacon.js.map +1 -0
- package/src/lib/components/video-player-frame.d.ts +58 -0
- package/src/lib/components/video-player-frame.js +51 -0
- package/src/lib/components/video-player-frame.js.map +1 -0
- package/src/lib/components/video.d.ts +220 -0
- package/src/lib/components/video.js +874 -0
- package/src/lib/components/video.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +17 -0
- package/src/lib/constants/bundle-common.js +18 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/constants/field-presets.d.ts +93 -0
- package/src/lib/constants/field-presets.js +248 -0
- package/src/lib/constants/field-presets.js.map +1 -0
- package/src/lib/plugin.d.ts +80 -0
- package/src/lib/plugin.js +599 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/utils/author-html-label.d.ts +73 -0
- package/src/lib/utils/author-html-label.js +89 -0
- package/src/lib/utils/author-html-label.js.map +1 -0
- package/src/lib/utils/drop-cleared-props.d.ts +18 -0
- package/src/lib/utils/drop-cleared-props.js +74 -0
- package/src/lib/utils/drop-cleared-props.js.map +1 -0
- package/src/lib/utils/element-picker.d.ts +99 -0
- package/src/lib/utils/element-picker.js +140 -0
- package/src/lib/utils/element-picker.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +19 -0
- package/src/lib/utils/generate-preset-id.js +25 -0
- package/src/lib/utils/generate-preset-id.js.map +1 -0
- package/src/lib/utils/link-target-props.d.ts +53 -0
- package/src/lib/utils/link-target-props.js +53 -0
- package/src/lib/utils/link-target-props.js.map +1 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The lightbox player's own controls (AGL-2802).
|
|
18
|
+
*
|
|
19
|
+
* ## Why not the browser's
|
|
20
|
+
*
|
|
21
|
+
* A `<video controls>` draws its buttons inside the browser's shadow tree, and
|
|
22
|
+
* Chrome's media controls consume `Escape` there before the page is sent
|
|
23
|
+
* anything: from the play, timeline, volume and full-screen stops, no
|
|
24
|
+
* `keydown` or `keyup` for it reaches the document in either phase. MUI's
|
|
25
|
+
* Modal closes on a bubbling `keydown`, so the dialog could not close from the
|
|
26
|
+
* four focus stops a keyboard visitor is on while watching, and no listener,
|
|
27
|
+
* capture phase included, can hear a key the page is never sent.
|
|
28
|
+
*
|
|
29
|
+
* These are ordinary buttons and range inputs. Every key pressed on one is
|
|
30
|
+
* dispatched through the document like any other key, so `Escape` bubbles to
|
|
31
|
+
* the Modal from each of them and focus goes back to the trigger.
|
|
32
|
+
*
|
|
33
|
+
* ## Why a module of its own
|
|
34
|
+
*
|
|
35
|
+
* Only `video-lightbox.tsx` imports it, and nothing else may. That module is
|
|
36
|
+
* reached solely through `lazy(() => import())` in `video.tsx`, so this file,
|
|
37
|
+
* and MUI's Slider with it, travels in the dialog's chunk: fetched by a
|
|
38
|
+
* visitor who asked to watch, never by a page that only shows a poster
|
|
39
|
+
* (AGL-2744).
|
|
40
|
+
*
|
|
41
|
+
* ## The keyboard
|
|
42
|
+
*
|
|
43
|
+
* | key | from | does |
|
|
44
|
+
* | -- | -- | -- |
|
|
45
|
+
* | `Space` | any stop but a button, which `Space` presses | play or pause |
|
|
46
|
+
* | `K` | any stop | play or pause |
|
|
47
|
+
* | `←` `→` | any stop but a slider | back or forward five seconds |
|
|
48
|
+
* | `M` | any stop | mute or unmute |
|
|
49
|
+
* | `F` | any stop | full screen on or off |
|
|
50
|
+
* | arrows | the seek slider | five seconds, ten with `Shift` |
|
|
51
|
+
* | `Page Up` `Page Down` | the seek slider | ten seconds |
|
|
52
|
+
* | `Home` `End` | the seek slider | the start, the end |
|
|
53
|
+
* | arrows | the volume slider | five percent |
|
|
54
|
+
* | `Escape` | any stop | leaves full screen if on, otherwise closes |
|
|
55
|
+
*
|
|
56
|
+
* A key held with `Ctrl`, `Alt` or `Meta` is never a shortcut: those belong
|
|
57
|
+
* to the browser and the operating system.
|
|
58
|
+
*/ 'use client';
|
|
59
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
60
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
61
|
+
import { mdiClosedCaption, mdiClosedCaptionOutline, mdiFullscreen, mdiFullscreenExit, mdiPause, mdiPlay, mdiVolumeHigh, mdiVolumeOff } from "@aglyn/shared-data-mdi";
|
|
62
|
+
import { MdiIcon } from "@aglyn/shared-ui-jsx";
|
|
63
|
+
import Box from "@mui/material/Box";
|
|
64
|
+
import { touchRippleClasses } from "@mui/material/ButtonBase";
|
|
65
|
+
import IconButton from "@mui/material/IconButton";
|
|
66
|
+
import Slider, { sliderClasses } from "@mui/material/Slider";
|
|
67
|
+
import { useTheme } from "@mui/material/styles";
|
|
68
|
+
import Typography from "@mui/material/Typography";
|
|
69
|
+
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
70
|
+
import { useEffect, useRef, useState } from "react";
|
|
71
|
+
/** One arrow press on the playhead, on the seek slider or from any other stop. */ export const SEEK_STEP_SECONDS = 5;
|
|
72
|
+
/** `Page Up` and `Page Down` on the seek slider, and an arrow held with `Shift`. */ export const SEEK_PAGE_SECONDS = 10;
|
|
73
|
+
/** One arrow press on the volume slider, in percent. */ const VOLUME_STEP_PERCENT = 5;
|
|
74
|
+
const ignore = ()=>undefined;
|
|
75
|
+
/**
|
|
76
|
+
* A player clock: `m:ss`, or `h:mm:ss` from an hour up.
|
|
77
|
+
*
|
|
78
|
+
* A duration the browser does not know yet (`NaN`, before metadata) or cannot
|
|
79
|
+
* know (`Infinity`, a stream) reads as `0:00` rather than as `NaN:NaN`.
|
|
80
|
+
*/ export function formatMediaTime(seconds) {
|
|
81
|
+
const whole = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0;
|
|
82
|
+
const hours = Math.floor(whole / 3600);
|
|
83
|
+
const minutes = Math.floor(whole / 60) % 60;
|
|
84
|
+
const rest = String(whole % 60).padStart(2, '0');
|
|
85
|
+
return hours > 0 ? `${hours}:${String(minutes).padStart(2, '0')}:${rest}` : `${minutes}:${rest}`;
|
|
86
|
+
}
|
|
87
|
+
/** What the seek slider says its value is: `1:23 of 4:10`. */ export function seekValueText(position, duration) {
|
|
88
|
+
return `${formatMediaTime(position)} of ${formatMediaTime(duration)}`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Play or pause.
|
|
92
|
+
*
|
|
93
|
+
* `play()` rejects when the browser refuses playback, and the refusal needs
|
|
94
|
+
* no handling: no `play` event fires, so the control goes on reporting a
|
|
95
|
+
* paused film, which is the truth.
|
|
96
|
+
*/ export function togglePlayback(video) {
|
|
97
|
+
if (video.paused || video.ended) {
|
|
98
|
+
void Promise.resolve(video.play()).catch(ignore);
|
|
99
|
+
} else {
|
|
100
|
+
video.pause();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Moves the playhead, clamped to the film, and returns where it landed.
|
|
105
|
+
* Before the duration is known there is nowhere to land, and nothing moves.
|
|
106
|
+
*/ export function seekPlayback(video, seconds) {
|
|
107
|
+
const { duration } = video;
|
|
108
|
+
if (!Number.isFinite(duration) || duration <= 0) return undefined;
|
|
109
|
+
const landed = Math.min(Math.max(seconds, 0), duration);
|
|
110
|
+
video.currentTime = landed;
|
|
111
|
+
return landed;
|
|
112
|
+
}
|
|
113
|
+
export function toggleMute(video) {
|
|
114
|
+
video.muted = !video.muted;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether a full-screen control would do anything here.
|
|
118
|
+
*
|
|
119
|
+
* `fullscreenEnabled` is false inside a frame whose embedder did not allow it,
|
|
120
|
+
* and there the control is left out rather than shown doing nothing. A phone
|
|
121
|
+
* with no element full screen at all still gets the video-only kind, which
|
|
122
|
+
* plays under the platform's own controls.
|
|
123
|
+
*/ export function fullscreenAvailable() {
|
|
124
|
+
if (typeof document === 'undefined') return false;
|
|
125
|
+
if (document.fullscreenEnabled) return true;
|
|
126
|
+
return !('requestFullscreen' in Element.prototype) && 'webkitEnterFullscreen' in HTMLVideoElement.prototype;
|
|
127
|
+
}
|
|
128
|
+
export function toggleFullscreen({ video, player }) {
|
|
129
|
+
var _this, _phone_webkitEnterFullscreen;
|
|
130
|
+
const doc = (_this = player != null ? player : video) == null ? void 0 : _this.ownerDocument;
|
|
131
|
+
if (!doc) return;
|
|
132
|
+
if (player && doc.fullscreenElement === player) {
|
|
133
|
+
void Promise.resolve(doc.exitFullscreen()).catch(ignore);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (player && doc.fullscreenEnabled) {
|
|
137
|
+
void Promise.resolve(player.requestFullscreen()).catch(ignore);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const phone = video;
|
|
141
|
+
phone == null ? void 0 : (_phone_webkitEnterFullscreen = phone.webkitEnterFullscreen) == null ? void 0 : _phone_webkitEnterFullscreen.call(phone);
|
|
142
|
+
}
|
|
143
|
+
/** The tracks a captions toggle turns on and off. */ function captionTracks(video) {
|
|
144
|
+
var _video_textTracks;
|
|
145
|
+
// Absent where a DOM implements no media pipeline.
|
|
146
|
+
const tracks = (_video_textTracks = video.textTracks) != null ? _video_textTracks : [];
|
|
147
|
+
return Array.from(tracks).filter((track)=>track.kind === 'captions' || track.kind === 'subtitles');
|
|
148
|
+
}
|
|
149
|
+
/** The keys a focused slider moves itself with. */ const SLIDER_KEYS = new Set([
|
|
150
|
+
'ArrowLeft',
|
|
151
|
+
'ArrowRight',
|
|
152
|
+
'ArrowUp',
|
|
153
|
+
'ArrowDown',
|
|
154
|
+
'PageUp',
|
|
155
|
+
'PageDown',
|
|
156
|
+
'Home',
|
|
157
|
+
'End'
|
|
158
|
+
]);
|
|
159
|
+
const ARROW_KEYS = new Set([
|
|
160
|
+
'ArrowLeft',
|
|
161
|
+
'ArrowRight',
|
|
162
|
+
'ArrowUp',
|
|
163
|
+
'ArrowDown'
|
|
164
|
+
]);
|
|
165
|
+
/**
|
|
166
|
+
* Whether the focused element uses `key` for itself, in which case no shortcut
|
|
167
|
+
* may take it: `Space` and `Enter` press a focused button, the arrows and
|
|
168
|
+
* `Home`/`End` move a focused slider, and a text field wants every key.
|
|
169
|
+
*/ export function keyBelongsToFocus(target, key) {
|
|
170
|
+
if (!(target instanceof Element)) return false;
|
|
171
|
+
if (target instanceof HTMLElement && target.isContentEditable) return true;
|
|
172
|
+
if (target.matches('input[type="range"], [role="slider"]')) {
|
|
173
|
+
return SLIDER_KEYS.has(key);
|
|
174
|
+
}
|
|
175
|
+
if (target.matches('button, [role="button"], a[href], summary, input[type="checkbox"], input[type="radio"]')) {
|
|
176
|
+
return key === ' ' || key === 'Enter';
|
|
177
|
+
}
|
|
178
|
+
return target.matches('input, textarea, select');
|
|
179
|
+
}
|
|
180
|
+
const SHORTCUTS = new Map([
|
|
181
|
+
[
|
|
182
|
+
' ',
|
|
183
|
+
{
|
|
184
|
+
run: togglePlayback,
|
|
185
|
+
repeats: false
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
[
|
|
189
|
+
'k',
|
|
190
|
+
{
|
|
191
|
+
run: togglePlayback,
|
|
192
|
+
repeats: false
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
[
|
|
196
|
+
'ArrowLeft',
|
|
197
|
+
{
|
|
198
|
+
run: (video)=>seekPlayback(video, video.currentTime - SEEK_STEP_SECONDS),
|
|
199
|
+
repeats: true
|
|
200
|
+
}
|
|
201
|
+
],
|
|
202
|
+
[
|
|
203
|
+
'ArrowRight',
|
|
204
|
+
{
|
|
205
|
+
run: (video)=>seekPlayback(video, video.currentTime + SEEK_STEP_SECONDS),
|
|
206
|
+
repeats: true
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
[
|
|
210
|
+
'm',
|
|
211
|
+
{
|
|
212
|
+
run: toggleMute,
|
|
213
|
+
repeats: false
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
'f',
|
|
218
|
+
{
|
|
219
|
+
run: (video, player)=>toggleFullscreen({
|
|
220
|
+
video,
|
|
221
|
+
player
|
|
222
|
+
}),
|
|
223
|
+
repeats: false
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
]);
|
|
227
|
+
/**
|
|
228
|
+
* The dialog's `onKeyDown`: the player's shortcuts, from every stop inside it.
|
|
229
|
+
*
|
|
230
|
+
* Bound on the Modal root rather than on the controls, because focus is not
|
|
231
|
+
* always on a control — a click on the film leaves it on the dialog container
|
|
232
|
+
* — and a key pressed anywhere in the dialog bubbles through the root. MUI
|
|
233
|
+
* calls this before its own `Escape` handling, and nothing here touches
|
|
234
|
+
* `Escape`.
|
|
235
|
+
*
|
|
236
|
+
* A key the focused control uses for itself is left alone, and so is any key
|
|
237
|
+
* a control already claimed: MUI's Slider calls `preventDefault` on the keys
|
|
238
|
+
* it moves with, which is how an arrow on a slider stays that slider's.
|
|
239
|
+
*/ export function handlePlayerShortcut(event, { video, player }) {
|
|
240
|
+
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
|
|
241
|
+
const shortcut = SHORTCUTS.get(key);
|
|
242
|
+
if (!shortcut || !video) return;
|
|
243
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (keyBelongsToFocus(event.target, event.key)) return;
|
|
247
|
+
event.preventDefault();
|
|
248
|
+
if (event.repeat && !shortcut.repeats) return;
|
|
249
|
+
shortcut.run(video, player);
|
|
250
|
+
}
|
|
251
|
+
/** A focus ring that reads on the player's black ground, whatever the site's palette. */ const focusRing = {
|
|
252
|
+
outline: '2px solid',
|
|
253
|
+
outlineColor: 'common.white',
|
|
254
|
+
outlineOffset: 2
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Every button on the player, the dialog's close button included.
|
|
258
|
+
*
|
|
259
|
+
* IconButton's own hover rule reads `--IconButton-hoverBg`, `hover: none`
|
|
260
|
+
* reset and all; its default is a tint of `action.active`, which on a light
|
|
261
|
+
* site's palette is black on this black ground and never shows. The ripple is
|
|
262
|
+
* motion, so it goes for a visitor who asked for less, and it is gated in CSS
|
|
263
|
+
* as the poster's play badge is so the setting resolves in both directions.
|
|
264
|
+
*/ export const controlButtonSx = (theme)=>({
|
|
265
|
+
color: 'common.white',
|
|
266
|
+
'--IconButton-hoverBg': (theme.vars || theme).palette.grey[800],
|
|
267
|
+
'&:focus-visible': focusRing,
|
|
268
|
+
'@media (prefers-reduced-motion: reduce)': {
|
|
269
|
+
[`& .${touchRippleClasses.root}`]: {
|
|
270
|
+
display: 'none'
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
const sliderSx = {
|
|
275
|
+
color: 'common.white',
|
|
276
|
+
// The focused element is the thumb's visually hidden range input, so the
|
|
277
|
+
// ring goes on the thumb MUI marks as focus-visible.
|
|
278
|
+
[`& .${sliderClasses.thumb}.${sliderClasses.focusVisible}`]: focusRing,
|
|
279
|
+
// The thumb glides to each new value; that glide is motion too.
|
|
280
|
+
'@media (prefers-reduced-motion: reduce)': {
|
|
281
|
+
[`& .${sliderClasses.thumb}, & .${sliderClasses.track}`]: {
|
|
282
|
+
transition: 'none'
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
/** Every event after which the element reports something the controls draw. */ const MEDIA_EVENTS = [
|
|
287
|
+
'play',
|
|
288
|
+
'pause',
|
|
289
|
+
'ended',
|
|
290
|
+
'timeupdate',
|
|
291
|
+
'seeked',
|
|
292
|
+
'durationchange',
|
|
293
|
+
'loadedmetadata',
|
|
294
|
+
'volumechange',
|
|
295
|
+
'emptied'
|
|
296
|
+
];
|
|
297
|
+
const snapshotOf = (video)=>({
|
|
298
|
+
paused: video.paused,
|
|
299
|
+
currentTime: video.currentTime,
|
|
300
|
+
duration: video.duration,
|
|
301
|
+
muted: video.muted,
|
|
302
|
+
volume: video.volume
|
|
303
|
+
});
|
|
304
|
+
/**
|
|
305
|
+
* The bar under the film: a seek slider across the top, then play, the clock,
|
|
306
|
+
* mute, volume, captions and full screen.
|
|
307
|
+
*
|
|
308
|
+
* Each control is a real `<button>` or a real range input with a name and a
|
|
309
|
+
* state: `aria-pressed` on the toggles, a spoken `1:23 of 4:10` on the seek
|
|
310
|
+
* slider, `aria-keyshortcuts` where a key does the same thing.
|
|
311
|
+
*
|
|
312
|
+
* What the controls draw is read back off the `<video>` on its own events
|
|
313
|
+
* rather than kept beside it, so a button, a shortcut, a click on the film and
|
|
314
|
+
* the platform's media keys all end in one place, and the play beacon's
|
|
315
|
+
* handlers on the element hear every one of them.
|
|
316
|
+
*/ export function VideoLightboxControls(props) {
|
|
317
|
+
const { videoRef, playerRef, captions = false } = props;
|
|
318
|
+
const [media, setMedia] = useState({
|
|
319
|
+
paused: true,
|
|
320
|
+
currentTime: 0,
|
|
321
|
+
duration: Number.NaN,
|
|
322
|
+
muted: false,
|
|
323
|
+
volume: 1
|
|
324
|
+
});
|
|
325
|
+
const [fullscreen, setFullscreen] = useState(false);
|
|
326
|
+
const [captionsShowing, setCaptionsShowing] = useState(captions);
|
|
327
|
+
const [canFullscreen] = useState(fullscreenAvailable);
|
|
328
|
+
const fullscreenButtonRef = useRef(null);
|
|
329
|
+
/**
|
|
330
|
+
* No volume slider at phone width: a phone's volume belongs to its hardware
|
|
331
|
+
* buttons, iOS ignores a page that sets it, and the row has no room for it.
|
|
332
|
+
* Read on the first render, so the slider never flashes in and out on open.
|
|
333
|
+
*/ const theme = useTheme();
|
|
334
|
+
const phone = useMediaQuery(theme.breakpoints.down('sm'), {
|
|
335
|
+
noSsr: true
|
|
336
|
+
});
|
|
337
|
+
useEffect(()=>{
|
|
338
|
+
var _tracks_addEventListener;
|
|
339
|
+
const video = videoRef.current;
|
|
340
|
+
if (!video) return undefined;
|
|
341
|
+
const doc = video.ownerDocument;
|
|
342
|
+
const tracks = video.textTracks;
|
|
343
|
+
const syncMedia = ()=>setMedia(snapshotOf(video));
|
|
344
|
+
/**
|
|
345
|
+
* Full screen moves the player into the top layer, and the browser blurs
|
|
346
|
+
* whatever had focus outside it: the dialog's own surface, where focus
|
|
347
|
+
* lands on open, is outside it. Focus then sits on the body, outside the
|
|
348
|
+
* Modal, where no shortcut and no `Escape` reaches the dialog. So focus
|
|
349
|
+
* follows the player in, onto the control that toggles it, and is put
|
|
350
|
+
* back there on the way out if the browser dropped it again.
|
|
351
|
+
*/ const syncFullscreen = ()=>{
|
|
352
|
+
const player = playerRef.current;
|
|
353
|
+
setFullscreen(player !== null && doc.fullscreenElement === player);
|
|
354
|
+
if (player && !player.contains(doc.activeElement)) {
|
|
355
|
+
var _fullscreenButtonRef_current;
|
|
356
|
+
(_fullscreenButtonRef_current = fullscreenButtonRef.current) == null ? void 0 : _fullscreenButtonRef_current.focus();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
const syncCaptions = ()=>setCaptionsShowing(captionTracks(video).some((track)=>track.mode === 'showing'));
|
|
360
|
+
syncMedia();
|
|
361
|
+
for (const type of MEDIA_EVENTS)video.addEventListener(type, syncMedia);
|
|
362
|
+
doc.addEventListener('fullscreenchange', syncFullscreen);
|
|
363
|
+
tracks == null ? void 0 : (_tracks_addEventListener = tracks.addEventListener) == null ? void 0 : _tracks_addEventListener.call(tracks, 'change', syncCaptions);
|
|
364
|
+
return ()=>{
|
|
365
|
+
var _tracks_removeEventListener;
|
|
366
|
+
for (const type of MEDIA_EVENTS)video.removeEventListener(type, syncMedia);
|
|
367
|
+
doc.removeEventListener('fullscreenchange', syncFullscreen);
|
|
368
|
+
tracks == null ? void 0 : (_tracks_removeEventListener = tracks.removeEventListener) == null ? void 0 : _tracks_removeEventListener.call(tracks, 'change', syncCaptions);
|
|
369
|
+
};
|
|
370
|
+
}, [
|
|
371
|
+
videoRef,
|
|
372
|
+
playerRef
|
|
373
|
+
]);
|
|
374
|
+
const seekable = Number.isFinite(media.duration) && media.duration > 0;
|
|
375
|
+
const position = seekable ? Math.min(Math.max(media.currentTime, 0), media.duration) : 0;
|
|
376
|
+
const playing = !media.paused;
|
|
377
|
+
const level = media.muted ? 0 : Math.round(media.volume * 100);
|
|
378
|
+
const withVideo = (action)=>()=>{
|
|
379
|
+
const video = videoRef.current;
|
|
380
|
+
if (video) action(video);
|
|
381
|
+
};
|
|
382
|
+
/**
|
|
383
|
+
* MUI's Slider steps an arrow by `step`, which is also the grain a pointer
|
|
384
|
+
* drag snaps to. One second keeps a drag as fine as the clock it moves, and
|
|
385
|
+
* this scales an arrow up to the same five seconds the arrows give from
|
|
386
|
+
* every other stop, in the direction MUI chose, so a right-to-left page's
|
|
387
|
+
* arrows still point the way the track runs.
|
|
388
|
+
*/ const onSeek = (event, value)=>{
|
|
389
|
+
const video = videoRef.current;
|
|
390
|
+
if (!video || typeof value !== 'number') return;
|
|
391
|
+
const arrow = event.type === 'keydown' && ARROW_KEYS.has(event.key);
|
|
392
|
+
const step = event.shiftKey ? SEEK_PAGE_SECONDS : SEEK_STEP_SECONDS;
|
|
393
|
+
const target = arrow ? video.currentTime + Math.sign(value - position) * step : value;
|
|
394
|
+
const landed = seekPlayback(video, target);
|
|
395
|
+
// Drawn now rather than on `seeked`, so the thumb does not spring back
|
|
396
|
+
// to where it was for the frames the seek takes.
|
|
397
|
+
if (landed !== undefined) {
|
|
398
|
+
setMedia((previous)=>_extends({}, previous, {
|
|
399
|
+
currentTime: landed
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* Zero mutes rather than zeroing the volume, so unmuting brings back the
|
|
405
|
+
* last level the visitor could hear.
|
|
406
|
+
*/ const onVolume = (_event, value)=>{
|
|
407
|
+
const video = videoRef.current;
|
|
408
|
+
if (!video || typeof value !== 'number') return;
|
|
409
|
+
if (value <= 0) {
|
|
410
|
+
video.muted = true;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
video.volume = value / 100;
|
|
414
|
+
video.muted = false;
|
|
415
|
+
};
|
|
416
|
+
const onCaptions = withVideo((video)=>{
|
|
417
|
+
const next = !captionsShowing;
|
|
418
|
+
for (const track of captionTracks(video)){
|
|
419
|
+
track.mode = next ? 'showing' : 'hidden';
|
|
420
|
+
}
|
|
421
|
+
setCaptionsShowing(next);
|
|
422
|
+
});
|
|
423
|
+
return /*#__PURE__*/ _jsxs(Box, {
|
|
424
|
+
children: [
|
|
425
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
426
|
+
sx: {
|
|
427
|
+
px: 2
|
|
428
|
+
},
|
|
429
|
+
children: /*#__PURE__*/ _jsx(Slider, {
|
|
430
|
+
"aria-label": "Seek",
|
|
431
|
+
size: "small",
|
|
432
|
+
min: 0,
|
|
433
|
+
max: seekable ? media.duration : 1,
|
|
434
|
+
step: 1,
|
|
435
|
+
shiftStep: SEEK_PAGE_SECONDS,
|
|
436
|
+
value: position,
|
|
437
|
+
disabled: !seekable,
|
|
438
|
+
getAriaValueText: (value)=>seekValueText(value, media.duration),
|
|
439
|
+
onChange: onSeek,
|
|
440
|
+
sx: sliderSx
|
|
441
|
+
})
|
|
442
|
+
}),
|
|
443
|
+
/*#__PURE__*/ _jsxs(Box, {
|
|
444
|
+
sx: {
|
|
445
|
+
display: 'flex',
|
|
446
|
+
alignItems: 'center',
|
|
447
|
+
gap: 1,
|
|
448
|
+
px: 1,
|
|
449
|
+
pb: 1
|
|
450
|
+
},
|
|
451
|
+
children: [
|
|
452
|
+
/*#__PURE__*/ _jsx(IconButton, {
|
|
453
|
+
"aria-label": "Play",
|
|
454
|
+
"aria-pressed": playing,
|
|
455
|
+
"aria-keyshortcuts": "k Space",
|
|
456
|
+
onClick: withVideo(togglePlayback),
|
|
457
|
+
sx: controlButtonSx,
|
|
458
|
+
children: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
459
|
+
path: playing ? mdiPause.path : mdiPlay.path
|
|
460
|
+
})
|
|
461
|
+
}),
|
|
462
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
463
|
+
component: "span",
|
|
464
|
+
variant: "body2",
|
|
465
|
+
// The seek slider already speaks this, as `1:23 of 4:10`.
|
|
466
|
+
"aria-hidden": true,
|
|
467
|
+
sx: {
|
|
468
|
+
color: 'common.white',
|
|
469
|
+
fontVariantNumeric: 'tabular-nums',
|
|
470
|
+
whiteSpace: 'nowrap'
|
|
471
|
+
},
|
|
472
|
+
children: `${formatMediaTime(position)} / ${formatMediaTime(media.duration)}`
|
|
473
|
+
}),
|
|
474
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
475
|
+
sx: {
|
|
476
|
+
flexGrow: 1
|
|
477
|
+
}
|
|
478
|
+
}),
|
|
479
|
+
/*#__PURE__*/ _jsx(IconButton, {
|
|
480
|
+
"aria-label": "Mute",
|
|
481
|
+
"aria-pressed": media.muted,
|
|
482
|
+
"aria-keyshortcuts": "m",
|
|
483
|
+
onClick: withVideo(toggleMute),
|
|
484
|
+
sx: controlButtonSx,
|
|
485
|
+
children: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
486
|
+
path: media.muted || media.volume === 0 ? mdiVolumeOff.path : mdiVolumeHigh.path
|
|
487
|
+
})
|
|
488
|
+
}),
|
|
489
|
+
phone ? null : /*#__PURE__*/ _jsx(Slider, {
|
|
490
|
+
"aria-label": "Volume",
|
|
491
|
+
size: "small",
|
|
492
|
+
min: 0,
|
|
493
|
+
max: 100,
|
|
494
|
+
step: VOLUME_STEP_PERCENT,
|
|
495
|
+
value: level,
|
|
496
|
+
getAriaValueText: (value)=>value === 0 ? 'Muted' : `${value}%`,
|
|
497
|
+
onChange: onVolume,
|
|
498
|
+
sx: [
|
|
499
|
+
sliderSx,
|
|
500
|
+
{
|
|
501
|
+
width: theme.spacing(12),
|
|
502
|
+
mx: 1
|
|
503
|
+
}
|
|
504
|
+
]
|
|
505
|
+
}),
|
|
506
|
+
captions ? /*#__PURE__*/ _jsx(IconButton, {
|
|
507
|
+
"aria-label": "Captions",
|
|
508
|
+
"aria-pressed": captionsShowing,
|
|
509
|
+
onClick: onCaptions,
|
|
510
|
+
sx: controlButtonSx,
|
|
511
|
+
children: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
512
|
+
path: captionsShowing ? mdiClosedCaption.path : mdiClosedCaptionOutline.path
|
|
513
|
+
})
|
|
514
|
+
}) : null,
|
|
515
|
+
canFullscreen ? /*#__PURE__*/ _jsx(IconButton, {
|
|
516
|
+
ref: fullscreenButtonRef,
|
|
517
|
+
"aria-label": "Full screen",
|
|
518
|
+
"aria-pressed": fullscreen,
|
|
519
|
+
"aria-keyshortcuts": "f",
|
|
520
|
+
onClick: ()=>toggleFullscreen({
|
|
521
|
+
video: videoRef.current,
|
|
522
|
+
player: playerRef.current
|
|
523
|
+
}),
|
|
524
|
+
sx: controlButtonSx,
|
|
525
|
+
children: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
526
|
+
path: fullscreen ? mdiFullscreenExit.path : mdiFullscreen.path
|
|
527
|
+
})
|
|
528
|
+
}) : null
|
|
529
|
+
]
|
|
530
|
+
})
|
|
531
|
+
]
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
VideoLightboxControls.displayName = 'VideoLightboxControls';
|
|
535
|
+
|
|
536
|
+
//# sourceMappingURL=video-lightbox-controls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/mui/src/lib/components/video-lightbox-controls.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The lightbox player's own controls (AGL-2802).\n *\n * ## Why not the browser's\n *\n * A `<video controls>` draws its buttons inside the browser's shadow tree, and\n * Chrome's media controls consume `Escape` there before the page is sent\n * anything: from the play, timeline, volume and full-screen stops, no\n * `keydown` or `keyup` for it reaches the document in either phase. MUI's\n * Modal closes on a bubbling `keydown`, so the dialog could not close from the\n * four focus stops a keyboard visitor is on while watching, and no listener,\n * capture phase included, can hear a key the page is never sent.\n *\n * These are ordinary buttons and range inputs. Every key pressed on one is\n * dispatched through the document like any other key, so `Escape` bubbles to\n * the Modal from each of them and focus goes back to the trigger.\n *\n * ## Why a module of its own\n *\n * Only `video-lightbox.tsx` imports it, and nothing else may. That module is\n * reached solely through `lazy(() => import())` in `video.tsx`, so this file,\n * and MUI's Slider with it, travels in the dialog's chunk: fetched by a\n * visitor who asked to watch, never by a page that only shows a poster\n * (AGL-2744).\n *\n * ## The keyboard\n *\n * | key | from | does |\n * | -- | -- | -- |\n * | `Space` | any stop but a button, which `Space` presses | play or pause |\n * | `K` | any stop | play or pause |\n * | `←` `→` | any stop but a slider | back or forward five seconds |\n * | `M` | any stop | mute or unmute |\n * | `F` | any stop | full screen on or off |\n * | arrows | the seek slider | five seconds, ten with `Shift` |\n * | `Page Up` `Page Down` | the seek slider | ten seconds |\n * | `Home` `End` | the seek slider | the start, the end |\n * | arrows | the volume slider | five percent |\n * | `Escape` | any stop | leaves full screen if on, otherwise closes |\n *\n * A key held with `Ctrl`, `Alt` or `Meta` is never a shortcut: those belong\n * to the browser and the operating system.\n */\n'use client'\n\nimport {\n mdiClosedCaption,\n mdiClosedCaptionOutline,\n mdiFullscreen,\n mdiFullscreenExit,\n mdiPause,\n mdiPlay,\n mdiVolumeHigh,\n mdiVolumeOff,\n} from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport Box from '@mui/material/Box'\nimport { touchRippleClasses } from '@mui/material/ButtonBase'\nimport IconButton from '@mui/material/IconButton'\nimport Slider, { sliderClasses } from '@mui/material/Slider'\nimport { type Theme, useTheme } from '@mui/material/styles'\nimport Typography from '@mui/material/Typography'\nimport useMediaQuery from '@mui/material/useMediaQuery'\nimport {\n type KeyboardEvent,\n type RefObject,\n useEffect,\n useRef,\n useState,\n} from 'react'\n\n/** One arrow press on the playhead, on the seek slider or from any other stop. */\nexport const SEEK_STEP_SECONDS = 5\n\n/** `Page Up` and `Page Down` on the seek slider, and an arrow held with `Shift`. */\nexport const SEEK_PAGE_SECONDS = 10\n\n/** One arrow press on the volume slider, in percent. */\nconst VOLUME_STEP_PERCENT = 5\n\nconst ignore = () => undefined\n\n/**\n * A player clock: `m:ss`, or `h:mm:ss` from an hour up.\n *\n * A duration the browser does not know yet (`NaN`, before metadata) or cannot\n * know (`Infinity`, a stream) reads as `0:00` rather than as `NaN:NaN`.\n */\nexport function formatMediaTime(seconds: number): string {\n const whole = Number.isFinite(seconds) && seconds > 0 ? Math.floor(seconds) : 0\n const hours = Math.floor(whole / 3600)\n const minutes = Math.floor(whole / 60) % 60\n const rest = String(whole % 60).padStart(2, '0')\n return hours > 0\n ? `${hours}:${String(minutes).padStart(2, '0')}:${rest}`\n : `${minutes}:${rest}`\n}\n\n/** What the seek slider says its value is: `1:23 of 4:10`. */\nexport function seekValueText(position: number, duration: number): string {\n return `${formatMediaTime(position)} of ${formatMediaTime(duration)}`\n}\n\n/** The two elements the controls and the keyboard act on. */\nexport interface LightboxPlayerElements {\n video: HTMLVideoElement | null\n /**\n * The frame that goes full screen: the film AND these controls, so they are\n * still on screen, and still reachable from the keyboard, once it has.\n */\n player: HTMLElement | null\n}\n\n/**\n * Play or pause.\n *\n * `play()` rejects when the browser refuses playback, and the refusal needs\n * no handling: no `play` event fires, so the control goes on reporting a\n * paused film, which is the truth.\n */\nexport function togglePlayback(video: HTMLVideoElement): void {\n if (video.paused || video.ended) {\n void Promise.resolve(video.play()).catch(ignore)\n } else {\n video.pause()\n }\n}\n\n/**\n * Moves the playhead, clamped to the film, and returns where it landed.\n * Before the duration is known there is nowhere to land, and nothing moves.\n */\nexport function seekPlayback(\n video: HTMLVideoElement,\n seconds: number,\n): number | undefined {\n const { duration } = video\n if (!Number.isFinite(duration) || duration <= 0) return undefined\n const landed = Math.min(Math.max(seconds, 0), duration)\n video.currentTime = landed\n return landed\n}\n\nexport function toggleMute(video: HTMLVideoElement): void {\n video.muted = !video.muted\n}\n\n/** iOS Safari on a phone, which can put only the `<video>` itself full screen. */\ntype PhoneVideoElement = HTMLVideoElement & { webkitEnterFullscreen?: () => void }\n\n/**\n * Whether a full-screen control would do anything here.\n *\n * `fullscreenEnabled` is false inside a frame whose embedder did not allow it,\n * and there the control is left out rather than shown doing nothing. A phone\n * with no element full screen at all still gets the video-only kind, which\n * plays under the platform's own controls.\n */\nexport function fullscreenAvailable(): boolean {\n if (typeof document === 'undefined') return false\n if (document.fullscreenEnabled) return true\n return (\n !('requestFullscreen' in Element.prototype) &&\n 'webkitEnterFullscreen' in HTMLVideoElement.prototype\n )\n}\n\nexport function toggleFullscreen({ video, player }: LightboxPlayerElements): void {\n const doc = (player ?? video)?.ownerDocument\n if (!doc) return\n if (player && doc.fullscreenElement === player) {\n void Promise.resolve(doc.exitFullscreen()).catch(ignore)\n return\n }\n if (player && doc.fullscreenEnabled) {\n void Promise.resolve(player.requestFullscreen()).catch(ignore)\n return\n }\n const phone = video as PhoneVideoElement | null\n phone?.webkitEnterFullscreen?.()\n}\n\n/** The tracks a captions toggle turns on and off. */\nfunction captionTracks(video: HTMLVideoElement): TextTrack[] {\n // Absent where a DOM implements no media pipeline.\n const tracks: ArrayLike<TextTrack> = video.textTracks ?? []\n return Array.from(tracks).filter(\n (track) => track.kind === 'captions' || track.kind === 'subtitles',\n )\n}\n\n/** The keys a focused slider moves itself with. */\nconst SLIDER_KEYS = new Set([\n 'ArrowLeft',\n 'ArrowRight',\n 'ArrowUp',\n 'ArrowDown',\n 'PageUp',\n 'PageDown',\n 'Home',\n 'End',\n])\n\nconst ARROW_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'])\n\n/**\n * Whether the focused element uses `key` for itself, in which case no shortcut\n * may take it: `Space` and `Enter` press a focused button, the arrows and\n * `Home`/`End` move a focused slider, and a text field wants every key.\n */\nexport function keyBelongsToFocus(\n target: EventTarget | null,\n key: string,\n): boolean {\n if (!(target instanceof Element)) return false\n if (target instanceof HTMLElement && target.isContentEditable) return true\n if (target.matches('input[type=\"range\"], [role=\"slider\"]')) {\n return SLIDER_KEYS.has(key)\n }\n if (\n target.matches(\n 'button, [role=\"button\"], a[href], summary, input[type=\"checkbox\"], input[type=\"radio\"]',\n )\n ) {\n return key === ' ' || key === 'Enter'\n }\n return target.matches('input, textarea, select')\n}\n\ninterface Shortcut {\n run: (video: HTMLVideoElement, player: HTMLElement | null) => void\n /** Whether holding the key keeps doing it. A toggle held down must not flicker. */\n repeats: boolean\n}\n\nconst SHORTCUTS = new Map<string, Shortcut>([\n [' ', { run: togglePlayback, repeats: false }],\n ['k', { run: togglePlayback, repeats: false }],\n [\n 'ArrowLeft',\n {\n run: (video) => seekPlayback(video, video.currentTime - SEEK_STEP_SECONDS),\n repeats: true,\n },\n ],\n [\n 'ArrowRight',\n {\n run: (video) => seekPlayback(video, video.currentTime + SEEK_STEP_SECONDS),\n repeats: true,\n },\n ],\n ['m', { run: toggleMute, repeats: false }],\n [\n 'f',\n { run: (video, player) => toggleFullscreen({ video, player }), repeats: false },\n ],\n])\n\n/**\n * The dialog's `onKeyDown`: the player's shortcuts, from every stop inside it.\n *\n * Bound on the Modal root rather than on the controls, because focus is not\n * always on a control — a click on the film leaves it on the dialog container\n * — and a key pressed anywhere in the dialog bubbles through the root. MUI\n * calls this before its own `Escape` handling, and nothing here touches\n * `Escape`.\n *\n * A key the focused control uses for itself is left alone, and so is any key\n * a control already claimed: MUI's Slider calls `preventDefault` on the keys\n * it moves with, which is how an arrow on a slider stays that slider's.\n */\nexport function handlePlayerShortcut(\n event: KeyboardEvent<Element>,\n { video, player }: LightboxPlayerElements,\n): void {\n const key = event.key.length === 1 ? event.key.toLowerCase() : event.key\n const shortcut = SHORTCUTS.get(key)\n if (!shortcut || !video) return\n if (\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.nativeEvent.isComposing\n ) {\n return\n }\n if (keyBelongsToFocus(event.target, event.key)) return\n event.preventDefault()\n if (event.repeat && !shortcut.repeats) return\n shortcut.run(video, player)\n}\n\n/** A focus ring that reads on the player's black ground, whatever the site's palette. */\nconst focusRing = {\n outline: '2px solid',\n outlineColor: 'common.white',\n outlineOffset: 2,\n}\n\n/**\n * Every button on the player, the dialog's close button included.\n *\n * IconButton's own hover rule reads `--IconButton-hoverBg`, `hover: none`\n * reset and all; its default is a tint of `action.active`, which on a light\n * site's palette is black on this black ground and never shows. The ripple is\n * motion, so it goes for a visitor who asked for less, and it is gated in CSS\n * as the poster's play badge is so the setting resolves in both directions.\n */\nexport const controlButtonSx = (theme: Theme) => ({\n color: 'common.white',\n '--IconButton-hoverBg': (theme.vars || theme).palette.grey[800],\n '&:focus-visible': focusRing,\n '@media (prefers-reduced-motion: reduce)': {\n [`& .${touchRippleClasses.root}`]: { display: 'none' },\n },\n})\n\nconst sliderSx = {\n color: 'common.white',\n // The focused element is the thumb's visually hidden range input, so the\n // ring goes on the thumb MUI marks as focus-visible.\n [`& .${sliderClasses.thumb}.${sliderClasses.focusVisible}`]: focusRing,\n // The thumb glides to each new value; that glide is motion too.\n '@media (prefers-reduced-motion: reduce)': {\n [`& .${sliderClasses.thumb}, & .${sliderClasses.track}`]: {\n transition: 'none',\n },\n },\n}\n\n/** The film's state as the controls draw it. */\ninterface MediaSnapshot {\n paused: boolean\n currentTime: number\n duration: number\n muted: boolean\n volume: number\n}\n\n/** Every event after which the element reports something the controls draw. */\nconst MEDIA_EVENTS = [\n 'play',\n 'pause',\n 'ended',\n 'timeupdate',\n 'seeked',\n 'durationchange',\n 'loadedmetadata',\n 'volumechange',\n 'emptied',\n] as const\n\nconst snapshotOf = (video: HTMLVideoElement): MediaSnapshot => ({\n paused: video.paused,\n currentTime: video.currentTime,\n duration: video.duration,\n muted: video.muted,\n volume: video.volume,\n})\n\nexport interface VideoLightboxControlsProps {\n videoRef: RefObject<HTMLVideoElement | null>\n /** See {@link LightboxPlayerElements.player}. */\n playerRef: RefObject<HTMLElement | null>\n /** The film carries a captions track, so a toggle for it belongs here. */\n captions?: boolean\n}\n\n/**\n * The bar under the film: a seek slider across the top, then play, the clock,\n * mute, volume, captions and full screen.\n *\n * Each control is a real `<button>` or a real range input with a name and a\n * state: `aria-pressed` on the toggles, a spoken `1:23 of 4:10` on the seek\n * slider, `aria-keyshortcuts` where a key does the same thing.\n *\n * What the controls draw is read back off the `<video>` on its own events\n * rather than kept beside it, so a button, a shortcut, a click on the film and\n * the platform's media keys all end in one place, and the play beacon's\n * handlers on the element hear every one of them.\n */\nexport function VideoLightboxControls(props: VideoLightboxControlsProps) {\n const { videoRef, playerRef, captions = false } = props\n const [media, setMedia] = useState<MediaSnapshot>({\n paused: true,\n currentTime: 0,\n duration: Number.NaN,\n muted: false,\n volume: 1,\n })\n const [fullscreen, setFullscreen] = useState(false)\n const [captionsShowing, setCaptionsShowing] = useState(captions)\n const [canFullscreen] = useState(fullscreenAvailable)\n const fullscreenButtonRef = useRef<HTMLButtonElement>(null)\n /**\n * No volume slider at phone width: a phone's volume belongs to its hardware\n * buttons, iOS ignores a page that sets it, and the row has no room for it.\n * Read on the first render, so the slider never flashes in and out on open.\n */\n const theme = useTheme()\n const phone = useMediaQuery(theme.breakpoints.down('sm'), { noSsr: true })\n\n useEffect(() => {\n const video = videoRef.current\n if (!video) return undefined\n const doc = video.ownerDocument\n const tracks = video.textTracks as TextTrackList | undefined\n const syncMedia = () => setMedia(snapshotOf(video))\n /**\n * Full screen moves the player into the top layer, and the browser blurs\n * whatever had focus outside it: the dialog's own surface, where focus\n * lands on open, is outside it. Focus then sits on the body, outside the\n * Modal, where no shortcut and no `Escape` reaches the dialog. So focus\n * follows the player in, onto the control that toggles it, and is put\n * back there on the way out if the browser dropped it again.\n */\n const syncFullscreen = () => {\n const player = playerRef.current\n setFullscreen(player !== null && doc.fullscreenElement === player)\n if (player && !player.contains(doc.activeElement)) {\n fullscreenButtonRef.current?.focus()\n }\n }\n const syncCaptions = () =>\n setCaptionsShowing(\n captionTracks(video).some((track) => track.mode === 'showing'),\n )\n syncMedia()\n for (const type of MEDIA_EVENTS) video.addEventListener(type, syncMedia)\n doc.addEventListener('fullscreenchange', syncFullscreen)\n tracks?.addEventListener?.('change', syncCaptions)\n return () => {\n for (const type of MEDIA_EVENTS) video.removeEventListener(type, syncMedia)\n doc.removeEventListener('fullscreenchange', syncFullscreen)\n tracks?.removeEventListener?.('change', syncCaptions)\n }\n }, [videoRef, playerRef])\n\n const seekable = Number.isFinite(media.duration) && media.duration > 0\n const position = seekable\n ? Math.min(Math.max(media.currentTime, 0), media.duration)\n : 0\n const playing = !media.paused\n const level = media.muted ? 0 : Math.round(media.volume * 100)\n\n const withVideo = (action: (video: HTMLVideoElement) => void) => () => {\n const video = videoRef.current\n if (video) action(video)\n }\n\n /**\n * MUI's Slider steps an arrow by `step`, which is also the grain a pointer\n * drag snaps to. One second keeps a drag as fine as the clock it moves, and\n * this scales an arrow up to the same five seconds the arrows give from\n * every other stop, in the direction MUI chose, so a right-to-left page's\n * arrows still point the way the track runs.\n */\n const onSeek = (event: Event, value: number | number[]) => {\n const video = videoRef.current\n if (!video || typeof value !== 'number') return\n const arrow =\n event.type === 'keydown' &&\n ARROW_KEYS.has((event as globalThis.KeyboardEvent).key)\n const step = (event as globalThis.KeyboardEvent).shiftKey\n ? SEEK_PAGE_SECONDS\n : SEEK_STEP_SECONDS\n const target = arrow\n ? video.currentTime + Math.sign(value - position) * step\n : value\n const landed = seekPlayback(video, target)\n // Drawn now rather than on `seeked`, so the thumb does not spring back\n // to where it was for the frames the seek takes.\n if (landed !== undefined) {\n setMedia((previous) => ({ ...previous, currentTime: landed }))\n }\n }\n\n /**\n * Zero mutes rather than zeroing the volume, so unmuting brings back the\n * last level the visitor could hear.\n */\n const onVolume = (_event: Event, value: number | number[]) => {\n const video = videoRef.current\n if (!video || typeof value !== 'number') return\n if (value <= 0) {\n video.muted = true\n return\n }\n video.volume = value / 100\n video.muted = false\n }\n\n const onCaptions = withVideo((video) => {\n const next = !captionsShowing\n for (const track of captionTracks(video)) {\n track.mode = next ? 'showing' : 'hidden'\n }\n setCaptionsShowing(next)\n })\n\n return (\n <Box>\n <Box sx={{ px: 2 }}>\n <Slider\n aria-label=\"Seek\"\n size=\"small\"\n min={0}\n max={seekable ? media.duration : 1}\n step={1}\n shiftStep={SEEK_PAGE_SECONDS}\n value={position}\n disabled={!seekable}\n getAriaValueText={(value) => seekValueText(value, media.duration)}\n onChange={onSeek}\n sx={sliderSx}\n />\n </Box>\n <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, px: 1, pb: 1 }}>\n <IconButton\n aria-label=\"Play\"\n aria-pressed={playing}\n aria-keyshortcuts=\"k Space\"\n onClick={withVideo(togglePlayback)}\n sx={controlButtonSx}\n >\n <MdiIcon path={playing ? mdiPause.path : mdiPlay.path} />\n </IconButton>\n <Typography\n component=\"span\"\n variant=\"body2\"\n // The seek slider already speaks this, as `1:23 of 4:10`.\n aria-hidden\n sx={{\n color: 'common.white',\n fontVariantNumeric: 'tabular-nums',\n whiteSpace: 'nowrap',\n }}\n >\n {`${formatMediaTime(position)} / ${formatMediaTime(media.duration)}`}\n </Typography>\n <Box sx={{ flexGrow: 1 }} />\n <IconButton\n aria-label=\"Mute\"\n aria-pressed={media.muted}\n aria-keyshortcuts=\"m\"\n onClick={withVideo(toggleMute)}\n sx={controlButtonSx}\n >\n <MdiIcon\n path={\n media.muted || media.volume === 0\n ? mdiVolumeOff.path\n : mdiVolumeHigh.path\n }\n />\n </IconButton>\n {phone ? null : (\n <Slider\n aria-label=\"Volume\"\n size=\"small\"\n min={0}\n max={100}\n step={VOLUME_STEP_PERCENT}\n value={level}\n getAriaValueText={(value) => (value === 0 ? 'Muted' : `${value}%`)}\n onChange={onVolume}\n sx={[sliderSx, { width: theme.spacing(12), mx: 1 }]}\n />\n )}\n {captions ? (\n <IconButton\n aria-label=\"Captions\"\n aria-pressed={captionsShowing}\n onClick={onCaptions}\n sx={controlButtonSx}\n >\n <MdiIcon\n path={\n captionsShowing\n ? mdiClosedCaption.path\n : mdiClosedCaptionOutline.path\n }\n />\n </IconButton>\n ) : null}\n {canFullscreen ? (\n <IconButton\n ref={fullscreenButtonRef}\n aria-label=\"Full screen\"\n aria-pressed={fullscreen}\n aria-keyshortcuts=\"f\"\n onClick={() =>\n toggleFullscreen({\n video: videoRef.current,\n player: playerRef.current,\n })\n }\n sx={controlButtonSx}\n >\n <MdiIcon path={fullscreen ? mdiFullscreenExit.path : mdiFullscreen.path} />\n </IconButton>\n ) : null}\n </Box>\n </Box>\n )\n}\nVideoLightboxControls.displayName = 'VideoLightboxControls'\n"],"names":["mdiClosedCaption","mdiClosedCaptionOutline","mdiFullscreen","mdiFullscreenExit","mdiPause","mdiPlay","mdiVolumeHigh","mdiVolumeOff","MdiIcon","Box","touchRippleClasses","IconButton","Slider","sliderClasses","useTheme","Typography","useMediaQuery","useEffect","useRef","useState","SEEK_STEP_SECONDS","SEEK_PAGE_SECONDS","VOLUME_STEP_PERCENT","ignore","undefined","formatMediaTime","seconds","whole","Number","isFinite","Math","floor","hours","minutes","rest","String","padStart","seekValueText","position","duration","togglePlayback","video","paused","ended","Promise","resolve","play","catch","pause","seekPlayback","landed","min","max","currentTime","toggleMute","muted","fullscreenAvailable","document","fullscreenEnabled","Element","prototype","HTMLVideoElement","toggleFullscreen","player","phone","doc","ownerDocument","fullscreenElement","exitFullscreen","requestFullscreen","webkitEnterFullscreen","captionTracks","tracks","textTracks","Array","from","filter","track","kind","SLIDER_KEYS","Set","ARROW_KEYS","keyBelongsToFocus","target","key","HTMLElement","isContentEditable","matches","has","SHORTCUTS","Map","run","repeats","handlePlayerShortcut","event","length","toLowerCase","shortcut","get","defaultPrevented","altKey","ctrlKey","metaKey","nativeEvent","isComposing","preventDefault","repeat","focusRing","outline","outlineColor","outlineOffset","controlButtonSx","theme","color","vars","palette","grey","root","display","sliderSx","thumb","focusVisible","transition","MEDIA_EVENTS","snapshotOf","volume","VideoLightboxControls","props","videoRef","playerRef","captions","media","setMedia","NaN","fullscreen","setFullscreen","captionsShowing","setCaptionsShowing","canFullscreen","fullscreenButtonRef","breakpoints","down","noSsr","current","syncMedia","syncFullscreen","contains","activeElement","focus","syncCaptions","some","mode","type","addEventListener","removeEventListener","seekable","playing","level","round","withVideo","action","onSeek","value","arrow","step","shiftKey","sign","previous","onVolume","_event","onCaptions","next","sx","px","aria-label","size","shiftStep","disabled","getAriaValueText","onChange","alignItems","gap","pb","aria-pressed","aria-keyshortcuts","onClick","path","component","variant","aria-hidden","fontVariantNumeric","whiteSpace","flexGrow","width","spacing","mx","ref","displayName"],"mappings":"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"}
|