@venizia/ardor-react 0.4.8-5 → 0.4.8-7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -16,7 +16,8 @@ export class ResizeObservation {
|
|
|
16
16
|
onFrame(batch);
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
|
-
|
|
19
|
+
let observer;
|
|
20
|
+
const onEntries = (entries) => {
|
|
20
21
|
for (const entry of entries) {
|
|
21
22
|
latest.set(entry.target, entry);
|
|
22
23
|
}
|
|
@@ -31,9 +32,14 @@ export class ResizeObservation {
|
|
|
31
32
|
frame = undefined;
|
|
32
33
|
flush();
|
|
33
34
|
});
|
|
34
|
-
}
|
|
35
|
+
};
|
|
35
36
|
const update = (targets) => {
|
|
36
37
|
const wanted = new Set(children ? targets.flatMap((target) => [target, ...target.children]) : targets);
|
|
38
|
+
// No target and no observer yet: there is nothing to watch, so none is made.
|
|
39
|
+
if (!observer && wanted.size === 0) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
observer ??= new ResizeObserver(onEntries);
|
|
37
43
|
for (const element of watched) {
|
|
38
44
|
if (!wanted.has(element)) {
|
|
39
45
|
observer.unobserve(element);
|
|
@@ -47,7 +53,7 @@ export class ResizeObservation {
|
|
|
47
53
|
}
|
|
48
54
|
watched = wanted;
|
|
49
55
|
};
|
|
50
|
-
|
|
56
|
+
let mutations;
|
|
51
57
|
return {
|
|
52
58
|
sync: (targets) => {
|
|
53
59
|
if (targets.length === current.length &&
|
|
@@ -57,13 +63,16 @@ export class ResizeObservation {
|
|
|
57
63
|
current = targets;
|
|
58
64
|
update(targets);
|
|
59
65
|
// A removed target stays registered on `mutations` until `stop`; its callback only updates the current targets.
|
|
60
|
-
|
|
61
|
-
mutations
|
|
66
|
+
if (children && targets.length > 0) {
|
|
67
|
+
mutations ??= new MutationObserver(() => update(current));
|
|
68
|
+
for (const target of targets) {
|
|
69
|
+
mutations.observe(target, { childList: true });
|
|
70
|
+
}
|
|
62
71
|
}
|
|
63
72
|
},
|
|
64
73
|
stop: () => {
|
|
65
74
|
mutations?.disconnect();
|
|
66
|
-
observer
|
|
75
|
+
observer?.disconnect();
|
|
67
76
|
if (frame !== undefined) {
|
|
68
77
|
cancelAnimationFrame(frame);
|
|
69
78
|
}
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { type TElementTarget } from './element-target.js';
|
|
2
|
-
/** The element `useIntersection` watches and when it delivers an entry. */
|
|
2
|
+
/** The element or elements `useIntersection` watches, against which root, and when it delivers an entry. */
|
|
3
3
|
export interface IUseIntersectionOptions {
|
|
4
|
-
/** Read after every render: an element, a ref to one, or `null
|
|
5
|
-
target: TElementTarget;
|
|
4
|
+
/** Read after every render: an element, a ref to one, or `null`; or a list of them to watch several. */
|
|
5
|
+
target: TElementTarget | TElementTarget[];
|
|
6
|
+
/**
|
|
7
|
+
* The scrolling container the target is measured against. A ref is read after every render, and nothing is
|
|
8
|
+
* observed while its `current` is `null`. A literal `null`, or leaving it out, is the viewport.
|
|
9
|
+
*/
|
|
10
|
+
root?: TElementTarget;
|
|
6
11
|
/** @default '0px' */
|
|
7
12
|
rootMargin?: string;
|
|
8
|
-
/** The ratio of the element in view at which an entry is delivered; a
|
|
9
|
-
threshold?: number;
|
|
13
|
+
/** The ratio of the element in view at which an entry is delivered; a number or a list of them. @default 0 */
|
|
14
|
+
threshold?: number | number[];
|
|
10
15
|
}
|
|
16
|
+
type TEntry = IntersectionObserverEntry | undefined;
|
|
11
17
|
/**
|
|
12
|
-
* The latest `IntersectionObserverEntry` of the target, or `undefined` before the first one and
|
|
13
|
-
*
|
|
14
|
-
*
|
|
18
|
+
* The latest `IntersectionObserverEntry` of the target, or `undefined` before the first one and where there is
|
|
19
|
+
* no `IntersectionObserver`. The state changes only when `isIntersecting` or the ratio changed.
|
|
20
|
+
*
|
|
21
|
+
* With a list as `target` it returns an array in target order: the latest entry of each target, `undefined` for
|
|
22
|
+
* one with no element or not reported yet. A new element, `root`, `rootMargin` or `threshold` starts again from
|
|
23
|
+
* `undefined` for every target; the browser reports each target again at once.
|
|
15
24
|
*
|
|
16
25
|
* @example
|
|
17
26
|
* ```tsx
|
|
@@ -25,5 +34,12 @@ export interface IUseIntersectionOptions {
|
|
|
25
34
|
* }
|
|
26
35
|
* ```
|
|
27
36
|
*/
|
|
28
|
-
export declare
|
|
37
|
+
export declare function useIntersection(opts: IUseIntersectionOptions & {
|
|
38
|
+
target: TElementTarget[];
|
|
39
|
+
}): TEntry[];
|
|
40
|
+
export declare function useIntersection(opts: IUseIntersectionOptions & {
|
|
41
|
+
target: TElementTarget;
|
|
42
|
+
}): TEntry;
|
|
43
|
+
export declare function useIntersection(opts: IUseIntersectionOptions): TEntry | TEntry[];
|
|
44
|
+
export {};
|
|
29
45
|
//# sourceMappingURL=use-intersection.d.ts.map
|
|
@@ -1,50 +1,74 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { ElementTargets } from './element-target.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
3
|
+
const EMPTY_SCOPE = { elements: [], root: null, isRootPending: false };
|
|
4
|
+
const NO_ENTRIES = new Map();
|
|
5
|
+
const resolveScope = (opts) => {
|
|
6
|
+
const { target, root } = opts;
|
|
7
|
+
const targets = Array.isArray(target) ? target : [target];
|
|
8
|
+
const rootElement = root === undefined ? null : ElementTargets.resolve({ target: root });
|
|
9
|
+
return {
|
|
10
|
+
elements: targets.map((item) => ElementTargets.resolve({ target: item })),
|
|
11
|
+
root: rootElement,
|
|
12
|
+
isRootPending: rootElement === null && root !== undefined && root !== null,
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
const sameScope = (a, b) => {
|
|
16
|
+
return (a.root === b.root &&
|
|
17
|
+
a.isRootPending === b.isRootPending &&
|
|
18
|
+
a.elements.length === b.elements.length &&
|
|
19
|
+
a.elements.every((element, index) => element === b.elements[index]));
|
|
20
|
+
};
|
|
21
|
+
export function useIntersection(opts) {
|
|
22
|
+
const { target, root, rootMargin, threshold } = opts;
|
|
23
|
+
const isList = Array.isArray(target);
|
|
24
|
+
const count = isList ? target.length : 1;
|
|
25
|
+
// A number list is compared by value, so a list made anew in each render observes nothing again.
|
|
26
|
+
const thresholdKey = threshold === undefined ? undefined : JSON.stringify(threshold);
|
|
27
|
+
const [scope, setScope] = React.useState(EMPTY_SCOPE);
|
|
28
|
+
const [entries, setEntries] = React.useState(NO_ENTRIES);
|
|
29
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps -- the target and root are read after every render, and a state set to an equal scope renders nothing
|
|
25
30
|
React.useEffect(() => {
|
|
26
|
-
|
|
31
|
+
const next = resolveScope({ target, root });
|
|
32
|
+
setScope((current) => (sameScope(current, next) ? current : next));
|
|
27
33
|
});
|
|
28
34
|
React.useEffect(() => {
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
setEntries((current) => (current.size === 0 ? current : NO_ENTRIES));
|
|
36
|
+
const watched = new Set(scope.elements.filter((element) => element !== null));
|
|
37
|
+
if (watched.size === 0 || scope.isRootPending || typeof IntersectionObserver === 'undefined') {
|
|
31
38
|
return () => { };
|
|
32
39
|
}
|
|
33
|
-
const observer = new IntersectionObserver((
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const observer = new IntersectionObserver((delivered) => {
|
|
41
|
+
setEntries((current) => {
|
|
42
|
+
let next = current;
|
|
43
|
+
for (const entry of delivered) {
|
|
44
|
+
const known = next.get(entry.target);
|
|
45
|
+
if (known?.isIntersecting === entry.isIntersecting &&
|
|
46
|
+
known.intersectionRatio === entry.intersectionRatio) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (next === current) {
|
|
50
|
+
next = new Map(current);
|
|
51
|
+
}
|
|
52
|
+
next.set(entry.target, entry);
|
|
53
|
+
}
|
|
54
|
+
return next;
|
|
43
55
|
});
|
|
44
|
-
}, {
|
|
45
|
-
|
|
56
|
+
}, {
|
|
57
|
+
root: scope.root ?? undefined,
|
|
58
|
+
rootMargin,
|
|
59
|
+
threshold: thresholdKey === undefined ? undefined : JSON.parse(thresholdKey),
|
|
60
|
+
});
|
|
61
|
+
for (const element of watched) {
|
|
62
|
+
observer.observe(element);
|
|
63
|
+
}
|
|
46
64
|
return () => observer.disconnect();
|
|
47
|
-
}, [
|
|
48
|
-
return
|
|
49
|
-
}
|
|
65
|
+
}, [scope, rootMargin, thresholdKey]);
|
|
66
|
+
return React.useMemo(() => {
|
|
67
|
+
const read = Array.from({ length: count }, (_, index) => {
|
|
68
|
+
const element = scope.elements[index];
|
|
69
|
+
return element ? entries.get(element) : undefined;
|
|
70
|
+
});
|
|
71
|
+
return isList ? read : read[0];
|
|
72
|
+
}, [count, isList, scope, entries]);
|
|
73
|
+
}
|
|
50
74
|
//# sourceMappingURL=use-intersection.js.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type IStorageCodec, type TStorageArea } from '@venizia/ardor-kernel';
|
|
2
2
|
/** What `useStoredState` stores, where and how. */
|
|
3
3
|
export interface IUseStoredStateOptions<TValue> {
|
|
4
|
-
/** The storage key. */
|
|
5
|
-
key: string;
|
|
4
|
+
/** The storage key. `null` turns the storage off: nothing is read, written or watched, `value` starts from `defaultValue` and `setValue` keeps it in memory only, as `useState` does. */
|
|
5
|
+
key: string | null;
|
|
6
6
|
/** The value while the key holds nothing. */
|
|
7
7
|
defaultValue: TValue;
|
|
8
8
|
/** @default 'local' */
|
|
@@ -42,7 +42,7 @@ export interface IUseStoredState {
|
|
|
42
42
|
* import { useStoredState } from '@venizia/ardor';
|
|
43
43
|
*
|
|
44
44
|
* export function ThemeToggle() {
|
|
45
|
-
* const { value, setValue } = useStoredState({ key: '@app/
|
|
45
|
+
* const { value, setValue } = useStoredState({ key: '@app/prefs/theme', defaultValue: 'light' });
|
|
46
46
|
* return <button onClick={() => setValue(value === 'light' ? 'dark' : 'light')}>{value}</button>;
|
|
47
47
|
* }
|
|
48
48
|
* ```
|
|
@@ -14,7 +14,7 @@ const isUpdater = (next) => typeof next === 'function';
|
|
|
14
14
|
* import { useStoredState } from '@venizia/ardor';
|
|
15
15
|
*
|
|
16
16
|
* export function ThemeToggle() {
|
|
17
|
-
* const { value, setValue } = useStoredState({ key: '@app/
|
|
17
|
+
* const { value, setValue } = useStoredState({ key: '@app/prefs/theme', defaultValue: 'light' });
|
|
18
18
|
* return <button onClick={() => setValue(value === 'light' ? 'dark' : 'light')}>{value}</button>;
|
|
19
19
|
* }
|
|
20
20
|
* ```
|
|
@@ -37,8 +37,9 @@ export const useStoredState = (opts) => {
|
|
|
37
37
|
}, [keep]);
|
|
38
38
|
const isDelayed = writeDelayMs > 0;
|
|
39
39
|
const flush = React.useCallback(() => {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const pending = keptRef.current;
|
|
41
|
+
if (timerRef.current !== undefined && pending?.key != null) {
|
|
42
|
+
write({ ...pending, key: pending.key });
|
|
42
43
|
}
|
|
43
44
|
}, [write]);
|
|
44
45
|
// A new key or storage, an unmount, and a page that closes or hides write the pending value to the
|
|
@@ -49,6 +50,10 @@ export const useStoredState = (opts) => {
|
|
|
49
50
|
flush();
|
|
50
51
|
}
|
|
51
52
|
};
|
|
53
|
+
// The value kept in memory under a `null` key does not follow the hook to a real key, or back to a later `null`.
|
|
54
|
+
if (key !== null && keptRef.current?.key === null) {
|
|
55
|
+
keep(undefined);
|
|
56
|
+
}
|
|
52
57
|
if (isDelayed) {
|
|
53
58
|
window.addEventListener('pagehide', flush);
|
|
54
59
|
document.addEventListener('visibilitychange', onHide);
|
|
@@ -58,8 +63,11 @@ export const useStoredState = (opts) => {
|
|
|
58
63
|
document.removeEventListener('visibilitychange', onHide);
|
|
59
64
|
flush();
|
|
60
65
|
};
|
|
61
|
-
}, [key, storage, isDelayed, flush]);
|
|
66
|
+
}, [key, storage, isDelayed, flush, keep]);
|
|
62
67
|
const subscribe = React.useCallback((listener) => {
|
|
68
|
+
if (key === null) {
|
|
69
|
+
return () => { };
|
|
70
|
+
}
|
|
63
71
|
return BrowserStorage.subscribe({
|
|
64
72
|
key,
|
|
65
73
|
storage,
|
|
@@ -70,6 +78,9 @@ export const useStoredState = (opts) => {
|
|
|
70
78
|
});
|
|
71
79
|
}, [key, storage, keep]);
|
|
72
80
|
const getSnapshot = React.useCallback(() => {
|
|
81
|
+
if (key === null) {
|
|
82
|
+
return defaultValue;
|
|
83
|
+
}
|
|
73
84
|
const text = BrowserStorage.get({ key, storage, fallback: null, codec: RAW_TEXT });
|
|
74
85
|
if (text === null) {
|
|
75
86
|
return defaultValue;
|
|
@@ -87,7 +98,12 @@ export const useStoredState = (opts) => {
|
|
|
87
98
|
const setValue = React.useCallback((next) => {
|
|
88
99
|
const hit = keptRef.current;
|
|
89
100
|
const current = hit?.key === key && hit.storage === storage ? hit.value : getSnapshot();
|
|
90
|
-
const
|
|
101
|
+
const nextValue = isUpdater(next) ? next(current) : next;
|
|
102
|
+
if (key === null) {
|
|
103
|
+
keep({ key, storage, codec, value: nextValue });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const entry = { key, storage, codec, value: nextValue };
|
|
91
107
|
if (!(writeDelayMs > 0)) {
|
|
92
108
|
write(entry);
|
|
93
109
|
return;
|
|
@@ -96,6 +112,10 @@ export const useStoredState = (opts) => {
|
|
|
96
112
|
timerRef.current = setTimeout(() => write(entry), writeDelayMs);
|
|
97
113
|
}, [key, storage, codec, writeDelayMs, getSnapshot, keep, write]);
|
|
98
114
|
const remove = React.useCallback(() => {
|
|
115
|
+
if (key === null) {
|
|
116
|
+
keep(undefined);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
99
119
|
const isRemoved = BrowserStorage.remove({ key, storage });
|
|
100
120
|
keep(isRemoved ? undefined : { key, storage, value: defaultValue });
|
|
101
121
|
}, [key, storage, defaultValue, keep]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venizia/ardor-react",
|
|
3
|
-
"version": "0.4.8-
|
|
3
|
+
"version": "0.4.8-7",
|
|
4
4
|
"description": "ARDOR React bindings - the application context, the injectable and logger hooks that read it, typed Redux hook factories, and the framework-agnostic UI hooks (debounce, autosave, confirm, clipboard, element size, window dimensions). No react-admin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ardor",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"prepublishOnly": "make rebuild"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@venizia/ardor-kernel": "0.4.8-
|
|
62
|
+
"@venizia/ardor-kernel": "0.4.8-7"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@happy-dom/global-registrator": "^21.0.4",
|
|
@@ -69,11 +69,11 @@
|
|
|
69
69
|
"@types/bun": "^1.4.3",
|
|
70
70
|
"@types/react": "^19",
|
|
71
71
|
"@types/react-dom": "^19",
|
|
72
|
-
"@venizia/dev-configs": "^0.4.8-
|
|
73
|
-
"@venizia/ignis-filter": "^0.4.7-
|
|
74
|
-
"@venizia/ignis-helpers": "^0.4.8-
|
|
75
|
-
"@venizia/ignis-inversion": "^0.4.7-
|
|
76
|
-
"@venizia/ignis-kernel": "^0.4.8-
|
|
72
|
+
"@venizia/dev-configs": "^0.4.8-3",
|
|
73
|
+
"@venizia/ignis-filter": "^0.4.7-4",
|
|
74
|
+
"@venizia/ignis-helpers": "^0.4.8-6",
|
|
75
|
+
"@venizia/ignis-inversion": "^0.4.7-3",
|
|
76
|
+
"@venizia/ignis-kernel": "^0.4.8-6",
|
|
77
77
|
"eslint": "^10.13.0",
|
|
78
78
|
"oxlint": "1.89.0",
|
|
79
79
|
"oxlint-tsgolint": "7.0.2004",
|
|
@@ -87,10 +87,10 @@
|
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
89
|
"@reduxjs/toolkit": ">=2.13.0",
|
|
90
|
-
"@venizia/ignis-filter": "^0.4.7-
|
|
91
|
-
"@venizia/ignis-helpers": "^0.4.8-
|
|
92
|
-
"@venizia/ignis-inversion": "^0.4.7-
|
|
93
|
-
"@venizia/ignis-kernel": "^0.4.8-
|
|
90
|
+
"@venizia/ignis-filter": "^0.4.7-4",
|
|
91
|
+
"@venizia/ignis-helpers": "^0.4.8-6",
|
|
92
|
+
"@venizia/ignis-inversion": "^0.4.7-3",
|
|
93
|
+
"@venizia/ignis-kernel": "^0.4.8-6",
|
|
94
94
|
"react": ">=19.3.1",
|
|
95
95
|
"react-redux": ">=9.3.0",
|
|
96
96
|
"reflect-metadata": ">=0.2.2"
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"react",
|
|
112
112
|
"react-redux"
|
|
113
113
|
],
|
|
114
|
-
"limit": "
|
|
114
|
+
"limit": "3875 B"
|
|
115
115
|
}
|
|
116
116
|
],
|
|
117
117
|
"engines": {
|