@design-edito/tools 0.5.5 → 0.5.7
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/css/index.d.ts +1 -1
- package/agnostic/css/index.js +1 -1
- package/agnostic/errors/index.d.ts +1 -1
- package/agnostic/errors/index.js +1 -1
- 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/index.d.ts +1 -0
- package/agnostic/index.js +1 -0
- package/agnostic/misc/index.d.ts +1 -1
- package/agnostic/misc/index.js +1 -1
- package/agnostic/objects/index.d.ts +2 -2
- package/agnostic/objects/index.js +2 -2
- package/agnostic/random/index.d.ts +1 -1
- package/agnostic/random/index.js +1 -1
- package/agnostic/strings/index.d.ts +1 -1
- package/agnostic/strings/index.js +1 -1
- package/agnostic/subtitles/index.d.ts +2 -0
- package/agnostic/subtitles/index.js +2 -0
- package/agnostic/subtitles/parse-srt/index.d.ts +17 -0
- package/agnostic/subtitles/parse-srt/index.js +67 -0
- package/agnostic/subtitles/parse-srt/index.test.d.ts +1 -0
- package/agnostic/subtitles/parse-srt/index.test.js +94 -0
- package/agnostic/subtitles/parse-srt/types.d.ts +22 -0
- package/agnostic/subtitles/parse-srt/types.js +1 -0
- package/agnostic/subtitles/srt-timecode-to-ms/index.d.ts +14 -0
- package/agnostic/subtitles/srt-timecode-to-ms/index.js +36 -0
- package/agnostic/subtitles/srt-timecode-to-ms/index.test.d.ts +1 -0
- package/agnostic/subtitles/srt-timecode-to-ms/index.test.js +44 -0
- package/agnostic/time/dates/index.d.ts +1 -1
- package/agnostic/time/dates/index.js +1 -1
- package/components/ListLoader/index.controlled.d.ts +8 -3
- package/components/ListLoader/index.controlled.js +2 -2
- package/components/ListLoader/index.d.ts +1 -1
- package/components/ListLoader/index.js +2 -2
- package/components/Paginator/index.d.ts +5 -0
- package/components/Paginator/index.js +2 -1
- package/components/Scrllgngn/index.d.ts +68 -8
- package/components/Scrllgngn/index.js +231 -27
- package/components/Scrllgngn/styles.module.css +30 -5
- package/components/Scrllgngn/utils.d.ts +222 -4
- package/components/Scrllgngn/utils.js +335 -8
- package/components/ScrollListener/index.d.ts +7 -3
- package/components/ScrollListener/index.js +2 -2
- package/components/Sequencer/index.d.ts +10 -1
- package/components/Sequencer/index.js +7 -3
- package/components/Subtitles/index.d.ts +53 -62
- package/components/Subtitles/index.js +90 -158
- package/components/Subtitles/utils.d.ts +60 -0
- package/components/Subtitles/utils.js +76 -0
- package/components/Video/index.controlled.d.ts +5 -0
- package/components/Video/index.controlled.js +22 -3
- package/components/Video/index.d.ts +47 -9
- package/components/Video/index.js +71 -20
- package/components/Video/utils.d.ts +14 -0
- package/components/Video/utils.js +18 -0
- package/components/utils/types.d.ts +32 -0
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/node/@google-cloud/storage/file/index.d.ts +1 -1
- package/node/@google-cloud/storage/file/index.js +1 -1
- package/node/files/index.d.ts +1 -1
- package/node/files/index.js +1 -1
- package/node/images/transform/operations/index.d.ts +1 -1
- package/node/images/transform/operations/index.js +1 -1
- package/package.json +26 -1
|
@@ -69,6 +69,8 @@ type TrackData = {
|
|
|
69
69
|
* seeks to the target right after, unless the time is controlled.
|
|
70
70
|
* @property onIsPlayingChanged - Called once the playback state has changed.
|
|
71
71
|
* @property onIsFullscreenChanged - Called once the fullscreen state has changed.
|
|
72
|
+
* @property onIsEndedChanged - Called after playback reached the end, and again
|
|
73
|
+
* once it left it — a seek back or a new play. Never on mount.
|
|
72
74
|
* @property onIsLoudChanged - Called once the mute state has changed (`true` = unmuted).
|
|
73
75
|
* @property onVolumeChanged - Called once the volume has changed (`0` to `1`).
|
|
74
76
|
* @property onPlaybackRateChanged - Called once the playback rate has changed.
|
|
@@ -109,6 +111,7 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
109
111
|
onIsPlayingChanged?: (isPlaying: boolean) => void;
|
|
110
112
|
onIsFullscreenChanged?: (isFullscreen: boolean) => void;
|
|
111
113
|
onIsLoudChanged?: (isLoud: boolean) => void;
|
|
114
|
+
onIsEndedChanged?: (isEnded: boolean) => void;
|
|
112
115
|
onVolumeChanged?: (volume: number) => void;
|
|
113
116
|
onPlaybackRateChanged?: (playbackRate: number) => void;
|
|
114
117
|
onCurrentTimeMsChanged?: (currentTimeMs: number) => void;
|
|
@@ -125,10 +128,12 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
125
128
|
* - `--play-on` / `--play-off` — reflects current playback state.
|
|
126
129
|
* - `--fullscreen-on` / `--fullscreen-off` — reflects fullscreen state.
|
|
127
130
|
* - `--loud` / `--muted` — reflects mute state.
|
|
131
|
+
* - `--ended` — playback reached the end and has not left it.
|
|
128
132
|
*
|
|
129
133
|
* ### Data attributes on the root element
|
|
130
134
|
* - `data-play-on` — present (empty string) when playing.
|
|
131
135
|
* - `data-play-off` — present (empty string) when paused.
|
|
136
|
+
* - `data-ended` — present (empty string) once playback reached the end.
|
|
132
137
|
* - `data-fullscreen-on` — present (empty string) when in fullscreen.
|
|
133
138
|
* - `data-fullscreen-off` — present (empty string) when not in fullscreen.
|
|
134
139
|
* - `data-loud` — present (empty string) when unmuted.
|
|
@@ -17,10 +17,12 @@ import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause,
|
|
|
17
17
|
* - `--play-on` / `--play-off` — reflects current playback state.
|
|
18
18
|
* - `--fullscreen-on` / `--fullscreen-off` — reflects fullscreen state.
|
|
19
19
|
* - `--loud` / `--muted` — reflects mute state.
|
|
20
|
+
* - `--ended` — playback reached the end and has not left it.
|
|
20
21
|
*
|
|
21
22
|
* ### Data attributes on the root element
|
|
22
23
|
* - `data-play-on` — present (empty string) when playing.
|
|
23
24
|
* - `data-play-off` — present (empty string) when paused.
|
|
25
|
+
* - `data-ended` — present (empty string) once playback reached the end.
|
|
24
26
|
* - `data-fullscreen-on` — present (empty string) when in fullscreen.
|
|
25
27
|
* - `data-fullscreen-off` — present (empty string) when not in fullscreen.
|
|
26
28
|
* - `data-loud` — present (empty string) when unmuted.
|
|
@@ -41,11 +43,16 @@ import { forceExitFullscreen, forceFullscreen, forceLoud, forceMute, forcePause,
|
|
|
41
43
|
* @returns A `<figure>` element containing the video, its controls, optional
|
|
42
44
|
* subtitles.
|
|
43
45
|
*/
|
|
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 }) => {
|
|
46
|
+
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, onIsEndedChanged, onVolumeChanged, onPlaybackRateChanged, onCurrentTimeMsChanged, onFullscreenChange, children, className, ...intrinsicVideoAttributes }) => {
|
|
45
47
|
const videoRef = useRef(null);
|
|
46
48
|
const [totalTime, setTotalTime] = useState(0);
|
|
47
49
|
const totalTimeMs = useMemo(() => secondsToMs(totalTime), [totalTime]);
|
|
48
50
|
const [internalCurrentTimeMs, setInternalCurrentTimeMs] = useState(0);
|
|
51
|
+
// Unlike play, mute and fullscreen, this one isn't a prop: nothing can *set* a
|
|
52
|
+
// media to ended, it is something the element reports. So it is read from the
|
|
53
|
+
// element, and the two events that turn it on and off are joined by a sync on
|
|
54
|
+
// every time update, which covers a seek away from the end.
|
|
55
|
+
const [isEnded, setIsEnded] = useState(false);
|
|
49
56
|
const isTimeControlled = givenCurrentTimeMs !== undefined;
|
|
50
57
|
// The parent owns the time as soon as it provides one, so that is what gets
|
|
51
58
|
// displayed — not what the element reported one render later.
|
|
@@ -73,9 +80,18 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
73
80
|
const video = e.currentTarget;
|
|
74
81
|
const newTimeMs = secondsToMs(video.currentTime);
|
|
75
82
|
setInternalCurrentTimeMs(newTimeMs);
|
|
83
|
+
setIsEnded(video.ended);
|
|
76
84
|
if (intrinsicVideoAttributes.onTimeUpdate !== undefined)
|
|
77
85
|
intrinsicVideoAttributes.onTimeUpdate(e);
|
|
78
86
|
}, [intrinsicVideoAttributes.onTimeUpdate]);
|
|
87
|
+
const handleEndedEvent = useCallback((e) => {
|
|
88
|
+
setIsEnded(true);
|
|
89
|
+
intrinsicVideoAttributes.onEnded?.(e);
|
|
90
|
+
}, [intrinsicVideoAttributes.onEnded]);
|
|
91
|
+
const handlePlayEvent = useCallback((e) => {
|
|
92
|
+
setIsEnded(false);
|
|
93
|
+
intrinsicVideoAttributes.onPlay?.(e);
|
|
94
|
+
}, [intrinsicVideoAttributes.onPlay]);
|
|
79
95
|
// Custom action handlers
|
|
80
96
|
const handlePlayButtonClick = useCallback((e) => {
|
|
81
97
|
const wasPlaying = videoRef.current?.paused === false;
|
|
@@ -118,6 +134,7 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
118
134
|
const rootClss = mergeClassNames(c(null, {
|
|
119
135
|
'play-on': isPlaying,
|
|
120
136
|
'play-off': !isPlaying,
|
|
137
|
+
ended: isEnded,
|
|
121
138
|
'fullscreen-on': isFullscreen,
|
|
122
139
|
'fullscreen-off': !isFullscreen,
|
|
123
140
|
'loud': isLoud,
|
|
@@ -129,6 +146,7 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
129
146
|
const rootAttributes = {
|
|
130
147
|
'data-play-on': isPlaying ? '' : undefined,
|
|
131
148
|
'data-play-off': !isPlaying ? '' : undefined,
|
|
149
|
+
'data-ended': isEnded ? '' : undefined,
|
|
132
150
|
'data-fullscreen-on': isFullscreen ? '' : undefined,
|
|
133
151
|
'data-fullscreen-off': !isFullscreen ? '' : undefined,
|
|
134
152
|
'data-loud': isLoud ? '' : undefined,
|
|
@@ -249,11 +267,12 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
249
267
|
useChangeDispatch(isPlaying, onIsPlayingChanged);
|
|
250
268
|
useChangeDispatch(isFullscreen, onIsFullscreenChanged);
|
|
251
269
|
useChangeDispatch(isLoud, onIsLoudChanged);
|
|
270
|
+
useChangeDispatch(isEnded, onIsEndedChanged);
|
|
252
271
|
useChangeDispatch(volume, onVolumeChanged);
|
|
253
272
|
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'
|
|
273
|
+
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, onEnded: handleEndedEvent, onPlay: handlePlayEvent, children: [parsedSources.map((source, index) => typeof source === 'string'
|
|
255
274
|
? _jsx("source", { src: source }, index)
|
|
256
275
|
: _jsx("source", { src: source.src, type: source.type }, index)), parsedTracks.map((track, index) => typeof track === 'string'
|
|
257
276
|
? _jsx("track", { src: track }, index)
|
|
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 })] });
|
|
277
|
+
: _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, isEnded: isEnded })] });
|
|
259
278
|
};
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'react';
|
|
2
|
+
import type { WithViewportObservation } from '../utils/types.js';
|
|
2
3
|
import { type Props as ControlledProps } from './index.controlled.js';
|
|
3
4
|
/**
|
|
4
5
|
* Props for the {@link Video} component.
|
|
5
6
|
*
|
|
6
7
|
* Extends all ControlledVideo props except play, mute, fullscreen, volume, playbackRate, and their associated event handlers
|
|
7
|
-
* @property autoPlayWhenVisible - When `true`,
|
|
8
|
-
* component
|
|
9
|
-
* @property
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @property
|
|
14
|
-
*
|
|
8
|
+
* @property autoPlayWhenVisible - When `true`, starts playback every time the
|
|
9
|
+
* component enters the viewport.
|
|
10
|
+
* @property autoPlayOnceVisible - Same, but only the first time it does.
|
|
11
|
+
* @property autoPauseWhenHidden - When `true`, pauses playback every time the
|
|
12
|
+
* component leaves the viewport.
|
|
13
|
+
* @property autoPauseOnceHidden - Same, but only the first time it does.
|
|
14
|
+
* @property autoLoudWhenVisible - When `true`, unmutes every time the component
|
|
15
|
+
* enters the viewport.
|
|
16
|
+
* @property autoLoudOnceVisible - Same, but only the first time it does.
|
|
17
|
+
* @property autoMuteWhenHidden - When `true`, mutes every time the component leaves
|
|
18
|
+
* the viewport.
|
|
19
|
+
* @property autoMuteOnceHidden - Same, but only the first time it does.
|
|
20
|
+
* @property threshold - How much of the component has to be in view before it
|
|
21
|
+
* counts as visible, forwarded to the internal {@link IntersectionObserver}. `0.3`
|
|
22
|
+
* to start on a third of it; omitted, a single pixel is enough.
|
|
23
|
+
* @property root - The observer's root. Defaults to the viewport.
|
|
24
|
+
* @property rootMargin - Grows or shrinks that root before measuring.
|
|
25
|
+
* @property onVisibilityChanged - Called on every crossing with the new value,
|
|
26
|
+
* whether or not an `auto…` behaviour is bound to it. Never on mount.
|
|
15
27
|
* @property currentTimeMs - When provided, hands ownership of the current time
|
|
16
28
|
* (in milliseconds) to the parent, which is then responsible for updating it —
|
|
17
29
|
* typically to scrub the video from scroll position. A controlled time implies a
|
|
@@ -23,11 +35,15 @@ import { type Props as ControlledProps } from './index.controlled.js';
|
|
|
23
35
|
* @property children - React children rendered inside the `<video>` element itself
|
|
24
36
|
* (e.g. fallback content).
|
|
25
37
|
*/
|
|
26
|
-
export type Props = Omit<ControlledProps, 'play' | 'fullscreen' | 'volume' | 'mute' | 'playbackRate'
|
|
38
|
+
export type Props = WithViewportObservation<Omit<ControlledProps, 'play' | 'fullscreen' | 'volume' | 'mute' | 'playbackRate'>> & {
|
|
27
39
|
autoPlayWhenVisible?: boolean;
|
|
40
|
+
autoPlayOnceVisible?: boolean;
|
|
28
41
|
autoPauseWhenHidden?: boolean;
|
|
42
|
+
autoPauseOnceHidden?: boolean;
|
|
29
43
|
autoLoudWhenVisible?: boolean;
|
|
44
|
+
autoLoudOnceVisible?: boolean;
|
|
30
45
|
autoMuteWhenHidden?: boolean;
|
|
46
|
+
autoMuteOnceHidden?: boolean;
|
|
31
47
|
wrapperClassName?: string;
|
|
32
48
|
};
|
|
33
49
|
/**
|
|
@@ -39,5 +55,27 @@ export type Props = Omit<ControlledProps, 'play' | 'fullscreen' | 'volume' | 'mu
|
|
|
39
55
|
* @see {@link Props}
|
|
40
56
|
* @returns A `<figure>` element containing the video, its controls and optional
|
|
41
57
|
* subtitles.
|
|
58
|
+
*
|
|
59
|
+
* @remarks
|
|
60
|
+
* **Audio-only media belongs here too**, carried by the same `<video>` element.
|
|
61
|
+
* Everything this component drives — play, mute, volume, rate, time, `ended` — is
|
|
62
|
+
* `HTMLMediaElement`, shared with `<audio>`; only fullscreen is video's own. And an
|
|
63
|
+
* `<audio>` tag can't be autoplayed: the muted-autoplay exemption browsers grant is
|
|
64
|
+
* for video, so an `<audio>` waits for a user gesture whatever its muted state.
|
|
65
|
+
* `autoPlayWhenVisible` on an audio file therefore needs `muted` and `playsInline`
|
|
66
|
+
* on a `<video>`, which is what this is. The one thing lost is the assistive-tech
|
|
67
|
+
* label: a screen reader announces a video player. Worth a `tag` prop the day that
|
|
68
|
+
* matters more than autoplay — not before.
|
|
69
|
+
*
|
|
70
|
+
* Each viewport-driven behaviour comes in two flavours: `…When…` fires on every
|
|
71
|
+
* crossing, `…Once…` only on the first one. A `…Once…` flag is armed by its own
|
|
72
|
+
* automatic trigger and by nothing else — pressing play does not spend the one
|
|
73
|
+
* automatic play the component still owed. Setting both flavours of the same
|
|
74
|
+
* behaviour is the same as setting the `…When…` one alone.
|
|
75
|
+
*
|
|
76
|
+
* Browsers refuse an unmuted `play()` outside a user gesture, so pairing an
|
|
77
|
+
* `autoLoud…` with an `autoPlay…` will usually have the playback rejected: the
|
|
78
|
+
* element stays paused while the controls believe otherwise. Autoplay muted, and
|
|
79
|
+
* leave unmuting to the reader.
|
|
42
80
|
*/
|
|
43
81
|
export declare const Video: FunctionComponent<Props>;
|
|
@@ -4,7 +4,7 @@ import { clss } from '../../agnostic/css/clss/index.js';
|
|
|
4
4
|
import { IntersectionObserverComponent } from '../IntersectionObserver/index.js';
|
|
5
5
|
import { mergeClassNames } from '../utils/index.js';
|
|
6
6
|
import { videoWrapper as publicClassName } from '../public-classnames.js';
|
|
7
|
-
import { muteAttributeWorkaround } from './utils.js';
|
|
7
|
+
import { muteAttributeWorkaround, shouldRunAutoBehaviour } from './utils.js';
|
|
8
8
|
import cssModule from './styles.module.css';
|
|
9
9
|
import { ControlledVideo } from './index.controlled.js';
|
|
10
10
|
/**
|
|
@@ -16,34 +16,72 @@ import { ControlledVideo } from './index.controlled.js';
|
|
|
16
16
|
* @see {@link Props}
|
|
17
17
|
* @returns A `<figure>` element containing the video, its controls and optional
|
|
18
18
|
* subtitles.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* **Audio-only media belongs here too**, carried by the same `<video>` element.
|
|
22
|
+
* Everything this component drives — play, mute, volume, rate, time, `ended` — is
|
|
23
|
+
* `HTMLMediaElement`, shared with `<audio>`; only fullscreen is video's own. And an
|
|
24
|
+
* `<audio>` tag can't be autoplayed: the muted-autoplay exemption browsers grant is
|
|
25
|
+
* for video, so an `<audio>` waits for a user gesture whatever its muted state.
|
|
26
|
+
* `autoPlayWhenVisible` on an audio file therefore needs `muted` and `playsInline`
|
|
27
|
+
* on a `<video>`, which is what this is. The one thing lost is the assistive-tech
|
|
28
|
+
* label: a screen reader announces a video player. Worth a `tag` prop the day that
|
|
29
|
+
* matters more than autoplay — not before.
|
|
30
|
+
*
|
|
31
|
+
* Each viewport-driven behaviour comes in two flavours: `…When…` fires on every
|
|
32
|
+
* crossing, `…Once…` only on the first one. A `…Once…` flag is armed by its own
|
|
33
|
+
* automatic trigger and by nothing else — pressing play does not spend the one
|
|
34
|
+
* automatic play the component still owed. Setting both flavours of the same
|
|
35
|
+
* behaviour is the same as setting the `…When…` one alone.
|
|
36
|
+
*
|
|
37
|
+
* Browsers refuse an unmuted `play()` outside a user gesture, so pairing an
|
|
38
|
+
* `autoLoud…` with an `autoPlay…` will usually have the playback rejected: the
|
|
39
|
+
* element stays paused while the controls believe otherwise. Autoplay muted, and
|
|
40
|
+
* leave unmuting to the reader.
|
|
19
41
|
*/
|
|
20
|
-
export const Video = ({ loop, autoPlayWhenVisible, autoPauseWhenHidden, autoMuteWhenHidden, autoLoudWhenVisible, wrapperClassName, onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, ...controlledProps }) => {
|
|
42
|
+
export const Video = ({ loop, autoPlayWhenVisible, autoPlayOnceVisible, autoPauseWhenHidden, autoPauseOnceHidden, autoMuteWhenHidden, autoMuteOnceHidden, autoLoudWhenVisible, autoLoudOnceVisible, threshold, root, rootMargin, onVisibilityChanged, wrapperClassName, onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, ...controlledProps }) => {
|
|
21
43
|
// State & refs
|
|
22
44
|
const [play, setPlay] = useState(false);
|
|
23
45
|
const [volume, setVolume] = useState(1);
|
|
24
46
|
const [mute, setMute] = useState(false);
|
|
25
47
|
const [playbackRate, setPlaybackRate] = useState(1);
|
|
26
48
|
const [fullscreen, setFullscreen] = useState(false);
|
|
27
|
-
|
|
49
|
+
// One flag per `…Once…` behaviour, and each is armed by that behaviour's own
|
|
50
|
+
// automatic trigger. A single shared flag conflated four questions, and being set
|
|
51
|
+
// on any `play` event — a user click included — meant the first press cancelled
|
|
52
|
+
// behaviours that had nothing to do with playback.
|
|
53
|
+
const hasAutoPlayedOnce = useRef(false);
|
|
54
|
+
const hasAutoPausedOnce = useRef(false);
|
|
55
|
+
const hasAutoLoudedOnce = useRef(false);
|
|
56
|
+
const hasAutoMutedOnce = useRef(false);
|
|
28
57
|
// Several paths below ask for playback — the play button, autoPlayWhenVisible,
|
|
29
58
|
// autoPlay itself. None of them may win over a parent-owned time, so the
|
|
30
59
|
// invariant is applied where the state is forwarded rather than guarded at each
|
|
31
60
|
// of those call sites.
|
|
32
61
|
const isTimeControlled = controlledProps.currentTimeMs !== undefined;
|
|
33
|
-
const needsObserve = useMemo(() =>
|
|
62
|
+
const needsObserve = useMemo(() => onVisibilityChanged !== undefined
|
|
63
|
+
|| autoLoudWhenVisible === true
|
|
64
|
+
|| autoLoudOnceVisible === true
|
|
34
65
|
|| autoMuteWhenHidden === true
|
|
66
|
+
|| autoMuteOnceHidden === true
|
|
35
67
|
|| autoPlayWhenVisible === true
|
|
36
|
-
||
|
|
68
|
+
|| autoPlayOnceVisible === true
|
|
69
|
+
|| autoPauseWhenHidden === true
|
|
70
|
+
|| autoPauseOnceHidden === true, [
|
|
37
71
|
autoLoudWhenVisible,
|
|
72
|
+
autoLoudOnceVisible,
|
|
38
73
|
autoMuteWhenHidden,
|
|
74
|
+
autoMuteOnceHidden,
|
|
39
75
|
autoPlayWhenVisible,
|
|
40
|
-
|
|
76
|
+
autoPlayOnceVisible,
|
|
77
|
+
autoPauseWhenHidden,
|
|
78
|
+
autoPauseOnceHidden,
|
|
79
|
+
onVisibilityChanged
|
|
41
80
|
]);
|
|
42
81
|
// Intrisic event handlers
|
|
43
82
|
const handleOnPlayEvent = useCallback((e) => {
|
|
44
83
|
controlledProps.onPlay?.(e);
|
|
45
84
|
setPlay(true);
|
|
46
|
-
hasBeenAutoPlayed.current = true;
|
|
47
85
|
}, [controlledProps.onPlay]);
|
|
48
86
|
const handleOnPauseEvent = useCallback((e) => {
|
|
49
87
|
setPlay(false);
|
|
@@ -100,28 +138,41 @@ export const Video = ({ loop, autoPlayWhenVisible, autoPauseWhenHidden, autoMute
|
|
|
100
138
|
if (ioEntry === undefined)
|
|
101
139
|
return;
|
|
102
140
|
const { isIntersecting } = ioEntry;
|
|
103
|
-
|
|
141
|
+
onVisibilityChanged?.(isIntersecting);
|
|
142
|
+
if (isIntersecting) {
|
|
143
|
+
if (shouldRunAutoBehaviour(autoPlayWhenVisible, autoPlayOnceVisible, hasAutoPlayedOnce.current)) {
|
|
144
|
+
hasAutoPlayedOnce.current = true;
|
|
145
|
+
setPlay(true);
|
|
146
|
+
}
|
|
147
|
+
if (shouldRunAutoBehaviour(autoLoudWhenVisible, autoLoudOnceVisible, hasAutoLoudedOnce.current)) {
|
|
148
|
+
hasAutoLoudedOnce.current = true;
|
|
149
|
+
setMute(false);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (shouldRunAutoBehaviour(autoPauseWhenHidden, autoPauseOnceHidden, hasAutoPausedOnce.current)) {
|
|
154
|
+
hasAutoPausedOnce.current = true;
|
|
104
155
|
setPlay(false);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
&& !hasBeenAutoPlayed.current
|
|
109
|
-
&& isIntersecting)
|
|
110
|
-
setPlay(true);
|
|
111
|
-
if (autoMuteWhenHidden === true
|
|
112
|
-
&& !hasBeenAutoPlayed.current
|
|
113
|
-
&& !isIntersecting)
|
|
156
|
+
}
|
|
157
|
+
if (shouldRunAutoBehaviour(autoMuteWhenHidden, autoMuteOnceHidden, hasAutoMutedOnce.current)) {
|
|
158
|
+
hasAutoMutedOnce.current = true;
|
|
114
159
|
setMute(true);
|
|
160
|
+
}
|
|
115
161
|
}, [
|
|
116
162
|
autoPlayWhenVisible,
|
|
163
|
+
autoPlayOnceVisible,
|
|
117
164
|
autoPauseWhenHidden,
|
|
165
|
+
autoPauseOnceHidden,
|
|
118
166
|
autoMuteWhenHidden,
|
|
119
|
-
|
|
167
|
+
autoMuteOnceHidden,
|
|
168
|
+
autoLoudWhenVisible,
|
|
169
|
+
autoLoudOnceVisible,
|
|
170
|
+
onVisibilityChanged
|
|
120
171
|
]);
|
|
121
172
|
// `autoPlay` is forwarded to the element, but the play state is owned here, so
|
|
122
173
|
// it has to be seeded once on mount for the controls to agree with the element.
|
|
123
174
|
useEffect(() => {
|
|
124
|
-
if (controlledProps.autoPlay === true
|
|
175
|
+
if (controlledProps.autoPlay === true)
|
|
125
176
|
setPlay(true);
|
|
126
177
|
}, []);
|
|
127
178
|
// Render
|
|
@@ -129,6 +180,6 @@ export const Video = ({ loop, autoPlayWhenVisible, autoPauseWhenHidden, autoMute
|
|
|
129
180
|
const rootClss = mergeClassNames(c(), wrapperClassName);
|
|
130
181
|
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
182
|
return _jsx("div", { className: rootClss, children: needsObserve
|
|
132
|
-
? _jsx(IntersectionObserverComponent, { onIntersected: onIntersected, children: videoContent })
|
|
183
|
+
? _jsx(IntersectionObserverComponent, { threshold: threshold, root: root, rootMargin: rootMargin, onIntersected: onIntersected, children: videoContent })
|
|
133
184
|
: videoContent });
|
|
134
185
|
};
|
|
@@ -45,3 +45,17 @@ export declare function formatTime(ms: number, format: string, fps?: number): st
|
|
|
45
45
|
* @returns A ratio between `0` and `1`, clamped to the element's own bounds.
|
|
46
46
|
*/
|
|
47
47
|
export declare const getTimelineClickProgress: (event: React.MouseEvent<HTMLDivElement>) => number;
|
|
48
|
+
/**
|
|
49
|
+
* Whether a viewport-driven behaviour should run on this crossing.
|
|
50
|
+
*
|
|
51
|
+
* Each behaviour comes in two flavours: `…When…` on every crossing, `…Once…` on the
|
|
52
|
+
* first one only. Setting both is the same as setting `…When…` alone.
|
|
53
|
+
*
|
|
54
|
+
* @param whenCrossed - The `…When…` prop.
|
|
55
|
+
* @param onceOnly - The `…Once…` prop.
|
|
56
|
+
* @param hasFired - Whether this behaviour's own automatic trigger already ran. It
|
|
57
|
+
* tracks that behaviour and nothing else: a user pressing play must not spend an
|
|
58
|
+
* automatic mute the component still owed.
|
|
59
|
+
* @returns Whether to apply it now.
|
|
60
|
+
*/
|
|
61
|
+
export declare const shouldRunAutoBehaviour: (whenCrossed: boolean | undefined, onceOnly: boolean | undefined, hasFired: boolean) => boolean;
|
|
@@ -160,3 +160,21 @@ export const getTimelineClickProgress = (event) => {
|
|
|
160
160
|
const position = event.clientX - timelineRect.left;
|
|
161
161
|
return Math.min(1, Math.max(0, position / timelineRect.width));
|
|
162
162
|
};
|
|
163
|
+
/**
|
|
164
|
+
* Whether a viewport-driven behaviour should run on this crossing.
|
|
165
|
+
*
|
|
166
|
+
* Each behaviour comes in two flavours: `…When…` on every crossing, `…Once…` on the
|
|
167
|
+
* first one only. Setting both is the same as setting `…When…` alone.
|
|
168
|
+
*
|
|
169
|
+
* @param whenCrossed - The `…When…` prop.
|
|
170
|
+
* @param onceOnly - The `…Once…` prop.
|
|
171
|
+
* @param hasFired - Whether this behaviour's own automatic trigger already ran. It
|
|
172
|
+
* tracks that behaviour and nothing else: a user pressing play must not spend an
|
|
173
|
+
* automatic mute the component still owed.
|
|
174
|
+
* @returns Whether to apply it now.
|
|
175
|
+
*/
|
|
176
|
+
export const shouldRunAutoBehaviour = (whenCrossed, onceOnly, hasFired) => {
|
|
177
|
+
if (whenCrossed === true)
|
|
178
|
+
return true;
|
|
179
|
+
return onceOnly === true && !hasFired;
|
|
180
|
+
};
|
|
@@ -6,3 +6,35 @@
|
|
|
6
6
|
export type WithClassName<T extends Record<string, unknown>> = T & {
|
|
7
7
|
className?: string | Array<string | null | undefined>;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* The settings of an {@link IntersectionObserver} a component mounts internally.
|
|
11
|
+
*
|
|
12
|
+
* A component that offers viewport-driven behaviour has to offer its dial too:
|
|
13
|
+
* `autoPlayWhenVisible` means little if "visible" can't be told to mean *thirty
|
|
14
|
+
* percent* visible, and an infinite list wants to start loading before its sentinel
|
|
15
|
+
* shows, which is `rootMargin`'s job.
|
|
16
|
+
*
|
|
17
|
+
* They mirror `ObserverOptions` of the `IntersectionObserver` component, where they
|
|
18
|
+
* end up. Restated here rather than imported from it: that component reads
|
|
19
|
+
* {@link WithClassName} from this very file, and the two shouldn't import each other.
|
|
20
|
+
*/
|
|
21
|
+
export type ViewportObserverOptions = {
|
|
22
|
+
threshold?: number | number[];
|
|
23
|
+
root?: HTMLElement;
|
|
24
|
+
rootMargin?: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Extends a type with an internal observer's settings and with what it sees.
|
|
28
|
+
*
|
|
29
|
+
* For a component observing **one** element — itself, usually. A component with
|
|
30
|
+
* several observers takes {@link ViewportObserverOptions} alone, since a single
|
|
31
|
+
* visibility handler couldn't say which of them crossed.
|
|
32
|
+
*
|
|
33
|
+
* `Scrllgngn` deliberately stays out of both: its observers take a `rootMargin`
|
|
34
|
+
* derived from its `viewportOffset*` props, which are its own version of the dial.
|
|
35
|
+
*
|
|
36
|
+
* @template T - The base type to extend.
|
|
37
|
+
*/
|
|
38
|
+
export type WithViewportObservation<T extends Record<string, unknown>> = T & ViewportObserverOptions & {
|
|
39
|
+
onVisibilityChanged?: (isVisible: boolean) => void;
|
|
40
|
+
};
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * as copy from './copy/index.js'
|
|
2
2
|
export * as download from './download/index.js'
|
|
3
|
-
export * as exists from './exists/index.js'
|
|
4
3
|
export * as generateSignedUrl from './generate-signed-url/index.js'
|
|
4
|
+
export * as exists from './exists/index.js'
|
|
5
5
|
export * as getMetadata from './get-metadata/index.js'
|
|
6
6
|
export * as getPermissions from './get-permissions/index.js'
|
|
7
7
|
export * as move from './move/index.js'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * as copy from './copy/index.js'
|
|
2
2
|
export * as download from './download/index.js'
|
|
3
|
-
export * as exists from './exists/index.js'
|
|
4
3
|
export * as generateSignedUrl from './generate-signed-url/index.js'
|
|
4
|
+
export * as exists from './exists/index.js'
|
|
5
5
|
export * as getMetadata from './get-metadata/index.js'
|
|
6
6
|
export * as getPermissions from './get-permissions/index.js'
|
|
7
7
|
export * as move from './move/index.js'
|
package/node/files/index.d.ts
CHANGED
package/node/files/index.js
CHANGED
|
@@ -3,8 +3,8 @@ export * as brighten from './brighten/index.js'
|
|
|
3
3
|
export * as extend from './extend/index.js'
|
|
4
4
|
export * as extract from './extract/index.js'
|
|
5
5
|
export * as flatten from './flatten/index.js'
|
|
6
|
-
export * as flop from './flop/index.js'
|
|
7
6
|
export * as flip from './flip/index.js'
|
|
7
|
+
export * as flop from './flop/index.js'
|
|
8
8
|
export * as hue from './hue/index.js'
|
|
9
9
|
export * as level from './level/index.js'
|
|
10
10
|
export * as lighten from './lighten/index.js'
|
|
@@ -3,8 +3,8 @@ export * as brighten from './brighten/index.js'
|
|
|
3
3
|
export * as extend from './extend/index.js'
|
|
4
4
|
export * as extract from './extract/index.js'
|
|
5
5
|
export * as flatten from './flatten/index.js'
|
|
6
|
-
export * as flop from './flop/index.js'
|
|
7
6
|
export * as flip from './flip/index.js'
|
|
7
|
+
export * as flop from './flop/index.js'
|
|
8
8
|
export * as hue from './hue/index.js'
|
|
9
9
|
export * as level from './level/index.js'
|
|
10
10
|
export * as lighten from './lighten/index.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design-edito/tools",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "Maxime Fabas",
|
|
6
6
|
"license": "ISC",
|
|
@@ -1275,6 +1275,31 @@
|
|
|
1275
1275
|
"./agnostic/strings/trim/index.js": {
|
|
1276
1276
|
"import": "./agnostic/strings/trim/index.js"
|
|
1277
1277
|
},
|
|
1278
|
+
"./agnostic/subtitles": {
|
|
1279
|
+
"import": "./agnostic/subtitles/index.js",
|
|
1280
|
+
"types": "./agnostic/subtitles/index.d.ts"
|
|
1281
|
+
},
|
|
1282
|
+
"./agnostic/subtitles/index.js": {
|
|
1283
|
+
"import": "./agnostic/subtitles/index.js"
|
|
1284
|
+
},
|
|
1285
|
+
"./agnostic/subtitles/parse-srt": {
|
|
1286
|
+
"import": "./agnostic/subtitles/parse-srt/index.js",
|
|
1287
|
+
"types": "./agnostic/subtitles/parse-srt/index.d.ts"
|
|
1288
|
+
},
|
|
1289
|
+
"./agnostic/subtitles/parse-srt/index.js": {
|
|
1290
|
+
"import": "./agnostic/subtitles/parse-srt/index.js"
|
|
1291
|
+
},
|
|
1292
|
+
"./agnostic/subtitles/parse-srt/types.js": {
|
|
1293
|
+
"import": "./agnostic/subtitles/parse-srt/types.js",
|
|
1294
|
+
"types": "./agnostic/subtitles/parse-srt/types.d.ts"
|
|
1295
|
+
},
|
|
1296
|
+
"./agnostic/subtitles/srt-timecode-to-ms": {
|
|
1297
|
+
"import": "./agnostic/subtitles/srt-timecode-to-ms/index.js",
|
|
1298
|
+
"types": "./agnostic/subtitles/srt-timecode-to-ms/index.d.ts"
|
|
1299
|
+
},
|
|
1300
|
+
"./agnostic/subtitles/srt-timecode-to-ms/index.js": {
|
|
1301
|
+
"import": "./agnostic/subtitles/srt-timecode-to-ms/index.js"
|
|
1302
|
+
},
|
|
1278
1303
|
"./agnostic/time": {
|
|
1279
1304
|
"import": "./agnostic/time/index.js",
|
|
1280
1305
|
"types": "./agnostic/time/index.d.ts"
|