@design-edito/tools 0.5.2 → 0.5.3
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/agnostic/html/deep-select/index.js +5 -3
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +8 -8
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +8 -8
- package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +2 -2
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +2 -2
- package/agnostic/html/index.d.ts +1 -1
- package/agnostic/html/index.js +1 -1
- package/agnostic/misc/index.d.ts +3 -3
- package/agnostic/misc/index.js +3 -3
- package/agnostic/numbers/index.d.ts +1 -1
- package/agnostic/numbers/index.js +1 -1
- package/agnostic/optim/index.d.ts +1 -1
- package/agnostic/optim/index.js +1 -1
- package/agnostic/strings/index.d.ts +1 -1
- package/agnostic/strings/index.js +1 -1
- package/agnostic/time/dates/format-date/index.d.ts +4 -2
- package/agnostic/time/dates/format-date/index.js +43 -94
- package/agnostic/time/dates/format-date/index.test.js +154 -0
- package/agnostic/time/dates/format-date/parts.d.ts +28 -0
- package/agnostic/time/dates/format-date/parts.js +136 -0
- package/agnostic/time/dates/format-date/types.d.ts +84 -0
- package/agnostic/time/dates/format-date/types.js +1 -0
- package/agnostic/time/duration/format-duration/index.d.ts +42 -0
- package/agnostic/time/duration/format-duration/index.js +94 -0
- package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
- package/agnostic/time/duration/format-duration/index.test.js +148 -0
- package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
- package/agnostic/time/duration/format-duration/parts.js +107 -0
- package/agnostic/time/duration/format-duration/types.d.ts +93 -0
- package/agnostic/time/duration/format-duration/types.js +1 -0
- package/agnostic/time/duration/index.d.ts +6 -1
- package/agnostic/time/duration/index.js +2 -2
- package/agnostic/time/duration/index.test.d.ts +1 -0
- package/agnostic/time/duration/index.test.js +168 -0
- package/agnostic/time/index.d.ts +1 -1
- package/agnostic/time/index.js +1 -1
- package/components/BeforeAfter/index.controlled.d.ts +25 -25
- package/components/BeforeAfter/index.controlled.js +47 -53
- package/components/BeforeAfter/index.d.ts +20 -23
- package/components/BeforeAfter/index.js +48 -40
- package/components/Button/index.js +1 -1
- package/components/Clippable/index.d.ts +17 -26
- package/components/Clippable/index.js +21 -23
- package/components/Disclaimer/index.d.ts +29 -22
- package/components/Disclaimer/index.js +29 -24
- package/components/Drawer/index.d.ts +45 -25
- package/components/Drawer/index.js +61 -52
- package/components/EventListener/index.d.ts +4 -5
- package/components/Gallery/index.d.ts +14 -16
- package/components/Gallery/index.js +25 -30
- package/components/Iframe/index.js +2 -2
- package/components/Image/index.d.ts +5 -12
- package/components/Image/index.js +11 -32
- package/components/Input/index.d.ts +23 -19
- package/components/Input/index.js +30 -20
- package/components/JsonEditor/index.d.ts +177 -9
- package/components/JsonEditor/index.js +133 -77
- package/components/JsonEditor/types.d.ts +11 -0
- package/components/JsonEditor/types.js +1 -0
- package/components/JsonEditor/utils.d.ts +8 -0
- package/components/JsonEditor/utils.js +23 -0
- package/components/ListLoader/index.controlled.d.ts +5 -5
- package/components/ListLoader/index.controlled.js +4 -4
- package/components/ListLoader/index.d.ts +11 -11
- package/components/ListLoader/index.js +12 -12
- package/components/Overlayer/index.js +4 -4
- package/components/Paginator/index.d.ts +7 -12
- package/components/Paginator/index.js +7 -11
- package/components/ResizeObserver/index.d.ts +14 -3
- package/components/ResizeObserver/index.js +49 -24
- package/components/Scrllgngn/index.d.ts +17 -13
- package/components/Scrllgngn/index.js +25 -84
- package/components/Scrllgngn/utils.d.ts +52 -0
- package/components/Scrllgngn/utils.js +102 -0
- package/components/ScrollListener/index.d.ts +43 -28
- package/components/ScrollListener/index.js +59 -79
- package/components/ScrollListener/utils.d.ts +37 -16
- package/components/ScrollListener/utils.js +141 -28
- package/components/Select/index.d.ts +24 -20
- package/components/Select/index.js +30 -20
- package/components/Sequencer/index.controlled.d.ts +16 -26
- package/components/Sequencer/index.controlled.js +15 -12
- package/components/Sequencer/index.d.ts +25 -30
- package/components/Sequencer/index.js +20 -34
- package/components/Subtitles/index.d.ts +29 -5
- package/components/Subtitles/index.js +90 -12
- package/components/Textarea/index.d.ts +26 -20
- package/components/Textarea/index.js +40 -21
- package/components/Theatre/index.d.ts +35 -39
- package/components/Theatre/index.js +46 -58
- package/components/UIModule/index.d.ts +25 -29
- package/components/UIModule/index.js +81 -110
- package/components/Video/index.controlled.d.ts +51 -51
- package/components/Video/index.controlled.js +75 -72
- package/components/Video/index.d.ts +12 -14
- package/components/Video/index.js +41 -86
- package/components/Video/utils.d.ts +35 -3
- package/components/Video/utils.js +37 -14
- package/components/index.d.ts +1 -1
- package/components/index.js +1 -1
- package/components/utils/index.d.ts +22 -0
- package/components/utils/index.js +31 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/node/@aws-s3/storage/file/index.d.ts +1 -1
- package/node/@aws-s3/storage/file/index.js +1 -1
- package/node/@google-cloud/storage/index.d.ts +1 -1
- package/node/@google-cloud/storage/index.js +1 -1
- package/node/images/index.d.ts +1 -1
- package/node/images/index.js +1 -1
- package/node/images/transform/operations/index.d.ts +1 -1
- package/node/images/transform/operations/index.js +1 -1
- package/node/index.d.ts +1 -1
- package/node/index.js +1 -1
- package/package.json +20 -5
- package/TODO.md +0 -269
- package/components/BeforeAfter/utils.d.ts +0 -4
- package/components/BeforeAfter/utils.js +0 -9
- package/components/Input/index.controlled.d.ts +0 -42
- package/components/Input/index.controlled.js +0 -54
- package/components/Select/index.controlled.d.ts +0 -43
- package/components/Select/index.controlled.js +0 -54
- package/components/Subtitles/types.d.ts +0 -24
- package/components/Subtitles/utils.d.ts +0 -32
- package/components/Subtitles/utils.js +0 -107
- package/components/Textarea/index.controlled.d.ts +0 -50
- package/components/Textarea/index.controlled.js +0 -75
- /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
- /package/components/ResizeObserver/{style.module.css → styles.module.css} +0 -0
|
@@ -1,36 +1,33 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { clss } from '../../agnostic/css/clss/index.js';
|
|
4
4
|
import { unknownToString } from '../../agnostic/errors/unknown-to-string/index.js';
|
|
5
5
|
import { isNonNullObject } from '../../agnostic/objects/is-object/index.js';
|
|
6
6
|
import { randomHash } from '../../agnostic/random/uuid/index.js';
|
|
7
|
-
import { mergeClassNames } from '../utils/index.js';
|
|
7
|
+
import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
|
|
8
8
|
import { uiModule as publicClassName } from '../public-classnames.js';
|
|
9
9
|
import cssModule from './styles.module.css';
|
|
10
10
|
/**
|
|
11
|
-
* Dynamic UI module host
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* error, and teardown states automatically.
|
|
11
|
+
* Dynamic UI module host. Asynchronously imports an ES module by URL, validates
|
|
12
|
+
* its exported interface, calls its `init` lifecycle to obtain a DOM `Element`,
|
|
13
|
+
* and appends that element to its own root `<div>`.
|
|
15
14
|
*
|
|
16
15
|
* The imported module is expected to conform to the {@link ModuleData} interface.
|
|
17
16
|
* Any violation (missing exports, wrong types, `init` not returning an `Element`)
|
|
18
17
|
* transitions the component into the `--error` state and logs to `console.error`.
|
|
19
18
|
*
|
|
20
|
-
* ###
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* -
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
28
|
-
* - `--initialized` — the `Element` returned by `init` has been appended to the
|
|
29
|
-
* host `<div>`.
|
|
19
|
+
* ### CSS modifiers
|
|
20
|
+
* Reflecting the current load lifecycle:
|
|
21
|
+
* - `loading` — the module fetch is in progress.
|
|
22
|
+
* - `no-module` — nothing has been loaded yet (`src` is undefined, or the
|
|
23
|
+
* effect has not run).
|
|
24
|
+
* - `error` — the import, the validation, or the `init` call failed.
|
|
25
|
+
* - `loaded` — the module passed validation and `init` returned successfully.
|
|
26
|
+
* - `initialized` — the `Element` returned by `init` has been appended.
|
|
30
27
|
*
|
|
31
28
|
* ### Root element attributes
|
|
32
|
-
* - `id` — a stable
|
|
33
|
-
*
|
|
29
|
+
* - `id` — a stable generated id, assigned once on mount and used to scope the
|
|
30
|
+
* module's `css` entries to this specific instance.
|
|
34
31
|
*
|
|
35
32
|
* @param props - Component properties.
|
|
36
33
|
* @see {@link Props}
|
|
@@ -38,107 +35,81 @@ import cssModule from './styles.module.css';
|
|
|
38
35
|
* @returns A host `<div>` into which the module's root `Element` is appended,
|
|
39
36
|
* along with any `<style>` blocks exported by the module.
|
|
40
37
|
*/
|
|
41
|
-
export const UIModule = ({ src, props,
|
|
38
|
+
export const UIModule = ({ src, props, onIdGenerated, onIsLoadingChanged, onLoadedModuleChanged, onModuleTargetChanged, className }) => {
|
|
42
39
|
// State & refs
|
|
43
|
-
const [id] = useState(`f${randomHash(10)}`);
|
|
44
|
-
const [
|
|
45
|
-
const [loadedModule,
|
|
46
|
-
const [moduleTarget,
|
|
40
|
+
const [id] = useState(() => `f${randomHash(10)}`);
|
|
41
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
42
|
+
const [loadedModule, setLoadedModule] = useState(null);
|
|
43
|
+
const [moduleTarget, setModuleTarget] = useState(null);
|
|
47
44
|
const rootRef = useRef(null);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// State changes dispatch
|
|
59
|
-
useEffect(() => { stateHandlers?.idChanged?.(id); }, [id, stateHandlers]);
|
|
60
|
-
useEffect(() => { stateHandlers?.isLoadingChanged?.(loading); }, [loading, stateHandlers]);
|
|
61
|
-
useEffect(() => { stateHandlers?.loadedModuleChanged?.(loadedModule); }, [loadedModule, stateHandlers]);
|
|
62
|
-
useEffect(() => { stateHandlers?.moduleTargetChanged?.(moduleTarget); }, [moduleTarget, stateHandlers]);
|
|
63
|
-
// Fx dep. `src` - load & init module effect
|
|
45
|
+
// What the teardown needs, held outside state so the load effect can depend on
|
|
46
|
+
// `src` alone and still destroy whatever is actually live at cleanup time.
|
|
47
|
+
const liveInstanceRef = useRef(null);
|
|
48
|
+
// State dispatch
|
|
49
|
+
useChangeDispatch(isLoading, onIsLoadingChanged);
|
|
50
|
+
useChangeDispatch(loadedModule, onLoadedModuleChanged);
|
|
51
|
+
useChangeDispatch(moduleTarget, onModuleTargetChanged);
|
|
52
|
+
// Fx. no dep. - report the generated id, which never changes afterwards
|
|
53
|
+
useEffect(() => { onIdGenerated?.(id); }, []);
|
|
54
|
+
// Fx. dep. `src` - import, validate and initialize the module
|
|
64
55
|
useEffect(() => {
|
|
65
56
|
if (src === undefined)
|
|
66
57
|
return;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- shape fully validated by the checks above
|
|
98
|
-
const module = data;
|
|
99
|
-
setLoadedModule(module);
|
|
100
|
-
try {
|
|
101
|
-
const target = module.init(props ?? {});
|
|
102
|
-
if (!(target instanceof Element))
|
|
103
|
-
return setLoadedModule(errs.initRetElt);
|
|
104
|
-
setModuleTarget(target);
|
|
105
|
-
}
|
|
106
|
-
catch (err) {
|
|
107
|
-
setModuleTarget(null);
|
|
108
|
-
const e = err instanceof Error
|
|
109
|
-
? err
|
|
110
|
-
: new Error(unknownToString(err));
|
|
111
|
-
setLoadedModule(e);
|
|
112
|
-
}
|
|
113
|
-
}).catch((err) => {
|
|
114
|
-
setLoading(false);
|
|
115
|
-
setLoadedModule(err instanceof Error ? err : new Error(unknownToString(err)));
|
|
58
|
+
setIsLoading(true);
|
|
59
|
+
void import(src)
|
|
60
|
+
.then(data => {
|
|
61
|
+
setIsLoading(false);
|
|
62
|
+
if (!isNonNullObject(data))
|
|
63
|
+
return setLoadedModule(new Error('Not a module'));
|
|
64
|
+
if (!('init' in data) || typeof data.init !== 'function')
|
|
65
|
+
return setLoadedModule(new Error('Module exported member `init` must be a function'));
|
|
66
|
+
if (!('destroy' in data) || typeof data.destroy !== 'function')
|
|
67
|
+
return setLoadedModule(new Error('Module exported member `destroy` must be a function'));
|
|
68
|
+
if ('css' in data) {
|
|
69
|
+
if (!Array.isArray(data.css))
|
|
70
|
+
return setLoadedModule(new Error('Module exported member `css` must be an array of strings'));
|
|
71
|
+
if (data.css.some(entry => typeof entry !== 'string'))
|
|
72
|
+
return setLoadedModule(new Error('Module exported member `css` must be an array of strings'));
|
|
73
|
+
}
|
|
74
|
+
if ('update' in data && typeof data.update !== 'function')
|
|
75
|
+
return setLoadedModule(new Error('Module exported member `update` must be a function'));
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- shape fully validated by the checks above
|
|
77
|
+
const module = data;
|
|
78
|
+
setLoadedModule(module);
|
|
79
|
+
try {
|
|
80
|
+
const target = module.init(props ?? {});
|
|
81
|
+
if (!(target instanceof Element))
|
|
82
|
+
return setLoadedModule(new Error('Module exported function `init` must return an Element'));
|
|
83
|
+
liveInstanceRef.current = { module, target };
|
|
84
|
+
setModuleTarget(target);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
liveInstanceRef.current = null;
|
|
116
88
|
setModuleTarget(null);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
89
|
+
setLoadedModule(err instanceof Error ? err : new Error(unknownToString(err)));
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.catch((err) => {
|
|
93
|
+
setIsLoading(false);
|
|
94
|
+
liveInstanceRef.current = null;
|
|
95
|
+
setLoadedModule(err instanceof Error ? err : new Error(unknownToString(err)));
|
|
96
|
+
setModuleTarget(null);
|
|
97
|
+
});
|
|
125
98
|
return () => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (loadedModuleRef.current instanceof Error)
|
|
129
|
-
return;
|
|
130
|
-
if (loadedModuleRef.current === null)
|
|
99
|
+
const liveInstance = liveInstanceRef.current;
|
|
100
|
+
if (liveInstance === null)
|
|
131
101
|
return;
|
|
132
|
-
|
|
102
|
+
liveInstance.module.destroy(liveInstance.target);
|
|
103
|
+
liveInstanceRef.current = null;
|
|
133
104
|
};
|
|
134
105
|
}, [src]);
|
|
135
|
-
// Fx dep. `loadedModule` -
|
|
106
|
+
// Fx. dep. `loadedModule` - surface load errors
|
|
136
107
|
useEffect(() => {
|
|
137
108
|
// eslint-disable-next-line no-console
|
|
138
109
|
if (loadedModule instanceof Error)
|
|
139
110
|
console.error(loadedModule);
|
|
140
111
|
}, [loadedModule]);
|
|
141
|
-
// Fx dep. `moduleTarget` - append the
|
|
112
|
+
// Fx. dep. `moduleTarget` - append the element the module built
|
|
142
113
|
useEffect(() => {
|
|
143
114
|
if (moduleTarget === null)
|
|
144
115
|
return;
|
|
@@ -148,14 +119,14 @@ export const UIModule = ({ src, props, stateHandlers, className }) => {
|
|
|
148
119
|
}, [moduleTarget]);
|
|
149
120
|
// Rendering
|
|
150
121
|
const c = clss(publicClassName, { cssModule });
|
|
122
|
+
const hasErrored = loadedModule instanceof Error;
|
|
151
123
|
const rootClss = mergeClassNames(c(null, {
|
|
152
|
-
loading,
|
|
124
|
+
'loading': isLoading,
|
|
153
125
|
'no-module': loadedModule === null,
|
|
154
|
-
'error':
|
|
155
|
-
'loaded': !
|
|
126
|
+
'error': hasErrored,
|
|
127
|
+
'loaded': !isLoading && loadedModule !== null && !hasErrored,
|
|
156
128
|
'initialized': moduleTarget !== null
|
|
157
129
|
}), className);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
&& loadedModule.css?.map(css => _jsx("style", { children: `.${publicClassName}#${id} { ${css} }` }))] });
|
|
130
|
+
const moduleCss = hasErrored || loadedModule === null ? [] : loadedModule.css ?? [];
|
|
131
|
+
return _jsx("div", { className: rootClss, ref: rootRef, id: id, children: moduleCss.map((css, cssPos) => _jsx("style", { children: `.${publicClassName}#${id} { ${css} }` }, cssPos)) });
|
|
161
132
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FunctionComponent, type PropsWithChildren, type VideoHTMLAttributes
|
|
1
|
+
import { type FunctionComponent, type PropsWithChildren, type VideoHTMLAttributes } from 'react';
|
|
2
2
|
import type { WithClassName } from '../utils/types.js';
|
|
3
3
|
import { type Props as SubsProps } from '../Subtitles/index.js';
|
|
4
4
|
/**
|
|
@@ -27,47 +27,6 @@ type TrackData = {
|
|
|
27
27
|
label?: string;
|
|
28
28
|
default?: boolean;
|
|
29
29
|
};
|
|
30
|
-
/**
|
|
31
|
-
* Callbacks for user actions on the video player controls.
|
|
32
|
-
* Allows you to intercept user actions on the player's buttons and sliders.
|
|
33
|
-
*
|
|
34
|
-
* @property playButtonClick - Called when the play button is clicked. Receives the event, isPlaying state and HTMLVideoElement before any change happens. If the time is not controlled by parent (no currentTime given as props), the component will play to the target time right after.
|
|
35
|
-
* @property pauseButtonClick - Called when the pause button is clicked. Receives the event, isPlaying state and HTMLVideoElement before any change happens. If the time is not controlled by parent (no currentTime given as props), the component will pause to the target time right after.
|
|
36
|
-
* @property loudButtonClick - Called when the "loud" (unmute) button is clicked. Receives the event, isLoud state and HTMLVideoElement before any change happens.
|
|
37
|
-
* @property muteButtonClick - Called when the mute button is clicked. Receives the event, isLoud state and HTMLVideoElement before any change happens.
|
|
38
|
-
* @property volumeRangeChange - Called when the volume slider is changed. Receives the event, target volume (0 to 1), current volume (0 to 1) and HTMLVideoElement before any change happens.
|
|
39
|
-
* @property fullscreenButtonClick - Called when the fullscreen button is clicked. Receives the event, isFullscreen state and HTMLVideoElement before any change happens.
|
|
40
|
-
* @property rateRangeChange - Called when the playback rate slider is changed. Receives the event, target rate, current rate and HTMLVideoElement before any change happens.
|
|
41
|
-
* @property timelineClick - Called when the timeline is clicked. Receives the event, target time (in seconds), current time (in seconds) and HTMLVideoElement before any change happens. If the time is not controlled by parent (no currentTime given as props), the component will update the video current time to the target time right after.
|
|
42
|
-
*/
|
|
43
|
-
export type ActionHandlersProps = {
|
|
44
|
-
playButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
|
|
45
|
-
pauseButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
|
|
46
|
-
loudButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
|
|
47
|
-
muteButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
|
|
48
|
-
volumeRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetVolumePercent: number, currentVolumePercent: number, video: HTMLVideoElement | null) => void;
|
|
49
|
-
fullscreenButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isFullscreen: boolean, video: HTMLVideoElement | null) => void;
|
|
50
|
-
rateRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetRate: number, rate: number, video: HTMLVideoElement | null) => void;
|
|
51
|
-
timelineClick?: (e: React.MouseEvent<HTMLDivElement>, time: number, currentTime: number, video: HTMLVideoElement | null) => void;
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* Callbacks to synchronize the internal player state with the outside.
|
|
55
|
-
*
|
|
56
|
-
* @property isPlaying - Called whenever the play/pause state changes.
|
|
57
|
-
* @property isFullscreen - Called whenever the fullscreen state changes.
|
|
58
|
-
* @property isLoud - Called whenever the mute/unmute state changes (true = unmuted, false = muted).
|
|
59
|
-
* @property volume - Called whenever the volume changes (value between 0 and 1).
|
|
60
|
-
* @property playbackRate - Called whenever the playback speed changes.
|
|
61
|
-
* @property currentTime - Called on every change of the current time (in seconds).
|
|
62
|
-
*/
|
|
63
|
-
export type StateHandlersProps = {
|
|
64
|
-
isPlaying?: (isPlaying: boolean) => void;
|
|
65
|
-
isFullscreen?: (isFullscreen: boolean) => void;
|
|
66
|
-
isLoud?: (isLoud: boolean) => void;
|
|
67
|
-
volume?: (volume: number) => void;
|
|
68
|
-
playbackRate?: (rate: number) => void;
|
|
69
|
-
currentTime?: (currentTime: number) => void;
|
|
70
|
-
};
|
|
71
30
|
/**
|
|
72
31
|
* Props for the ControlledVideo component.
|
|
73
32
|
*
|
|
@@ -84,11 +43,41 @@ export type StateHandlersProps = {
|
|
|
84
43
|
* @property volume - External control of volume (0 to 1).
|
|
85
44
|
* @property mute - External control of mute (true = muted).
|
|
86
45
|
* @property playbackRate - External control of playback speed.
|
|
87
|
-
* @property currentTimeMs - External control of current time (in ms).
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
46
|
+
* @property currentTimeMs - External control of current time (in ms). Providing
|
|
47
|
+
* it hands ownership of the time to the parent: the value is displayed as given,
|
|
48
|
+
* the element is seeked to it on every change, and user interactions that would
|
|
49
|
+
* otherwise move the time (timeline click) no longer do — the parent is expected
|
|
50
|
+
* to update the prop instead. Because a playing element advances the time by
|
|
51
|
+
* itself, a controlled time also implies a stopped video: `play` and `autoPlay`
|
|
52
|
+
* are ignored for as long as this prop is provided.
|
|
53
|
+
* @property onPlayButtonClicked - Called when the play button is clicked, before
|
|
54
|
+
* the component reacts, with the playback state as it was.
|
|
55
|
+
* @property onPauseButtonClicked - Called when the pause button is clicked, before
|
|
56
|
+
* the component reacts, with the playback state as it was.
|
|
57
|
+
* @property onLoudButtonClicked - Called when the unmute button is clicked, before
|
|
58
|
+
* the component reacts, with the mute state as it was (`true` = unmuted).
|
|
59
|
+
* @property onMuteButtonClicked - Called when the mute button is clicked, before
|
|
60
|
+
* the component reacts, with the mute state as it was (`true` = unmuted).
|
|
61
|
+
* @property onVolumeRangeChanged - Called when the volume slider moves, before the
|
|
62
|
+
* component reacts, with the target and current volumes (both `0` to `1`).
|
|
63
|
+
* @property onRateRangeChanged - Called when the playback rate slider moves, before
|
|
64
|
+
* the component reacts, with the target and current rates.
|
|
65
|
+
* @property onFullscreenButtonClicked - Called when the fullscreen button is
|
|
66
|
+
* clicked, before the component reacts, with the fullscreen state as it was.
|
|
67
|
+
* @property onTimelineClicked - Called when the timeline is clicked, before the
|
|
68
|
+
* component reacts, with the target and current times (in seconds). The component
|
|
69
|
+
* seeks to the target right after, unless the time is controlled.
|
|
70
|
+
* @property onIsPlayingChanged - Called once the playback state has changed.
|
|
71
|
+
* @property onIsFullscreenChanged - Called once the fullscreen state has changed.
|
|
72
|
+
* @property onIsLoudChanged - Called once the mute state has changed (`true` = unmuted).
|
|
73
|
+
* @property onVolumeChanged - Called once the volume has changed (`0` to `1`).
|
|
74
|
+
* @property onPlaybackRateChanged - Called once the playback rate has changed.
|
|
75
|
+
* @property onCurrentTimeMsChanged - Called once the current time has changed, in
|
|
76
|
+
* milliseconds, to match the `currentTimeMs` prop.
|
|
77
|
+
* @property onFullscreenChange - Called when the *browser* enters or leaves
|
|
78
|
+
* fullscreen on its own — pressing Escape, typically — with the new state. This is
|
|
79
|
+
* the signal a parent needs to update its `fullscreen` prop, and it is distinct
|
|
80
|
+
* from `onIsFullscreenChanged`, which merely echoes that prop back once changed.
|
|
92
81
|
* @property className - Additional CSS class for the root element.
|
|
93
82
|
* @property children - React content inserted into the <video> tag (fallback, etc).
|
|
94
83
|
*
|
|
@@ -109,10 +98,21 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
109
98
|
mute?: boolean;
|
|
110
99
|
playbackRate?: number;
|
|
111
100
|
currentTimeMs?: number;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
onPlayButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
|
|
102
|
+
onPauseButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
|
|
103
|
+
onLoudButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
|
|
104
|
+
onMuteButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
|
|
105
|
+
onVolumeRangeChanged?: (e: React.ChangeEvent<HTMLInputElement>, targetVolume: number, currentVolume: number, video: HTMLVideoElement | null) => void;
|
|
106
|
+
onRateRangeChanged?: (e: React.ChangeEvent<HTMLInputElement>, targetRate: number, currentRate: number, video: HTMLVideoElement | null) => void;
|
|
107
|
+
onFullscreenButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isFullscreen: boolean, video: HTMLVideoElement | null) => void;
|
|
108
|
+
onTimelineClicked?: (e: React.MouseEvent<HTMLDivElement>, targetTime: number, currentTime: number, video: HTMLVideoElement | null) => void;
|
|
109
|
+
onIsPlayingChanged?: (isPlaying: boolean) => void;
|
|
110
|
+
onIsFullscreenChanged?: (isFullscreen: boolean) => void;
|
|
111
|
+
onIsLoudChanged?: (isLoud: boolean) => void;
|
|
112
|
+
onVolumeChanged?: (volume: number) => void;
|
|
113
|
+
onPlaybackRateChanged?: (playbackRate: number) => void;
|
|
114
|
+
onCurrentTimeMsChanged?: (currentTimeMs: number) => void;
|
|
115
|
+
onFullscreenChange?: (isFullscreen: boolean) => void;
|
|
116
116
|
}> & VideoHTMLAttributes<HTMLVideoElement>>;
|
|
117
117
|
/**
|
|
118
118
|
* Full-featured video player component. Wraps a native `<video>` element with
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
|
3
3
|
import { clss } from '../../agnostic/css/clss/index.js';
|
|
4
|
-
import { mergeClassNames } from '../utils/index.js';
|
|
4
|
+
import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
|
|
5
5
|
import cssModule from './styles.module.css';
|
|
6
6
|
import { video as publicClassName } from '../public-classnames.js';
|
|
7
7
|
import { Subtitles } from '../Subtitles/index.js';
|
|
@@ -41,79 +41,79 @@ import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause,
|
|
|
41
41
|
* @returns A `<figure>` element containing the video, its controls, optional
|
|
42
42
|
* subtitles.
|
|
43
43
|
*/
|
|
44
|
-
export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pauseBtnContent, loudBtnContent, muteBtnContent, fullscreenBtnContent, play, fullscreen, mute, muted, volume = 1, playbackRate = 1, currentTimeMs: givenCurrentTimeMs,
|
|
44
|
+
export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pauseBtnContent, loudBtnContent, muteBtnContent, fullscreenBtnContent, play, fullscreen, mute, muted, volume = 1, playbackRate = 1, currentTimeMs: givenCurrentTimeMs, onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, onTimelineClicked, onIsPlayingChanged, onIsFullscreenChanged, onIsLoudChanged, onVolumeChanged, onPlaybackRateChanged, onCurrentTimeMsChanged, onFullscreenChange, children, className, ...intrinsicVideoAttributes }) => {
|
|
45
45
|
const videoRef = useRef(null);
|
|
46
46
|
const [totalTime, setTotalTime] = useState(0);
|
|
47
47
|
const totalTimeMs = useMemo(() => secondsToMs(totalTime), [totalTime]);
|
|
48
|
-
const [
|
|
48
|
+
const [internalCurrentTimeMs, setInternalCurrentTimeMs] = useState(0);
|
|
49
|
+
const isTimeControlled = givenCurrentTimeMs !== undefined;
|
|
50
|
+
// The parent owns the time as soon as it provides one, so that is what gets
|
|
51
|
+
// displayed — not what the element reported one render later.
|
|
52
|
+
const currentTimeMs = givenCurrentTimeMs ?? internalCurrentTimeMs;
|
|
49
53
|
const currentTime = useMemo(() => msToSeconds(currentTimeMs), [currentTimeMs]);
|
|
50
|
-
const isTimeControlled = useMemo(() => givenCurrentTimeMs !== undefined, [givenCurrentTimeMs]);
|
|
51
54
|
const volumePercent = useMemo(() => volume * 100, [volume]);
|
|
55
|
+
// Every state item this component displays is read from its props, never from
|
|
56
|
+
// the element: it is the parent's job to keep them in sync.
|
|
57
|
+
const isPlaying = play ?? false;
|
|
58
|
+
const isLoud = !(mute ?? false);
|
|
59
|
+
const isFullscreen = fullscreen ?? false;
|
|
52
60
|
// Intrinsic event handler
|
|
53
61
|
const handleMetadataLoadEvent = useCallback((e) => {
|
|
54
62
|
if (videoRef.current === null)
|
|
55
63
|
return;
|
|
56
64
|
const video = videoRef.current;
|
|
57
65
|
setTotalTime(video.duration);
|
|
66
|
+
// A seek asked for before the metadata landed was silently dropped by the
|
|
67
|
+
// element — replay it now that it can be honoured.
|
|
68
|
+
if (givenCurrentTimeMs !== undefined)
|
|
69
|
+
video.currentTime = msToSeconds(givenCurrentTimeMs);
|
|
58
70
|
intrinsicVideoAttributes.onLoadedMetadata?.(e);
|
|
59
|
-
}, [intrinsicVideoAttributes.onLoadedMetadata]);
|
|
71
|
+
}, [intrinsicVideoAttributes.onLoadedMetadata, givenCurrentTimeMs]);
|
|
60
72
|
const handleOnTimeUpdateEvent = useCallback((e) => {
|
|
61
73
|
const video = e.currentTarget;
|
|
62
74
|
const newTimeMs = secondsToMs(video.currentTime);
|
|
63
|
-
|
|
75
|
+
setInternalCurrentTimeMs(newTimeMs);
|
|
64
76
|
if (intrinsicVideoAttributes.onTimeUpdate !== undefined)
|
|
65
77
|
intrinsicVideoAttributes.onTimeUpdate(e);
|
|
66
78
|
}, [intrinsicVideoAttributes.onTimeUpdate]);
|
|
67
|
-
const handleOnPlayEvent = useCallback((e) => {
|
|
68
|
-
/* We must never play a video that has controlled time */
|
|
69
|
-
if (isTimeControlled) {
|
|
70
|
-
videoRef.current?.pause();
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
intrinsicVideoAttributes.onPlay?.(e);
|
|
74
|
-
}, [isTimeControlled, intrinsicVideoAttributes.onPlay]);
|
|
75
79
|
// Custom action handlers
|
|
76
80
|
const handlePlayButtonClick = useCallback((e) => {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
}, [
|
|
81
|
+
const wasPlaying = videoRef.current?.paused === false;
|
|
82
|
+
onPlayButtonClicked?.(e, wasPlaying, videoRef.current);
|
|
83
|
+
}, [onPlayButtonClicked]);
|
|
80
84
|
const handlePauseButtonClick = useCallback((e) => {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
}, [
|
|
85
|
+
const wasPlaying = videoRef.current?.paused === false;
|
|
86
|
+
onPauseButtonClicked?.(e, wasPlaying, videoRef.current);
|
|
87
|
+
}, [onPauseButtonClicked]);
|
|
84
88
|
const handleLoudButtonClick = useCallback((e) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}, [actionHandlers?.loudButtonClick, mute]);
|
|
89
|
+
onLoudButtonClicked?.(e, isLoud, videoRef.current);
|
|
90
|
+
}, [onLoudButtonClicked, isLoud]);
|
|
88
91
|
const handleMuteButtonClick = useCallback((e) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}, [actionHandlers?.muteButtonClick, mute]);
|
|
92
|
+
onMuteButtonClicked?.(e, isLoud, videoRef.current);
|
|
93
|
+
}, [onMuteButtonClicked, isLoud]);
|
|
92
94
|
const handleFullscreenButtonClick = useCallback((e) => {
|
|
93
|
-
|
|
94
|
-
}, [
|
|
95
|
+
onFullscreenButtonClicked?.(e, isFullscreen, videoRef.current);
|
|
96
|
+
}, [onFullscreenButtonClicked, isFullscreen]);
|
|
95
97
|
const handleVolumeRangeChange = useCallback((e) => {
|
|
96
98
|
const targetVolume = Number(e.currentTarget.value) / 100;
|
|
97
|
-
|
|
98
|
-
}, [
|
|
99
|
+
onVolumeRangeChanged?.(e, targetVolume, volume, videoRef.current);
|
|
100
|
+
}, [onVolumeRangeChanged, volume]);
|
|
99
101
|
const handleRateRangeChange = useCallback((e) => {
|
|
100
|
-
|
|
101
|
-
}, [
|
|
102
|
+
onRateRangeChanged?.(e, Number(e.currentTarget.value), playbackRate, videoRef.current);
|
|
103
|
+
}, [onRateRangeChanged, playbackRate]);
|
|
102
104
|
const handleTimelineClick = useCallback((e) => {
|
|
103
105
|
if (videoRef.current === null)
|
|
104
106
|
return;
|
|
105
|
-
const progress = getTimelineClickProgress(e
|
|
107
|
+
const progress = getTimelineClickProgress(e);
|
|
106
108
|
const targetTime = progress * totalTime;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
onTimelineClicked?.(e, targetTime, currentTime, videoRef.current);
|
|
110
|
+
// A controlled time is the parent's to move: it is expected to update the
|
|
111
|
+
// prop in response to this very handler.
|
|
109
112
|
if (!isTimeControlled) {
|
|
110
113
|
videoRef.current.currentTime = targetTime;
|
|
111
114
|
}
|
|
112
|
-
}, [
|
|
115
|
+
}, [onTimelineClicked, totalTime, currentTime, isTimeControlled]);
|
|
113
116
|
// Rendering
|
|
114
|
-
const isPlaying = play ?? false;
|
|
115
|
-
const isLoud = mute ?? false;
|
|
116
|
-
const isFullscreen = fullscreen ?? false;
|
|
117
117
|
const c = clss(publicClassName, { cssModule });
|
|
118
118
|
const rootClss = mergeClassNames(c(null, {
|
|
119
119
|
'play-on': isPlaying,
|
|
@@ -121,10 +121,11 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
121
121
|
'fullscreen-on': isFullscreen,
|
|
122
122
|
'fullscreen-off': !isFullscreen,
|
|
123
123
|
'loud': isLoud,
|
|
124
|
-
'muted': !isLoud
|
|
125
|
-
..._modifiers
|
|
124
|
+
'muted': !isLoud
|
|
126
125
|
}), className);
|
|
127
|
-
|
|
126
|
+
// Guarded: the duration is unknown until the metadata lands, and an unguarded
|
|
127
|
+
// division would expose the string 'NaN' on every render until then.
|
|
128
|
+
const currentTimeRatio = totalTime > 0 ? currentTime / totalTime : 0;
|
|
128
129
|
const rootAttributes = {
|
|
129
130
|
'data-play-on': isPlaying ? '' : undefined,
|
|
130
131
|
'data-play-off': !isPlaying ? '' : undefined,
|
|
@@ -132,15 +133,15 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
132
133
|
'data-fullscreen-off': !isFullscreen ? '' : undefined,
|
|
133
134
|
'data-loud': isLoud ? '' : undefined,
|
|
134
135
|
'data-muted': !isLoud ? '' : undefined,
|
|
135
|
-
'data-volume':
|
|
136
|
+
'data-volume': volume.toFixed(8),
|
|
136
137
|
'data-volume-percent': volumePercent,
|
|
137
138
|
'data-playback-rate': playbackRate,
|
|
138
139
|
'data-current-time-ms': currentTimeMs.toFixed(2),
|
|
139
|
-
'data-current-time-ratio':
|
|
140
|
+
'data-current-time-ratio': currentTimeRatio.toFixed(8),
|
|
140
141
|
'data-total-time-ms': totalTimeMs
|
|
141
142
|
};
|
|
142
143
|
const rootStyles = {
|
|
143
|
-
[`--${publicClassName}-current-time-ratio`]:
|
|
144
|
+
[`--${publicClassName}-current-time-ratio`]: currentTimeRatio.toFixed(8)
|
|
144
145
|
};
|
|
145
146
|
const parsedSources = useMemo(() => {
|
|
146
147
|
if (sources === undefined)
|
|
@@ -201,8 +202,13 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
201
202
|
};
|
|
202
203
|
}, [fullscreen]);
|
|
203
204
|
useEffect(() => {
|
|
204
|
-
|
|
205
|
+
// A controlled time belongs to the parent, and a playing element advances
|
|
206
|
+
// the time on its own — so the two cannot coexist. Applied once, on entering
|
|
207
|
+
// the mode, rather than fought on every tick.
|
|
208
|
+
if (isTimeControlled) {
|
|
209
|
+
void forcePause(videoRef.current);
|
|
205
210
|
return;
|
|
211
|
+
}
|
|
206
212
|
if (play === true) {
|
|
207
213
|
void forcePlay(videoRef.current);
|
|
208
214
|
}
|
|
@@ -214,10 +220,9 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
214
220
|
forceVolume(videoRef.current, volume);
|
|
215
221
|
}, [volume]);
|
|
216
222
|
useEffect(() => {
|
|
217
|
-
if (givenCurrentTimeMs
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
223
|
+
if (givenCurrentTimeMs === undefined || videoRef.current === null)
|
|
224
|
+
return;
|
|
225
|
+
videoRef.current.currentTime = msToSeconds(givenCurrentTimeMs);
|
|
221
226
|
}, [givenCurrentTimeMs]);
|
|
222
227
|
useEffect(() => {
|
|
223
228
|
if (mute === true) {
|
|
@@ -227,30 +232,28 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
227
232
|
forceLoud(videoRef.current);
|
|
228
233
|
}
|
|
229
234
|
}, [mute]);
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
stateHandlers.currentTime(msToSeconds(currentTimeMs));
|
|
234
|
-
}
|
|
235
|
-
}, [currentTimeMs, stateHandlers?.currentTime]);
|
|
236
|
-
useEffect(() => {
|
|
237
|
-
stateHandlers?.isPlaying?.(isPlaying);
|
|
238
|
-
}, [isPlaying, stateHandlers?.isPlaying]);
|
|
239
|
-
useEffect(() => {
|
|
240
|
-
stateHandlers?.isFullscreen?.(isFullscreen);
|
|
241
|
-
}, [isFullscreen, stateHandlers?.isFullscreen]);
|
|
235
|
+
// The browser can leave fullscreen without going through our button — Escape,
|
|
236
|
+
// or the platform's own exit gesture. Nothing in React reports it, so the real
|
|
237
|
+
// event is the only way for a parent to keep its `fullscreen` prop truthful.
|
|
242
238
|
useEffect(() => {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}, [
|
|
251
|
-
|
|
239
|
+
if (onFullscreenChange === undefined)
|
|
240
|
+
return;
|
|
241
|
+
const onDocumentFullscreenChange = () => {
|
|
242
|
+
onFullscreenChange(document.fullscreenElement !== null);
|
|
243
|
+
};
|
|
244
|
+
document.addEventListener('fullscreenchange', onDocumentFullscreenChange);
|
|
245
|
+
return () => { document.removeEventListener('fullscreenchange', onDocumentFullscreenChange); };
|
|
246
|
+
}, [onFullscreenChange]);
|
|
247
|
+
// State handlers
|
|
248
|
+
useChangeDispatch(currentTimeMs, onCurrentTimeMsChanged);
|
|
249
|
+
useChangeDispatch(isPlaying, onIsPlayingChanged);
|
|
250
|
+
useChangeDispatch(isFullscreen, onIsFullscreenChanged);
|
|
251
|
+
useChangeDispatch(isLoud, onIsLoudChanged);
|
|
252
|
+
useChangeDispatch(volume, onVolumeChanged);
|
|
253
|
+
useChangeDispatch(playbackRate, onPlaybackRateChanged);
|
|
254
|
+
return _jsxs("figure", { className: rootClss, style: rootStyles, ...rootAttributes, children: [_jsxs("video", { ref: videoRef, className: videoClss, ...intrinsicVideoAttributes, autoPlay: isTimeControlled ? false : intrinsicVideoAttributes.autoPlay, onLoadedMetadata: handleMetadataLoadEvent, onTimeUpdate: handleOnTimeUpdateEvent, children: [parsedSources.map((source, index) => typeof source === 'string'
|
|
252
255
|
? _jsx("source", { src: source }, index)
|
|
253
256
|
: _jsx("source", { src: source.src, type: source.type }, index)), parsedTracks.map((track, index) => typeof track === 'string'
|
|
254
257
|
? _jsx("track", { src: track }, index)
|
|
255
|
-
: _jsx("track", { src: track.src, kind: track.kind, srcLang: track.srclang, label: track.label, default: track.default }, index)), children] }), _jsxs("div", { className: videoControlsClss, children: [_jsx("button", { className: playBtnClss, onClick: handlePlayButtonClick, children: playBtnContent }), _jsx("button", { className: pauseBtnClss, onClick: handlePauseButtonClick, children: pauseBtnContent }), _jsx("button", { className: loudBtnClss, onClick: handleLoudButtonClick, children: loudBtnContent }), _jsx("button", { className: muteBtnClss, onClick: handleMuteButtonClick, children: muteBtnContent }), _jsx("input", { type:
|
|
258
|
+
: _jsx("track", { src: track.src, kind: track.kind, srcLang: track.srclang, label: track.label, default: track.default }, index)), children] }), _jsxs("div", { className: videoControlsClss, children: [_jsx("button", { className: playBtnClss, onClick: handlePlayButtonClick, children: playBtnContent }), _jsx("button", { className: pauseBtnClss, onClick: handlePauseButtonClick, children: pauseBtnContent }), _jsx("button", { className: loudBtnClss, onClick: handleLoudButtonClick, children: loudBtnContent }), _jsx("button", { className: muteBtnClss, onClick: handleMuteButtonClick, children: muteBtnContent }), _jsx("input", { type: 'range', className: volumeRangeClss, value: volumePercent, onChange: handleVolumeRangeChange, min: 0, max: 100, step: 1 }), _jsx("span", { className: volumePcntClss, children: Math.round(volumePercent) }), _jsx("button", { className: fullscreenBtnClss, onClick: handleFullscreenButtonClick, children: fullscreenBtnContent }), _jsx("input", { type: 'range', className: playbackRateRangeClss, value: playbackRate, onChange: handleRateRangeChange, min: 0.25, max: 4, step: 0.25 }), _jsx("span", { className: playbackRateClss, children: playbackRate })] }), _jsxs("div", { className: timeControlsClss, children: [_jsx("span", { className: currentTimeClss, children: formatTime(currentTimeMs, 'mm:ss:ms') }), _jsx("span", { className: totalTimeClss, children: formatTime(totalTimeMs, 'mm:ss:ms') }), _jsx("div", { className: timelineClss, onClick: handleTimelineClick })] }), subtitles !== undefined && _jsx(Subtitles, { ...subtitles, timecodeMs: currentTimeMs })] });
|
|
256
259
|
};
|