@design-edito/tools 0.3.11 → 0.3.12
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 +2 -2
- package/agnostic/colors/index.js +2 -2
- package/agnostic/css/index.d.ts +1 -1
- package/agnostic/css/index.js +1 -1
- package/agnostic/errors/index.d.ts +1 -1
- package/agnostic/errors/index.js +1 -1
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +14 -14
- package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +14 -14
- 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/index.d.ts +4 -4
- package/agnostic/index.js +4 -4
- package/agnostic/misc/crossenv/index.d.ts +1 -1
- package/agnostic/misc/crossenv/index.js +1 -1
- 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 +2 -2
- package/agnostic/objects/index.js +2 -2
- package/agnostic/optim/index.d.ts +1 -1
- package/agnostic/optim/index.js +1 -1
- package/agnostic/random/index.d.ts +1 -1
- package/agnostic/random/index.js +1 -1
- package/agnostic/sanitization/index.d.ts +1 -1
- package/agnostic/sanitization/index.js +1 -1
- package/agnostic/strings/index.d.ts +2 -2
- package/agnostic/strings/index.js +2 -2
- package/agnostic/time/index.d.ts +2 -2
- package/agnostic/time/index.js +2 -2
- package/components/BeforeAfter/index.controlled.d.ts +46 -0
- package/components/BeforeAfter/index.d.ts +55 -0
- package/components/BeforeAfter/index.js +40 -0
- package/components/BeforeAfter/styles.module.css +0 -0
- package/components/BeforeAfter/utils.d.ts +4 -0
- package/components/ScrollListener/index.d.ts +47 -0
- package/components/ScrollListener/index.js +110 -0
- package/components/ScrollListener/styles.module.css +0 -0
- package/components/ScrollListener/utils.d.ts +41 -0
- package/components/Theatre/index.d.ts +2 -2
- package/components/Theatre/index.js +4 -4
- package/components/UIModule/index.d.ts +85 -0
- package/components/UIModule/index.js +134 -0
- package/components/UIModule/styles.module.css +0 -0
- package/components/Video/index.d.ts +20 -0
- package/components/Video/index.js +61 -34
- package/components/Video/utils.d.ts +4 -0
- package/components/index.d.ts +3 -0
- package/components/index.js +3 -0
- package/components/public-classnames.d.ts +3 -0
- package/node/@aws-s3/storage/directory/index.d.ts +1 -1
- package/node/@aws-s3/storage/directory/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 +1 -1
- package/node/@google-cloud/storage/directory/index.js +1 -1
- 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 +2 -2
- package/node/cloud-storage/operations/index.js +2 -2
- package/node/encryption/index.d.ts +1 -1
- package/node/encryption/index.js +1 -1
- package/node/files/index.d.ts +1 -1
- package/node/files/index.js +1 -1
- 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 +2 -2
- package/node/images/index.js +2 -2
- package/node/images/transform/operations/index.d.ts +3 -3
- package/node/images/transform/operations/index.js +3 -3
- package/node/index.d.ts +4 -4
- package/node/index.js +4 -4
- package/node/sftp/file/index.d.ts +2 -2
- package/node/sftp/file/index.js +2 -2
- package/package.json +22 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type FunctionComponent, type PropsWithChildren } from 'react';
|
|
2
|
+
import type { WithClassName } from '../utils/types.js';
|
|
3
|
+
import { type ScrollState } from './utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Props for the ScrollListener component.
|
|
6
|
+
*
|
|
7
|
+
* @property startOnVisible - When `true`, scroll tracking starts only when the component
|
|
8
|
+
* becomes visible in the viewport. Visibility is detected using an IntersectionObserver.
|
|
9
|
+
* @property stopOnHidden - When `true`, scroll tracking stops when the component leaves
|
|
10
|
+
* the viewport.
|
|
11
|
+
* @property stateHandlers - Optional callbacks invoked when internal state changes.
|
|
12
|
+
* @property stateHandlers.scrollDataChanged - Called whenever the computed scroll data
|
|
13
|
+
* changes. Receives the current {@link ScrollState} or `undefined`.
|
|
14
|
+
* @property stateHandlers.visibilityChanged - Called when the visibility state of the
|
|
15
|
+
* component changes. Receives `true` when the component is intersecting the viewport,
|
|
16
|
+
* otherwise `false`.
|
|
17
|
+
* @property className - Optional additional class name(s) applied to the root element.
|
|
18
|
+
* @property children - React nodes rendered inside the scroll listener container.
|
|
19
|
+
*/
|
|
20
|
+
export type Props = PropsWithChildren<WithClassName<{
|
|
21
|
+
stopOnHidden?: boolean;
|
|
22
|
+
startOnVisible?: boolean;
|
|
23
|
+
stateHandlers?: {
|
|
24
|
+
scrollDataChanged?: (scrollData?: ScrollState) => void;
|
|
25
|
+
visibilityChanged?: (isVisible: boolean) => void;
|
|
26
|
+
};
|
|
27
|
+
}>>;
|
|
28
|
+
/**
|
|
29
|
+
* Component that listens to global scroll events and exposes scroll-related data
|
|
30
|
+
* through CSS custom properties and optional state callbacks.
|
|
31
|
+
*
|
|
32
|
+
* The component measures both global scroll metrics (window size, document size,
|
|
33
|
+
* scroll offsets) and local metrics relative to the component (dimensions and
|
|
34
|
+
* position within the document).
|
|
35
|
+
*
|
|
36
|
+
* Computed values are exposed as CSS variables on the root element, enabling
|
|
37
|
+
* scroll-driven styling without additional JavaScript.
|
|
38
|
+
*
|
|
39
|
+
* Scroll observation can optionally start only when the component becomes visible
|
|
40
|
+
* and stop when it leaves the viewport.
|
|
41
|
+
*
|
|
42
|
+
* @param props - Component properties.
|
|
43
|
+
* @see {@link Props}
|
|
44
|
+
* @returns A container element exposing scroll metrics through CSS variables and
|
|
45
|
+
* wrapping its children inside an IntersectionObserver boundary.
|
|
46
|
+
*/
|
|
47
|
+
export declare const ScrollListener: FunctionComponent<Props>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/components/ScrollListener/index.tsx
|
|
2
|
+
import {
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
useCallback
|
|
7
|
+
} from "react";
|
|
8
|
+
import { clss } from "../../agnostic/css/clss/index.js";
|
|
9
|
+
import { randomHash } from "../../agnostic/random/uuid/index.js";
|
|
10
|
+
import { mergeClassNames } from "../utils/index.js";
|
|
11
|
+
import {
|
|
12
|
+
IntersectionObserverComponent
|
|
13
|
+
} from "../IntersectionObserver/index.js";
|
|
14
|
+
import { scrollListener as publicClassName } from "../public-classnames.js";
|
|
15
|
+
import { register, unregister } from "./utils.js";
|
|
16
|
+
import cssModule from "./styles.module.css";
|
|
17
|
+
import { jsx } from "react/jsx-runtime";
|
|
18
|
+
var ScrollListener = ({
|
|
19
|
+
startOnVisible,
|
|
20
|
+
stopOnHidden,
|
|
21
|
+
stateHandlers,
|
|
22
|
+
className,
|
|
23
|
+
children
|
|
24
|
+
}) => {
|
|
25
|
+
const [privateId] = useState(randomHash(6));
|
|
26
|
+
const [scrollData, setScrollData] = useState();
|
|
27
|
+
const rootRef = useRef(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
stateHandlers?.scrollDataChanged?.(scrollData);
|
|
30
|
+
}, [scrollData]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
register({ id: privateId, rootRef, setData: setScrollData });
|
|
33
|
+
return () => {
|
|
34
|
+
unregister(privateId);
|
|
35
|
+
};
|
|
36
|
+
}, []);
|
|
37
|
+
const handleIntersection = useCallback(({ ioEntry, observer }) => {
|
|
38
|
+
stateHandlers?.visibilityChanged?.(ioEntry?.isIntersecting ?? false);
|
|
39
|
+
if (startOnVisible !== true && stopOnHidden !== true) return;
|
|
40
|
+
if (ioEntry?.isIntersecting === true && startOnVisible === true) register({ id: privateId, rootRef, setData: setScrollData });
|
|
41
|
+
if (ioEntry?.isIntersecting !== true && stopOnHidden === true) unregister(privateId);
|
|
42
|
+
}, [startOnVisible, stopOnHidden]);
|
|
43
|
+
const customProps = Object.entries(
|
|
44
|
+
scrollData !== void 0 ? {
|
|
45
|
+
// Global
|
|
46
|
+
[`--${publicClassName}-window-width`]: scrollData.global.win.width,
|
|
47
|
+
[`--${publicClassName}-window-height`]: scrollData.global.win.height,
|
|
48
|
+
[`--${publicClassName}-html-width`]: scrollData.global.html.width,
|
|
49
|
+
[`--${publicClassName}-html-height`]: scrollData.global.html.height,
|
|
50
|
+
[`--${publicClassName}-scroll-x`]: scrollData.global.scroll.x,
|
|
51
|
+
[`--${publicClassName}-scroll-y`]: scrollData.global.scroll.y,
|
|
52
|
+
// Local
|
|
53
|
+
[`--${publicClassName}-width`]: scrollData.local.width,
|
|
54
|
+
[`--${publicClassName}-height`]: scrollData.local.height,
|
|
55
|
+
[`--${publicClassName}-offset-x`]: scrollData.local.offsetX,
|
|
56
|
+
[`--${publicClassName}-offset-y`]: scrollData.local.offsetY
|
|
57
|
+
} : {}
|
|
58
|
+
).reduce((acc, [key, val]) => ({
|
|
59
|
+
...acc,
|
|
60
|
+
[key]: val,
|
|
61
|
+
[`${key}-px`]: `${val}px`
|
|
62
|
+
}), {});
|
|
63
|
+
if (scrollData !== void 0) {
|
|
64
|
+
customProps[`--${publicClassName}-window-scrolled-x-ratio`] = scrollData.global.scroll.x / Math.max(scrollData.global.html.width - scrollData.global.win.width, 1);
|
|
65
|
+
customProps[`--${publicClassName}-window-scrolled-y-ratio`] = scrollData.global.scroll.y / Math.max(scrollData.global.html.height - scrollData.global.win.height, 1);
|
|
66
|
+
const topTouchesTop = scrollData.local.offsetY;
|
|
67
|
+
const topTouchesBottom = scrollData.local.offsetY - scrollData.global.win.height;
|
|
68
|
+
const bottomTouchesTop = scrollData.local.offsetY + scrollData.local.height;
|
|
69
|
+
const bottomTouchesBottom = scrollData.local.offsetY + scrollData.local.height - scrollData.global.win.height;
|
|
70
|
+
const leftTouchesLeft = scrollData.local.offsetX;
|
|
71
|
+
const leftTouchesRight = scrollData.local.offsetX - scrollData.global.win.width;
|
|
72
|
+
const rightTouchesLeft = scrollData.local.offsetX + scrollData.local.width;
|
|
73
|
+
const rightTouchesRight = scrollData.local.offsetX + scrollData.local.width - scrollData.global.win.width;
|
|
74
|
+
const innerYRange = [bottomTouchesBottom, topTouchesTop];
|
|
75
|
+
const outerYRange = [topTouchesBottom, bottomTouchesTop];
|
|
76
|
+
const innerXRange = [rightTouchesRight, leftTouchesLeft];
|
|
77
|
+
const outerXRange = [leftTouchesRight, rightTouchesLeft];
|
|
78
|
+
const innerScrollYRatio = (scrollData.global.scroll.y - innerYRange[0]) / Math.max(innerYRange[1] - innerYRange[0], 1);
|
|
79
|
+
const outerScrollYRatio = (scrollData.global.scroll.y - outerYRange[0]) / Math.max(outerYRange[1] - outerYRange[0], 1);
|
|
80
|
+
const innerScrollXRatio = (scrollData.global.scroll.x - innerXRange[0]) / Math.max(innerXRange[1] - innerXRange[0], 1);
|
|
81
|
+
const outerScrollXRatio = (scrollData.global.scroll.x - outerXRange[0]) / Math.max(outerXRange[1] - outerXRange[0], 1);
|
|
82
|
+
customProps[`--${publicClassName}-self-inner-scrolled-y-ratio`] = innerScrollYRatio;
|
|
83
|
+
customProps[`--${publicClassName}-self-outer-scrolled-y-ratio`] = outerScrollYRatio;
|
|
84
|
+
customProps[`--${publicClassName}-self-inner-scrolled-x-ratio`] = innerScrollXRatio;
|
|
85
|
+
customProps[`--${publicClassName}-self-outer-scrolled-x-ratio`] = outerScrollXRatio;
|
|
86
|
+
}
|
|
87
|
+
const c = clss(publicClassName, { cssModule });
|
|
88
|
+
const rootClss = mergeClassNames(
|
|
89
|
+
c(),
|
|
90
|
+
className
|
|
91
|
+
);
|
|
92
|
+
return /* @__PURE__ */ jsx(
|
|
93
|
+
"div",
|
|
94
|
+
{
|
|
95
|
+
className: rootClss,
|
|
96
|
+
ref: rootRef,
|
|
97
|
+
style: { ...customProps },
|
|
98
|
+
children: /* @__PURE__ */ jsx(
|
|
99
|
+
IntersectionObserverComponent,
|
|
100
|
+
{
|
|
101
|
+
onIntersected: handleIntersection,
|
|
102
|
+
children
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
export {
|
|
109
|
+
ScrollListener
|
|
110
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { RefObject, Dispatch, SetStateAction } from 'react';
|
|
2
|
+
export type GlobalScrollData = {
|
|
3
|
+
win: {
|
|
4
|
+
height: number;
|
|
5
|
+
width: number;
|
|
6
|
+
};
|
|
7
|
+
html: {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
scroll: {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
};
|
|
15
|
+
viewport: {
|
|
16
|
+
minX: number;
|
|
17
|
+
minY: number;
|
|
18
|
+
maxX: number;
|
|
19
|
+
maxY: number;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export type LocalScrollData = {
|
|
23
|
+
offsetX: number;
|
|
24
|
+
offsetY: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
};
|
|
28
|
+
export type ScrollState = {
|
|
29
|
+
global: GlobalScrollData;
|
|
30
|
+
local: LocalScrollData;
|
|
31
|
+
};
|
|
32
|
+
export declare const globalDocumentScrollListener: () => GlobalScrollData;
|
|
33
|
+
export declare const localElementScrollListener: (div: HTMLDivElement) => LocalScrollData;
|
|
34
|
+
export type RegisterEntry = {
|
|
35
|
+
id: string;
|
|
36
|
+
rootRef: RefObject<HTMLDivElement | null>;
|
|
37
|
+
setData: Dispatch<SetStateAction<ScrollState | undefined>>;
|
|
38
|
+
};
|
|
39
|
+
export declare const registeredIds: Map<string, RegisterEntry>;
|
|
40
|
+
export declare const register: (props: RegisterEntry) => void;
|
|
41
|
+
export declare const unregister: (id: string) => void;
|
|
@@ -14,7 +14,7 @@ import type { WithClassName } from '../utils/types.js';
|
|
|
14
14
|
* @property stateHandlers - Callbacks called after the internal state changed
|
|
15
15
|
* @property stateHandlers.toggled - Callback invoked after the state changed
|
|
16
16
|
* @property actionHandlers - Callbacks called after a user action on children elements
|
|
17
|
-
* @property actionHandlers.
|
|
17
|
+
* @property actionHandlers.toggleClick - Callback invoked when either the open or close
|
|
18
18
|
* button is clicked, the 'esc' key pressed or the background clicked. Receives the theatre state value (`isOn`) at the time of the click,
|
|
19
19
|
* i.e. the previous state before the toggle.
|
|
20
20
|
* @property className - Optional additional class name(s) applied to the root element.
|
|
@@ -32,7 +32,7 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
32
32
|
toggled?: (isOn: boolean) => void;
|
|
33
33
|
};
|
|
34
34
|
actionHandlers?: {
|
|
35
|
-
|
|
35
|
+
toggleClick?: (prevIsOn: boolean) => void;
|
|
36
36
|
};
|
|
37
37
|
}>>;
|
|
38
38
|
/**
|
|
@@ -26,17 +26,17 @@ var Theatre = ({
|
|
|
26
26
|
const isTheatreOn = isOn ?? internalIsOn;
|
|
27
27
|
const prevIsTheatreOnRef = useRef(isTheatreOn);
|
|
28
28
|
const handleCloseBtnClick = () => {
|
|
29
|
-
actionHandlers?.
|
|
29
|
+
actionHandlers?.toggleClick?.(isTheatreOn);
|
|
30
30
|
if (isOn === void 0) setInternalIsOn(false);
|
|
31
31
|
};
|
|
32
32
|
const handleOpenBtnClick = () => {
|
|
33
|
-
actionHandlers?.
|
|
33
|
+
actionHandlers?.toggleClick?.(isTheatreOn);
|
|
34
34
|
if (isOn === void 0) setInternalIsOn(true);
|
|
35
35
|
};
|
|
36
36
|
const handleStageBgClick = (e) => {
|
|
37
37
|
if (exitOnBgClick !== true) return;
|
|
38
38
|
if (e.target !== stageRef.current) return;
|
|
39
|
-
actionHandlers?.
|
|
39
|
+
actionHandlers?.toggleClick?.(isTheatreOn);
|
|
40
40
|
if (isOn === void 0) setInternalIsOn(false);
|
|
41
41
|
};
|
|
42
42
|
useEffect(() => {
|
|
@@ -49,7 +49,7 @@ var Theatre = ({
|
|
|
49
49
|
if (exitOnEscape === true || !isTheatreOn || isOn !== void 0) return;
|
|
50
50
|
const listener = (e) => {
|
|
51
51
|
if (e.key !== "Escape") return;
|
|
52
|
-
actionHandlers?.
|
|
52
|
+
actionHandlers?.toggleClick?.(isTheatreOn);
|
|
53
53
|
setInternalIsOn(false);
|
|
54
54
|
};
|
|
55
55
|
window.addEventListener("keydown", listener);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'react';
|
|
2
|
+
import type { WithClassName } from '../utils/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Describes the contract a dynamically imported UI module must satisfy.
|
|
5
|
+
* Every member is validated at runtime after the import resolves.
|
|
6
|
+
*
|
|
7
|
+
* @property init - Called once after the module loads. Receives the current
|
|
8
|
+
* `props` and must return the root `Element` that will be appended to the
|
|
9
|
+
* host `<div>`. Throwing inside `init` is caught and surfaced as an error state.
|
|
10
|
+
* @property destroy - Called when the component unmounts or `src` changes.
|
|
11
|
+
* Receives the `Element` previously returned by `init`. Use it to tear down
|
|
12
|
+
* event listeners, timers, or third-party instances.
|
|
13
|
+
* @property update - Optional. Called when `props` change after the module is
|
|
14
|
+
* already initialized. Receives the live `Element` and the new props object.
|
|
15
|
+
* @property css - Optional array of raw CSS strings scoped automatically to
|
|
16
|
+
* the host element via `.<publicClassName>#<id> { … }` and injected as
|
|
17
|
+
* `<style>` elements.
|
|
18
|
+
*/
|
|
19
|
+
type ModuleData = {
|
|
20
|
+
init: (props: Record<string, unknown>) => Element;
|
|
21
|
+
destroy: (target: Element) => void;
|
|
22
|
+
update?: (target: Element, props: Record<string, unknown>) => void;
|
|
23
|
+
css?: string[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Props for the {@link UIModule} component.
|
|
27
|
+
*
|
|
28
|
+
* @property src - URL of the ES module to import dynamically. The module must
|
|
29
|
+
* satisfy the {@link ModuleData} interface — `init` and `destroy` are required,
|
|
30
|
+
* `update` and `css` are optional. When `undefined`, nothing is loaded and the
|
|
31
|
+
* component stays in the `--no-module` state.
|
|
32
|
+
* @property props - Arbitrary key-value object forwarded verbatim to the
|
|
33
|
+
* module's `init` call and, on subsequent changes, to `update` (if exported).
|
|
34
|
+
* @property stateHandlers - Optional callbacks invoked whenever internal state changes:
|
|
35
|
+
* - `idChanged` — called with the stable instance ID once on mount.
|
|
36
|
+
* - `isLoadingChanged` — called with the new loading state on every transition.
|
|
37
|
+
* - `loadedModuleChanged` — called with the new module value (`ModuleData`, `Error`, or `null`)
|
|
38
|
+
* after each load attempt or teardown.
|
|
39
|
+
* - `moduleTargetChanged` — called with the `Element` returned by `init`, or `null`
|
|
40
|
+
* when the module is unloaded or errored.
|
|
41
|
+
* @property className - Optional additional class name(s) applied to the root element.
|
|
42
|
+
*/
|
|
43
|
+
export type Props = WithClassName<{
|
|
44
|
+
src?: string;
|
|
45
|
+
props?: Record<string, unknown>;
|
|
46
|
+
stateHandlers?: {
|
|
47
|
+
idChanged?: (id: string) => void;
|
|
48
|
+
isLoadingChanged?: (isLoading: boolean) => void;
|
|
49
|
+
loadedModuleChanged?: (loadedModule: ModuleData | Error | null) => void;
|
|
50
|
+
moduleTargetChanged?: (moduleTarget: Element | null) => void;
|
|
51
|
+
};
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Dynamic UI module host component. Asynchronously imports an ES module by URL,
|
|
55
|
+
* validates its exported interface, calls its `init` lifecycle to obtain a DOM
|
|
56
|
+
* `Element`, and appends that element to its own root `<div>`. Handles loading,
|
|
57
|
+
* error, and teardown states automatically.
|
|
58
|
+
*
|
|
59
|
+
* The imported module is expected to conform to the {@link ModuleData} interface.
|
|
60
|
+
* Any violation (missing exports, wrong types, `init` not returning an `Element`)
|
|
61
|
+
* transitions the component into the `--error` state and logs to `console.error`.
|
|
62
|
+
*
|
|
63
|
+
* ### Root element modifiers
|
|
64
|
+
* The root `<div>` receives the public class name defined by `uiModule` and
|
|
65
|
+
* the following BEM-style modifier classes reflecting the current load lifecycle:
|
|
66
|
+
* - `--loading` — the module fetch is in progress.
|
|
67
|
+
* - `--no-module` — no module has been loaded yet (`src` is undefined or the
|
|
68
|
+
* effect has not run).
|
|
69
|
+
* - `--error` — the import, validation, or `init` call failed.
|
|
70
|
+
* - `--loaded` — the module passed validation and `init` returned successfully.
|
|
71
|
+
* - `--initialized` — the `Element` returned by `init` has been appended to the
|
|
72
|
+
* host `<div>`.
|
|
73
|
+
*
|
|
74
|
+
* ### Root element attributes
|
|
75
|
+
* - `id` — a stable randomly generated ID (prefixed `f`) assigned once on mount.
|
|
76
|
+
* Used to scope the module's `css` entries to this specific instance.
|
|
77
|
+
*
|
|
78
|
+
* @param props - Component properties.
|
|
79
|
+
* @see {@link Props}
|
|
80
|
+
* @see {@link ModuleData}
|
|
81
|
+
* @returns A host `<div>` into which the module's root `Element` is appended,
|
|
82
|
+
* along with any `<style>` blocks exported by the module.
|
|
83
|
+
*/
|
|
84
|
+
export declare const UIModule: FunctionComponent<Props>;
|
|
85
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// src/components/UIModule/index.tsx
|
|
2
|
+
import {
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState
|
|
6
|
+
} from "react";
|
|
7
|
+
import { clss } from "../../agnostic/css/clss/index.js";
|
|
8
|
+
import { unknownToString } from "../../agnostic/errors/unknown-to-string/index.js";
|
|
9
|
+
import { isNonNullObject } from "../../agnostic/objects/is-object/index.js";
|
|
10
|
+
import { randomHash } from "../../agnostic/random/uuid/index.js";
|
|
11
|
+
import { mergeClassNames } from "../utils/index.js";
|
|
12
|
+
import { uiModule as publicClassName } from "../public-classnames.js";
|
|
13
|
+
import cssModule from "./styles.module.css";
|
|
14
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
var UIModule = ({
|
|
16
|
+
src,
|
|
17
|
+
props,
|
|
18
|
+
stateHandlers,
|
|
19
|
+
className
|
|
20
|
+
}) => {
|
|
21
|
+
const [id] = useState(`f${randomHash(10)}`);
|
|
22
|
+
const [loading, setLoading] = useState(false);
|
|
23
|
+
const [loadedModule, _setLoadedModule] = useState(null);
|
|
24
|
+
const [moduleTarget, _setModuleTarget] = useState(null);
|
|
25
|
+
const rootRef = useRef(null);
|
|
26
|
+
const loadedModuleRef = useRef(null);
|
|
27
|
+
const moduleTargetRef = useRef(null);
|
|
28
|
+
const setLoadedModule = (data) => {
|
|
29
|
+
loadedModuleRef.current = data;
|
|
30
|
+
_setLoadedModule(data);
|
|
31
|
+
};
|
|
32
|
+
const setModuleTarget = (data) => {
|
|
33
|
+
moduleTargetRef.current = data;
|
|
34
|
+
_setModuleTarget(data);
|
|
35
|
+
};
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
stateHandlers?.idChanged?.(id);
|
|
38
|
+
}, [id, stateHandlers]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
stateHandlers?.isLoadingChanged?.(loading);
|
|
41
|
+
}, [loading, stateHandlers]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
stateHandlers?.loadedModuleChanged?.(loadedModule);
|
|
44
|
+
}, [loadedModule, stateHandlers]);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
stateHandlers?.moduleTargetChanged?.(moduleTarget);
|
|
47
|
+
}, [moduleTarget, stateHandlers]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (src === void 0) return;
|
|
50
|
+
try {
|
|
51
|
+
setLoading(true);
|
|
52
|
+
import(src).then((data) => {
|
|
53
|
+
setLoading(false);
|
|
54
|
+
const errs = {
|
|
55
|
+
notMod: new Error("Not a module"),
|
|
56
|
+
initFunc: new Error("Module exported member `init` must be a function"),
|
|
57
|
+
destroyFunc: new Error("Module exported member `destroy` must be a function"),
|
|
58
|
+
cssStrArr: new Error("Module exported member `css` must be an array of strings"),
|
|
59
|
+
updFunc: new Error("Module exported member `update` must be a function"),
|
|
60
|
+
initRetElt: new Error("Module exported function `init` must return an Element"),
|
|
61
|
+
initRetFirstElt: new Error("Module exported function `init` must return an array containing an Element in its first position")
|
|
62
|
+
};
|
|
63
|
+
if (!isNonNullObject(data)) return setLoadedModule(errs.notMod);
|
|
64
|
+
if (!("init" in data)) return setLoadedModule(errs.initFunc);
|
|
65
|
+
if (typeof data.init !== "function") return setLoadedModule(errs.initFunc);
|
|
66
|
+
if (!("destroy" in data) || typeof data.destroy !== "function") return setLoadedModule(errs.destroyFunc);
|
|
67
|
+
if ("css" in data) {
|
|
68
|
+
if (!Array.isArray(data.css)) return setLoadedModule(errs.cssStrArr);
|
|
69
|
+
if (data.css.some((i) => typeof i !== "string")) return setLoadedModule(errs.cssStrArr);
|
|
70
|
+
}
|
|
71
|
+
if ("update" in data && typeof data.update !== "function") return setLoadedModule(errs.updFunc);
|
|
72
|
+
const module = data;
|
|
73
|
+
setLoadedModule(module);
|
|
74
|
+
try {
|
|
75
|
+
const target = module.init(props ?? {});
|
|
76
|
+
if (!(target instanceof Element)) return setLoadedModule(errs.initRetElt);
|
|
77
|
+
setModuleTarget(target);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
setModuleTarget(null);
|
|
80
|
+
const e = err instanceof Error ? err : new Error(unknownToString(err));
|
|
81
|
+
setLoadedModule(e);
|
|
82
|
+
}
|
|
83
|
+
}).catch((err) => {
|
|
84
|
+
setLoading(false);
|
|
85
|
+
setLoadedModule(err instanceof Error ? err : new Error(unknownToString(err)));
|
|
86
|
+
setModuleTarget(null);
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (err instanceof Error) return setLoadedModule(err);
|
|
90
|
+
const errStr = unknownToString(err);
|
|
91
|
+
return setLoadedModule(new Error(errStr));
|
|
92
|
+
}
|
|
93
|
+
return () => {
|
|
94
|
+
if (moduleTargetRef.current === null) return;
|
|
95
|
+
if (loadedModuleRef.current instanceof Error) return;
|
|
96
|
+
if (loadedModuleRef.current === null) return;
|
|
97
|
+
loadedModuleRef.current.destroy(moduleTargetRef.current);
|
|
98
|
+
};
|
|
99
|
+
}, [src]);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (loadedModule instanceof Error) console.error(loadedModule);
|
|
102
|
+
}, [loadedModule]);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (moduleTarget === null) return;
|
|
105
|
+
if (rootRef.current === null) return;
|
|
106
|
+
rootRef.current.appendChild(moduleTarget);
|
|
107
|
+
}, [moduleTarget]);
|
|
108
|
+
const c = clss(publicClassName, { cssModule });
|
|
109
|
+
const rootClss = mergeClassNames(
|
|
110
|
+
c(null, {
|
|
111
|
+
loading,
|
|
112
|
+
"no-module": loadedModule === null,
|
|
113
|
+
"error": loadedModule instanceof Error,
|
|
114
|
+
"loaded": !loading && loadedModule !== null && !(loadedModule instanceof Error),
|
|
115
|
+
"initialized": moduleTarget !== null
|
|
116
|
+
}),
|
|
117
|
+
className
|
|
118
|
+
);
|
|
119
|
+
return /* @__PURE__ */ jsxs(
|
|
120
|
+
"div",
|
|
121
|
+
{
|
|
122
|
+
className: rootClss,
|
|
123
|
+
ref: rootRef,
|
|
124
|
+
id,
|
|
125
|
+
children: [
|
|
126
|
+
loadedModule === null && "",
|
|
127
|
+
loadedModule !== null && !(loadedModule instanceof Error) && loadedModule.css?.map((css) => /* @__PURE__ */ jsx("style", { children: `.${publicClassName}#${id} { ${css} }` }))
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
export {
|
|
133
|
+
UIModule
|
|
134
|
+
};
|
|
File without changes
|
|
@@ -28,6 +28,24 @@ type TrackData = {
|
|
|
28
28
|
label?: string;
|
|
29
29
|
default?: boolean;
|
|
30
30
|
};
|
|
31
|
+
type ActionHandlersProps = {
|
|
32
|
+
playButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
|
|
33
|
+
pauseButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isPlaying: boolean, video: HTMLVideoElement | null) => void;
|
|
34
|
+
loudButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
|
|
35
|
+
muteButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isLoud: boolean, video: HTMLVideoElement | null) => void;
|
|
36
|
+
volumeRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetRangeVolume: number, volume: number, video: HTMLVideoElement | null) => void;
|
|
37
|
+
fullscreenButtonClick?: (e: React.MouseEvent<HTMLButtonElement>, isFullscreen: boolean, video: HTMLVideoElement | null) => void;
|
|
38
|
+
rateRangeChange?: (e: React.ChangeEvent<HTMLInputElement>, targetRangeRate: number, rate: number, video: HTMLVideoElement | null) => void;
|
|
39
|
+
timelineClick?: (e: React.MouseEvent<HTMLDivElement>, targetTimeSec: number, timeSec: number, video: HTMLVideoElement | null) => void;
|
|
40
|
+
};
|
|
41
|
+
type StateHandlersProps = {
|
|
42
|
+
isPlaying?: (isPlaying: boolean) => void;
|
|
43
|
+
isFullScreen?: (isFullScreen: boolean) => void;
|
|
44
|
+
isLoud?: (isLoud: boolean) => void;
|
|
45
|
+
volume?: (volume: number) => void;
|
|
46
|
+
currentTime?: (currentTime: number) => void;
|
|
47
|
+
playbackRate?: (rate: number) => void;
|
|
48
|
+
};
|
|
31
49
|
/**
|
|
32
50
|
* Props for the {@link Video} component.
|
|
33
51
|
*
|
|
@@ -79,6 +97,8 @@ export type Props = PropsWithChildren<WithClassName<{
|
|
|
79
97
|
autoPauseWhenHidden?: boolean;
|
|
80
98
|
autoLoudWhenVisible?: boolean;
|
|
81
99
|
autoMuteWhenHidden?: boolean;
|
|
100
|
+
actionHandlers: ActionHandlersProps;
|
|
101
|
+
stateHandlers: StateHandlersProps;
|
|
82
102
|
}> & VideoHTMLAttributes<HTMLVideoElement>>;
|
|
83
103
|
/**
|
|
84
104
|
* Full-featured video player component. Wraps a native `<video>` element with
|
|
@@ -25,7 +25,11 @@ import {
|
|
|
25
25
|
forceLoud,
|
|
26
26
|
forceMute,
|
|
27
27
|
forceFullScreen,
|
|
28
|
-
forceExitFullScreen
|
|
28
|
+
forceExitFullScreen,
|
|
29
|
+
forceVolume,
|
|
30
|
+
forceCurrentTime,
|
|
31
|
+
getTimelineClickProgress,
|
|
32
|
+
forcePlaybackRate
|
|
29
33
|
} from "./utils.js";
|
|
30
34
|
import cssModule from "./styles.module.css";
|
|
31
35
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -43,6 +47,8 @@ var Video = ({
|
|
|
43
47
|
autoPauseWhenHidden,
|
|
44
48
|
autoLoudWhenVisible,
|
|
45
49
|
autoMuteWhenHidden,
|
|
50
|
+
actionHandlers,
|
|
51
|
+
stateHandlers,
|
|
46
52
|
children,
|
|
47
53
|
className,
|
|
48
54
|
...intrinsicVideoAttributes
|
|
@@ -83,65 +89,68 @@ var Video = ({
|
|
|
83
89
|
setVolume(video.volume);
|
|
84
90
|
setIsLoud(!video.muted);
|
|
85
91
|
intrinsicVideoAttributes.onLoadedMetadata?.(e);
|
|
86
|
-
}, []);
|
|
92
|
+
}, [intrinsicVideoAttributes.onLoadedMetadata, intrinsicVideoAttributes.muted]);
|
|
87
93
|
const handleVolumeChangeEvent = useCallback((e) => {
|
|
88
|
-
const
|
|
94
|
+
const volume2 = e.currentTarget.volume;
|
|
89
95
|
setVolume(volume2);
|
|
90
96
|
intrinsicVideoAttributes.onVolumeChange?.(e);
|
|
91
|
-
}, []);
|
|
97
|
+
}, [intrinsicVideoAttributes.onVolumeChange]);
|
|
92
98
|
const handlePlayEvent = useCallback((e) => {
|
|
93
99
|
setIsPlaying(true);
|
|
94
100
|
setHasBeenAutoPlayed(true);
|
|
95
101
|
intrinsicVideoAttributes.onPlay?.(e);
|
|
96
|
-
}, []);
|
|
102
|
+
}, [intrinsicVideoAttributes.onPlay]);
|
|
97
103
|
const handlePauseEvent = useCallback((e) => {
|
|
98
104
|
setIsPlaying(false);
|
|
99
105
|
intrinsicVideoAttributes.onPause?.(e);
|
|
100
|
-
}, []);
|
|
106
|
+
}, [intrinsicVideoAttributes.onPause]);
|
|
101
107
|
const handleRateChangeEvent = useCallback((e) => {
|
|
102
|
-
const
|
|
103
|
-
setPlaybackRate(
|
|
108
|
+
const currentRate = e.currentTarget.playbackRate;
|
|
109
|
+
setPlaybackRate(currentRate);
|
|
104
110
|
intrinsicVideoAttributes.onRateChange?.(e);
|
|
105
|
-
}, []);
|
|
111
|
+
}, [intrinsicVideoAttributes.onRateChange]);
|
|
106
112
|
const handleTimeUpdateEvent = useCallback((e) => {
|
|
107
113
|
const currentTime2 = e.currentTarget.currentTime;
|
|
108
114
|
setCurrentTime(currentTime2);
|
|
109
115
|
intrinsicVideoAttributes?.onTimeUpdate?.(e);
|
|
110
|
-
}, [totalTime]);
|
|
116
|
+
}, [totalTime, intrinsicVideoAttributes.onTimeUpdate]);
|
|
111
117
|
const handleVolumeRangeChange = useCallback((e) => {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
const targetVolume = Number(e.currentTarget.value);
|
|
119
|
+
actionHandlers?.volumeRangeChange?.(e, targetVolume, volume, videoRef.current);
|
|
120
|
+
forceVolume(videoRef.current, targetVolume, setVolume);
|
|
121
|
+
}, [actionHandlers?.volumeRangeChange, volume]);
|
|
122
|
+
const handleLoudButtonClick = useCallback((e) => {
|
|
123
|
+
actionHandlers?.loudButtonClick?.(e, isLoud, videoRef.current);
|
|
118
124
|
forceLoud(videoRef.current, setIsLoud);
|
|
119
|
-
}, [
|
|
120
|
-
const handleMuteButtonClick = useCallback(() => {
|
|
125
|
+
}, [actionHandlers?.loudButtonClick, isLoud]);
|
|
126
|
+
const handleMuteButtonClick = useCallback((e) => {
|
|
127
|
+
actionHandlers?.muteButtonClick?.(e, isLoud, videoRef.current);
|
|
121
128
|
forceMute(videoRef.current, setIsLoud);
|
|
122
|
-
}, []);
|
|
123
|
-
const handlePlayButtonClick = useCallback(() => {
|
|
129
|
+
}, [actionHandlers?.muteButtonClick, isLoud]);
|
|
130
|
+
const handlePlayButtonClick = useCallback((e) => {
|
|
131
|
+
actionHandlers?.playButtonClick?.(e, isPlaying, videoRef.current);
|
|
124
132
|
void forcePlay(videoRef.current, shouldDisclaimerBeOn, setIsPlaying);
|
|
125
|
-
}, [
|
|
126
|
-
const handlePauseButtonClick = useCallback(() => {
|
|
133
|
+
}, [actionHandlers?.playButtonClick, isPlaying, shouldDisclaimerBeOn]);
|
|
134
|
+
const handlePauseButtonClick = useCallback((e) => {
|
|
135
|
+
actionHandlers?.pauseButtonClick?.(e, isPlaying, videoRef.current);
|
|
127
136
|
forcePause(videoRef.current, setIsPlaying);
|
|
128
|
-
}, []);
|
|
129
|
-
const handleFullScreenButtonClick = useCallback(() => {
|
|
137
|
+
}, [actionHandlers?.pauseButtonClick, isPlaying]);
|
|
138
|
+
const handleFullScreenButtonClick = useCallback((e) => {
|
|
139
|
+
actionHandlers?.fullscreenButtonClick?.(e, isFullScreen, videoRef.current);
|
|
130
140
|
if (isFullScreen) void forceExitFullScreen(videoRef.current, setIsFullScreen);
|
|
131
141
|
else void forceFullScreen(videoRef.current, shouldDisclaimerBeOn, setIsFullScreen);
|
|
132
142
|
}, [isFullScreen, shouldDisclaimerBeOn]);
|
|
133
143
|
const handleRateRangeChange = useCallback((e) => {
|
|
134
144
|
const rate = Number(e.currentTarget.value);
|
|
135
|
-
|
|
136
|
-
videoRef.current
|
|
137
|
-
}, []);
|
|
145
|
+
actionHandlers?.rateRangeChange?.(e, rate, playbackRate, videoRef.current);
|
|
146
|
+
forcePlaybackRate(videoRef.current, rate, setPlaybackRate);
|
|
147
|
+
}, [actionHandlers?.rateRangeChange, playbackRate]);
|
|
138
148
|
const handleTimelineClick = useCallback((e) => {
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}, []);
|
|
149
|
+
const progress = getTimelineClickProgress(e, e.currentTarget, videoRef.current);
|
|
150
|
+
const time = progress * totalTime;
|
|
151
|
+
actionHandlers?.timelineClick?.(e, time, currentTime, videoRef.current);
|
|
152
|
+
forceCurrentTime(videoRef.current, time, setCurrentTime);
|
|
153
|
+
}, [actionHandlers?.timelineClick, totalTime, currentTime]);
|
|
145
154
|
const handleDisclaimerDismiss = useCallback(() => {
|
|
146
155
|
setIsDisclaimerOn(false);
|
|
147
156
|
if (intrinsicVideoAttributes.autoPlay === true && !hasBeenAutoPlayed) void forcePlay(videoRef.current, shouldDisclaimerBeOn, setIsPlaying);
|
|
@@ -162,6 +171,24 @@ var Video = ({
|
|
|
162
171
|
intrinsicVideoAttributes,
|
|
163
172
|
hasBeenAutoPlayed
|
|
164
173
|
]);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
stateHandlers?.isPlaying?.(isPlaying);
|
|
176
|
+
}, [isPlaying, stateHandlers?.isPlaying]);
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
stateHandlers?.isFullScreen?.(isFullScreen);
|
|
179
|
+
}, [isFullScreen, stateHandlers?.isFullScreen]);
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
stateHandlers?.isLoud?.(isLoud);
|
|
182
|
+
}, [isLoud, stateHandlers?.isLoud]);
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
stateHandlers?.volume?.(volume);
|
|
185
|
+
}, [volume, stateHandlers?.volume]);
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
stateHandlers?.playbackRate?.(playbackRate);
|
|
188
|
+
}, [playbackRate, stateHandlers?.playbackRate]);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
stateHandlers?.currentTime?.(currentTime);
|
|
191
|
+
}, [currentTime, stateHandlers?.currentTime]);
|
|
165
192
|
const parsedSources = useMemo(() => {
|
|
166
193
|
if (sources === void 0) return [];
|
|
167
194
|
if (typeof sources === "string") return [{ src: sources }];
|
|
@@ -206,7 +233,7 @@ var Video = ({
|
|
|
206
233
|
"data-total-time-ms": totalTimeMs
|
|
207
234
|
};
|
|
208
235
|
const rootStyles = {
|
|
209
|
-
[`--${publicClassName}-
|
|
236
|
+
[`--${publicClassName}-elapsed-time-ratio`]: (currentTime / totalTime).toFixed(8)
|
|
210
237
|
};
|
|
211
238
|
const videoClss = c("video");
|
|
212
239
|
const videoControlsClss = c("video-controls");
|