@design-edito/tools 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agnostic/arrays/index.d.ts +1 -1
- package/agnostic/arrays/index.js +1 -1
- package/agnostic/colors/index.d.ts +1 -1
- package/agnostic/colors/index.js +1 -1
- package/agnostic/css/index.d.ts +1 -1
- package/agnostic/css/index.js +1 -1
- package/agnostic/html/deep-select/index.js +5 -3
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +9 -9
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +9 -9
- package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +1 -1
- package/agnostic/html/hyper-json/smart-tags/isolated/index.js +1 -1
- package/agnostic/html/index.d.ts +3 -3
- package/agnostic/html/index.js +3 -3
- package/agnostic/index.d.ts +3 -3
- package/agnostic/index.js +3 -3
- package/agnostic/misc/index.d.ts +1 -1
- package/agnostic/misc/index.js +1 -1
- package/agnostic/misc/logs/index.d.ts +1 -1
- package/agnostic/misc/logs/index.js +1 -1
- package/agnostic/numbers/index.d.ts +3 -3
- package/agnostic/numbers/index.js +3 -3
- package/agnostic/objects/index.d.ts +3 -3
- package/agnostic/objects/index.js +3 -3
- package/agnostic/random/index.d.ts +1 -1
- package/agnostic/random/index.js +1 -1
- package/agnostic/strings/index.d.ts +3 -3
- package/agnostic/strings/index.js +3 -3
- package/agnostic/time/dates/format-date/index.d.ts +4 -2
- package/agnostic/time/dates/format-date/index.js +43 -94
- package/agnostic/time/dates/format-date/index.test.js +154 -0
- package/agnostic/time/dates/format-date/parts.d.ts +28 -0
- package/agnostic/time/dates/format-date/parts.js +136 -0
- package/agnostic/time/dates/format-date/types.d.ts +84 -0
- package/agnostic/time/dates/format-date/types.js +1 -0
- package/agnostic/time/duration/format-duration/index.d.ts +42 -0
- package/agnostic/time/duration/format-duration/index.js +94 -0
- package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
- package/agnostic/time/duration/format-duration/index.test.js +148 -0
- package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
- package/agnostic/time/duration/format-duration/parts.js +107 -0
- package/agnostic/time/duration/format-duration/types.d.ts +93 -0
- package/agnostic/time/duration/format-duration/types.js +1 -0
- package/agnostic/time/duration/index.d.ts +6 -1
- package/agnostic/time/duration/index.js +2 -2
- package/agnostic/time/duration/index.test.d.ts +1 -0
- package/agnostic/time/duration/index.test.js +168 -0
- package/components/BeforeAfter/index.controlled.d.ts +25 -25
- package/components/BeforeAfter/index.controlled.js +47 -53
- package/components/BeforeAfter/index.d.ts +20 -23
- package/components/BeforeAfter/index.js +48 -40
- package/components/Button/index.js +1 -1
- package/components/Clippable/index.d.ts +17 -26
- package/components/Clippable/index.js +21 -23
- package/components/Disclaimer/index.d.ts +29 -22
- package/components/Disclaimer/index.js +29 -24
- package/components/Drawer/index.d.ts +45 -25
- package/components/Drawer/index.js +61 -52
- package/components/EventListener/index.d.ts +4 -5
- package/components/Gallery/index.d.ts +14 -16
- package/components/Gallery/index.js +25 -30
- package/components/Iframe/index.js +2 -2
- package/components/Image/index.d.ts +5 -12
- package/components/Image/index.js +11 -32
- package/components/Input/index.d.ts +23 -19
- package/components/Input/index.js +30 -20
- package/components/JsonEditor/index.d.ts +177 -9
- package/components/JsonEditor/index.js +133 -75
- package/components/JsonEditor/types.d.ts +11 -0
- package/components/JsonEditor/types.js +1 -0
- package/components/JsonEditor/utils.d.ts +8 -0
- package/components/JsonEditor/utils.js +23 -0
- package/components/ListLoader/index.controlled.d.ts +78 -0
- package/components/ListLoader/index.controlled.js +99 -0
- package/components/ListLoader/index.d.ts +69 -0
- package/components/ListLoader/index.js +146 -0
- package/components/ListLoader/utils.d.ts +8 -0
- package/components/ListLoader/utils.js +10 -0
- package/components/Overlayer/index.js +4 -4
- package/components/Paginator/index.d.ts +7 -12
- package/components/Paginator/index.js +7 -11
- package/components/ResizeObserver/index.d.ts +14 -3
- package/components/ResizeObserver/index.js +49 -24
- package/components/ResizeObserver/styles.module.css +0 -0
- package/components/Scrllgngn/index.d.ts +17 -13
- package/components/Scrllgngn/index.js +25 -84
- package/components/Scrllgngn/utils.d.ts +52 -0
- package/components/Scrllgngn/utils.js +102 -0
- package/components/ScrollListener/index.d.ts +43 -28
- package/components/ScrollListener/index.js +59 -79
- package/components/ScrollListener/utils.d.ts +37 -16
- package/components/ScrollListener/utils.js +141 -28
- package/components/Select/index.d.ts +24 -20
- package/components/Select/index.js +30 -20
- package/components/Sequencer/index.controlled.d.ts +16 -26
- package/components/Sequencer/index.controlled.js +15 -12
- package/components/Sequencer/index.d.ts +25 -30
- package/components/Sequencer/index.js +20 -34
- package/components/Subtitles/index.d.ts +29 -5
- package/components/Subtitles/index.js +90 -12
- package/components/Textarea/index.d.ts +26 -20
- package/components/Textarea/index.js +40 -21
- package/components/Theatre/index.d.ts +35 -39
- package/components/Theatre/index.js +46 -58
- package/components/UIModule/index.d.ts +25 -29
- package/components/UIModule/index.js +81 -110
- package/components/Video/index.controlled.d.ts +51 -51
- package/components/Video/index.controlled.js +75 -72
- package/components/Video/index.d.ts +12 -14
- package/components/Video/index.js +41 -86
- package/components/Video/utils.d.ts +35 -3
- package/components/Video/utils.js +37 -14
- package/components/index.d.ts +5 -4
- package/components/index.js +5 -4
- package/components/public-classnames.d.ts +1 -0
- package/components/public-classnames.js +1 -0
- package/components/utils/index.d.ts +22 -0
- package/components/utils/index.js +31 -0
- package/node/@google-cloud/storage/directory/index.d.ts +2 -2
- package/node/@google-cloud/storage/directory/index.js +2 -2
- package/node/@google-cloud/storage/file/index.d.ts +2 -2
- package/node/@google-cloud/storage/file/index.js +2 -2
- package/node/@google-cloud/storage/index.d.ts +1 -1
- package/node/@google-cloud/storage/index.js +1 -1
- package/node/cloud-storage/operations/index.d.ts +2 -2
- package/node/cloud-storage/operations/index.js +2 -2
- package/node/ftps/directory/index.d.ts +1 -1
- package/node/ftps/directory/index.js +1 -1
- package/node/ftps/file/index.d.ts +2 -2
- package/node/ftps/file/index.js +2 -2
- package/node/images/index.d.ts +1 -1
- package/node/images/index.js +1 -1
- package/node/images/transform/operations/index.d.ts +3 -3
- package/node/images/transform/operations/index.js +3 -3
- package/node/index.d.ts +2 -2
- package/node/index.js +2 -2
- package/node/process/index.d.ts +1 -1
- package/node/process/index.js +1 -1
- package/node/sftp/directory/index.d.ts +1 -1
- package/node/sftp/directory/index.js +1 -1
- package/node/sftp/file/index.d.ts +1 -1
- package/node/sftp/file/index.js +1 -1
- package/package.json +27 -5
- package/TODO.md +0 -269
- package/components/BeforeAfter/utils.d.ts +0 -4
- package/components/BeforeAfter/utils.js +0 -9
- package/components/Input/index.controlled.d.ts +0 -42
- package/components/Input/index.controlled.js +0 -54
- package/components/Select/index.controlled.d.ts +0 -43
- package/components/Select/index.controlled.js +0 -54
- package/components/Subtitles/types.d.ts +0 -24
- package/components/Subtitles/utils.d.ts +0 -32
- package/components/Subtitles/utils.js +0 -107
- package/components/Textarea/index.controlled.d.ts +0 -50
- package/components/Textarea/index.controlled.js +0 -75
- /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
- /package/components/{ResizeObserver/style.module.css → ListLoader/styles.module.css} +0 -0
|
@@ -1,90 +1,78 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState, useRef } from 'react';
|
|
3
3
|
import { clss } from '../../agnostic/css/clss/index.js';
|
|
4
|
-
import { mergeClassNames } from '../utils/index.js';
|
|
4
|
+
import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
|
|
5
5
|
import { theatre as publicClassName } from '../public-classnames.js';
|
|
6
6
|
import cssModule from './styles.module.css';
|
|
7
7
|
/**
|
|
8
8
|
* Theatre mode component. Wraps content in a toggleable fullscreen-like "stage"
|
|
9
|
-
* overlay.
|
|
9
|
+
* overlay.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* ### CSS modifiers
|
|
12
|
+
* - `on` — theatre mode is active.
|
|
13
|
+
* - `off` — theatre mode is inactive.
|
|
14
14
|
*
|
|
15
|
-
* ###
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
19
|
-
* - `--off` — when theatre mode is inactive.
|
|
20
|
-
*
|
|
21
|
-
* ### Child elements
|
|
22
|
-
* - `__stage` — container rendered inside the root that holds the duplicated
|
|
23
|
-
* `children` when theatre mode is active. Only mounted when `isOn` is `true`.
|
|
24
|
-
* - `__open-btn` — clickable element that activates theatre mode.
|
|
25
|
-
* - `__close-btn` — clickable element that deactivates theatre mode.
|
|
15
|
+
* ### CSS elements
|
|
16
|
+
* - `stage` — holds the duplicated `children`, mounted only when `isOn`.
|
|
17
|
+
* - `open-btn`
|
|
18
|
+
* - `close-btn`
|
|
26
19
|
*
|
|
27
20
|
* @param props - Component properties.
|
|
28
21
|
* @see {@link Props}
|
|
29
22
|
* @returns A root `<div>` containing the children in their original position,
|
|
30
|
-
* a stage overlay with the duplicated children
|
|
31
|
-
*
|
|
23
|
+
* a stage overlay with the duplicated children when active, and both toggles.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* - In controlled mode (`isOn` defined), the state is fully driven by the
|
|
27
|
+
* parent and internal state is never updated.
|
|
28
|
+
* - `onToggleClicked` fires in both modes, for all four toggle sources — a
|
|
29
|
+
* controlled parent needs it to know a toggle was requested at all.
|
|
30
|
+
* - `onIsOnChanged` fires in both modes too, and never on mount.
|
|
32
31
|
*/
|
|
33
|
-
export const Theatre = ({ closeBtnContent, openBtnContent, isOn, defaultIsOn, exitOnEscape, exitOnBgClick,
|
|
32
|
+
export const Theatre = ({ closeBtnContent, openBtnContent, isOn: isOnProp, defaultIsOn = false, exitOnEscape, exitOnBgClick, onToggleClicked, onIsOnChanged, children, className }) => {
|
|
34
33
|
// State & refs
|
|
35
|
-
const [internalIsOn, setInternalIsOn] = useState(defaultIsOn
|
|
34
|
+
const [internalIsOn, setInternalIsOn] = useState(defaultIsOn);
|
|
36
35
|
const stageRef = useRef(null);
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (isOn === undefined)
|
|
48
|
-
setInternalIsOn(true);
|
|
36
|
+
const isControlled = isOnProp !== undefined;
|
|
37
|
+
const isOn = isOnProp ?? internalIsOn;
|
|
38
|
+
// State dispatch
|
|
39
|
+
useChangeDispatch(isOn, onIsOnChanged);
|
|
40
|
+
// User action handlers
|
|
41
|
+
const requestToggle = (targetIsOn) => {
|
|
42
|
+
onToggleClicked?.(isOn);
|
|
43
|
+
if (isControlled)
|
|
44
|
+
return;
|
|
45
|
+
setInternalIsOn(targetIsOn);
|
|
49
46
|
};
|
|
47
|
+
const handleOpenBtnClick = () => requestToggle(true);
|
|
48
|
+
const handleCloseBtnClick = () => requestToggle(false);
|
|
50
49
|
const handleStageBgClick = e => {
|
|
51
50
|
if (exitOnBgClick !== true)
|
|
52
51
|
return;
|
|
53
52
|
if (e.target !== stageRef.current)
|
|
54
53
|
return;
|
|
55
|
-
|
|
56
|
-
if (isOn === undefined)
|
|
57
|
-
setInternalIsOn(false);
|
|
54
|
+
requestToggle(false);
|
|
58
55
|
};
|
|
59
|
-
//
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
if (prevIsTheatreOnRef.current !== isTheatreOn) {
|
|
62
|
-
stateHandlers?.toggled?.(isTheatreOn);
|
|
63
|
-
prevIsTheatreOnRef.current = isTheatreOn;
|
|
64
|
-
}
|
|
65
|
-
}, [isTheatreOn, stateHandlers]);
|
|
56
|
+
// Fx. dep. `exitOnEscape`, `isOn` - close the stage on the Escape key
|
|
66
57
|
useEffect(() => {
|
|
67
|
-
if (exitOnEscape
|
|
68
|
-
|| !isTheatreOn
|
|
69
|
-
|| isOn !== undefined)
|
|
58
|
+
if (exitOnEscape !== true || !isOn)
|
|
70
59
|
return;
|
|
71
|
-
const
|
|
60
|
+
const handleKeyDown = (e) => {
|
|
72
61
|
if (e.key !== 'Escape')
|
|
73
62
|
return;
|
|
74
|
-
|
|
75
|
-
setInternalIsOn(false);
|
|
63
|
+
requestToggle(false);
|
|
76
64
|
};
|
|
77
|
-
window.addEventListener('keydown',
|
|
78
|
-
return () => window.removeEventListener('keydown',
|
|
79
|
-
}, [exitOnEscape,
|
|
80
|
-
//
|
|
65
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
66
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
67
|
+
}, [exitOnEscape, isOn, isControlled]);
|
|
68
|
+
// Rendering
|
|
81
69
|
const c = clss(publicClassName, { cssModule });
|
|
82
70
|
const rootClss = mergeClassNames(c(null, {
|
|
83
|
-
'on':
|
|
84
|
-
'off': !
|
|
71
|
+
'on': isOn,
|
|
72
|
+
'off': !isOn
|
|
85
73
|
}), className);
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
return _jsxs("div", { className: rootClss, children: [children, _jsx("div", { className:
|
|
74
|
+
const stageClss = c('stage');
|
|
75
|
+
const openBtnClss = c('open-btn');
|
|
76
|
+
const closeBtnClss = c('close-btn');
|
|
77
|
+
return _jsxs("div", { className: rootClss, children: [children, _jsx("div", { className: stageClss, onClick: handleStageBgClick, ref: stageRef, children: isOn && children }), _jsx("div", { className: closeBtnClss, onClick: handleCloseBtnClick, children: closeBtnContent }), _jsx("div", { className: openBtnClss, onClick: handleOpenBtnClick, children: openBtnContent })] });
|
|
90
78
|
};
|
|
@@ -31,49 +31,45 @@ type ModuleData = {
|
|
|
31
31
|
* component stays in the `--no-module` state.
|
|
32
32
|
* @property props - Arbitrary key-value object forwarded verbatim to the
|
|
33
33
|
* module's `init` call and, on subsequent changes, to `update` (if exported).
|
|
34
|
-
* @property
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* after
|
|
39
|
-
*
|
|
40
|
-
*
|
|
34
|
+
* @property onIdGenerated - Called once on mount with the instance's generated
|
|
35
|
+
* `id`. The id never changes afterwards, so this fires exactly once.
|
|
36
|
+
* @property onIsLoadingChanged - Called after the loading state changed, with
|
|
37
|
+
* the new value.
|
|
38
|
+
* @property onLoadedModuleChanged - Called after the loaded module changed, with
|
|
39
|
+
* the new value: the validated {@link ModuleData}, an `Error`, or `null`.
|
|
40
|
+
* @property onModuleTargetChanged - Called after the hosted element changed,
|
|
41
|
+
* with the `Element` returned by `init`, or `null` once unloaded or errored.
|
|
41
42
|
* @property className - Optional additional class name(s) applied to the root element.
|
|
42
43
|
*/
|
|
43
44
|
export type Props = WithClassName<{
|
|
44
45
|
src?: string;
|
|
45
46
|
props?: Record<string, unknown>;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
moduleTargetChanged?: (moduleTarget: Element | null) => void;
|
|
51
|
-
};
|
|
47
|
+
onIdGenerated?: (id: string) => void;
|
|
48
|
+
onIsLoadingChanged?: (isLoading: boolean) => void;
|
|
49
|
+
onLoadedModuleChanged?: (loadedModule: ModuleData | Error | null) => void;
|
|
50
|
+
onModuleTargetChanged?: (moduleTarget: Element | null) => void;
|
|
52
51
|
}>;
|
|
53
52
|
/**
|
|
54
|
-
* Dynamic UI module host
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* error, and teardown states automatically.
|
|
53
|
+
* Dynamic UI module host. Asynchronously imports an ES module by URL, validates
|
|
54
|
+
* its exported interface, calls its `init` lifecycle to obtain a DOM `Element`,
|
|
55
|
+
* and appends that element to its own root `<div>`.
|
|
58
56
|
*
|
|
59
57
|
* The imported module is expected to conform to the {@link ModuleData} interface.
|
|
60
58
|
* Any violation (missing exports, wrong types, `init` not returning an `Element`)
|
|
61
59
|
* transitions the component into the `--error` state and logs to `console.error`.
|
|
62
60
|
*
|
|
63
|
-
* ###
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* -
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* -
|
|
70
|
-
* -
|
|
71
|
-
* - `--initialized` — the `Element` returned by `init` has been appended to the
|
|
72
|
-
* host `<div>`.
|
|
61
|
+
* ### CSS modifiers
|
|
62
|
+
* Reflecting the current load lifecycle:
|
|
63
|
+
* - `loading` — the module fetch is in progress.
|
|
64
|
+
* - `no-module` — nothing has been loaded yet (`src` is undefined, or the
|
|
65
|
+
* effect has not run).
|
|
66
|
+
* - `error` — the import, the validation, or the `init` call failed.
|
|
67
|
+
* - `loaded` — the module passed validation and `init` returned successfully.
|
|
68
|
+
* - `initialized` — the `Element` returned by `init` has been appended.
|
|
73
69
|
*
|
|
74
70
|
* ### Root element attributes
|
|
75
|
-
* - `id` — a stable
|
|
76
|
-
*
|
|
71
|
+
* - `id` — a stable generated id, assigned once on mount and used to scope the
|
|
72
|
+
* module's `css` entries to this specific instance.
|
|
77
73
|
*
|
|
78
74
|
* @param props - Component properties.
|
|
79
75
|
* @see {@link Props}
|
|
@@ -1,36 +1,33 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { clss } from '../../agnostic/css/clss/index.js';
|
|
4
4
|
import { unknownToString } from '../../agnostic/errors/unknown-to-string/index.js';
|
|
5
5
|
import { isNonNullObject } from '../../agnostic/objects/is-object/index.js';
|
|
6
6
|
import { randomHash } from '../../agnostic/random/uuid/index.js';
|
|
7
|
-
import { mergeClassNames } from '../utils/index.js';
|
|
7
|
+
import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
|
|
8
8
|
import { uiModule as publicClassName } from '../public-classnames.js';
|
|
9
9
|
import cssModule from './styles.module.css';
|
|
10
10
|
/**
|
|
11
|
-
* Dynamic UI module host
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* error, and teardown states automatically.
|
|
11
|
+
* Dynamic UI module host. Asynchronously imports an ES module by URL, validates
|
|
12
|
+
* its exported interface, calls its `init` lifecycle to obtain a DOM `Element`,
|
|
13
|
+
* and appends that element to its own root `<div>`.
|
|
15
14
|
*
|
|
16
15
|
* The imported module is expected to conform to the {@link ModuleData} interface.
|
|
17
16
|
* Any violation (missing exports, wrong types, `init` not returning an `Element`)
|
|
18
17
|
* transitions the component into the `--error` state and logs to `console.error`.
|
|
19
18
|
*
|
|
20
|
-
* ###
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* -
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
28
|
-
* - `--initialized` — the `Element` returned by `init` has been appended to the
|
|
29
|
-
* host `<div>`.
|
|
19
|
+
* ### CSS modifiers
|
|
20
|
+
* Reflecting the current load lifecycle:
|
|
21
|
+
* - `loading` — the module fetch is in progress.
|
|
22
|
+
* - `no-module` — nothing has been loaded yet (`src` is undefined, or the
|
|
23
|
+
* effect has not run).
|
|
24
|
+
* - `error` — the import, the validation, or the `init` call failed.
|
|
25
|
+
* - `loaded` — the module passed validation and `init` returned successfully.
|
|
26
|
+
* - `initialized` — the `Element` returned by `init` has been appended.
|
|
30
27
|
*
|
|
31
28
|
* ### Root element attributes
|
|
32
|
-
* - `id` — a stable
|
|
33
|
-
*
|
|
29
|
+
* - `id` — a stable generated id, assigned once on mount and used to scope the
|
|
30
|
+
* module's `css` entries to this specific instance.
|
|
34
31
|
*
|
|
35
32
|
* @param props - Component properties.
|
|
36
33
|
* @see {@link Props}
|
|
@@ -38,107 +35,81 @@ import cssModule from './styles.module.css';
|
|
|
38
35
|
* @returns A host `<div>` into which the module's root `Element` is appended,
|
|
39
36
|
* along with any `<style>` blocks exported by the module.
|
|
40
37
|
*/
|
|
41
|
-
export const UIModule = ({ src, props,
|
|
38
|
+
export const UIModule = ({ src, props, onIdGenerated, onIsLoadingChanged, onLoadedModuleChanged, onModuleTargetChanged, className }) => {
|
|
42
39
|
// State & refs
|
|
43
|
-
const [id] = useState(`f${randomHash(10)}`);
|
|
44
|
-
const [
|
|
45
|
-
const [loadedModule,
|
|
46
|
-
const [moduleTarget,
|
|
40
|
+
const [id] = useState(() => `f${randomHash(10)}`);
|
|
41
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
42
|
+
const [loadedModule, setLoadedModule] = useState(null);
|
|
43
|
+
const [moduleTarget, setModuleTarget] = useState(null);
|
|
47
44
|
const rootRef = useRef(null);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// State changes dispatch
|
|
59
|
-
useEffect(() => { stateHandlers?.idChanged?.(id); }, [id, stateHandlers]);
|
|
60
|
-
useEffect(() => { stateHandlers?.isLoadingChanged?.(loading); }, [loading, stateHandlers]);
|
|
61
|
-
useEffect(() => { stateHandlers?.loadedModuleChanged?.(loadedModule); }, [loadedModule, stateHandlers]);
|
|
62
|
-
useEffect(() => { stateHandlers?.moduleTargetChanged?.(moduleTarget); }, [moduleTarget, stateHandlers]);
|
|
63
|
-
// Fx dep. `src` - load & init module effect
|
|
45
|
+
// What the teardown needs, held outside state so the load effect can depend on
|
|
46
|
+
// `src` alone and still destroy whatever is actually live at cleanup time.
|
|
47
|
+
const liveInstanceRef = useRef(null);
|
|
48
|
+
// State dispatch
|
|
49
|
+
useChangeDispatch(isLoading, onIsLoadingChanged);
|
|
50
|
+
useChangeDispatch(loadedModule, onLoadedModuleChanged);
|
|
51
|
+
useChangeDispatch(moduleTarget, onModuleTargetChanged);
|
|
52
|
+
// Fx. no dep. - report the generated id, which never changes afterwards
|
|
53
|
+
useEffect(() => { onIdGenerated?.(id); }, []);
|
|
54
|
+
// Fx. dep. `src` - import, validate and initialize the module
|
|
64
55
|
useEffect(() => {
|
|
65
56
|
if (src === undefined)
|
|
66
57
|
return;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- shape fully validated by the checks above
|
|
98
|
-
const module = data;
|
|
99
|
-
setLoadedModule(module);
|
|
100
|
-
try {
|
|
101
|
-
const target = module.init(props ?? {});
|
|
102
|
-
if (!(target instanceof Element))
|
|
103
|
-
return setLoadedModule(errs.initRetElt);
|
|
104
|
-
setModuleTarget(target);
|
|
105
|
-
}
|
|
106
|
-
catch (err) {
|
|
107
|
-
setModuleTarget(null);
|
|
108
|
-
const e = err instanceof Error
|
|
109
|
-
? err
|
|
110
|
-
: new Error(unknownToString(err));
|
|
111
|
-
setLoadedModule(e);
|
|
112
|
-
}
|
|
113
|
-
}).catch((err) => {
|
|
114
|
-
setLoading(false);
|
|
115
|
-
setLoadedModule(err instanceof Error ? err : new Error(unknownToString(err)));
|
|
58
|
+
setIsLoading(true);
|
|
59
|
+
void import(src)
|
|
60
|
+
.then(data => {
|
|
61
|
+
setIsLoading(false);
|
|
62
|
+
if (!isNonNullObject(data))
|
|
63
|
+
return setLoadedModule(new Error('Not a module'));
|
|
64
|
+
if (!('init' in data) || typeof data.init !== 'function')
|
|
65
|
+
return setLoadedModule(new Error('Module exported member `init` must be a function'));
|
|
66
|
+
if (!('destroy' in data) || typeof data.destroy !== 'function')
|
|
67
|
+
return setLoadedModule(new Error('Module exported member `destroy` must be a function'));
|
|
68
|
+
if ('css' in data) {
|
|
69
|
+
if (!Array.isArray(data.css))
|
|
70
|
+
return setLoadedModule(new Error('Module exported member `css` must be an array of strings'));
|
|
71
|
+
if (data.css.some(entry => typeof entry !== 'string'))
|
|
72
|
+
return setLoadedModule(new Error('Module exported member `css` must be an array of strings'));
|
|
73
|
+
}
|
|
74
|
+
if ('update' in data && typeof data.update !== 'function')
|
|
75
|
+
return setLoadedModule(new Error('Module exported member `update` must be a function'));
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- shape fully validated by the checks above
|
|
77
|
+
const module = data;
|
|
78
|
+
setLoadedModule(module);
|
|
79
|
+
try {
|
|
80
|
+
const target = module.init(props ?? {});
|
|
81
|
+
if (!(target instanceof Element))
|
|
82
|
+
return setLoadedModule(new Error('Module exported function `init` must return an Element'));
|
|
83
|
+
liveInstanceRef.current = { module, target };
|
|
84
|
+
setModuleTarget(target);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
liveInstanceRef.current = null;
|
|
116
88
|
setModuleTarget(null);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
89
|
+
setLoadedModule(err instanceof Error ? err : new Error(unknownToString(err)));
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.catch((err) => {
|
|
93
|
+
setIsLoading(false);
|
|
94
|
+
liveInstanceRef.current = null;
|
|
95
|
+
setLoadedModule(err instanceof Error ? err : new Error(unknownToString(err)));
|
|
96
|
+
setModuleTarget(null);
|
|
97
|
+
});
|
|
125
98
|
return () => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (loadedModuleRef.current instanceof Error)
|
|
129
|
-
return;
|
|
130
|
-
if (loadedModuleRef.current === null)
|
|
99
|
+
const liveInstance = liveInstanceRef.current;
|
|
100
|
+
if (liveInstance === null)
|
|
131
101
|
return;
|
|
132
|
-
|
|
102
|
+
liveInstance.module.destroy(liveInstance.target);
|
|
103
|
+
liveInstanceRef.current = null;
|
|
133
104
|
};
|
|
134
105
|
}, [src]);
|
|
135
|
-
// Fx dep. `loadedModule` -
|
|
106
|
+
// Fx. dep. `loadedModule` - surface load errors
|
|
136
107
|
useEffect(() => {
|
|
137
108
|
// eslint-disable-next-line no-console
|
|
138
109
|
if (loadedModule instanceof Error)
|
|
139
110
|
console.error(loadedModule);
|
|
140
111
|
}, [loadedModule]);
|
|
141
|
-
// Fx dep. `moduleTarget` - append the
|
|
112
|
+
// Fx. dep. `moduleTarget` - append the element the module built
|
|
142
113
|
useEffect(() => {
|
|
143
114
|
if (moduleTarget === null)
|
|
144
115
|
return;
|
|
@@ -148,14 +119,14 @@ export const UIModule = ({ src, props, stateHandlers, className }) => {
|
|
|
148
119
|
}, [moduleTarget]);
|
|
149
120
|
// Rendering
|
|
150
121
|
const c = clss(publicClassName, { cssModule });
|
|
122
|
+
const hasErrored = loadedModule instanceof Error;
|
|
151
123
|
const rootClss = mergeClassNames(c(null, {
|
|
152
|
-
loading,
|
|
124
|
+
'loading': isLoading,
|
|
153
125
|
'no-module': loadedModule === null,
|
|
154
|
-
'error':
|
|
155
|
-
'loaded': !
|
|
126
|
+
'error': hasErrored,
|
|
127
|
+
'loaded': !isLoading && loadedModule !== null && !hasErrored,
|
|
156
128
|
'initialized': moduleTarget !== null
|
|
157
129
|
}), className);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
&& loadedModule.css?.map(css => _jsx("style", { children: `.${publicClassName}#${id} { ${css} }` }))] });
|
|
130
|
+
const moduleCss = hasErrored || loadedModule === null ? [] : loadedModule.css ?? [];
|
|
131
|
+
return _jsx("div", { className: rootClss, ref: rootRef, id: id, children: moduleCss.map((css, cssPos) => _jsx("style", { children: `.${publicClassName}#${id} { ${css} }` }, cssPos)) });
|
|
161
132
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FunctionComponent, type PropsWithChildren, type VideoHTMLAttributes
|
|
1
|
+
import { type FunctionComponent, type PropsWithChildren, type VideoHTMLAttributes } from 'react';
|
|
2
2
|
import type { WithClassName } from '../utils/types.js';
|
|
3
3
|
import { type Props as SubsProps } from '../Subtitles/index.js';
|
|
4
4
|
/**
|
|
@@ -27,47 +27,6 @@ type TrackData = {
|
|
|
27
27
|
label?: string;
|
|
28
28
|
default?: boolean;
|
|
29
29
|
};
|
|
30
|
-
/**
|
|
31
|
-
* Callbacks for user actions on the video player controls.
|
|
32
|
-
* Allows you to intercept user actions on the player's buttons and sliders.
|
|
33
|
-
*
|
|
34
|
-
* @property playButtonClick - Called when the play button is clicked. Receives the event, isPlaying state and HTMLVideoElement before any change happens. If the time is not controlled by parent (no currentTime given as props), the component will play to the target time right after.
|
|
35
|
-
* @property pauseButtonClick - Called when the pause button is clicked. Receives the event, isPlaying state and HTMLVideoElement before any change happens. If the time is not controlled by parent (no currentTime given as props), the component will pause to the target time right after.
|
|
36
|
-
* @property loudButtonClick - Called when the "loud" (unmute) button is clicked. Receives the event, isLoud state and HTMLVideoElement before any change happens.
|
|
37
|
-
* @property muteButtonClick - Called when the mute button is clicked. Receives the event, isLoud state and HTMLVideoElement before any change happens.
|
|
38
|
-
* @property volumeRangeChange - Called when the volume slider is changed. Receives the event, target volume (0 to 1), current volume (0 to 1) and HTMLVideoElement before any change happens.
|
|
39
|
-
* @property fullscreenButtonClick - Called when the fullscreen button is clicked. Receives the event, isFullscreen state and HTMLVideoElement before any change happens.
|
|
40
|
-
* @property rateRangeChange - Called when the playback rate slider is changed. Receives the event, target rate, current rate and HTMLVideoElement before any change happens.
|
|
41
|
-
* @property timelineClick - Called when the timeline is clicked. Receives the event, target time (in seconds), current time (in seconds) and HTMLVideoElement before any change happens. If the time is not controlled by parent (no currentTime given as props), the component will update the video current time to the target time right after.
|
|
42
|
-
*/
|
|
43
|
-
export type ActionHandlersProps = {
|
|
44
|
-
playButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
|
|
45
|
-
pauseButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
|
|
46
|
-
loudButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
|
|
47
|
-
muteButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
|
|
48
|
-
volumeRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetVolumePercent: number, currentVolumePercent: number, video: HTMLVideoElement | null) => void;
|
|
49
|
-
fullscreenButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isFullscreen: boolean, video: HTMLVideoElement | null) => void;
|
|
50
|
-
rateRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetRate: number, rate: number, video: HTMLVideoElement | null) => void;
|
|
51
|
-
timelineClick?: (e: React.MouseEvent<HTMLDivElement>, time: number, currentTime: number, video: HTMLVideoElement | null) => void;
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* Callbacks to synchronize the internal player state with the outside.
|
|
55
|
-
*
|
|
56
|
-
* @property isPlaying - Called whenever the play/pause state changes.
|
|
57
|
-
* @property isFullscreen - Called whenever the fullscreen state changes.
|
|
58
|
-
* @property isLoud - Called whenever the mute/unmute state changes (true = unmuted, false = muted).
|
|
59
|
-
* @property volume - Called whenever the volume changes (value between 0 and 1).
|
|
60
|
-
* @property playbackRate - Called whenever the playback speed changes.
|
|
61
|
-
* @property currentTime - Called on every change of the current time (in seconds).
|
|
62
|
-
*/
|
|
63
|
-
export type StateHandlersProps = {
|
|
64
|
-
isPlaying?: (isPlaying: boolean) => void;
|
|
65
|
-
isFullscreen?: (isFullscreen: boolean) => void;
|
|
66
|
-
isLoud?: (isLoud: boolean) => void;
|
|
67
|
-
volume?: (volume: number) => void;
|
|
68
|
-
playbackRate?: (rate: number) => void;
|
|
69
|
-
currentTime?: (currentTime: number) => void;
|
|
70
|
-
};
|
|
71
30
|
/**
|
|
72
31
|
* Props for the ControlledVideo component.
|
|
73
32
|
*
|
|
@@ -84,11 +43,41 @@ export type StateHandlersProps = {
|
|
|
84
43
|
* @property volume - External control of volume (0 to 1).
|
|
85
44
|
* @property mute - External control of mute (true = muted).
|
|
86
45
|
* @property playbackRate - External control of playback speed.
|
|
87
|
-
* @property currentTimeMs - External control of current time (in ms).
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
46
|
+
* @property currentTimeMs - External control of current time (in ms). Providing
|
|
47
|
+
* it hands ownership of the time to the parent: the value is displayed as given,
|
|
48
|
+
* the element is seeked to it on every change, and user interactions that would
|
|
49
|
+
* otherwise move the time (timeline click) no longer do — the parent is expected
|
|
50
|
+
* to update the prop instead. Because a playing element advances the time by
|
|
51
|
+
* itself, a controlled time also implies a stopped video: `play` and `autoPlay`
|
|
52
|
+
* are ignored for as long as this prop is provided.
|
|
53
|
+
* @property onPlayButtonClicked - Called when the play button is clicked, before
|
|
54
|
+
* the component reacts, with the playback state as it was.
|
|
55
|
+
* @property onPauseButtonClicked - Called when the pause button is clicked, before
|
|
56
|
+
* the component reacts, with the playback state as it was.
|
|
57
|
+
* @property onLoudButtonClicked - Called when the unmute button is clicked, before
|
|
58
|
+
* the component reacts, with the mute state as it was (`true` = unmuted).
|
|
59
|
+
* @property onMuteButtonClicked - Called when the mute button is clicked, before
|
|
60
|
+
* the component reacts, with the mute state as it was (`true` = unmuted).
|
|
61
|
+
* @property onVolumeRangeChanged - Called when the volume slider moves, before the
|
|
62
|
+
* component reacts, with the target and current volumes (both `0` to `1`).
|
|
63
|
+
* @property onRateRangeChanged - Called when the playback rate slider moves, before
|
|
64
|
+
* the component reacts, with the target and current rates.
|
|
65
|
+
* @property onFullscreenButtonClicked - Called when the fullscreen button is
|
|
66
|
+
* clicked, before the component reacts, with the fullscreen state as it was.
|
|
67
|
+
* @property onTimelineClicked - Called when the timeline is clicked, before the
|
|
68
|
+
* component reacts, with the target and current times (in seconds). The component
|
|
69
|
+
* seeks to the target right after, unless the time is controlled.
|
|
70
|
+
* @property onIsPlayingChanged - Called once the playback state has changed.
|
|
71
|
+
* @property onIsFullscreenChanged - Called once the fullscreen state has changed.
|
|
72
|
+
* @property onIsLoudChanged - Called once the mute state has changed (`true` = unmuted).
|
|
73
|
+
* @property onVolumeChanged - Called once the volume has changed (`0` to `1`).
|
|
74
|
+
* @property onPlaybackRateChanged - Called once the playback rate has changed.
|
|
75
|
+
* @property onCurrentTimeMsChanged - Called once the current time has changed, in
|
|
76
|
+
* milliseconds, to match the `currentTimeMs` prop.
|
|
77
|
+
* @property onFullscreenChange - Called when the *browser* enters or leaves
|
|
78
|
+
* fullscreen on its own — pressing Escape, typically — with the new state. This is
|
|
79
|
+
* the signal a parent needs to update its `fullscreen` prop, and it is distinct
|
|
80
|
+
* from `onIsFullscreenChanged`, which merely echoes that prop back once changed.
|
|
92
81
|
* @property className - Additional CSS class for the root element.
|
|
93
82
|
* @property children - React content inserted into the <video> tag (fallback, etc).
|
|
94
83
|
*
|
|
@@ -109,10 +98,21 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
109
98
|
mute?: boolean;
|
|
110
99
|
playbackRate?: number;
|
|
111
100
|
currentTimeMs?: number;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
onPlayButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
|
|
102
|
+
onPauseButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
|
|
103
|
+
onLoudButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
|
|
104
|
+
onMuteButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
|
|
105
|
+
onVolumeRangeChanged?: (e: React.ChangeEvent<HTMLInputElement>, targetVolume: number, currentVolume: number, video: HTMLVideoElement | null) => void;
|
|
106
|
+
onRateRangeChanged?: (e: React.ChangeEvent<HTMLInputElement>, targetRate: number, currentRate: number, video: HTMLVideoElement | null) => void;
|
|
107
|
+
onFullscreenButtonClicked?: (e: React.MouseEvent<HTMLButtonElement>, isFullscreen: boolean, video: HTMLVideoElement | null) => void;
|
|
108
|
+
onTimelineClicked?: (e: React.MouseEvent<HTMLDivElement>, targetTime: number, currentTime: number, video: HTMLVideoElement | null) => void;
|
|
109
|
+
onIsPlayingChanged?: (isPlaying: boolean) => void;
|
|
110
|
+
onIsFullscreenChanged?: (isFullscreen: boolean) => void;
|
|
111
|
+
onIsLoudChanged?: (isLoud: boolean) => void;
|
|
112
|
+
onVolumeChanged?: (volume: number) => void;
|
|
113
|
+
onPlaybackRateChanged?: (playbackRate: number) => void;
|
|
114
|
+
onCurrentTimeMsChanged?: (currentTimeMs: number) => void;
|
|
115
|
+
onFullscreenChange?: (isFullscreen: boolean) => void;
|
|
116
116
|
}> & VideoHTMLAttributes<HTMLVideoElement>>;
|
|
117
117
|
/**
|
|
118
118
|
* Full-featured video player component. Wraps a native `<video>` element with
|