@design-edito/tools 0.5.1 → 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/arrays/index.d.ts +1 -1
- package/agnostic/arrays/index.js +1 -1
- package/agnostic/colors/index.d.ts +1 -1
- package/agnostic/colors/index.js +1 -1
- package/agnostic/css/index.d.ts +1 -1
- package/agnostic/css/index.js +1 -1
- package/agnostic/html/deep-select/index.js +5 -3
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +9 -9
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +9 -9
- package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +1 -1
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +1 -1
- package/agnostic/html/index.d.ts +3 -3
- package/agnostic/html/index.js +3 -3
- package/agnostic/index.d.ts +3 -3
- package/agnostic/index.js +3 -3
- package/agnostic/misc/index.d.ts +1 -1
- package/agnostic/misc/index.js +1 -1
- package/agnostic/misc/logs/index.d.ts +1 -1
- package/agnostic/misc/logs/index.js +1 -1
- package/agnostic/numbers/index.d.ts +3 -3
- package/agnostic/numbers/index.js +3 -3
- package/agnostic/objects/index.d.ts +3 -3
- package/agnostic/objects/index.js +3 -3
- package/agnostic/random/index.d.ts +1 -1
- package/agnostic/random/index.js +1 -1
- package/agnostic/strings/index.d.ts +3 -3
- package/agnostic/strings/index.js +3 -3
- 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/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 -75
- 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 +78 -0
- package/components/ListLoader/index.controlled.js +99 -0
- package/components/ListLoader/index.d.ts +69 -0
- package/components/ListLoader/index.js +146 -0
- package/components/ListLoader/utils.d.ts +8 -0
- package/components/ListLoader/utils.js +10 -0
- 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/ResizeObserver/styles.module.css +0 -0
- 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 +5 -4
- package/components/index.js +5 -4
- package/components/public-classnames.d.ts +1 -0
- package/components/public-classnames.js +1 -0
- package/components/utils/index.d.ts +22 -0
- package/components/utils/index.js +31 -0
- package/node/@google-cloud/storage/directory/index.d.ts +2 -2
- package/node/@google-cloud/storage/directory/index.js +2 -2
- package/node/@google-cloud/storage/file/index.d.ts +2 -2
- package/node/@google-cloud/storage/file/index.js +2 -2
- package/node/@google-cloud/storage/index.d.ts +1 -1
- package/node/@google-cloud/storage/index.js +1 -1
- package/node/cloud-storage/operations/index.d.ts +2 -2
- package/node/cloud-storage/operations/index.js +2 -2
- package/node/ftps/directory/index.d.ts +1 -1
- package/node/ftps/directory/index.js +1 -1
- package/node/ftps/file/index.d.ts +2 -2
- package/node/ftps/file/index.js +2 -2
- package/node/images/index.d.ts +1 -1
- package/node/images/index.js +1 -1
- package/node/images/transform/operations/index.d.ts +3 -3
- package/node/images/transform/operations/index.js +3 -3
- package/node/index.d.ts +2 -2
- package/node/index.js +2 -2
- package/node/process/index.d.ts +1 -1
- package/node/process/index.js +1 -1
- package/node/sftp/directory/index.d.ts +1 -1
- package/node/sftp/directory/index.js +1 -1
- package/node/sftp/file/index.d.ts +1 -1
- package/node/sftp/file/index.js +1 -1
- package/package.json +27 -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 → ListLoader/styles.module.css} +0 -0
|
@@ -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
|
};
|
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'react';
|
|
2
|
-
import { type Props as DisclaimerProps } from '../Disclaimer/index.js';
|
|
3
2
|
import { type Props as ControlledProps } from './index.controlled.js';
|
|
4
3
|
/**
|
|
5
4
|
* Props for the {@link Video} component.
|
|
6
5
|
*
|
|
7
6
|
* Extends all ControlledVideo props except play, mute, fullscreen, volume, playbackRate, and their associated event handlers
|
|
8
|
-
* @property disclaimer - Props forwarded to the internal {@link Disclaimer} component.
|
|
9
|
-
* While the disclaimer is active, `autoPlay` and `muted` are suppressed on the
|
|
10
|
-
* underlying `<video>` element.
|
|
11
7
|
* @property autoPlayWhenVisible - When `true`, triggers playback the first time the
|
|
12
|
-
* component intersects the viewport
|
|
8
|
+
* component intersects the viewport.
|
|
13
9
|
* @property autoPauseWhenHidden - When `true`, pauses playback whenever the component
|
|
14
10
|
* leaves the viewport.
|
|
15
11
|
* @property autoLoudWhenVisible - When `true`, unmutes the video the first time the
|
|
16
|
-
* component intersects the viewport
|
|
12
|
+
* component intersects the viewport.
|
|
17
13
|
* @property autoMuteWhenHidden - When `true`, mutes the video whenever the component
|
|
18
14
|
* leaves the viewport.
|
|
19
|
-
* @property currentTimeMs - When provided,
|
|
15
|
+
* @property currentTimeMs - When provided, hands ownership of the current time
|
|
16
|
+
* (in milliseconds) to the parent, which is then responsible for updating it —
|
|
17
|
+
* typically to scrub the video from scroll position. A controlled time implies a
|
|
18
|
+
* stopped video, since a playing element would advance a value it does not own:
|
|
19
|
+
* `autoPlay`, `autoPlayWhenVisible` and the play button have no effect for as
|
|
20
|
+
* long as this prop is provided.
|
|
20
21
|
* @property wrapperClassName - Optional additional class name(s) applied to the root wrapper element.
|
|
21
|
-
* @property stateHandlers - Optional callbacks invoked whenever the corresponding
|
|
22
|
-
* state changes. Useful for synchronizing external state with the internal video state.
|
|
23
22
|
* @property className - Optional additional class name(s) applied to the root element.
|
|
24
23
|
* @property children - React children rendered inside the `<video>` element itself
|
|
25
24
|
* (e.g. fallback content).
|
|
26
25
|
*/
|
|
27
26
|
export type Props = Omit<ControlledProps, 'play' | 'fullscreen' | 'volume' | 'mute' | 'playbackRate'> & {
|
|
28
|
-
disclaimer?: DisclaimerProps;
|
|
29
27
|
autoPlayWhenVisible?: boolean;
|
|
30
28
|
autoPauseWhenHidden?: boolean;
|
|
31
29
|
autoLoudWhenVisible?: boolean;
|
|
@@ -34,12 +32,12 @@ export type Props = Omit<ControlledProps, 'play' | 'fullscreen' | 'volume' | 'mu
|
|
|
34
32
|
};
|
|
35
33
|
/**
|
|
36
34
|
* Full-featured video player component. Wraps a native `<video>` element with
|
|
37
|
-
* playback controls, volume, playback rate, a timeline, optional subtitles,
|
|
38
|
-
*
|
|
35
|
+
* playback controls, volume, playback rate, a timeline, optional subtitles, and
|
|
36
|
+
* viewport-driven auto-play/mute behaviours.
|
|
39
37
|
*
|
|
40
38
|
* @param props - Component properties.
|
|
41
39
|
* @see {@link Props}
|
|
42
|
-
* @returns A `<figure>` element containing the video, its controls
|
|
43
|
-
* subtitles
|
|
40
|
+
* @returns A `<figure>` element containing the video, its controls and optional
|
|
41
|
+
* subtitles.
|
|
44
42
|
*/
|
|
45
43
|
export declare const Video: FunctionComponent<Props>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { clss } from '../../agnostic/css/clss/index.js';
|
|
4
|
-
import { Disclaimer } from '../Disclaimer/index.js';
|
|
5
4
|
import { IntersectionObserverComponent } from '../IntersectionObserver/index.js';
|
|
6
5
|
import { mergeClassNames } from '../utils/index.js';
|
|
7
6
|
import { videoWrapper as publicClassName } from '../public-classnames.js';
|
|
@@ -10,33 +9,27 @@ import cssModule from './styles.module.css';
|
|
|
10
9
|
import { ControlledVideo } from './index.controlled.js';
|
|
11
10
|
/**
|
|
12
11
|
* Full-featured video player component. Wraps a native `<video>` element with
|
|
13
|
-
* playback controls, volume, playback rate, a timeline, optional subtitles,
|
|
14
|
-
*
|
|
12
|
+
* playback controls, volume, playback rate, a timeline, optional subtitles, and
|
|
13
|
+
* viewport-driven auto-play/mute behaviours.
|
|
15
14
|
*
|
|
16
15
|
* @param props - Component properties.
|
|
17
16
|
* @see {@link Props}
|
|
18
|
-
* @returns A `<figure>` element containing the video, its controls
|
|
19
|
-
* subtitles
|
|
17
|
+
* @returns A `<figure>` element containing the video, its controls and optional
|
|
18
|
+
* subtitles.
|
|
20
19
|
*/
|
|
21
|
-
export const Video = ({ loop,
|
|
20
|
+
export const Video = ({ loop, autoPlayWhenVisible, autoPauseWhenHidden, autoMuteWhenHidden, autoLoudWhenVisible, wrapperClassName, onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, ...controlledProps }) => {
|
|
22
21
|
// State & refs
|
|
23
22
|
const [play, setPlay] = useState(false);
|
|
24
23
|
const [volume, setVolume] = useState(1);
|
|
25
24
|
const [mute, setMute] = useState(false);
|
|
26
25
|
const [playbackRate, setPlaybackRate] = useState(1);
|
|
27
26
|
const [fullscreen, setFullscreen] = useState(false);
|
|
28
|
-
const [isDisclaimerOn, setIsDisclaimerOn] = useState(disclaimer?.isOn === true
|
|
29
|
-
|| disclaimer?.defaultIsOn === true
|
|
30
|
-
|| (disclaimer !== undefined && disclaimer.defaultIsOn === undefined));
|
|
31
27
|
const hasBeenAutoPlayed = useRef(false);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return true;
|
|
38
|
-
return isDisclaimerOn;
|
|
39
|
-
}, [disclaimer, isDisclaimerOn]);
|
|
28
|
+
// Several paths below ask for playback — the play button, autoPlayWhenVisible,
|
|
29
|
+
// autoPlay itself. None of them may win over a parent-owned time, so the
|
|
30
|
+
// invariant is applied where the state is forwarded rather than guarded at each
|
|
31
|
+
// of those call sites.
|
|
32
|
+
const isTimeControlled = controlledProps.currentTimeMs !== undefined;
|
|
40
33
|
const needsObserve = useMemo(() => autoLoudWhenVisible === true
|
|
41
34
|
|| autoMuteWhenHidden === true
|
|
42
35
|
|| autoPlayWhenVisible === true
|
|
@@ -49,13 +42,9 @@ export const Video = ({ loop, disclaimer, autoPlayWhenVisible, autoPauseWhenHidd
|
|
|
49
42
|
// Intrisic event handlers
|
|
50
43
|
const handleOnPlayEvent = useCallback((e) => {
|
|
51
44
|
controlledProps.onPlay?.(e);
|
|
52
|
-
if (shouldDisclaimerBeOn) {
|
|
53
|
-
setPlay(false);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
45
|
setPlay(true);
|
|
57
46
|
hasBeenAutoPlayed.current = true;
|
|
58
|
-
}, [controlledProps.onPlay
|
|
47
|
+
}, [controlledProps.onPlay]);
|
|
59
48
|
const handleOnPauseEvent = useCallback((e) => {
|
|
60
49
|
setPlay(false);
|
|
61
50
|
controlledProps.onPause?.(e);
|
|
@@ -69,45 +58,44 @@ export const Video = ({ loop, disclaimer, autoPlayWhenVisible, autoPauseWhenHidd
|
|
|
69
58
|
setPlaybackRate(e.currentTarget.playbackRate);
|
|
70
59
|
controlledProps.onRateChange?.(e);
|
|
71
60
|
}, [controlledProps.onRateChange]);
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
61
|
+
const handleFullscreenChange = useCallback((isFullscreen) => {
|
|
62
|
+
if (!isFullscreen)
|
|
74
63
|
setFullscreen(false);
|
|
75
|
-
|
|
76
|
-
}, [shouldDisclaimerBeOn]);
|
|
64
|
+
}, []);
|
|
77
65
|
const handleOnLoadedMetadataEvent = useCallback((e) => {
|
|
78
66
|
muteAttributeWorkaround(e.currentTarget, controlledProps.muted ?? false);
|
|
79
67
|
controlledProps.onLoadedMetadata?.(e);
|
|
80
68
|
}, [controlledProps.onLoadedMetadata, controlledProps.muted]);
|
|
81
69
|
// User actions
|
|
82
70
|
const handlePlayButtonClick = useCallback((e, isPlaying, video) => {
|
|
83
|
-
|
|
84
|
-
setPlay(
|
|
85
|
-
}, [
|
|
71
|
+
onPlayButtonClicked?.(e, isPlaying, video);
|
|
72
|
+
setPlay(true);
|
|
73
|
+
}, [onPlayButtonClicked]);
|
|
86
74
|
const handlePauseButtonClick = useCallback((e, isPlaying, video) => {
|
|
87
|
-
|
|
75
|
+
onPauseButtonClicked?.(e, isPlaying, video);
|
|
88
76
|
setPlay(false);
|
|
89
|
-
}, [
|
|
77
|
+
}, [onPauseButtonClicked]);
|
|
90
78
|
const handleLoudButtonClick = useCallback((e, isLoud, video) => {
|
|
91
|
-
|
|
79
|
+
onLoudButtonClicked?.(e, isLoud, video);
|
|
92
80
|
setMute(false);
|
|
93
|
-
}, [
|
|
81
|
+
}, [onLoudButtonClicked]);
|
|
94
82
|
const handleMuteButtonClick = useCallback((e, isLoud, video) => {
|
|
95
|
-
|
|
83
|
+
onMuteButtonClicked?.(e, isLoud, video);
|
|
96
84
|
setMute(true);
|
|
97
|
-
}, [
|
|
85
|
+
}, [onMuteButtonClicked]);
|
|
98
86
|
const handleRateRangeChange = useCallback((e, targetRate, currentRate, video) => {
|
|
99
|
-
|
|
87
|
+
onRateRangeChanged?.(e, targetRate, currentRate, video);
|
|
100
88
|
setPlaybackRate(targetRate);
|
|
101
|
-
}, [
|
|
89
|
+
}, [onRateRangeChanged]);
|
|
102
90
|
const handleVolumeRangeChange = useCallback((e, targetVolume, currentVolume, video) => {
|
|
103
|
-
|
|
91
|
+
onVolumeRangeChanged?.(e, targetVolume, currentVolume, video);
|
|
104
92
|
setVolume(targetVolume);
|
|
105
|
-
}, [
|
|
93
|
+
}, [onVolumeRangeChanged]);
|
|
106
94
|
const handleFullscreenButtonClick = useCallback((e, isFullscreen, video) => {
|
|
107
|
-
|
|
108
|
-
setFullscreen(
|
|
109
|
-
}, [
|
|
110
|
-
// Intersection Observer
|
|
95
|
+
onFullscreenButtonClicked?.(e, isFullscreen, video);
|
|
96
|
+
setFullscreen(!isFullscreen);
|
|
97
|
+
}, [onFullscreenButtonClicked]);
|
|
98
|
+
// Intersection Observer
|
|
111
99
|
const onIntersected = useCallback(({ ioEntry }) => {
|
|
112
100
|
if (ioEntry === undefined)
|
|
113
101
|
return;
|
|
@@ -117,12 +105,10 @@ export const Video = ({ loop, disclaimer, autoPlayWhenVisible, autoPauseWhenHidd
|
|
|
117
105
|
if (autoLoudWhenVisible === true && isIntersecting)
|
|
118
106
|
setMute(false);
|
|
119
107
|
if (autoPlayWhenVisible === true
|
|
120
|
-
&& !shouldDisclaimerBeOn
|
|
121
108
|
&& !hasBeenAutoPlayed.current
|
|
122
109
|
&& isIntersecting)
|
|
123
110
|
setPlay(true);
|
|
124
111
|
if (autoMuteWhenHidden === true
|
|
125
|
-
&& !shouldDisclaimerBeOn
|
|
126
112
|
&& !hasBeenAutoPlayed.current
|
|
127
113
|
&& !isIntersecting)
|
|
128
114
|
setMute(true);
|
|
@@ -130,50 +116,19 @@ export const Video = ({ loop, disclaimer, autoPlayWhenVisible, autoPauseWhenHidd
|
|
|
130
116
|
autoPlayWhenVisible,
|
|
131
117
|
autoPauseWhenHidden,
|
|
132
118
|
autoMuteWhenHidden,
|
|
133
|
-
autoLoudWhenVisible
|
|
134
|
-
shouldDisclaimerBeOn
|
|
119
|
+
autoLoudWhenVisible
|
|
135
120
|
]);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (controlledProps.autoPlay === true && !hasBeenAutoPlayed.current) {
|
|
139
|
-
setPlay(true);
|
|
140
|
-
}
|
|
141
|
-
disclaimer?.actionHandlers?.dismissClick?.(prevIsOn);
|
|
142
|
-
}, [controlledProps.autoPlay, disclaimer?.actionHandlers?.dismissClick]);
|
|
121
|
+
// `autoPlay` is forwarded to the element, but the play state is owned here, so
|
|
122
|
+
// it has to be seeded once on mount for the controls to agree with the element.
|
|
143
123
|
useEffect(() => {
|
|
144
|
-
if (
|
|
145
|
-
setIsDisclaimerOn(shouldDisclaimerBeOn);
|
|
146
|
-
}, [isDisclaimerOn, shouldDisclaimerBeOn]);
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
if (shouldDisclaimerBeOn) {
|
|
149
|
-
setMute(true);
|
|
150
|
-
setPlay(false);
|
|
151
|
-
setFullscreen(false);
|
|
152
|
-
}
|
|
153
|
-
else if (controlledProps.autoPlay === true && !hasBeenAutoPlayed.current) {
|
|
124
|
+
if (controlledProps.autoPlay === true && !hasBeenAutoPlayed.current)
|
|
154
125
|
setPlay(true);
|
|
155
|
-
|
|
156
|
-
}, [shouldDisclaimerBeOn]);
|
|
126
|
+
}, []);
|
|
157
127
|
// Render
|
|
158
128
|
const c = clss(publicClassName, { cssModule });
|
|
159
|
-
const rootClss = mergeClassNames(c(
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
loudButtonClick: handleLoudButtonClick,
|
|
165
|
-
muteButtonClick: handleMuteButtonClick,
|
|
166
|
-
volumeRangeChange: handleVolumeRangeChange,
|
|
167
|
-
rateRangeChange: handleRateRangeChange,
|
|
168
|
-
fullscreenButtonClick: handleFullscreenButtonClick
|
|
169
|
-
} });
|
|
170
|
-
const disclaimedContent = needsDisclaimer
|
|
171
|
-
? _jsx(Disclaimer, { ...disclaimer, actionHandlers: {
|
|
172
|
-
...disclaimer?.actionHandlers,
|
|
173
|
-
dismissClick: handleDiclaimerDismiss
|
|
174
|
-
}, children: sensitiveContent })
|
|
175
|
-
: sensitiveContent;
|
|
176
|
-
return (_jsx("div", { className: rootClss, children: needsObserve
|
|
177
|
-
? _jsx(IntersectionObserverComponent, { onIntersected: onIntersected, children: disclaimedContent })
|
|
178
|
-
: disclaimedContent }));
|
|
129
|
+
const rootClss = mergeClassNames(c(), wrapperClassName);
|
|
130
|
+
const videoContent = _jsx(ControlledVideo, { ...controlledProps, play: play && !isTimeControlled, volume: volume, mute: mute, playbackRate: playbackRate, fullscreen: fullscreen, onPlay: handleOnPlayEvent, onPause: handleOnPauseEvent, onVolumeChange: handleOnVolumeChangeEvent, onRateChange: handleOnRateChangeEvent, onLoadedMetadata: handleOnLoadedMetadataEvent, onFullscreenChange: handleFullscreenChange, onPlayButtonClicked: handlePlayButtonClick, onPauseButtonClicked: handlePauseButtonClick, onLoudButtonClicked: handleLoudButtonClick, onMuteButtonClicked: handleMuteButtonClick, onVolumeRangeChanged: handleVolumeRangeChange, onRateRangeChanged: handleRateRangeChange, onFullscreenButtonClicked: handleFullscreenButtonClick });
|
|
131
|
+
return _jsx("div", { className: rootClss, children: needsObserve
|
|
132
|
+
? _jsx(IntersectionObserverComponent, { onIntersected: onIntersected, children: videoContent })
|
|
133
|
+
: videoContent });
|
|
179
134
|
};
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type { Dispatch, SetStateAction } from 'react';
|
|
2
1
|
export declare const muteAttributeWorkaround: (video: HTMLVideoElement | null, shouldMute: boolean) => void;
|
|
3
2
|
export declare const forceMute: (video: HTMLVideoElement | null) => void;
|
|
4
3
|
export declare const forceLoud: (video: HTMLVideoElement | null) => void;
|
|
5
4
|
export declare const forceVolume: (video: HTMLVideoElement | null, volume: number) => void;
|
|
6
|
-
export declare const forceCurrentTime: (video: HTMLVideoElement | null, time: number, setCurrentTime: Dispatch<SetStateAction<number>>) => void;
|
|
7
5
|
export declare const forcePlay: (video: HTMLVideoElement | null) => Promise<boolean>;
|
|
8
6
|
export declare const forcePause: (video: HTMLVideoElement | null) => boolean;
|
|
9
7
|
export declare const forcePlaybackRate: (video: HTMLVideoElement | null, rate: number) => void;
|
|
@@ -11,5 +9,39 @@ export declare const forceFullscreen: (video: HTMLVideoElement | null) => Promis
|
|
|
11
9
|
export declare const forceExitFullscreen: (video: HTMLVideoElement | null) => Promise<boolean>;
|
|
12
10
|
export declare function secondsToMs(seconds: number): number;
|
|
13
11
|
export declare function msToSeconds(ms: number): number;
|
|
12
|
+
/**
|
|
13
|
+
* Renders a duration into a caller-defined pattern.
|
|
14
|
+
*
|
|
15
|
+
* The pattern is a free-form string in which the following tokens are replaced,
|
|
16
|
+
* longest first, so that `mm` is consumed before `m`:
|
|
17
|
+
*
|
|
18
|
+
* - `hh`, `mm`, `ss` — hours, minutes and seconds, zero-padded to two digits.
|
|
19
|
+
* - `ms` — the remaining milliseconds, zero-padded to three digits.
|
|
20
|
+
* - `frame` — the remaining milliseconds expressed in frames, zero-padded to two
|
|
21
|
+
* digits.
|
|
22
|
+
* - `h`, `m`, `s`, `f` — the same four values, unpadded.
|
|
23
|
+
*
|
|
24
|
+
* @param ms - The duration, in milliseconds.
|
|
25
|
+
* @param format - The pattern to fill, e.g. `'mm:ss:ms'`.
|
|
26
|
+
* @param fps - Frame rate used to derive the `frame` and `f` tokens. Defaults to `25`.
|
|
27
|
+
* @returns The formatted duration.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Every field is a remainder of the one above it, never a total: at one hour and
|
|
31
|
+
* a half, `mm` is `30`, not `90`. A pattern that omits `hh` therefore loses the
|
|
32
|
+
* hours rather than folding them into the minutes.
|
|
33
|
+
*
|
|
34
|
+
* Since `h`, `m`, `s` and `f` are tokens on their own, **every** occurrence of
|
|
35
|
+
* those letters is substituted, wherever it sits: `'mm min ss'` renders as
|
|
36
|
+
* `'01 1in 01'`. Separate the fields with punctuation. This is what
|
|
37
|
+
* `agnostic/time/dates/format-date` avoids by delimiting its tokens with
|
|
38
|
+
* `{{…}}` — worth aligning on the day this one is made public.
|
|
39
|
+
*/
|
|
14
40
|
export declare function formatTime(ms: number, format: string, fps?: number): string;
|
|
15
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Where along the timeline a click landed.
|
|
43
|
+
*
|
|
44
|
+
* @param event - The click on the timeline element.
|
|
45
|
+
* @returns A ratio between `0` and `1`, clamped to the element's own bounds.
|
|
46
|
+
*/
|
|
47
|
+
export declare const getTimelineClickProgress: (event: React.MouseEvent<HTMLDivElement>) => number;
|
|
@@ -28,13 +28,6 @@ export const forceVolume = (video, volume) => {
|
|
|
28
28
|
// eslint-disable-next-line no-param-reassign
|
|
29
29
|
video.volume = volume;
|
|
30
30
|
};
|
|
31
|
-
export const forceCurrentTime = (video, time, setCurrentTime) => {
|
|
32
|
-
if (video === null)
|
|
33
|
-
return;
|
|
34
|
-
// eslint-disable-next-line no-param-reassign
|
|
35
|
-
video.currentTime = time;
|
|
36
|
-
setCurrentTime(time);
|
|
37
|
-
};
|
|
38
31
|
export const forcePlay = async (video) => {
|
|
39
32
|
if (video === null)
|
|
40
33
|
return false;
|
|
@@ -105,6 +98,34 @@ export function secondsToMs(seconds) {
|
|
|
105
98
|
export function msToSeconds(ms) {
|
|
106
99
|
return ms / 1000;
|
|
107
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Renders a duration into a caller-defined pattern.
|
|
103
|
+
*
|
|
104
|
+
* The pattern is a free-form string in which the following tokens are replaced,
|
|
105
|
+
* longest first, so that `mm` is consumed before `m`:
|
|
106
|
+
*
|
|
107
|
+
* - `hh`, `mm`, `ss` — hours, minutes and seconds, zero-padded to two digits.
|
|
108
|
+
* - `ms` — the remaining milliseconds, zero-padded to three digits.
|
|
109
|
+
* - `frame` — the remaining milliseconds expressed in frames, zero-padded to two
|
|
110
|
+
* digits.
|
|
111
|
+
* - `h`, `m`, `s`, `f` — the same four values, unpadded.
|
|
112
|
+
*
|
|
113
|
+
* @param ms - The duration, in milliseconds.
|
|
114
|
+
* @param format - The pattern to fill, e.g. `'mm:ss:ms'`.
|
|
115
|
+
* @param fps - Frame rate used to derive the `frame` and `f` tokens. Defaults to `25`.
|
|
116
|
+
* @returns The formatted duration.
|
|
117
|
+
*
|
|
118
|
+
* @remarks
|
|
119
|
+
* Every field is a remainder of the one above it, never a total: at one hour and
|
|
120
|
+
* a half, `mm` is `30`, not `90`. A pattern that omits `hh` therefore loses the
|
|
121
|
+
* hours rather than folding them into the minutes.
|
|
122
|
+
*
|
|
123
|
+
* Since `h`, `m`, `s` and `f` are tokens on their own, **every** occurrence of
|
|
124
|
+
* those letters is substituted, wherever it sits: `'mm min ss'` renders as
|
|
125
|
+
* `'01 1in 01'`. Separate the fields with punctuation. This is what
|
|
126
|
+
* `agnostic/time/dates/format-date` avoids by delimiting its tokens with
|
|
127
|
+
* `{{…}}` — worth aligning on the day this one is made public.
|
|
128
|
+
*/
|
|
108
129
|
export function formatTime(ms, format, fps = 25) {
|
|
109
130
|
const totalSeconds = Math.floor(ms / 1000);
|
|
110
131
|
const hours = Math.floor(totalSeconds / 3600);
|
|
@@ -123,17 +144,19 @@ export function formatTime(ms, format, fps = 25) {
|
|
|
123
144
|
s: seconds,
|
|
124
145
|
f: frames
|
|
125
146
|
};
|
|
126
|
-
|
|
147
|
+
return Object
|
|
127
148
|
.keys(tokens)
|
|
128
149
|
.sort((a, b) => b.length - a.length)
|
|
129
150
|
.reduce((acc, t) => acc.replace(new RegExp(t, 'gv'), String(tokens[t])), format);
|
|
130
|
-
return result;
|
|
131
151
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Where along the timeline a click landed.
|
|
154
|
+
*
|
|
155
|
+
* @param event - The click on the timeline element.
|
|
156
|
+
* @returns A ratio between `0` and `1`, clamped to the element's own bounds.
|
|
157
|
+
*/
|
|
158
|
+
export const getTimelineClickProgress = (event) => {
|
|
135
159
|
const timelineRect = event.currentTarget.getBoundingClientRect();
|
|
136
160
|
const position = event.clientX - timelineRect.left;
|
|
137
|
-
|
|
138
|
-
return progress;
|
|
161
|
+
return Math.min(1, Math.max(0, position / timelineRect.width));
|
|
139
162
|
};
|