@design-edito/tools 0.5.8 → 0.5.10
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 +3 -3
- package/agnostic/arrays/index.js +3 -3
- package/agnostic/colors/index.d.ts +5 -5
- package/agnostic/colors/index.js +5 -5
- 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 +11 -11
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +11 -11
- package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +3 -3
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +3 -3
- package/agnostic/html/index.d.ts +2 -2
- package/agnostic/html/index.js +2 -2
- package/agnostic/index.d.ts +4 -4
- package/agnostic/index.js +4 -4
- package/agnostic/misc/index.d.ts +3 -3
- package/agnostic/misc/index.js +3 -3
- package/agnostic/numbers/index.d.ts +1 -1
- package/agnostic/numbers/index.js +1 -1
- package/agnostic/objects/index.d.ts +3 -3
- package/agnostic/objects/index.js +3 -3
- package/agnostic/strings/index.d.ts +1 -1
- package/agnostic/strings/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 +1 -1
- package/agnostic/time/index.js +1 -1
- package/components/Lightbox/index.d.ts +106 -0
- package/components/Lightbox/index.js +191 -0
- package/components/Lightbox/store.d.ts +43 -0
- package/components/Lightbox/store.js +94 -0
- package/components/Paginator/index.d.ts +7 -2
- package/components/Paginator/index.js +43 -6
- package/components/Video/index.controlled.d.ts +7 -2
- package/components/Video/index.controlled.js +22 -6
- package/components/Video/index.d.ts +5 -3
- package/components/Video/index.js +14 -4
- package/components/Video/utils.d.ts +11 -0
- package/components/Video/utils.js +14 -5
- package/components/index.d.ts +6 -6
- package/components/index.js +6 -6
- package/components/public-classnames.d.ts +1 -1
- package/components/public-classnames.js +1 -1
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/node/@aws-s3/storage/file/index.d.ts +1 -1
- package/node/@aws-s3/storage/file/index.js +1 -1
- package/node/@google-cloud/storage/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 +1 -1
- package/node/cloud-storage/operations/index.js +1 -1
- package/node/images/index.d.ts +2 -2
- package/node/images/index.js +2 -2
- package/node/images/transform/operations/index.d.ts +6 -6
- package/node/images/transform/operations/index.js +6 -6
- 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 +8 -8
- package/components/Theatre/index.d.ts +0 -70
- package/components/Theatre/index.js +0 -85
- /package/components/{Theatre → Lightbox}/styles.module.css +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { clss } from '../../agnostic/css/clss/index.js';
|
|
5
|
+
import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
|
|
6
|
+
import { lightbox as publicClassName } from '../public-classnames.js';
|
|
7
|
+
import { close as closeLightbox, getState, membersOf, open as openLightbox, register, soloGroupOf, step, subscribe, unregister } from './store.js';
|
|
8
|
+
import cssModule from './styles.module.css';
|
|
9
|
+
/**
|
|
10
|
+
* Lightbox component. Holds content in place until opened, then moves it into an
|
|
11
|
+
* overlay covering the page.
|
|
12
|
+
*
|
|
13
|
+
* ### CSS modifiers
|
|
14
|
+
* - `on` - this member is open.
|
|
15
|
+
* - `off` - it is not.
|
|
16
|
+
* - `grouped` - it belongs to a group it did not invent, so navigation is possible.
|
|
17
|
+
*
|
|
18
|
+
* ### CSS elements
|
|
19
|
+
* - `placeholder` - holds the content's last measured size while it is away.
|
|
20
|
+
* - `backdrop` - the overlay, portalled to `document.body`, mounted only while open.
|
|
21
|
+
* - `content` - wraps the moved content inside the lightbox.
|
|
22
|
+
* - `open-btn`, `close-btn`, `prev-btn`, `next-btn`
|
|
23
|
+
*
|
|
24
|
+
* @param props - Component properties.
|
|
25
|
+
* @see {@link Props}
|
|
26
|
+
* @returns A root `<div>` holding the content or its placeholder, plus, while open, a
|
|
27
|
+
* lightbox portalled to the end of `document.body`.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* - **[WIP]** Moving a `<video>` keeps it playing on desktop browsers; iOS has been
|
|
31
|
+
* known to pause on a DOM move. Not worth guarding against for now.
|
|
32
|
+
* - **The content is moved, not copied.** A portal relocates the DOM node without
|
|
33
|
+
* changing the element's place in the React tree, so the instance is never
|
|
34
|
+
* unmounted: a playing video keeps playing, at its timecode, and lands back where it
|
|
35
|
+
* was on close. Rendering `children` twice would mount a second, fresh instance and
|
|
36
|
+
* leave the first one running behind the lightbox.
|
|
37
|
+
* - **The lightbox is portalled to `document.body`.** A `position: fixed` overlay left in
|
|
38
|
+
* the article would be trapped by the first ancestor carrying a `transform`, a
|
|
39
|
+
* `filter` or a `contain`, which becomes its containing block.
|
|
40
|
+
* - **A placeholder keeps the layout still.** Its size is the content's last measured
|
|
41
|
+
* one, read while closed; without it the article would collapse around the hole the
|
|
42
|
+
* content leaves.
|
|
43
|
+
* - **Groups are held outside React**, in `./store.ts`: a consumer may render each
|
|
44
|
+
* component in a root of its own, in which case members of a group share no tree
|
|
45
|
+
* and a context could not reach from one to the other.
|
|
46
|
+
* - The lightbox is rendered by whichever member is on it, so navigating hands it over.
|
|
47
|
+
* None of the content is remounted in the process.
|
|
48
|
+
* - This component ships **no appearance**: covering the page, the backdrop and the
|
|
49
|
+
* buttons are the consumer stylesheet's business. Without one, the content is moved
|
|
50
|
+
* to the end of the document and nothing looks like a lightbox.
|
|
51
|
+
*/
|
|
52
|
+
export const Lightbox = ({ group, closeBtnContent, openBtnContent, prevBtnContent, nextBtnContent, isOn: isOnProp, defaultIsOn = false, exitOnEscape, exitOnBgClick, navOnArrowKeys, onOpenButtonClicked, onCloseButtonClicked, onPrevButtonClicked, onNextButtonClicked, onBackgroundClicked, onEscapePressed, onPrevArrowPressed, onNextArrowPressed, onIsOnChanged, children, className }) => {
|
|
53
|
+
// State & refs
|
|
54
|
+
const id = useId();
|
|
55
|
+
const rootRef = useRef(null);
|
|
56
|
+
const backdropRef = useRef(null);
|
|
57
|
+
const sizeRef = useRef(null);
|
|
58
|
+
const [hasAppliedDefault, setHasAppliedDefault] = useState(false);
|
|
59
|
+
const isControlled = isOnProp !== undefined;
|
|
60
|
+
const declaredGroups = group === undefined
|
|
61
|
+
? []
|
|
62
|
+
: (Array.isArray(group) ? group : [group]);
|
|
63
|
+
const isGrouped = declaredGroups.length > 0;
|
|
64
|
+
// A member without a group still needs one to be opened through: its own.
|
|
65
|
+
const groups = isGrouped ? declaredGroups : [soloGroupOf(id)];
|
|
66
|
+
// `groups` is never empty: a member without a declared group gets its own.
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
68
|
+
const ownGroup = groups[0];
|
|
69
|
+
const groupsKey = groups.join(' ');
|
|
70
|
+
const storeState = useSyncExternalStore(subscribe, getState, getState);
|
|
71
|
+
const isOn = isOnProp ?? (storeState.openMemberId === id);
|
|
72
|
+
const openGroup = storeState.openGroup;
|
|
73
|
+
const siblingsCount = isOn && openGroup !== null ? membersOf(openGroup).length : 0;
|
|
74
|
+
// Fx. dep. id, groupsKey - Joins the shared registry, which is what lets another
|
|
75
|
+
// member reach this one. `getElement` rather than the element itself: the registry
|
|
76
|
+
// reads it when it needs to order the group, and never holds it.
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
register({ id, groups, getElement: () => rootRef.current });
|
|
79
|
+
return () => unregister(id);
|
|
80
|
+
}, [id, groupsKey]);
|
|
81
|
+
// Keeps the last size the content had in place, so the placeholder can hold it once
|
|
82
|
+
// the content is gone. Measured while closed only, on render rather than through a
|
|
83
|
+
// standing observer: the content of a lightbox rarely resizes, and one observer per
|
|
84
|
+
// image on a page is a cost with no return.
|
|
85
|
+
useLayoutEffect(() => {
|
|
86
|
+
if (isOn)
|
|
87
|
+
return;
|
|
88
|
+
const root = rootRef.current;
|
|
89
|
+
if (root === null)
|
|
90
|
+
return;
|
|
91
|
+
const { width, height } = root.getBoundingClientRect();
|
|
92
|
+
if (width === 0 && height === 0)
|
|
93
|
+
return;
|
|
94
|
+
sizeRef.current = { width, height };
|
|
95
|
+
});
|
|
96
|
+
// State dispatch
|
|
97
|
+
useChangeDispatch(isOn, onIsOnChanged);
|
|
98
|
+
// User action handlers
|
|
99
|
+
const requestOpen = useCallback(() => {
|
|
100
|
+
if (isControlled)
|
|
101
|
+
return;
|
|
102
|
+
openLightbox(ownGroup, id);
|
|
103
|
+
}, [isControlled, ownGroup, id]);
|
|
104
|
+
const requestClose = useCallback(() => {
|
|
105
|
+
if (isControlled)
|
|
106
|
+
return;
|
|
107
|
+
closeLightbox();
|
|
108
|
+
}, [isControlled]);
|
|
109
|
+
const handleOpenButtonClick = () => {
|
|
110
|
+
onOpenButtonClicked?.(isOn);
|
|
111
|
+
requestOpen();
|
|
112
|
+
};
|
|
113
|
+
const handleCloseButtonClick = () => {
|
|
114
|
+
onCloseButtonClicked?.(isOn);
|
|
115
|
+
requestClose();
|
|
116
|
+
};
|
|
117
|
+
const handlePrevButtonClick = () => {
|
|
118
|
+
onPrevButtonClicked?.(isOn);
|
|
119
|
+
step(-1);
|
|
120
|
+
};
|
|
121
|
+
const handleNextButtonClick = () => {
|
|
122
|
+
onNextButtonClicked?.(isOn);
|
|
123
|
+
step(1);
|
|
124
|
+
};
|
|
125
|
+
const handleBackdropClick = e => {
|
|
126
|
+
if (exitOnBgClick !== true)
|
|
127
|
+
return;
|
|
128
|
+
if (e.target !== backdropRef.current)
|
|
129
|
+
return;
|
|
130
|
+
onBackgroundClicked?.(isOn);
|
|
131
|
+
requestClose();
|
|
132
|
+
};
|
|
133
|
+
// Fx. dep. exitOnEscape, isOn, requestClose, onEscapePressed - Escape closes the
|
|
134
|
+
// lightbox. Listens only while open, so a page of closed lightboxes holds no
|
|
135
|
+
// listener.
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (exitOnEscape !== true || !isOn)
|
|
138
|
+
return;
|
|
139
|
+
const handleKeyDown = (e) => {
|
|
140
|
+
if (e.key !== 'Escape')
|
|
141
|
+
return;
|
|
142
|
+
onEscapePressed?.(isOn);
|
|
143
|
+
requestClose();
|
|
144
|
+
};
|
|
145
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
146
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
147
|
+
}, [exitOnEscape, isOn, requestClose, onEscapePressed]);
|
|
148
|
+
// Fx. dep. navOnArrowKeys, isOn, siblingsCount, onPrevArrowPressed,
|
|
149
|
+
// onNextArrowPressed - The arrow keys walk the open group. Bound only while the
|
|
150
|
+
// lightbox is open and the group holds someone else, so the keys stay the page's the
|
|
151
|
+
// rest of the time.
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (navOnArrowKeys !== true || !isOn || siblingsCount < 2)
|
|
154
|
+
return;
|
|
155
|
+
const handleKeyDown = (e) => {
|
|
156
|
+
if (e.key === 'ArrowLeft') {
|
|
157
|
+
onPrevArrowPressed?.(isOn);
|
|
158
|
+
step(-1);
|
|
159
|
+
}
|
|
160
|
+
else if (e.key === 'ArrowRight') {
|
|
161
|
+
onNextArrowPressed?.(isOn);
|
|
162
|
+
step(1);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
166
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
167
|
+
}, [navOnArrowKeys, isOn, siblingsCount, onPrevArrowPressed, onNextArrowPressed]);
|
|
168
|
+
// Fx. dep. hasAppliedDefault, isControlled, defaultIsOn, ownGroup, id - An
|
|
169
|
+
// uncontrolled lightbox asked to start open says so once, and never again: the
|
|
170
|
+
// registry owns the state from then on.
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (hasAppliedDefault || isControlled || !defaultIsOn)
|
|
173
|
+
return;
|
|
174
|
+
setHasAppliedDefault(true);
|
|
175
|
+
openLightbox(ownGroup, id);
|
|
176
|
+
}, [hasAppliedDefault, isControlled, defaultIsOn, ownGroup, id]);
|
|
177
|
+
// Rendering
|
|
178
|
+
const c = clss(publicClassName, { cssModule });
|
|
179
|
+
const rootClss = mergeClassNames(c(null, {
|
|
180
|
+
on: isOn,
|
|
181
|
+
off: !isOn,
|
|
182
|
+
grouped: isGrouped
|
|
183
|
+
}), className);
|
|
184
|
+
const size = sizeRef.current;
|
|
185
|
+
const overlay = isOn
|
|
186
|
+
? createPortal(_jsxs("div", { className: c('backdrop'), onClick: handleBackdropClick, ref: backdropRef, children: [_jsx("div", { className: c('content'), children: children }), _jsx("button", { type: 'button', className: c('close-btn'), onClick: handleCloseButtonClick, children: closeBtnContent }), siblingsCount > 1 && _jsxs(_Fragment, { children: [_jsx("button", { type: 'button', className: c('prev-btn'), onClick: handlePrevButtonClick, children: prevBtnContent }), _jsx("button", { type: 'button', className: c('next-btn'), onClick: handleNextButtonClick, children: nextBtnContent })] })] }), document.body)
|
|
187
|
+
: null;
|
|
188
|
+
return _jsxs("div", { className: rootClss, ref: rootRef, children: [isOn
|
|
189
|
+
? _jsx("div", { className: c('placeholder'), style: size === null ? undefined : { width: size.width, height: size.height } })
|
|
190
|
+
: children, _jsx("button", { type: 'button', className: c('open-btn'), onClick: handleOpenButtonClick, children: openBtnContent }), overlay] });
|
|
191
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry of lightbox groups, shared by every {@link Lightbox} on the page.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately outside React. lm-link renders each component in its **own React
|
|
5
|
+
* root** — `renderInTarget` calls `createRoot` per component — so two `<lm-image>`
|
|
6
|
+
* in the same article never share a tree, and a context could not reach from one to
|
|
7
|
+
* the other. A module-level store can.
|
|
8
|
+
*
|
|
9
|
+
* Members are kept unordered here; the order that matters is the **document's**, and
|
|
10
|
+
* it is read from the DOM when a group opens. React's mount order is not it.
|
|
11
|
+
*/
|
|
12
|
+
/** What a member hands the store so the group can find and place it. */
|
|
13
|
+
export type LightboxMember = {
|
|
14
|
+
id: string;
|
|
15
|
+
groups: string[];
|
|
16
|
+
/** The member's anchor in the page, for ordering. `null` before it mounts. */
|
|
17
|
+
getElement: () => HTMLElement | null;
|
|
18
|
+
};
|
|
19
|
+
export type LightboxState = {
|
|
20
|
+
/** The group currently open, `null` when nothing is. */
|
|
21
|
+
openGroup: string | null;
|
|
22
|
+
/** The member currently shown. */
|
|
23
|
+
openMemberId: string | null;
|
|
24
|
+
};
|
|
25
|
+
export declare function subscribe(listener: () => void): () => void;
|
|
26
|
+
export declare function getState(): LightboxState;
|
|
27
|
+
/** A member's own group, when it declared none: it is a group of one. */
|
|
28
|
+
export declare function soloGroupOf(id: string): string;
|
|
29
|
+
export declare function register(member: LightboxMember): void;
|
|
30
|
+
export declare function unregister(id: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* The members of a group, in **document order**.
|
|
33
|
+
*
|
|
34
|
+
* `compareDocumentPosition` rather than mount order: React mounts a tree in its own
|
|
35
|
+
* order, and lm-link mounts each component in a root of its own, so nothing
|
|
36
|
+
* guarantees the two match. Only runs when a group opens or navigates, over a
|
|
37
|
+
* handful of members.
|
|
38
|
+
*/
|
|
39
|
+
export declare function membersOf(group: string): LightboxMember[];
|
|
40
|
+
export declare function open(group: string, memberId: string): void;
|
|
41
|
+
export declare function close(): void;
|
|
42
|
+
/** Moves to another member of the open group by `offset`, clamped at both ends. */
|
|
43
|
+
export declare function step(offset: number): void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry of lightbox groups, shared by every {@link Lightbox} on the page.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately outside React. lm-link renders each component in its **own React
|
|
5
|
+
* root** — `renderInTarget` calls `createRoot` per component — so two `<lm-image>`
|
|
6
|
+
* in the same article never share a tree, and a context could not reach from one to
|
|
7
|
+
* the other. A module-level store can.
|
|
8
|
+
*
|
|
9
|
+
* Members are kept unordered here; the order that matters is the **document's**, and
|
|
10
|
+
* it is read from the DOM when a group opens. React's mount order is not it.
|
|
11
|
+
*/
|
|
12
|
+
const members = new Map();
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
let state = {
|
|
15
|
+
openGroup: null,
|
|
16
|
+
openMemberId: null
|
|
17
|
+
};
|
|
18
|
+
function emit() {
|
|
19
|
+
for (const listener of listeners)
|
|
20
|
+
listener();
|
|
21
|
+
}
|
|
22
|
+
function setState(next) {
|
|
23
|
+
if (next.openGroup === state.openGroup
|
|
24
|
+
&& next.openMemberId === state.openMemberId)
|
|
25
|
+
return;
|
|
26
|
+
state = next;
|
|
27
|
+
emit();
|
|
28
|
+
}
|
|
29
|
+
export function subscribe(listener) {
|
|
30
|
+
listeners.add(listener);
|
|
31
|
+
return () => { listeners.delete(listener); };
|
|
32
|
+
}
|
|
33
|
+
export function getState() {
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
/** A member's own group, when it declared none: it is a group of one. */
|
|
37
|
+
export function soloGroupOf(id) {
|
|
38
|
+
return `solo:${id}`;
|
|
39
|
+
}
|
|
40
|
+
export function register(member) {
|
|
41
|
+
members.set(member.id, member);
|
|
42
|
+
emit();
|
|
43
|
+
}
|
|
44
|
+
export function unregister(id) {
|
|
45
|
+
members.delete(id);
|
|
46
|
+
// A member leaving while it is the one shown would strand the group on nothing.
|
|
47
|
+
if (state.openMemberId === id)
|
|
48
|
+
setState({ openGroup: null, openMemberId: null });
|
|
49
|
+
else
|
|
50
|
+
emit();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The members of a group, in **document order**.
|
|
54
|
+
*
|
|
55
|
+
* `compareDocumentPosition` rather than mount order: React mounts a tree in its own
|
|
56
|
+
* order, and lm-link mounts each component in a root of its own, so nothing
|
|
57
|
+
* guarantees the two match. Only runs when a group opens or navigates, over a
|
|
58
|
+
* handful of members.
|
|
59
|
+
*/
|
|
60
|
+
export function membersOf(group) {
|
|
61
|
+
const found = [...members.values()].filter(member => member.groups.includes(group));
|
|
62
|
+
return found.sort((a, b) => {
|
|
63
|
+
const aElt = a.getElement();
|
|
64
|
+
const bElt = b.getElement();
|
|
65
|
+
if (aElt === null || bElt === null)
|
|
66
|
+
return 0;
|
|
67
|
+
const position = aElt.compareDocumentPosition(bElt);
|
|
68
|
+
if ((position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0)
|
|
69
|
+
return -1;
|
|
70
|
+
if ((position & Node.DOCUMENT_POSITION_PRECEDING) !== 0)
|
|
71
|
+
return 1;
|
|
72
|
+
return 0;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function open(group, memberId) {
|
|
76
|
+
setState({ openGroup: group, openMemberId: memberId });
|
|
77
|
+
}
|
|
78
|
+
export function close() {
|
|
79
|
+
setState({ openGroup: null, openMemberId: null });
|
|
80
|
+
}
|
|
81
|
+
/** Moves to another member of the open group by `offset`, clamped at both ends. */
|
|
82
|
+
export function step(offset) {
|
|
83
|
+
const { openGroup, openMemberId } = state;
|
|
84
|
+
if (openGroup === null || openMemberId === null)
|
|
85
|
+
return;
|
|
86
|
+
const ordered = membersOf(openGroup);
|
|
87
|
+
const current = ordered.findIndex(member => member.id === openMemberId);
|
|
88
|
+
if (current === -1)
|
|
89
|
+
return;
|
|
90
|
+
const target = ordered[Math.min(ordered.length - 1, Math.max(0, current + offset))];
|
|
91
|
+
if (target === undefined)
|
|
92
|
+
return;
|
|
93
|
+
setState({ openGroup, openMemberId: target.id });
|
|
94
|
+
}
|
|
@@ -25,7 +25,9 @@ type DirectionState = 'forwards' | 'backwards' | null;
|
|
|
25
25
|
*
|
|
26
26
|
* @property thresholdOffsetPercent - Optional percentage offset used to compute
|
|
27
27
|
* the {@link IntersectionObserver} root margin. Determines how far into the viewport
|
|
28
|
-
* a page must be before it is considered `'curr'`. Defaults to `0`.
|
|
28
|
+
* a page must be before it is considered `'curr'`. Defaults to `0`. Clamped to
|
|
29
|
+
* 0–100, with a `console.warn` naming the value received — outside that range the
|
|
30
|
+
* root margin is invalid and the observer would throw.
|
|
29
31
|
*
|
|
30
32
|
* @property onDirectionChanged - Called after the scroll direction changed, with
|
|
31
33
|
* the new {@link DirectionState}. Repeated scrolls in the same direction do not
|
|
@@ -64,7 +66,10 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
64
66
|
* when the direction actually changes, using an internal ref to avoid stale
|
|
65
67
|
* closure comparisons.
|
|
66
68
|
* - Page visibility is tracked via a single {@link IntersectionObserver} instance
|
|
67
|
-
* that is recreated when `thresholdOffsetPercent` or
|
|
69
|
+
* that is recreated when `thresholdOffsetPercent` or the **number** of children
|
|
70
|
+
* changes. Not on `children` itself: a parent building its child list inline hands
|
|
71
|
+
* over a new array on every render, which would tear down and rebuild one observer
|
|
72
|
+
* per page each time, for nothing.
|
|
68
73
|
* - `currCount` on each {@link PageState} increments each time a page transitions
|
|
69
74
|
* into the `'curr'` position, making it useful as a re-entry counter.
|
|
70
75
|
*/
|
|
@@ -4,6 +4,18 @@ import { clss } from '../../agnostic/css/clss/index.js';
|
|
|
4
4
|
import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
|
|
5
5
|
import { paginator as publicClassName } from '../public-classnames.js';
|
|
6
6
|
import cssModule from './styles.module.css';
|
|
7
|
+
/**
|
|
8
|
+
* Brings `thresholdOffsetPercent` back into the 0–100 range `rootMargin` accepts.
|
|
9
|
+
*
|
|
10
|
+
* Outside it the margin comes out as `--20%`, which the `IntersectionObserver`
|
|
11
|
+
* constructor rejects by throwing — taking the whole component down with it over a
|
|
12
|
+
* single setting. A non-finite value falls back to `0` for the same reason.
|
|
13
|
+
*/
|
|
14
|
+
function toThresholdPercent(percent) {
|
|
15
|
+
if (percent === undefined || !Number.isFinite(percent))
|
|
16
|
+
return 0;
|
|
17
|
+
return Math.min(100, Math.max(0, percent));
|
|
18
|
+
}
|
|
7
19
|
/**
|
|
8
20
|
* A scroll-driven pagination component that tracks which child page is currently
|
|
9
21
|
* visible in the viewport and the direction of scroll.
|
|
@@ -23,7 +35,10 @@ import cssModule from './styles.module.css';
|
|
|
23
35
|
* when the direction actually changes, using an internal ref to avoid stale
|
|
24
36
|
* closure comparisons.
|
|
25
37
|
* - Page visibility is tracked via a single {@link IntersectionObserver} instance
|
|
26
|
-
* that is recreated when `thresholdOffsetPercent` or
|
|
38
|
+
* that is recreated when `thresholdOffsetPercent` or the **number** of children
|
|
39
|
+
* changes. Not on `children` itself: a parent building its child list inline hands
|
|
40
|
+
* over a new array on every render, which would tear down and rebuild one observer
|
|
41
|
+
* per page each time, for nothing.
|
|
27
42
|
* - `currCount` on each {@link PageState} increments each time a page transitions
|
|
28
43
|
* into the `'curr'` position, making it useful as a re-entry counter.
|
|
29
44
|
*/
|
|
@@ -33,6 +48,20 @@ export const Paginator = ({ thresholdOffsetPercent, onDirectionChanged, onPagesC
|
|
|
33
48
|
const [directionState, setDirectionState] = useState(null);
|
|
34
49
|
const pagesRef = useRef(null);
|
|
35
50
|
const directionRef = useRef(null);
|
|
51
|
+
const childrenArr = Children.toArray(children);
|
|
52
|
+
const pagesCount = childrenArr.length;
|
|
53
|
+
const thresholdPercent = toThresholdPercent(thresholdOffsetPercent);
|
|
54
|
+
// Fx. dep. thresholdOffsetPercent, thresholdPercent - Says it once per offending
|
|
55
|
+
// value rather than on every render. Clamping without a word would turn a caller's
|
|
56
|
+
// bug into a scrolling glitch, hunted for somewhere else entirely.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (thresholdOffsetPercent === undefined)
|
|
59
|
+
return;
|
|
60
|
+
if (thresholdPercent === thresholdOffsetPercent)
|
|
61
|
+
return;
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.warn('Paginator: thresholdOffsetPercent must be a number between 0 and 100, received', thresholdOffsetPercent, `— clamped to ${thresholdPercent}.`);
|
|
64
|
+
}, [thresholdOffsetPercent, thresholdPercent]);
|
|
36
65
|
// State dispatch
|
|
37
66
|
useChangeDispatch(pagesState, pages => onPagesChanged?.(Array
|
|
38
67
|
.from(pages)
|
|
@@ -62,15 +91,24 @@ export const Paginator = ({ thresholdOffsetPercent, onDirectionChanged, onPagesC
|
|
|
62
91
|
window.removeEventListener('resize', handleScroll);
|
|
63
92
|
};
|
|
64
93
|
}, []);
|
|
65
|
-
//
|
|
94
|
+
// Fx. dep. pagesCount - The page slots are this component's own divs, reused as
|
|
95
|
+
// long as their number holds, so the set only changes when the count does.
|
|
66
96
|
useEffect(() => {
|
|
67
97
|
if (pagesRef.current === null)
|
|
68
98
|
return;
|
|
69
99
|
const pages = Array.from(pagesRef.current.children);
|
|
70
100
|
onPageElementsChanged?.(pages.filter(page => page instanceof HTMLElement));
|
|
71
|
-
|
|
101
|
+
}, [pagesCount]);
|
|
102
|
+
// Detect active pages with Intersection Observer
|
|
103
|
+
// Fx. dep. thresholdPercent, pagesCount - Same reasoning: rebuilding one observer
|
|
104
|
+
// per page on every render would cost a full measurement pass for an unchanged set.
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (pagesRef.current === null)
|
|
107
|
+
return;
|
|
108
|
+
const pages = Array.from(pagesRef.current.children);
|
|
109
|
+
const observerRootMargin = `-${thresholdPercent}%`
|
|
72
110
|
+ ' 0px'
|
|
73
|
-
+ ` -${100 -
|
|
111
|
+
+ ` -${100 - thresholdPercent}%`
|
|
74
112
|
+ ' 0px';
|
|
75
113
|
const observer = new IntersectionObserver(entries => {
|
|
76
114
|
setPagesState(prevState => {
|
|
@@ -100,7 +138,7 @@ export const Paginator = ({ thresholdOffsetPercent, onDirectionChanged, onPagesC
|
|
|
100
138
|
observer.observe(page);
|
|
101
139
|
});
|
|
102
140
|
return () => observer.disconnect();
|
|
103
|
-
}, [
|
|
141
|
+
}, [thresholdPercent, pagesCount]);
|
|
104
142
|
// Rendering
|
|
105
143
|
const c = clss(publicClassName, { cssModule });
|
|
106
144
|
const rootClss = mergeClassNames(c(null, {
|
|
@@ -108,7 +146,6 @@ export const Paginator = ({ thresholdOffsetPercent, onDirectionChanged, onPagesC
|
|
|
108
146
|
backwards: directionState === 'backwards'
|
|
109
147
|
}), className);
|
|
110
148
|
const pagesClss = c('pages');
|
|
111
|
-
const childrenArr = Children.toArray(children);
|
|
112
149
|
return _jsx("div", { className: rootClss, children: _jsx("div", { className: pagesClss, ref: pagesRef, children: childrenArr.map((child, pos) => {
|
|
113
150
|
const state = pagesState.get(pos);
|
|
114
151
|
const pageClss = c('page', {
|
|
@@ -38,7 +38,9 @@ type TrackData = {
|
|
|
38
38
|
* @property loudBtnContent - React content for the "loud" (unmute) button.
|
|
39
39
|
* @property muteBtnContent - React content for the mute button.
|
|
40
40
|
* @property fullscreenBtnContent - React content for the fullscreen button.
|
|
41
|
-
* @property play -
|
|
41
|
+
* @property play - Whether the media should be playing. A **request**, not a
|
|
42
|
+
* setting: a browser holds a veto over playback — an unmuted media outside a user
|
|
43
|
+
* gesture is refused — so this prop asks, and `onIsPlayingChanged` answers.
|
|
42
44
|
* @property fullscreen - External control of fullscreen mode.
|
|
43
45
|
* @property volume - External control of volume (0 to 1).
|
|
44
46
|
* @property mute - External control of mute (true = muted).
|
|
@@ -67,7 +69,10 @@ type TrackData = {
|
|
|
67
69
|
* @property onTimelineClicked - Called when the timeline is clicked, before the
|
|
68
70
|
* component reacts, with the target and current times (in seconds). The component
|
|
69
71
|
* seeks to the target right after, unless the time is controlled.
|
|
70
|
-
* @property onIsPlayingChanged - Called
|
|
72
|
+
* @property onIsPlayingChanged - Called with what the **element** is doing, not
|
|
73
|
+
* with what `play` asked for. It is how a refused play surfaces, that being the one
|
|
74
|
+
* case where nothing else does: the promise rejects, no event fires, and without
|
|
75
|
+
* this a parent would go on believing a media that never started. Never on mount.
|
|
71
76
|
* @property onIsFullscreenChanged - Called once the fullscreen state has changed.
|
|
72
77
|
* @property onIsEndedChanged - Called after playback reached the end, and again
|
|
73
78
|
* once it left it — a seek back or a new play. Never on mount.
|
|
@@ -59,6 +59,10 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
59
59
|
// element, and the two events that turn it on and off are joined by a sync on
|
|
60
60
|
// every time update, which covers a seek away from the end.
|
|
61
61
|
const [isEnded, setIsEnded] = useState(false);
|
|
62
|
+
// What the element is actually doing, as opposed to what `play` asked for. The
|
|
63
|
+
// two part ways whenever a browser refuses a play, which it does without firing
|
|
64
|
+
// anything — see `forcePlay`.
|
|
65
|
+
const [isElementPlaying, setIsElementPlaying] = useState(false);
|
|
62
66
|
const isTimeControlled = givenCurrentTimeMs !== undefined;
|
|
63
67
|
// The parent owns the time as soon as it provides one, so that is what gets
|
|
64
68
|
// displayed — not what the element reported one render later.
|
|
@@ -92,12 +96,18 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
92
96
|
}, [intrinsicVideoAttributes.onTimeUpdate]);
|
|
93
97
|
const handleEndedEvent = useCallback((e) => {
|
|
94
98
|
setIsEnded(true);
|
|
99
|
+
setIsElementPlaying(false);
|
|
95
100
|
intrinsicVideoAttributes.onEnded?.(e);
|
|
96
101
|
}, [intrinsicVideoAttributes.onEnded]);
|
|
97
102
|
const handlePlayEvent = useCallback((e) => {
|
|
98
103
|
setIsEnded(false);
|
|
104
|
+
setIsElementPlaying(true);
|
|
99
105
|
intrinsicVideoAttributes.onPlay?.(e);
|
|
100
106
|
}, [intrinsicVideoAttributes.onPlay]);
|
|
107
|
+
const handlePauseEvent = useCallback((e) => {
|
|
108
|
+
setIsElementPlaying(false);
|
|
109
|
+
intrinsicVideoAttributes.onPause?.(e);
|
|
110
|
+
}, [intrinsicVideoAttributes.onPause]);
|
|
101
111
|
// Custom action handlers
|
|
102
112
|
const handlePlayButtonClick = useCallback((e) => {
|
|
103
113
|
const wasPlaying = videoRef.current?.paused === false;
|
|
@@ -239,12 +249,18 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
239
249
|
void forcePause(videoRef.current);
|
|
240
250
|
return;
|
|
241
251
|
}
|
|
242
|
-
if (play
|
|
243
|
-
void forcePlay(videoRef.current);
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
252
|
+
if (play !== true) {
|
|
246
253
|
void forcePause(videoRef.current);
|
|
254
|
+
return;
|
|
247
255
|
}
|
|
256
|
+
let isCurrent = true;
|
|
257
|
+
void forcePlay(videoRef.current).then(isNowPlaying => {
|
|
258
|
+
// A refused play fires nothing, so without this the component would go on
|
|
259
|
+
// believing a media that never started.
|
|
260
|
+
if (isCurrent && !isNowPlaying)
|
|
261
|
+
setIsElementPlaying(false);
|
|
262
|
+
});
|
|
263
|
+
return () => { isCurrent = false; };
|
|
248
264
|
}, [play, isTimeControlled]);
|
|
249
265
|
useEffect(() => {
|
|
250
266
|
forceVolume(videoRef.current, volume);
|
|
@@ -276,13 +292,13 @@ export const ControlledVideo = ({ sources, tracks, subtitles, playBtnContent, pa
|
|
|
276
292
|
}, [onFullscreenChange]);
|
|
277
293
|
// State handlers
|
|
278
294
|
useChangeDispatch(currentTimeMs, onCurrentTimeMsChanged);
|
|
279
|
-
useChangeDispatch(
|
|
295
|
+
useChangeDispatch(isElementPlaying, onIsPlayingChanged);
|
|
280
296
|
useChangeDispatch(isFullscreen, onIsFullscreenChanged);
|
|
281
297
|
useChangeDispatch(isLoud, onIsLoudChanged);
|
|
282
298
|
useChangeDispatch(isEnded, onIsEndedChanged);
|
|
283
299
|
useChangeDispatch(volume, onVolumeChanged);
|
|
284
300
|
useChangeDispatch(playbackRate, onPlaybackRateChanged);
|
|
285
|
-
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'
|
|
301
|
+
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, onPause: handlePauseEvent, children: [parsedSources.map((source, index) => typeof source === 'string'
|
|
286
302
|
? _jsx("source", { src: source }, index)
|
|
287
303
|
: _jsx("source", { src: source.src, type: source.type }, index)), parsedTracks.map((track, index) => typeof track === 'string'
|
|
288
304
|
? _jsx("track", { src: track }, index)
|
|
@@ -74,8 +74,10 @@ export type Props = WithViewportObservation<Omit<ControlledProps, 'play' | 'full
|
|
|
74
74
|
* behaviour is the same as setting the `…When…` one alone.
|
|
75
75
|
*
|
|
76
76
|
* Browsers refuse an unmuted `play()` outside a user gesture, so pairing an
|
|
77
|
-
* `autoLoud…` with an `autoPlay…` will usually have the playback rejected
|
|
78
|
-
*
|
|
79
|
-
*
|
|
77
|
+
* `autoLoud…` with an `autoPlay…` will usually have the playback rejected. The
|
|
78
|
+
* refusal is caught rather than ignored — the element is read back once the attempt
|
|
79
|
+
* settles, and the play state follows what it says — so the controls stay truthful.
|
|
80
|
+
* The media still won't play, though: autoplay muted, and leave unmuting to the
|
|
81
|
+
* reader.
|
|
80
82
|
*/
|
|
81
83
|
export declare const Video: FunctionComponent<Props>;
|
|
@@ -35,9 +35,11 @@ import { ControlledVideo } from './index.controlled.js';
|
|
|
35
35
|
* behaviour is the same as setting the `…When…` one alone.
|
|
36
36
|
*
|
|
37
37
|
* Browsers refuse an unmuted `play()` outside a user gesture, so pairing an
|
|
38
|
-
* `autoLoud…` with an `autoPlay…` will usually have the playback rejected
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* `autoLoud…` with an `autoPlay…` will usually have the playback rejected. The
|
|
39
|
+
* refusal is caught rather than ignored — the element is read back once the attempt
|
|
40
|
+
* settles, and the play state follows what it says — so the controls stay truthful.
|
|
41
|
+
* The media still won't play, though: autoplay muted, and leave unmuting to the
|
|
42
|
+
* reader.
|
|
41
43
|
*/
|
|
42
44
|
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 }) => {
|
|
43
45
|
// State & refs
|
|
@@ -87,6 +89,14 @@ export const Video = ({ loop, autoPlayWhenVisible, autoPlayOnceVisible, autoPaus
|
|
|
87
89
|
setPlay(false);
|
|
88
90
|
controlledProps.onPause?.(e);
|
|
89
91
|
}, [controlledProps.onPause]);
|
|
92
|
+
// The element's own account of whether it is playing. It covers what the `play`
|
|
93
|
+
// and `pause` events do, plus the one case they can't: a play the browser refused,
|
|
94
|
+
// which fires nothing at all. Without it the controls would stay on `--play-on`
|
|
95
|
+
// over a media that never started.
|
|
96
|
+
const handleIsPlayingChanged = useCallback((isPlaying) => {
|
|
97
|
+
setPlay(isPlaying);
|
|
98
|
+
controlledProps.onIsPlayingChanged?.(isPlaying);
|
|
99
|
+
}, [controlledProps.onIsPlayingChanged]);
|
|
90
100
|
const handleOnVolumeChangeEvent = useCallback((e) => {
|
|
91
101
|
setMute(e.currentTarget.muted);
|
|
92
102
|
setVolume(e.currentTarget.volume);
|
|
@@ -178,7 +188,7 @@ export const Video = ({ loop, autoPlayWhenVisible, autoPlayOnceVisible, autoPaus
|
|
|
178
188
|
// Render
|
|
179
189
|
const c = clss(publicClassName, { cssModule });
|
|
180
190
|
const rootClss = mergeClassNames(c(), wrapperClassName);
|
|
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 });
|
|
191
|
+
const videoContent = _jsx(ControlledVideo, { ...controlledProps, play: play && !isTimeControlled, volume: volume, mute: mute, playbackRate: playbackRate, fullscreen: fullscreen, onPlay: handleOnPlayEvent, onPause: handleOnPauseEvent, onIsPlayingChanged: handleIsPlayingChanged, onVolumeChange: handleOnVolumeChangeEvent, onRateChange: handleOnRateChangeEvent, onLoadedMetadata: handleOnLoadedMetadataEvent, onFullscreenChange: handleFullscreenChange, onPlayButtonClicked: handlePlayButtonClick, onPauseButtonClicked: handlePauseButtonClick, onLoudButtonClicked: handleLoudButtonClick, onMuteButtonClicked: handleMuteButtonClick, onVolumeRangeChanged: handleVolumeRangeChange, onRateRangeChanged: handleRateRangeChange, onFullscreenButtonClicked: handleFullscreenButtonClick });
|
|
182
192
|
return _jsx("div", { className: rootClss, children: needsObserve
|
|
183
193
|
? _jsx(IntersectionObserverComponent, { threshold: threshold, root: root, rootMargin: rootMargin, onIntersected: onIntersected, children: videoContent })
|
|
184
194
|
: videoContent });
|
|
@@ -2,6 +2,17 @@ export declare const muteAttributeWorkaround: (video: HTMLVideoElement | null, s
|
|
|
2
2
|
export declare const forceMute: (video: HTMLVideoElement | null) => void;
|
|
3
3
|
export declare const forceLoud: (video: HTMLVideoElement | null) => void;
|
|
4
4
|
export declare const forceVolume: (video: HTMLVideoElement | null, volume: number) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Asks the element to play, and reports what came of it.
|
|
7
|
+
*
|
|
8
|
+
* A browser may refuse — an unmuted media outside a user gesture, typically — and
|
|
9
|
+
* it refuses **silently**: the promise rejects and no event fires, so this is the
|
|
10
|
+
* only moment the outcome can be known. Hence the return value, and hence no
|
|
11
|
+
* logging: a refusal is not an anomaly to print, it is an answer to hand back.
|
|
12
|
+
*
|
|
13
|
+
* @param video - The element, or `null` before it mounts.
|
|
14
|
+
* @returns Whether it is playing now.
|
|
15
|
+
*/
|
|
5
16
|
export declare const forcePlay: (video: HTMLVideoElement | null) => Promise<boolean>;
|
|
6
17
|
export declare const forcePause: (video: HTMLVideoElement | null) => boolean;
|
|
7
18
|
export declare const forcePlaybackRate: (video: HTMLVideoElement | null, rate: number) => void;
|
|
@@ -28,6 +28,17 @@ export const forceVolume = (video, volume) => {
|
|
|
28
28
|
// eslint-disable-next-line no-param-reassign
|
|
29
29
|
video.volume = volume;
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Asks the element to play, and reports what came of it.
|
|
33
|
+
*
|
|
34
|
+
* A browser may refuse — an unmuted media outside a user gesture, typically — and
|
|
35
|
+
* it refuses **silently**: the promise rejects and no event fires, so this is the
|
|
36
|
+
* only moment the outcome can be known. Hence the return value, and hence no
|
|
37
|
+
* logging: a refusal is not an anomaly to print, it is an answer to hand back.
|
|
38
|
+
*
|
|
39
|
+
* @param video - The element, or `null` before it mounts.
|
|
40
|
+
* @returns Whether it is playing now.
|
|
41
|
+
*/
|
|
31
42
|
export const forcePlay = async (video) => {
|
|
32
43
|
if (video === null)
|
|
33
44
|
return false;
|
|
@@ -35,13 +46,11 @@ export const forcePlay = async (video) => {
|
|
|
35
46
|
return true;
|
|
36
47
|
try {
|
|
37
48
|
await video.play();
|
|
38
|
-
return video.paused;
|
|
39
49
|
}
|
|
40
|
-
catch (
|
|
41
|
-
//
|
|
42
|
-
console.error(e);
|
|
50
|
+
catch (err) {
|
|
51
|
+
// The refusal itself is the information, and it is in `video.paused` below.
|
|
43
52
|
}
|
|
44
|
-
return
|
|
53
|
+
return !video.paused;
|
|
45
54
|
};
|
|
46
55
|
export const forcePause = (video) => {
|
|
47
56
|
if (video === null)
|