@dbcdk/react-components 0.0.181 → 0.0.183
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/dist/client.cjs +21 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +3 -0
- package/dist/components/clear-button/ClearButton.cjs +35 -12
- package/dist/components/clear-button/ClearButton.d.ts +17 -1
- package/dist/components/clear-button/ClearButton.js +35 -12
- package/dist/components/clear-button/ClearButton.module.css +12 -0
- package/dist/components/forms/multi-select/MultiSelect.cjs +2 -1
- package/dist/components/forms/multi-select/MultiSelect.js +2 -1
- package/dist/components/forms/select/Select.cjs +3 -1
- package/dist/components/forms/select/Select.js +3 -1
- package/dist/components/interval-select/IntervalSelect.cjs +2 -1
- package/dist/components/interval-select/IntervalSelect.js +2 -1
- package/dist/components/json-viewer/JsonViewer.cjs +3 -2
- package/dist/components/json-viewer/JsonViewer.d.ts +1 -1
- package/dist/components/json-viewer/JsonViewer.js +3 -2
- package/dist/components/json-viewer/JsonViewer.module.css +7 -0
- package/dist/components/json-viewer/types.d.ts +6 -0
- package/dist/components/line-viewer/LineViewer.cjs +174 -0
- package/dist/components/line-viewer/LineViewer.d.ts +20 -0
- package/dist/components/line-viewer/LineViewer.js +168 -0
- package/dist/components/line-viewer/LineViewer.module.css +112 -0
- package/dist/components/line-viewer/tokenizeLineFormat.cjs +52 -0
- package/dist/components/line-viewer/tokenizeLineFormat.d.ts +14 -0
- package/dist/components/line-viewer/tokenizeLineFormat.js +50 -0
- package/dist/components/tree-view/TreeNodeRow.cjs +99 -0
- package/dist/components/tree-view/TreeNodeRow.d.ts +18 -0
- package/dist/components/tree-view/TreeNodeRow.js +93 -0
- package/dist/components/tree-view/TreeView.cjs +93 -0
- package/dist/components/tree-view/TreeView.d.ts +20 -0
- package/dist/components/tree-view/TreeView.js +79 -0
- package/dist/components/tree-view/TreeView.module.css +141 -0
- package/dist/components/tree-view/types.cjs +2 -0
- package/dist/components/tree-view/types.d.ts +44 -0
- package/dist/components/tree-view/types.js +1 -0
- package/dist/components/tree-view/useTreeExpansion.cjs +47 -0
- package/dist/components/tree-view/useTreeExpansion.d.ts +17 -0
- package/dist/components/tree-view/useTreeExpansion.js +45 -0
- package/dist/components/tree-view/utils.cjs +32 -0
- package/dist/components/tree-view/utils.d.ts +16 -0
- package/dist/components/tree-view/utils.js +29 -0
- package/dist/components/xml-viewer/XmlViewer.cjs +175 -0
- package/dist/components/xml-viewer/XmlViewer.d.ts +20 -0
- package/dist/components/xml-viewer/XmlViewer.js +169 -0
- package/dist/components/xml-viewer/XmlViewer.module.css +109 -0
- package/dist/components/xml-viewer/tokenizeXml.cjs +50 -0
- package/dist/components/xml-viewer/tokenizeXml.d.ts +12 -0
- package/dist/components/xml-viewer/tokenizeXml.js +48 -0
- package/dist/index.cjs +7 -0
- package/dist/index.css +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/date/formatDate.cjs +29 -17
- package/dist/utils/date/formatDate.d.ts +7 -0
- package/dist/utils/date/formatDate.js +29 -17
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var TreeNodeRow = require('./TreeNodeRow');
|
|
7
|
+
var styles = require('./TreeView.module.css');
|
|
8
|
+
var useTreeExpansion = require('./useTreeExpansion');
|
|
9
|
+
var utils = require('./utils');
|
|
10
|
+
|
|
11
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
|
|
13
|
+
var styles__default = /*#__PURE__*/_interopDefault(styles);
|
|
14
|
+
|
|
15
|
+
function TreeView({
|
|
16
|
+
data,
|
|
17
|
+
loadChildren,
|
|
18
|
+
defaultExpandedIds,
|
|
19
|
+
expandedIds,
|
|
20
|
+
onExpandedIdsChange,
|
|
21
|
+
selectedId,
|
|
22
|
+
onSelectNode,
|
|
23
|
+
renderLabel,
|
|
24
|
+
emptyMessage = "Intet at vise.",
|
|
25
|
+
loadErrorMessage = () => "Der skete en fejl under indl\xE6sning.",
|
|
26
|
+
className,
|
|
27
|
+
...rest
|
|
28
|
+
}) {
|
|
29
|
+
const { isExpanded, toggle, expand } = useTreeExpansion.useTreeExpansion(
|
|
30
|
+
defaultExpandedIds,
|
|
31
|
+
expandedIds,
|
|
32
|
+
onExpandedIdsChange
|
|
33
|
+
);
|
|
34
|
+
const [resolvedChildren, setResolvedChildren] = react.useState({});
|
|
35
|
+
const [loadingIds, setLoadingIds] = react.useState(/* @__PURE__ */ new Set());
|
|
36
|
+
const [errorIds, setErrorIds] = react.useState({});
|
|
37
|
+
const requestLoad = react.useCallback(
|
|
38
|
+
(node) => {
|
|
39
|
+
if (!loadChildren || loadingIds.has(node.id)) return;
|
|
40
|
+
setLoadingIds((prev) => new Set(prev).add(node.id));
|
|
41
|
+
setErrorIds((prev) => {
|
|
42
|
+
if (!(node.id in prev)) return prev;
|
|
43
|
+
const next = { ...prev };
|
|
44
|
+
delete next[node.id];
|
|
45
|
+
return next;
|
|
46
|
+
});
|
|
47
|
+
loadChildren(node).then((children) => {
|
|
48
|
+
setResolvedChildren((prev) => ({ ...prev, [node.id]: children }));
|
|
49
|
+
expand(node.id);
|
|
50
|
+
}).catch((err) => {
|
|
51
|
+
setErrorIds((prev) => ({ ...prev, [node.id]: loadErrorMessage(err, node) }));
|
|
52
|
+
}).finally(() => {
|
|
53
|
+
setLoadingIds((prev) => {
|
|
54
|
+
const next = new Set(prev);
|
|
55
|
+
next.delete(node.id);
|
|
56
|
+
return next;
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
[loadChildren, loadingIds, expand, loadErrorMessage]
|
|
61
|
+
);
|
|
62
|
+
if (data.length === 0) {
|
|
63
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: [styles__default.default.emptyState, className].filter(Boolean).join(" "), ...rest, children: emptyMessage });
|
|
64
|
+
}
|
|
65
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tree", className: [styles__default.default.tree, className].filter(Boolean).join(" "), ...rest, children: data.map((node) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
66
|
+
TreeNodeRow.TreeNodeRow,
|
|
67
|
+
{
|
|
68
|
+
node,
|
|
69
|
+
depth: 0,
|
|
70
|
+
isExpanded,
|
|
71
|
+
toggle,
|
|
72
|
+
resolvedChildren,
|
|
73
|
+
loadingIds,
|
|
74
|
+
errorIds,
|
|
75
|
+
requestLoad,
|
|
76
|
+
loadChildren,
|
|
77
|
+
selectedId,
|
|
78
|
+
onSelectNode,
|
|
79
|
+
renderLabel
|
|
80
|
+
},
|
|
81
|
+
node.id
|
|
82
|
+
)) });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Object.defineProperty(exports, "collectExpandableIds", {
|
|
86
|
+
enumerable: true,
|
|
87
|
+
get: function () { return utils.collectExpandableIds; }
|
|
88
|
+
});
|
|
89
|
+
Object.defineProperty(exports, "findAncestorIds", {
|
|
90
|
+
enumerable: true,
|
|
91
|
+
get: function () { return utils.findAncestorIds; }
|
|
92
|
+
});
|
|
93
|
+
exports.TreeView = TreeView;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
import type { TreeViewProps } from './types';
|
|
3
|
+
export type { LoadTreeChildren, TreeNode, TreeViewProps } from './types';
|
|
4
|
+
export { collectExpandableIds, findAncestorIds } from './utils';
|
|
5
|
+
/**
|
|
6
|
+
* A single component for both shapes of tree data:
|
|
7
|
+
* - Eager: every node's full subtree is already in `data` via `children`.
|
|
8
|
+
* No `loadChildren` needed - toggling a node only ever flips its
|
|
9
|
+
* expanded/collapsed state.
|
|
10
|
+
* - Lazy: `data` only has to contain what you already know; pass
|
|
11
|
+
* `loadChildren` and a node whose `children` is undefined gets fetched
|
|
12
|
+
* the first time it's expanded (see TreeNode's docs on `hasChildren` for
|
|
13
|
+
* how a not-yet-fetched node still shows a toggle).
|
|
14
|
+
*
|
|
15
|
+
* Expand/collapse state follows the same model as the Sidebar: a single
|
|
16
|
+
* `Set<string>` of expanded ids is the source of truth (see
|
|
17
|
+
* useTreeExpansion), and it can be left uncontrolled or owned by the
|
|
18
|
+
* caller via `expandedIds`/`onExpandedIdsChange`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function TreeView<T = unknown>({ data, loadChildren, defaultExpandedIds, expandedIds, onExpandedIdsChange, selectedId, onSelectNode, renderLabel, emptyMessage, loadErrorMessage, className, ...rest }: TreeViewProps<T>): JSX.Element;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import { TreeNodeRow } from './TreeNodeRow';
|
|
5
|
+
import styles from './TreeView.module.css';
|
|
6
|
+
import { useTreeExpansion } from './useTreeExpansion';
|
|
7
|
+
export { collectExpandableIds, findAncestorIds } from './utils';
|
|
8
|
+
|
|
9
|
+
function TreeView({
|
|
10
|
+
data,
|
|
11
|
+
loadChildren,
|
|
12
|
+
defaultExpandedIds,
|
|
13
|
+
expandedIds,
|
|
14
|
+
onExpandedIdsChange,
|
|
15
|
+
selectedId,
|
|
16
|
+
onSelectNode,
|
|
17
|
+
renderLabel,
|
|
18
|
+
emptyMessage = "Intet at vise.",
|
|
19
|
+
loadErrorMessage = () => "Der skete en fejl under indl\xE6sning.",
|
|
20
|
+
className,
|
|
21
|
+
...rest
|
|
22
|
+
}) {
|
|
23
|
+
const { isExpanded, toggle, expand } = useTreeExpansion(
|
|
24
|
+
defaultExpandedIds,
|
|
25
|
+
expandedIds,
|
|
26
|
+
onExpandedIdsChange
|
|
27
|
+
);
|
|
28
|
+
const [resolvedChildren, setResolvedChildren] = useState({});
|
|
29
|
+
const [loadingIds, setLoadingIds] = useState(/* @__PURE__ */ new Set());
|
|
30
|
+
const [errorIds, setErrorIds] = useState({});
|
|
31
|
+
const requestLoad = useCallback(
|
|
32
|
+
(node) => {
|
|
33
|
+
if (!loadChildren || loadingIds.has(node.id)) return;
|
|
34
|
+
setLoadingIds((prev) => new Set(prev).add(node.id));
|
|
35
|
+
setErrorIds((prev) => {
|
|
36
|
+
if (!(node.id in prev)) return prev;
|
|
37
|
+
const next = { ...prev };
|
|
38
|
+
delete next[node.id];
|
|
39
|
+
return next;
|
|
40
|
+
});
|
|
41
|
+
loadChildren(node).then((children) => {
|
|
42
|
+
setResolvedChildren((prev) => ({ ...prev, [node.id]: children }));
|
|
43
|
+
expand(node.id);
|
|
44
|
+
}).catch((err) => {
|
|
45
|
+
setErrorIds((prev) => ({ ...prev, [node.id]: loadErrorMessage(err, node) }));
|
|
46
|
+
}).finally(() => {
|
|
47
|
+
setLoadingIds((prev) => {
|
|
48
|
+
const next = new Set(prev);
|
|
49
|
+
next.delete(node.id);
|
|
50
|
+
return next;
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
[loadChildren, loadingIds, expand, loadErrorMessage]
|
|
55
|
+
);
|
|
56
|
+
if (data.length === 0) {
|
|
57
|
+
return /* @__PURE__ */ jsx("div", { className: [styles.emptyState, className].filter(Boolean).join(" "), ...rest, children: emptyMessage });
|
|
58
|
+
}
|
|
59
|
+
return /* @__PURE__ */ jsx("div", { role: "tree", className: [styles.tree, className].filter(Boolean).join(" "), ...rest, children: data.map((node) => /* @__PURE__ */ jsx(
|
|
60
|
+
TreeNodeRow,
|
|
61
|
+
{
|
|
62
|
+
node,
|
|
63
|
+
depth: 0,
|
|
64
|
+
isExpanded,
|
|
65
|
+
toggle,
|
|
66
|
+
resolvedChildren,
|
|
67
|
+
loadingIds,
|
|
68
|
+
errorIds,
|
|
69
|
+
requestLoad,
|
|
70
|
+
loadChildren,
|
|
71
|
+
selectedId,
|
|
72
|
+
onSelectNode,
|
|
73
|
+
renderLabel
|
|
74
|
+
},
|
|
75
|
+
node.id
|
|
76
|
+
)) });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { TreeView };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
.tree {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 1px;
|
|
5
|
+
font-family: inherit;
|
|
6
|
+
font-size: var(--font-size-sm);
|
|
7
|
+
color: var(--color-fg-default);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.children {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
gap: 1px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.row {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
min-inline-size: 0;
|
|
20
|
+
padding-left: calc(var(--tree-depth) * var(--spacing-lg));
|
|
21
|
+
border-radius: var(--border-radius-default);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.row[data-selected='true'] {
|
|
25
|
+
background: color-mix(in oklab, var(--color-brand) 16%, transparent);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.toggle,
|
|
29
|
+
.labelButton {
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: var(--spacing-xs);
|
|
33
|
+
min-inline-size: 0;
|
|
34
|
+
padding-block: var(--spacing-2xs);
|
|
35
|
+
background: transparent;
|
|
36
|
+
border: 0;
|
|
37
|
+
border-radius: var(--border-radius-default);
|
|
38
|
+
color: inherit;
|
|
39
|
+
font: inherit;
|
|
40
|
+
text-align: left;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.toggle {
|
|
45
|
+
flex: 0 0 auto;
|
|
46
|
+
padding-inline: var(--spacing-2xs);
|
|
47
|
+
color: var(--color-fg-subtle);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.toggle:disabled {
|
|
51
|
+
cursor: default;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.toggle svg {
|
|
55
|
+
inline-size: var(--icon-size-sm);
|
|
56
|
+
block-size: var(--icon-size-sm);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.leafSpacer {
|
|
60
|
+
display: inline-block;
|
|
61
|
+
inline-size: var(--icon-size-sm);
|
|
62
|
+
block-size: var(--icon-size-sm);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.toggleUnresolved {
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
opacity: 0.45;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.spinner {
|
|
71
|
+
display: inline-flex;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.spinner svg {
|
|
75
|
+
inline-size: var(--icon-size-sm);
|
|
76
|
+
block-size: var(--icon-size-sm);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.labelButton {
|
|
80
|
+
flex: 1 1 auto;
|
|
81
|
+
min-inline-size: 0;
|
|
82
|
+
padding-inline: var(--spacing-xs);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.row:hover .toggle:not(:disabled),
|
|
86
|
+
.row:hover .labelButton,
|
|
87
|
+
.toggle:focus-visible,
|
|
88
|
+
.labelButton:focus-visible {
|
|
89
|
+
background: var(--color-bg-hover-subtle);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.toggle:focus-visible,
|
|
93
|
+
.labelButton:focus-visible {
|
|
94
|
+
outline: none;
|
|
95
|
+
box-shadow: var(--focus-ring);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.icon {
|
|
99
|
+
display: inline-flex;
|
|
100
|
+
flex: 0 0 auto;
|
|
101
|
+
color: var(--color-fg-subtle);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.icon svg {
|
|
105
|
+
inline-size: var(--icon-size-sm);
|
|
106
|
+
block-size: var(--icon-size-sm);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.label {
|
|
110
|
+
min-inline-size: 0;
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
text-overflow: ellipsis;
|
|
113
|
+
white-space: nowrap;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.errorRow {
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: var(--spacing-xs);
|
|
120
|
+
padding-left: calc((var(--tree-depth) * var(--spacing-lg)) + var(--spacing-lg));
|
|
121
|
+
padding-block: var(--spacing-2xs);
|
|
122
|
+
font-size: var(--font-size-xs);
|
|
123
|
+
color: var(--color-status-error-fg);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.retryButton {
|
|
127
|
+
padding: 0;
|
|
128
|
+
background: transparent;
|
|
129
|
+
border: 0;
|
|
130
|
+
color: var(--color-link);
|
|
131
|
+
font: inherit;
|
|
132
|
+
font-size: inherit;
|
|
133
|
+
text-decoration: underline;
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.emptyState {
|
|
138
|
+
padding: var(--spacing-md);
|
|
139
|
+
color: var(--color-fg-muted);
|
|
140
|
+
text-align: center;
|
|
141
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type TreeNode<T = unknown> = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
data?: T;
|
|
7
|
+
/**
|
|
8
|
+
* Present (even as an empty array) means this node's children are
|
|
9
|
+
* already known - eager mode relies entirely on this and never calls
|
|
10
|
+
* `loadChildren`. Leave it undefined for a node whose children haven't
|
|
11
|
+
* been fetched yet (lazy mode).
|
|
12
|
+
*/
|
|
13
|
+
children?: TreeNode<T>[];
|
|
14
|
+
/**
|
|
15
|
+
* Only consulted while `children` is undefined and `loadChildren` is
|
|
16
|
+
* given: says whether this node is expected to have children at all.
|
|
17
|
+
* Defaults to true (assume expandable) so a node the caller hasn't
|
|
18
|
+
* inspected yet still shows a toggle; set it to false for a node you
|
|
19
|
+
* already know is a leaf, to skip showing a toggle for it entirely.
|
|
20
|
+
*/
|
|
21
|
+
hasChildren?: boolean;
|
|
22
|
+
};
|
|
23
|
+
/** Fetches one node's children. Called once per node, the first time it's expanded (and again on retry after a failed load). Presence of this prop on TreeView is what switches it into lazy mode. */
|
|
24
|
+
export type LoadTreeChildren<T = unknown> = (node: TreeNode<T>) => Promise<TreeNode<T>[]>;
|
|
25
|
+
export type TreeViewProps<T = unknown> = {
|
|
26
|
+
/** Top-level nodes. In eager mode every node's full subtree must already be present via `children`. In lazy mode only the nodes you already know about need to be here - deeper levels arrive via `loadChildren`. */
|
|
27
|
+
data: TreeNode<T>[];
|
|
28
|
+
/** Omit for eager mode. Provide for lazy mode. */
|
|
29
|
+
loadChildren?: LoadTreeChildren<T>;
|
|
30
|
+
/** Uncontrolled initial expanded ids. Ignored if `expandedIds` is given. */
|
|
31
|
+
defaultExpandedIds?: string[];
|
|
32
|
+
/** Controlled expanded ids - pass together with `onExpandedIdsChange` to own the expand/collapse state yourself (e.g. to sync it with a URL). */
|
|
33
|
+
expandedIds?: string[];
|
|
34
|
+
onExpandedIdsChange?: (ids: string[]) => void;
|
|
35
|
+
selectedId?: string;
|
|
36
|
+
onSelectNode?: (node: TreeNode<T>) => void;
|
|
37
|
+
/** Customize how a node's row content renders; defaults to `node.icon` + `node.label`. */
|
|
38
|
+
renderLabel?: (node: TreeNode<T>) => ReactNode;
|
|
39
|
+
emptyMessage?: ReactNode;
|
|
40
|
+
/** Formats the error shown under a node whose `loadChildren` call rejected. */
|
|
41
|
+
loadErrorMessage?: (error: unknown, node: TreeNode<T>) => ReactNode;
|
|
42
|
+
className?: string;
|
|
43
|
+
'data-testid'?: string;
|
|
44
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
function useTreeExpansion(defaultExpandedIds = [], controlledIds, onChange) {
|
|
6
|
+
const [uncontrolledIds, setUncontrolledIds] = react.useState(
|
|
7
|
+
() => new Set(defaultExpandedIds)
|
|
8
|
+
);
|
|
9
|
+
const isControlled = controlledIds !== void 0;
|
|
10
|
+
const expandedIds = react.useMemo(
|
|
11
|
+
() => isControlled ? new Set(controlledIds) : uncontrolledIds,
|
|
12
|
+
[isControlled, controlledIds, uncontrolledIds]
|
|
13
|
+
);
|
|
14
|
+
const commit = react.useCallback(
|
|
15
|
+
(next) => {
|
|
16
|
+
if (!isControlled) setUncontrolledIds(next);
|
|
17
|
+
onChange == null ? void 0 : onChange(Array.from(next));
|
|
18
|
+
},
|
|
19
|
+
[isControlled, onChange]
|
|
20
|
+
);
|
|
21
|
+
const expand = react.useCallback(
|
|
22
|
+
(id) => {
|
|
23
|
+
if (expandedIds.has(id)) return;
|
|
24
|
+
commit(new Set(expandedIds).add(id));
|
|
25
|
+
},
|
|
26
|
+
[expandedIds, commit]
|
|
27
|
+
);
|
|
28
|
+
const collapse = react.useCallback(
|
|
29
|
+
(id) => {
|
|
30
|
+
if (!expandedIds.has(id)) return;
|
|
31
|
+
const next = new Set(expandedIds);
|
|
32
|
+
next.delete(id);
|
|
33
|
+
commit(next);
|
|
34
|
+
},
|
|
35
|
+
[expandedIds, commit]
|
|
36
|
+
);
|
|
37
|
+
const toggle = react.useCallback(
|
|
38
|
+
(id) => expandedIds.has(id) ? collapse(id) : expand(id),
|
|
39
|
+
[expandedIds, expand, collapse]
|
|
40
|
+
);
|
|
41
|
+
const expandAll = react.useCallback((ids) => commit(new Set(ids)), [commit]);
|
|
42
|
+
const collapseAll = react.useCallback(() => commit(/* @__PURE__ */ new Set()), [commit]);
|
|
43
|
+
const isExpanded = react.useCallback((id) => expandedIds.has(id), [expandedIds]);
|
|
44
|
+
return { expandedIds, expand, collapse, toggle, expandAll, collapseAll, isExpanded };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
exports.useTreeExpansion = useTreeExpansion;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* expandedIds is the single source of truth for "open nodes", same model
|
|
3
|
+
* as the Sidebar's expandedItems: a plain Set of ids, with expand/collapse
|
|
4
|
+
* no-ops guarded so an already-open (or already-closed) node doesn't
|
|
5
|
+
* trigger a re-render. Supports both controlled (`controlledIds` +
|
|
6
|
+
* `onChange`, mirroring the Sidebar's activeLink-driven expansion) and
|
|
7
|
+
* uncontrolled usage.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useTreeExpansion(defaultExpandedIds?: string[], controlledIds?: string[], onChange?: (ids: string[]) => void): {
|
|
10
|
+
expandedIds: Set<string>;
|
|
11
|
+
expand: (id: string) => void;
|
|
12
|
+
collapse: (id: string) => void;
|
|
13
|
+
toggle: (id: string) => void;
|
|
14
|
+
expandAll: (ids: string[]) => void;
|
|
15
|
+
collapseAll: () => void;
|
|
16
|
+
isExpanded: (id: string) => boolean;
|
|
17
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
function useTreeExpansion(defaultExpandedIds = [], controlledIds, onChange) {
|
|
4
|
+
const [uncontrolledIds, setUncontrolledIds] = useState(
|
|
5
|
+
() => new Set(defaultExpandedIds)
|
|
6
|
+
);
|
|
7
|
+
const isControlled = controlledIds !== void 0;
|
|
8
|
+
const expandedIds = useMemo(
|
|
9
|
+
() => isControlled ? new Set(controlledIds) : uncontrolledIds,
|
|
10
|
+
[isControlled, controlledIds, uncontrolledIds]
|
|
11
|
+
);
|
|
12
|
+
const commit = useCallback(
|
|
13
|
+
(next) => {
|
|
14
|
+
if (!isControlled) setUncontrolledIds(next);
|
|
15
|
+
onChange == null ? void 0 : onChange(Array.from(next));
|
|
16
|
+
},
|
|
17
|
+
[isControlled, onChange]
|
|
18
|
+
);
|
|
19
|
+
const expand = useCallback(
|
|
20
|
+
(id) => {
|
|
21
|
+
if (expandedIds.has(id)) return;
|
|
22
|
+
commit(new Set(expandedIds).add(id));
|
|
23
|
+
},
|
|
24
|
+
[expandedIds, commit]
|
|
25
|
+
);
|
|
26
|
+
const collapse = useCallback(
|
|
27
|
+
(id) => {
|
|
28
|
+
if (!expandedIds.has(id)) return;
|
|
29
|
+
const next = new Set(expandedIds);
|
|
30
|
+
next.delete(id);
|
|
31
|
+
commit(next);
|
|
32
|
+
},
|
|
33
|
+
[expandedIds, commit]
|
|
34
|
+
);
|
|
35
|
+
const toggle = useCallback(
|
|
36
|
+
(id) => expandedIds.has(id) ? collapse(id) : expand(id),
|
|
37
|
+
[expandedIds, expand, collapse]
|
|
38
|
+
);
|
|
39
|
+
const expandAll = useCallback((ids) => commit(new Set(ids)), [commit]);
|
|
40
|
+
const collapseAll = useCallback(() => commit(/* @__PURE__ */ new Set()), [commit]);
|
|
41
|
+
const isExpanded = useCallback((id) => expandedIds.has(id), [expandedIds]);
|
|
42
|
+
return { expandedIds, expand, collapse, toggle, expandAll, collapseAll, isExpanded };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { useTreeExpansion };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function collectExpandableIds(nodes) {
|
|
4
|
+
const ids = [];
|
|
5
|
+
const walk = (list) => {
|
|
6
|
+
for (const node of list) {
|
|
7
|
+
if (node.children && node.children.length > 0) {
|
|
8
|
+
ids.push(node.id);
|
|
9
|
+
walk(node.children);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
walk(nodes);
|
|
14
|
+
return ids;
|
|
15
|
+
}
|
|
16
|
+
function findAncestorIds(nodes, targetId) {
|
|
17
|
+
var _a;
|
|
18
|
+
const dfs = (list, trail) => {
|
|
19
|
+
for (const node of list) {
|
|
20
|
+
if (node.id === targetId) return trail;
|
|
21
|
+
if (node.children) {
|
|
22
|
+
const found = dfs(node.children, [...trail, node.id]);
|
|
23
|
+
if (found) return found;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
};
|
|
28
|
+
return (_a = dfs(nodes, [])) != null ? _a : [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
exports.collectExpandableIds = collectExpandableIds;
|
|
32
|
+
exports.findAncestorIds = findAncestorIds;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TreeNode } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* All node ids that already have at least one known child - the eager
|
|
4
|
+
* "expand all" target set. A node whose children haven't been resolved yet
|
|
5
|
+
* (lazy mode, before its first load) is skipped: expanding it needs a
|
|
6
|
+
* fetch, not a state flip, so it isn't safe to include here.
|
|
7
|
+
*/
|
|
8
|
+
export declare function collectExpandableIds<T>(nodes: TreeNode<T>[]): string[];
|
|
9
|
+
/**
|
|
10
|
+
* DFS chain of ancestor ids leading to `targetId` within an already-known
|
|
11
|
+
* tree - pass the result to `defaultExpandedIds`/`expandedIds` to reveal a
|
|
12
|
+
* specific node (mirrors the Sidebar's `findExpandableParentChain`). Only
|
|
13
|
+
* searches subtrees already present via `children`, so in lazy mode this
|
|
14
|
+
* can only find a target inside branches already loaded.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findAncestorIds<T>(nodes: TreeNode<T>[], targetId: string): string[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function collectExpandableIds(nodes) {
|
|
2
|
+
const ids = [];
|
|
3
|
+
const walk = (list) => {
|
|
4
|
+
for (const node of list) {
|
|
5
|
+
if (node.children && node.children.length > 0) {
|
|
6
|
+
ids.push(node.id);
|
|
7
|
+
walk(node.children);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
walk(nodes);
|
|
12
|
+
return ids;
|
|
13
|
+
}
|
|
14
|
+
function findAncestorIds(nodes, targetId) {
|
|
15
|
+
var _a;
|
|
16
|
+
const dfs = (list, trail) => {
|
|
17
|
+
for (const node of list) {
|
|
18
|
+
if (node.id === targetId) return trail;
|
|
19
|
+
if (node.children) {
|
|
20
|
+
const found = dfs(node.children, [...trail, node.id]);
|
|
21
|
+
if (found) return found;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
return (_a = dfs(nodes, [])) != null ? _a : [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { collectExpandableIds, findAncestorIds };
|