@design-edito/tools 0.5.16 → 0.5.18
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/colors/index.d.ts +4 -4
- package/agnostic/colors/index.js +4 -4
- package/agnostic/css/index.d.ts +1 -1
- package/agnostic/css/index.js +1 -1
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +10 -10
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +10 -10
- package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +6 -6
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +6 -6
- package/agnostic/html/index.d.ts +2 -2
- package/agnostic/html/index.js +2 -2
- package/agnostic/index.d.ts +5 -5
- package/agnostic/index.js +5 -5
- package/agnostic/misc/index.d.ts +5 -5
- package/agnostic/misc/index.js +5 -5
- package/agnostic/numbers/index.d.ts +2 -2
- package/agnostic/numbers/index.js +2 -2
- package/agnostic/objects/index.d.ts +2 -2
- package/agnostic/objects/index.js +2 -2
- package/agnostic/sanitization/index.d.ts +1 -1
- package/agnostic/sanitization/index.js +1 -1
- package/agnostic/subtitles/index.d.ts +1 -1
- package/agnostic/subtitles/index.js +1 -1
- package/agnostic/time/index.d.ts +2 -2
- package/agnostic/time/index.js +2 -2
- package/components/Image/index.d.ts +6 -0
- package/components/Image/index.js +3 -18
- package/components/IntersectionObserver/index.d.ts +28 -3
- package/components/IntersectionObserver/index.js +51 -20
- package/components/Video/index.controlled.d.ts +6 -22
- package/components/Video/index.controlled.js +28 -42
- package/components/Video/index.d.ts +28 -39
- package/components/Video/index.js +90 -92
- package/components/Video/types.d.ts +26 -0
- package/components/Video/types.js +1 -0
- package/components/Video/utils.d.ts +38 -1
- package/components/Video/utils.js +23 -4
- package/components/index.d.ts +5 -5
- package/components/index.js +5 -5
- package/components/public-classnames.d.ts +0 -1
- package/components/public-classnames.js +0 -1
- package/components/utils/index.d.ts +21 -0
- package/components/utils/index.js +40 -0
- package/components/utils/viewport-behaviours/index.d.ts +54 -0
- package/components/utils/viewport-behaviours/index.js +163 -0
- package/components/utils/viewport-behaviours/types.d.ts +109 -0
- package/components/utils/viewport-behaviours/types.js +14 -0
- package/components/utils/viewport-behaviours/utils.d.ts +26 -0
- package/components/utils/viewport-behaviours/utils.js +55 -0
- package/node/@aws-s3/storage/file/index.d.ts +2 -2
- package/node/@aws-s3/storage/file/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/cloud-storage/operations/index.d.ts +1 -1
- package/node/cloud-storage/operations/index.js +1 -1
- package/node/files/index.d.ts +1 -1
- package/node/files/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 +5 -5
- package/node/images/transform/operations/index.js +5 -5
- package/node/process/index.d.ts +1 -1
- package/node/process/index.js +1 -1
- package/package.json +16 -1
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { mergeClassNames } from '../utils/index.js';
|
|
6
|
-
import { videoWrapper as publicClassName } from '../public-classnames.js';
|
|
7
|
-
import { muteAttributeWorkaround, shouldRunAutoBehaviour } from './utils.js';
|
|
8
|
-
import cssModule from './styles.module.css';
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useViewportBehaviours } from '../utils/viewport-behaviours/index.js';
|
|
4
|
+
import { forceJumpTo, muteAttributeWorkaround } from './utils.js';
|
|
9
5
|
import { ControlledVideo } from './index.controlled.js';
|
|
10
6
|
/**
|
|
11
7
|
* Full-featured video player component. Wraps a native `<video>` element with
|
|
@@ -28,11 +24,18 @@ import { ControlledVideo } from './index.controlled.js';
|
|
|
28
24
|
* label: a screen reader announces a video player. Worth a `tag` prop the day that
|
|
29
25
|
* matters more than autoplay — not before.
|
|
30
26
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
27
|
+
* **Viewport-driven behaviour is declared, not named by a prop.** `whenVisible` and
|
|
28
|
+
* `whenHidden` take a verb or a list of them, out of {@link VideoAction} — `'play'`,
|
|
29
|
+
* `'mute'`, `'jump-to:500'` — each optionally suffixed by `':once'` and `':force'`. See
|
|
30
|
+
* `components/utils/viewport-behaviours` for the grammar and its rules, and the
|
|
31
|
+
* `visibility…` props for what « visible » means and how long
|
|
32
|
+
* it has to have been true.
|
|
33
|
+
*
|
|
34
|
+
* **An instruction yields to the reader by default.** Touching a control surrenders its
|
|
35
|
+
* domain for the rest of the mount: playback covers the play and pause buttons, the
|
|
36
|
+
* picture when it toggles, and the timeline, since seeking is saying where one wants to
|
|
37
|
+
* be. Sound covers the two sound buttons and the volume slider. Fullscreen and playback
|
|
38
|
+
* rate surrender nothing, being neither. `':force'` opts out.
|
|
36
39
|
*
|
|
37
40
|
* **`loop` reaches the element untouched**, like every other native media attribute
|
|
38
41
|
* this component does not drive itself. It was once destructured out of the props and
|
|
@@ -41,14 +44,14 @@ import { ControlledVideo } from './index.controlled.js';
|
|
|
41
44
|
* consequence worth knowing: a looping element never fires `ended`, so the `--ended`
|
|
42
45
|
* modifier and the `isEnded` it feeds to `Subtitles` simply never come.
|
|
43
46
|
*
|
|
44
|
-
* Browsers refuse an unmuted `play()` outside a user gesture, so pairing
|
|
45
|
-
*
|
|
47
|
+
* Browsers refuse an unmuted `play()` outside a user gesture, so pairing a `'loud'`
|
|
48
|
+
* instruction with a `'play'` one will usually have the playback rejected. The
|
|
46
49
|
* refusal is caught rather than ignored — the element is read back once the attempt
|
|
47
50
|
* settles, and the play state follows what it says — so the controls stay truthful.
|
|
48
51
|
* The media still won't play, though: autoplay muted, and leave unmuting to the
|
|
49
52
|
* reader.
|
|
50
53
|
*/
|
|
51
|
-
export const Video = ({ defaultSubtitlesOn = true,
|
|
54
|
+
export const Video = ({ defaultSubtitlesOn = true, visibilityThreshold, visibilityRoot, visibilityRootMargin, visibilityOnAfterMs, visibilityOffAfterMs, whenVisible, whenHidden, onVisibilityChanged, onVideoClicked, onPlayButtonClicked, onPauseButtonClicked, onLoudButtonClicked, onMuteButtonClicked, onVolumeRangeChanged, onRateRangeChanged, onFullscreenButtonClicked, onSubtitlesButtonClicked, ...controlledProps }) => {
|
|
52
55
|
// State & refs
|
|
53
56
|
const [play, setPlay] = useState(false);
|
|
54
57
|
const [volume, setVolume] = useState(1);
|
|
@@ -56,38 +59,55 @@ export const Video = ({ defaultSubtitlesOn = true, autoPlayWhenVisible, autoPlay
|
|
|
56
59
|
const [playbackRate, setPlaybackRate] = useState(1);
|
|
57
60
|
const [fullscreen, setFullscreen] = useState(false);
|
|
58
61
|
const [subtitlesOn, setSubtitlesOn] = useState(defaultSubtitlesOn);
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// behaviours that had nothing to do with playback.
|
|
63
|
-
const hasAutoPlayedOnce = useRef(false);
|
|
64
|
-
const hasAutoPausedOnce = useRef(false);
|
|
65
|
-
const hasAutoLoudedOnce = useRef(false);
|
|
66
|
-
const hasAutoMutedOnce = useRef(false);
|
|
67
|
-
// Several paths below ask for playback — the play button, autoPlayWhenVisible,
|
|
68
|
-
// autoPlay itself. None of them may win over a parent-owned time, so the
|
|
69
|
-
// invariant is applied where the state is forwarded rather than guarded at each
|
|
70
|
-
// of those call sites.
|
|
62
|
+
// Several paths below ask for playback — the play button, a `'play'` instruction,
|
|
63
|
+
// `autoPlay` itself. None of them may win over a parent-owned time, so the invariant
|
|
64
|
+
// is applied where the state is forwarded rather than guarded at each call site.
|
|
71
65
|
const isTimeControlled = controlledProps.currentTimeMs !== undefined;
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
66
|
+
const rootRef = useRef(null);
|
|
67
|
+
const videoRef = useRef(null);
|
|
68
|
+
// Viewport behaviours
|
|
69
|
+
// What each verb does, and what it competes with. Rebuilt on every render — it closes
|
|
70
|
+
// over the setters — which is why the generic layer reads it through a ref rather than
|
|
71
|
+
// a dependency list.
|
|
72
|
+
//
|
|
73
|
+
// **Seeking belongs to `playback`.** A reader who has dragged the timeline has said
|
|
74
|
+
// where they want to be, and an automatic jump would take it back from them.
|
|
75
|
+
const actions = {
|
|
76
|
+
play: { kind: 'start', domain: 'playback', run: () => setPlay(true) },
|
|
77
|
+
pause: { kind: 'stop', domain: 'playback', run: () => setPlay(false) },
|
|
78
|
+
loud: { kind: 'start', domain: 'sound', run: () => setMute(false) },
|
|
79
|
+
mute: { kind: 'stop', domain: 'sound', run: () => setMute(true) },
|
|
80
|
+
// A seek starts nothing on its own — it moves a playhead, playing or not — so a gate
|
|
81
|
+
// has no reason to hold it back.
|
|
82
|
+
'jump-to': {
|
|
83
|
+
kind: 'stop',
|
|
84
|
+
domain: 'playback',
|
|
85
|
+
run: arg => {
|
|
86
|
+
const targetMs = Number(arg);
|
|
87
|
+
if (!Number.isFinite(targetMs))
|
|
88
|
+
return;
|
|
89
|
+
forceJumpTo(videoRef.current, targetMs);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
'jump-start': { kind: 'stop', domain: 'playback', run: () => forceJumpTo(videoRef.current, 0) },
|
|
93
|
+
'jump-end': { kind: 'stop', domain: 'playback', run: () => forceJumpTo(videoRef.current, -1) }
|
|
94
|
+
};
|
|
95
|
+
const { surrender: surrenderAnyDomain } = useViewportBehaviours(rootRef, {
|
|
96
|
+
visibilityThreshold,
|
|
97
|
+
visibilityRoot,
|
|
98
|
+
visibilityRootMargin,
|
|
99
|
+
visibilityOnAfterMs,
|
|
100
|
+
visibilityOffAfterMs,
|
|
101
|
+
whenVisible,
|
|
102
|
+
whenHidden,
|
|
89
103
|
onVisibilityChanged
|
|
90
|
-
|
|
104
|
+
}, actions);
|
|
105
|
+
// Narrowed back to this component's own domains. The generic layer takes a `string`,
|
|
106
|
+
// having no vocabulary of its own; handing the handlers a typed door means a
|
|
107
|
+
// misspelled domain fails to compile instead of quietly never surrendering anything.
|
|
108
|
+
const surrender = useCallback((domain) => {
|
|
109
|
+
surrenderAnyDomain(domain);
|
|
110
|
+
}, [surrenderAnyDomain]);
|
|
91
111
|
// Intrisic event handlers
|
|
92
112
|
const handleOnPlayEvent = useCallback((e) => {
|
|
93
113
|
controlledProps.onPlay?.(e);
|
|
@@ -125,28 +145,41 @@ export const Video = ({ defaultSubtitlesOn = true, autoPlayWhenVisible, autoPlay
|
|
|
125
145
|
// User actions
|
|
126
146
|
const handlePlayButtonClick = useCallback((e, isPlaying, video) => {
|
|
127
147
|
onPlayButtonClicked?.(e, isPlaying, video);
|
|
148
|
+
surrender('playback');
|
|
128
149
|
setPlay(true);
|
|
129
|
-
}, [onPlayButtonClicked]);
|
|
150
|
+
}, [onPlayButtonClicked, surrender]);
|
|
130
151
|
const handlePauseButtonClick = useCallback((e, isPlaying, video) => {
|
|
131
152
|
onPauseButtonClicked?.(e, isPlaying, video);
|
|
153
|
+
surrender('playback');
|
|
132
154
|
setPlay(false);
|
|
133
|
-
}, [onPauseButtonClicked]);
|
|
155
|
+
}, [onPauseButtonClicked, surrender]);
|
|
156
|
+
// The picture toggles, where the two buttons each say one thing. It is the same gesture
|
|
157
|
+
// as pressing one of them — a consumer tracking whether the reader has taken over
|
|
158
|
+
// playback has to count this one too, since it is the same decision made elsewhere.
|
|
159
|
+
const handleVideoClick = useCallback((e, isPlaying, video) => {
|
|
160
|
+
onVideoClicked?.(e, isPlaying, video);
|
|
161
|
+
surrender('playback');
|
|
162
|
+
setPlay(!isPlaying);
|
|
163
|
+
}, [onVideoClicked, surrender]);
|
|
134
164
|
const handleLoudButtonClick = useCallback((e, isLoud, video) => {
|
|
135
165
|
onLoudButtonClicked?.(e, isLoud, video);
|
|
166
|
+
surrender('sound');
|
|
136
167
|
setMute(false);
|
|
137
|
-
}, [onLoudButtonClicked]);
|
|
168
|
+
}, [onLoudButtonClicked, surrender]);
|
|
138
169
|
const handleMuteButtonClick = useCallback((e, isLoud, video) => {
|
|
139
170
|
onMuteButtonClicked?.(e, isLoud, video);
|
|
171
|
+
surrender('sound');
|
|
140
172
|
setMute(true);
|
|
141
|
-
}, [onMuteButtonClicked]);
|
|
173
|
+
}, [onMuteButtonClicked, surrender]);
|
|
142
174
|
const handleRateRangeChange = useCallback((e, targetRate, currentRate, video) => {
|
|
143
175
|
onRateRangeChanged?.(e, targetRate, currentRate, video);
|
|
144
176
|
setPlaybackRate(targetRate);
|
|
145
177
|
}, [onRateRangeChanged]);
|
|
146
178
|
const handleVolumeRangeChange = useCallback((e, targetVolume, currentVolume, video) => {
|
|
147
179
|
onVolumeRangeChanged?.(e, targetVolume, currentVolume, video);
|
|
180
|
+
surrender('sound');
|
|
148
181
|
setVolume(targetVolume);
|
|
149
|
-
}, [onVolumeRangeChanged]);
|
|
182
|
+
}, [onVolumeRangeChanged, surrender]);
|
|
150
183
|
const handleFullscreenButtonClick = useCallback((e, isFullscreen, video) => {
|
|
151
184
|
onFullscreenButtonClicked?.(e, isFullscreen, video);
|
|
152
185
|
setFullscreen(!isFullscreen);
|
|
@@ -159,42 +192,6 @@ export const Video = ({ defaultSubtitlesOn = true, autoPlayWhenVisible, autoPlay
|
|
|
159
192
|
onSubtitlesButtonClicked?.(e, isSubtitlesOn, video);
|
|
160
193
|
setSubtitlesOn(!isSubtitlesOn);
|
|
161
194
|
}, [onSubtitlesButtonClicked]);
|
|
162
|
-
// Intersection Observer
|
|
163
|
-
const onIntersected = useCallback(({ ioEntry }) => {
|
|
164
|
-
if (ioEntry === undefined)
|
|
165
|
-
return;
|
|
166
|
-
const { isIntersecting } = ioEntry;
|
|
167
|
-
onVisibilityChanged?.(isIntersecting);
|
|
168
|
-
if (isIntersecting) {
|
|
169
|
-
if (shouldRunAutoBehaviour(autoPlayWhenVisible, autoPlayOnceVisible, hasAutoPlayedOnce.current)) {
|
|
170
|
-
hasAutoPlayedOnce.current = true;
|
|
171
|
-
setPlay(true);
|
|
172
|
-
}
|
|
173
|
-
if (shouldRunAutoBehaviour(autoLoudWhenVisible, autoLoudOnceVisible, hasAutoLoudedOnce.current)) {
|
|
174
|
-
hasAutoLoudedOnce.current = true;
|
|
175
|
-
setMute(false);
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (shouldRunAutoBehaviour(autoPauseWhenHidden, autoPauseOnceHidden, hasAutoPausedOnce.current)) {
|
|
180
|
-
hasAutoPausedOnce.current = true;
|
|
181
|
-
setPlay(false);
|
|
182
|
-
}
|
|
183
|
-
if (shouldRunAutoBehaviour(autoMuteWhenHidden, autoMuteOnceHidden, hasAutoMutedOnce.current)) {
|
|
184
|
-
hasAutoMutedOnce.current = true;
|
|
185
|
-
setMute(true);
|
|
186
|
-
}
|
|
187
|
-
}, [
|
|
188
|
-
autoPlayWhenVisible,
|
|
189
|
-
autoPlayOnceVisible,
|
|
190
|
-
autoPauseWhenHidden,
|
|
191
|
-
autoPauseOnceHidden,
|
|
192
|
-
autoMuteWhenHidden,
|
|
193
|
-
autoMuteOnceHidden,
|
|
194
|
-
autoLoudWhenVisible,
|
|
195
|
-
autoLoudOnceVisible,
|
|
196
|
-
onVisibilityChanged
|
|
197
|
-
]);
|
|
198
195
|
// `autoPlay` is forwarded to the element, but the play state is owned here, so
|
|
199
196
|
// it has to be seeded once on mount for the controls to agree with the element.
|
|
200
197
|
useEffect(() => {
|
|
@@ -202,10 +199,11 @@ export const Video = ({ defaultSubtitlesOn = true, autoPlayWhenVisible, autoPlay
|
|
|
202
199
|
setPlay(true);
|
|
203
200
|
}, []);
|
|
204
201
|
// Render
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
202
|
+
//
|
|
203
|
+
// **The `<figure>` is the root, and there is nothing above it.** The observer runs on
|
|
204
|
+
// that element rather than on a box built to hold it: a wrapper would take the outer
|
|
205
|
+
// position — the one a consumer lays out — and leave `className` naming an inner
|
|
206
|
+
// element, which is the wrong way round. So no wrapper, and no second class-name prop
|
|
207
|
+
// to reach past it.
|
|
208
|
+
return _jsx(ControlledVideo, { ...controlledProps, rootRef: rootRef, videoRef: videoRef, play: play && !isTimeControlled, volume: volume, mute: mute, playbackRate: playbackRate, fullscreen: fullscreen, subtitlesOn: subtitlesOn, onPlay: handleOnPlayEvent, onPause: handleOnPauseEvent, onIsPlayingChanged: handleIsPlayingChanged, onVolumeChange: handleOnVolumeChangeEvent, onRateChange: handleOnRateChangeEvent, onLoadedMetadata: handleOnLoadedMetadataEvent, onFullscreenChange: handleFullscreenChange, onVideoClicked: handleVideoClick, onPlayButtonClicked: handlePlayButtonClick, onPauseButtonClicked: handlePauseButtonClick, onLoudButtonClicked: handleLoudButtonClick, onMuteButtonClicked: handleMuteButtonClick, onVolumeRangeChanged: handleVolumeRangeChange, onRateRangeChanged: handleRateRangeChange, onFullscreenButtonClicked: handleFullscreenButtonClick, onSubtitlesButtonClicked: handleSubtitlesButtonClick });
|
|
211
209
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verbs a video answers to, as the table keys them.
|
|
3
|
+
*
|
|
4
|
+
* `jump-to` is the only one taking an argument, and the table is keyed by the verb
|
|
5
|
+
* alone — the argument is parsed out of the instruction before the lookup.
|
|
6
|
+
*/
|
|
7
|
+
export type VideoVerb = 'play' | 'pause' | 'loud' | 'mute' | 'jump-to' | 'jump-start' | 'jump-end';
|
|
8
|
+
/**
|
|
9
|
+
* The instructions a consumer writes in `whenVisible` / `whenHidden`.
|
|
10
|
+
*
|
|
11
|
+
* `'jump-to:500'` seeks to 500 ms. **A negative value counts from the end**:
|
|
12
|
+
* `'jump-to:-1'` is the last reachable timecode, and it is `-1` rather than `-0`
|
|
13
|
+
* because `-0 === 0` in JavaScript and would collide with the start. `'jump-start'`
|
|
14
|
+
* and `'jump-end'` are the shorthands for `'jump-to:0'` and `'jump-to:-1'`.
|
|
15
|
+
*
|
|
16
|
+
* Every one of them takes the `':once'` and `':force'` suffixes, which are the generic
|
|
17
|
+
* layer's and are not restated here.
|
|
18
|
+
*/
|
|
19
|
+
export type VideoAction = Exclude<VideoVerb, 'jump-to'> | `jump-to:${number}`;
|
|
20
|
+
/**
|
|
21
|
+
* What a verb competes with, so that taking one control over doesn't silence the rest.
|
|
22
|
+
*
|
|
23
|
+
* Seeking belongs to `'playback'`: a reader who drags the timeline has said where they
|
|
24
|
+
* want to be, and an automatic jump would take it back from them.
|
|
25
|
+
*/
|
|
26
|
+
export type VideoDomain = 'playback' | 'sound';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two list shapes a `<video>` accepts as children, as records.
|
|
3
|
+
*
|
|
4
|
+
* They live here and not next to the component because nothing about them is React:
|
|
5
|
+
* they describe a `<source>` and a `<track>` element.
|
|
6
|
+
*
|
|
7
|
+
* **A video source is not a picture source**, which is why `Image` keeps a shape of its
|
|
8
|
+
* own rather than sharing this one: a `<source>` inside a `<video>` carries `src`, one
|
|
9
|
+
* inside a `<picture>` carries `srcSet`, `media` and `sizes`, and neither element accepts
|
|
10
|
+
* the other's attributes. What the two do share is how a list of them is read — see
|
|
11
|
+
* `parseSourceList` in `components/utils`.
|
|
12
|
+
*/
|
|
13
|
+
export type SourceData = {
|
|
14
|
+
src?: string;
|
|
15
|
+
type?: string;
|
|
16
|
+
};
|
|
17
|
+
export type TrackData = {
|
|
18
|
+
src?: string;
|
|
19
|
+
kind?: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
|
|
20
|
+
srclang?: string;
|
|
21
|
+
label?: string;
|
|
22
|
+
default?: boolean;
|
|
23
|
+
};
|
|
1
24
|
export declare const muteAttributeWorkaround: (video: HTMLVideoElement | null, shouldMute: boolean) => void;
|
|
2
25
|
export declare const forceMute: (video: HTMLVideoElement | null) => void;
|
|
3
26
|
export declare const forceLoud: (video: HTMLVideoElement | null) => void;
|
|
@@ -40,4 +63,18 @@ export declare const getTimelineClickProgress: (event: React.MouseEvent<HTMLDivE
|
|
|
40
63
|
* automatic mute the component still owed.
|
|
41
64
|
* @returns Whether to apply it now.
|
|
42
65
|
*/
|
|
43
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Seeks the element, in milliseconds, counting from the end when asked negatively.
|
|
68
|
+
*
|
|
69
|
+
* **A negative target counts back from the duration**, so `-1` is the last reachable
|
|
70
|
+
* timecode. It is `-1` and not `-0` because `-0 === 0` in JavaScript, which would make
|
|
71
|
+
* the end of a video indistinguishable from its start.
|
|
72
|
+
*
|
|
73
|
+
* Nothing happens before the metadata lands: the duration is `NaN` until then, and a
|
|
74
|
+
* seek computed against it would land anywhere. An instruction asking for the end of a
|
|
75
|
+
* video whose length is still unknown is simply not yet answerable.
|
|
76
|
+
*
|
|
77
|
+
* @param video - The element, or `null` before it mounts.
|
|
78
|
+
* @param targetMs - Where to go. Negative counts from the end.
|
|
79
|
+
*/
|
|
80
|
+
export declare const forceJumpTo: (video: HTMLVideoElement | null, targetMs: number) => void;
|
|
@@ -131,8 +131,27 @@ export const getTimelineClickProgress = (event) => {
|
|
|
131
131
|
* automatic mute the component still owed.
|
|
132
132
|
* @returns Whether to apply it now.
|
|
133
133
|
*/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Seeks the element, in milliseconds, counting from the end when asked negatively.
|
|
136
|
+
*
|
|
137
|
+
* **A negative target counts back from the duration**, so `-1` is the last reachable
|
|
138
|
+
* timecode. It is `-1` and not `-0` because `-0 === 0` in JavaScript, which would make
|
|
139
|
+
* the end of a video indistinguishable from its start.
|
|
140
|
+
*
|
|
141
|
+
* Nothing happens before the metadata lands: the duration is `NaN` until then, and a
|
|
142
|
+
* seek computed against it would land anywhere. An instruction asking for the end of a
|
|
143
|
+
* video whose length is still unknown is simply not yet answerable.
|
|
144
|
+
*
|
|
145
|
+
* @param video - The element, or `null` before it mounts.
|
|
146
|
+
* @param targetMs - Where to go. Negative counts from the end.
|
|
147
|
+
*/
|
|
148
|
+
export const forceJumpTo = (video, targetMs) => {
|
|
149
|
+
if (video === null)
|
|
150
|
+
return;
|
|
151
|
+
const durationMs = secondsToMs(video.duration);
|
|
152
|
+
if (!Number.isFinite(durationMs))
|
|
153
|
+
return;
|
|
154
|
+
const absolute = targetMs < 0 ? durationMs + targetMs : targetMs;
|
|
155
|
+
// eslint-disable-next-line no-param-reassign
|
|
156
|
+
video.currentTime = msToSeconds(Math.min(Math.max(absolute, 0), durationMs));
|
|
138
157
|
};
|
package/components/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export * as beforeAfter from './BeforeAfter/index.js'
|
|
2
2
|
export * as button from './Button/index.js'
|
|
3
|
-
export * as clippable from './Clippable/index.js'
|
|
4
3
|
export * as disclaimer from './Disclaimer/index.js'
|
|
5
|
-
export * as
|
|
4
|
+
export * as clippable from './Clippable/index.js'
|
|
6
5
|
export * as eventListener from './EventListener/index.js'
|
|
7
|
-
export * as
|
|
6
|
+
export * as drawer from './Drawer/index.js'
|
|
8
7
|
export * as iframe from './Iframe/index.js'
|
|
8
|
+
export * as gallery from './Gallery/index.js'
|
|
9
9
|
export * as image from './Image/index.js'
|
|
10
10
|
export * as input from './Input/index.js'
|
|
11
11
|
export * as intersectionObserver from './IntersectionObserver/index.js'
|
|
@@ -13,15 +13,15 @@ export * as jsonEditor from './JsonEditor/index.js'
|
|
|
13
13
|
export * as lightbox from './Lightbox/index.js'
|
|
14
14
|
export * as listLoader from './ListLoader/index.js'
|
|
15
15
|
export * as overlayer from './Overlayer/index.js'
|
|
16
|
-
export * as paginator from './Paginator/index.js'
|
|
17
16
|
export * as resizeObserver from './ResizeObserver/index.js'
|
|
17
|
+
export * as paginator from './Paginator/index.js'
|
|
18
18
|
export * as scrllgngn from './Scrllgngn/index.js'
|
|
19
19
|
export * as scrollListener from './ScrollListener/index.js'
|
|
20
20
|
export * as select from './Select/index.js'
|
|
21
21
|
export * as sequencer from './Sequencer/index.js'
|
|
22
22
|
export * as shadowRoot from './ShadowRoot/index.js'
|
|
23
|
-
export * as subtitles from './Subtitles/index.js'
|
|
24
23
|
export * as textarea from './Textarea/index.js'
|
|
24
|
+
export * as subtitles from './Subtitles/index.js'
|
|
25
25
|
export * as uiModule from './UIModule/index.js'
|
|
26
26
|
export * as video from './Video/index.js'
|
|
27
27
|
export * as wipAudioQuote from './_WIP_AudioQuote/index.js'
|
package/components/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export * as beforeAfter from './BeforeAfter/index.js'
|
|
2
2
|
export * as button from './Button/index.js'
|
|
3
|
-
export * as clippable from './Clippable/index.js'
|
|
4
3
|
export * as disclaimer from './Disclaimer/index.js'
|
|
5
|
-
export * as
|
|
4
|
+
export * as clippable from './Clippable/index.js'
|
|
6
5
|
export * as eventListener from './EventListener/index.js'
|
|
7
|
-
export * as
|
|
6
|
+
export * as drawer from './Drawer/index.js'
|
|
8
7
|
export * as iframe from './Iframe/index.js'
|
|
8
|
+
export * as gallery from './Gallery/index.js'
|
|
9
9
|
export * as image from './Image/index.js'
|
|
10
10
|
export * as input from './Input/index.js'
|
|
11
11
|
export * as intersectionObserver from './IntersectionObserver/index.js'
|
|
@@ -13,15 +13,15 @@ export * as jsonEditor from './JsonEditor/index.js'
|
|
|
13
13
|
export * as lightbox from './Lightbox/index.js'
|
|
14
14
|
export * as listLoader from './ListLoader/index.js'
|
|
15
15
|
export * as overlayer from './Overlayer/index.js'
|
|
16
|
-
export * as paginator from './Paginator/index.js'
|
|
17
16
|
export * as resizeObserver from './ResizeObserver/index.js'
|
|
17
|
+
export * as paginator from './Paginator/index.js'
|
|
18
18
|
export * as scrllgngn from './Scrllgngn/index.js'
|
|
19
19
|
export * as scrollListener from './ScrollListener/index.js'
|
|
20
20
|
export * as select from './Select/index.js'
|
|
21
21
|
export * as sequencer from './Sequencer/index.js'
|
|
22
22
|
export * as shadowRoot from './ShadowRoot/index.js'
|
|
23
|
-
export * as subtitles from './Subtitles/index.js'
|
|
24
23
|
export * as textarea from './Textarea/index.js'
|
|
24
|
+
export * as subtitles from './Subtitles/index.js'
|
|
25
25
|
export * as uiModule from './UIModule/index.js'
|
|
26
26
|
export * as video from './Video/index.js'
|
|
27
27
|
export * as wipAudioQuote from './_WIP_AudioQuote/index.js'
|
|
@@ -21,3 +21,24 @@ export declare function mergeClassNames(...names: Array<string | null | undefine
|
|
|
21
21
|
* effect fires, so it is never a stale one.
|
|
22
22
|
*/
|
|
23
23
|
export declare function useChangeDispatch<T>(value: T, onChange?: (value: T) => void, isEqual?: (a: T, b: T) => boolean): void;
|
|
24
|
+
/**
|
|
25
|
+
* The three forms a `<source>` list is written in, reduced to the one a render uses.
|
|
26
|
+
*
|
|
27
|
+
* A bare string is a single source, an array of strings is several, an array of records
|
|
28
|
+
* is taken as given. **The array is read as homogeneous** — the first element decides for
|
|
29
|
+
* all of them —, which is what someone writing one by hand means anyway, and the only
|
|
30
|
+
* reading a static hyper-json array could support.
|
|
31
|
+
*
|
|
32
|
+
* `stringKey` is the whole reason this is shared rather than written twice. The parsing
|
|
33
|
+
* is identical for `Video` and `Image`, but **the shorthand does not name the same
|
|
34
|
+
* attribute**: a `<source>` inside a `<video>` carries `src`, one inside a `<picture>`
|
|
35
|
+
* carries `srcSet`. The two record shapes stay apart for the same reason — the picture
|
|
36
|
+
* source also takes `media` and `sizes`, which a video source has no use for, and neither
|
|
37
|
+
* element accepts the other's key. One element, one shape; only the reading is common.
|
|
38
|
+
*
|
|
39
|
+
* @template T - The record shape the caller's element accepts.
|
|
40
|
+
* @param given - What the consumer wrote.
|
|
41
|
+
* @param stringKey - Where a bare string goes.
|
|
42
|
+
* @returns The list as records, empty when there is nothing to render.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseSourceList<T extends Record<string, unknown>>(given: string | string[] | T[] | undefined, stringKey: keyof T & string): T[];
|
|
@@ -41,3 +41,43 @@ export function useChangeDispatch(value, onChange, isEqual = Object.is) {
|
|
|
41
41
|
onChange?.(value);
|
|
42
42
|
}, [value]);
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The three forms a `<source>` list is written in, reduced to the one a render uses.
|
|
46
|
+
*
|
|
47
|
+
* A bare string is a single source, an array of strings is several, an array of records
|
|
48
|
+
* is taken as given. **The array is read as homogeneous** — the first element decides for
|
|
49
|
+
* all of them —, which is what someone writing one by hand means anyway, and the only
|
|
50
|
+
* reading a static hyper-json array could support.
|
|
51
|
+
*
|
|
52
|
+
* `stringKey` is the whole reason this is shared rather than written twice. The parsing
|
|
53
|
+
* is identical for `Video` and `Image`, but **the shorthand does not name the same
|
|
54
|
+
* attribute**: a `<source>` inside a `<video>` carries `src`, one inside a `<picture>`
|
|
55
|
+
* carries `srcSet`. The two record shapes stay apart for the same reason — the picture
|
|
56
|
+
* source also takes `media` and `sizes`, which a video source has no use for, and neither
|
|
57
|
+
* element accepts the other's key. One element, one shape; only the reading is common.
|
|
58
|
+
*
|
|
59
|
+
* @template T - The record shape the caller's element accepts.
|
|
60
|
+
* @param given - What the consumer wrote.
|
|
61
|
+
* @param stringKey - Where a bare string goes.
|
|
62
|
+
* @returns The list as records, empty when there is nothing to render.
|
|
63
|
+
*/
|
|
64
|
+
export function parseSourceList(given, stringKey) {
|
|
65
|
+
const fromString = (value) => {
|
|
66
|
+
const single = { [stringKey]: value };
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- a one-key record is the narrowest `T` a bare string can describe; every other field is optional by contract
|
|
68
|
+
return single;
|
|
69
|
+
};
|
|
70
|
+
if (given === undefined)
|
|
71
|
+
return [];
|
|
72
|
+
if (typeof given === 'string')
|
|
73
|
+
return [fromString(given)];
|
|
74
|
+
if (!Array.isArray(given))
|
|
75
|
+
return [];
|
|
76
|
+
if (given.length === 0)
|
|
77
|
+
return [];
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- first element sampled just above; array is read as homogeneous
|
|
79
|
+
if (typeof given[0] === 'string')
|
|
80
|
+
return given.map(fromString);
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- first element was checked not to be a string just above; array is read as homogeneous
|
|
82
|
+
return given;
|
|
83
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import type { ActionTable, ViewportBehaviours, VisibilityOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether the element counts as visible — **as a state, not as a crossing**.
|
|
5
|
+
*
|
|
6
|
+
* That distinction is the whole point. An `IntersectionObserver` reports an event, and
|
|
7
|
+
* a behaviour hung on the event only ever runs when the screen is traversed. A
|
|
8
|
+
* component already visible whose behaviour was held back — behind a lm-link gate,
|
|
9
|
+
* typically — never gets a second chance, because nothing crosses when the gate lifts.
|
|
10
|
+
* Held as a state, the condition can be recombined with whatever else gates it.
|
|
11
|
+
*
|
|
12
|
+
* The delays are a debounce **on this state**: the element has to hold its new value
|
|
13
|
+
* for that long before the value is committed. A component crossed while scrolling fast
|
|
14
|
+
* therefore never counts as seen at all, rather than counting and being undone.
|
|
15
|
+
*
|
|
16
|
+
* @param targetRef - The element to watch.
|
|
17
|
+
* @param options - The observer's settings, plus the two delays.
|
|
18
|
+
* @param enabled - `false` mounts no observer. A hook can't be skipped; this is how
|
|
19
|
+
* a caller with nothing to watch for says so.
|
|
20
|
+
* @returns `true` or `false` once the first observation has settled, `undefined` before.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useVisibilityState(targetRef: RefObject<Element | null>, options?: VisibilityOptions, enabled?: boolean): boolean | undefined;
|
|
23
|
+
/** The shape `useViewportBehaviours` hands back. */
|
|
24
|
+
export type ViewportBehavioursResult = {
|
|
25
|
+
isVisible: boolean | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Tells the layer that the reader has taken over a domain, and that its instructions
|
|
28
|
+
* are to stop firing. Call it from the handlers that count as taking over — and only
|
|
29
|
+
* those: seeking on a timeline or going fullscreen is not deciding about playback.
|
|
30
|
+
*/
|
|
31
|
+
surrender: (domain: string) => void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Runs a component's visibility instructions, and holds everything that decides whether
|
|
35
|
+
* they may run at all.
|
|
36
|
+
*
|
|
37
|
+
* @template A - The component's vocabulary.
|
|
38
|
+
* @param targetRef - The element to watch.
|
|
39
|
+
* @param props - The component's visibility props.
|
|
40
|
+
* @param table - What each of its verbs does. @see {@link ActionTable}
|
|
41
|
+
* @param suspended - Holds back the verbs that **start** something, and lets through
|
|
42
|
+
* those that stop something. This is a capability's doing — a lm-link gate — and it is
|
|
43
|
+
* not the same as a surrender: a gate speaks of consent not yet given, so `':force'`
|
|
44
|
+
* does not override it.
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* **An instruction yields to the reader by default.** The failure modes are not
|
|
48
|
+
* symmetrical: not restarting on its own is a disappointment, restarting against a
|
|
49
|
+
* reader who has just pressed pause is an hostility they will meet again at every pass.
|
|
50
|
+
* `':force'` opts out, and is expected to be rare — which is why it is the written form
|
|
51
|
+
* and yielding is the silent one.
|
|
52
|
+
*/
|
|
53
|
+
export declare function useViewportBehaviours<A extends string>(targetRef: RefObject<Element | null>, props: ViewportBehaviours<A>, table: ActionTable<A>, suspended?: boolean): ViewportBehavioursResult;
|
|
54
|
+
export type { ActionSpec, ActionTable, Instruction, Modifier, ParsedInstruction, ViewportBehaviours, VisibilityOptions } from './types.js';
|