@react-fabric/data 1.0.0-beta.6 → 1.0.0-beta.61
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/{src/histogram/Histogram.module.css → css/_histogram.css} +16 -13
- package/{dist/esm/tree/types.js → css/styles.css} +2 -7
- package/dist/cjs/checklist/CheckList.js +27 -4
- package/dist/cjs/histogram/Histogram.js +39 -9
- package/dist/cjs/hooks/useFilteredList.js +88 -10
- package/dist/cjs/hooks/usePagination.js +27 -4
- package/dist/cjs/hooks/useSelectableList.js +29 -0
- package/dist/cjs/i18n/ar.json.js +8 -1
- package/dist/cjs/i18n/en.json.js +8 -1
- package/dist/cjs/json/JsonViewer.js +68 -28
- package/dist/cjs/pagination/Pagination.js +14 -4
- package/dist/cjs/table/AddColumn.js +3 -3
- package/dist/cjs/table/BodyCell.js +14 -6
- package/dist/cjs/table/BodyRow.js +4 -4
- package/dist/cjs/table/CheckboxCell.js +2 -2
- package/dist/cjs/table/ColumnFilters.js +97 -0
- package/dist/cjs/table/HeaderCell.js +71 -43
- package/dist/cjs/table/Table.js +62 -13
- package/dist/cjs/table/types.js +29 -0
- package/dist/cjs/table/useResizer.js +4 -4
- package/dist/cjs/table/useVirtualTable.js +127 -19
- package/dist/cjs/tree/TreeNode.js +50 -32
- package/dist/cjs/tree/TreePanel.js +52 -19
- package/dist/cjs/tree/useTree.js +199 -38
- package/dist/cjs/tree/utils.js +39 -12
- package/dist/cjs/virtual/Gallery.js +97 -45
- package/dist/cjs/virtual/List.js +93 -39
- package/dist/cjs/virtual/Scroller.js +16 -8
- package/dist/esm/checklist/CheckList.js +28 -5
- package/dist/esm/histogram/Histogram.js +41 -11
- package/dist/esm/hooks/useFilteredList.js +89 -11
- package/dist/esm/hooks/usePagination.js +27 -4
- package/dist/esm/hooks/useSelectableList.js +29 -0
- package/dist/esm/i18n/ar.json.js +8 -2
- package/dist/esm/i18n/en.json.js +8 -2
- package/dist/esm/json/JsonViewer.js +69 -29
- package/dist/esm/pagination/Pagination.js +15 -5
- package/dist/esm/table/AddColumn.js +4 -4
- package/dist/esm/table/BodyCell.js +15 -7
- package/dist/esm/table/BodyRow.js +4 -4
- package/dist/esm/table/CheckboxCell.js +3 -3
- package/dist/esm/table/ColumnFilters.js +97 -0
- package/dist/esm/table/HeaderCell.js +72 -44
- package/dist/esm/table/Table.js +63 -14
- package/dist/esm/table/types.js +29 -0
- package/dist/esm/table/useResizer.js +4 -4
- package/dist/esm/table/useVirtualTable.js +129 -21
- package/dist/esm/tree/TreeNode.js +53 -35
- package/dist/esm/tree/TreePanel.js +54 -21
- package/dist/esm/tree/useTree.js +202 -41
- package/dist/esm/tree/utils.js +40 -13
- package/dist/esm/virtual/Gallery.js +97 -45
- package/dist/esm/virtual/List.js +93 -39
- package/dist/esm/virtual/Scroller.js +17 -9
- package/dist/types/checklist/CheckList.d.ts +22 -0
- package/dist/types/histogram/Histogram.d.ts +28 -1
- package/dist/types/hooks/useFilteredList.d.ts +66 -5
- package/dist/types/hooks/usePagination.d.ts +19 -0
- package/dist/types/hooks/useSelectableList.d.ts +29 -0
- package/dist/types/json/JsonViewer.d.ts +27 -2
- package/dist/types/pagination/Pagination.d.ts +10 -1
- package/dist/types/table/ColumnFilters.d.ts +9 -0
- package/dist/types/table/HeaderCell.d.ts +1 -1
- package/dist/types/table/Table.d.ts +14 -1
- package/dist/types/table/types.d.ts +46 -7
- package/dist/types/table/useVirtualTable.d.ts +23 -9
- package/dist/types/tree/TreeNode.d.ts +1 -1
- package/dist/types/tree/TreePanel.d.ts +42 -1
- package/dist/types/tree/types.d.ts +65 -21
- package/dist/types/tree/useTree.d.ts +9 -10
- package/dist/types/tree/utils.d.ts +10 -3
- package/dist/types/virtual/Gallery.d.ts +42 -3
- package/dist/types/virtual/List.d.ts +42 -3
- package/package.json +16 -14
- package/src/checklist/CheckList.tsx +39 -7
- package/src/histogram/Histogram.tsx +43 -19
- package/src/hooks/useFilteredList.tsx +109 -9
- package/src/hooks/usePagination.tsx +32 -7
- package/src/hooks/useSelectableList.ts +29 -0
- package/src/i18n/ar.json +5 -0
- package/src/i18n/en.json +5 -0
- package/src/json/JsonViewer.tsx +76 -37
- package/src/pagination/Pagination.tsx +15 -5
- package/src/table/AddColumn.tsx +4 -4
- package/src/table/BodyCell.tsx +24 -7
- package/src/table/BodyRow.tsx +7 -4
- package/src/table/CheckboxCell.tsx +5 -5
- package/src/table/ColumnFilters.tsx +105 -0
- package/src/table/HeaderCell.tsx +74 -21
- package/src/table/Table.tsx +107 -12
- package/src/table/types.ts +49 -10
- package/src/table/useResizer.ts +5 -3
- package/src/table/useVirtualTable.ts +158 -27
- package/src/tree/TreeNode.tsx +96 -62
- package/src/tree/TreePanel.tsx +88 -22
- package/src/tree/types.ts +73 -23
- package/src/tree/useTree.ts +220 -44
- package/src/tree/utils.ts +58 -20
- package/src/virtual/Gallery.tsx +118 -54
- package/src/virtual/List.tsx +99 -39
- package/src/virtual/Scroller.tsx +25 -9
- package/dist/cjs/histogram/Histogram.module.css +0 -1
- package/dist/cjs/tree/types.js +0 -30
- package/dist/esm/histogram/Histogram.module.css +0 -1
|
@@ -21,37 +21,59 @@
|
|
|
21
21
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
24
|
+
import { compareValues, debounce, groupBy } from "@react-fabric/utilities";
|
|
25
|
+
import {
|
|
26
|
+
defaultRangeExtractor,
|
|
27
|
+
type Range,
|
|
28
|
+
useVirtualizer,
|
|
29
|
+
} from "@tanstack/react-virtual";
|
|
30
|
+
import {
|
|
31
|
+
useCallback,
|
|
32
|
+
useEffect,
|
|
33
|
+
useMemo,
|
|
34
|
+
useReducer,
|
|
35
|
+
useRef,
|
|
36
|
+
useState,
|
|
37
|
+
} from "react";
|
|
26
38
|
|
|
27
|
-
interface DataState {
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
interface DataState<T = KeyValue> {
|
|
40
|
+
data: T[];
|
|
41
|
+
items: T[];
|
|
42
|
+
grouped: KeyValue<T[]>;
|
|
43
|
+
groups: AnyObject[];
|
|
44
|
+
expanded: AnyObject[];
|
|
45
|
+
checked: AnyObject[];
|
|
30
46
|
allChecked: 0 | 1 | 2;
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
type DataAction =
|
|
34
|
-
| { type: "
|
|
35
|
-
| { type: "
|
|
50
|
+
| { type: "updateState"; checked: AnyObject[] }
|
|
51
|
+
| { type: "groupExpand"; key: AnyObject }
|
|
52
|
+
| { type: "reset"; key: AnyObject; data: AnyObject[] }
|
|
53
|
+
| { type: "expand"; key: AnyObject }
|
|
54
|
+
| { type: "check"; key: AnyObject }
|
|
36
55
|
| { type: "checkall" };
|
|
37
56
|
|
|
38
|
-
export const useVirtualTable = (
|
|
39
|
-
data:
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
export const useVirtualTable = <T extends KeyValue = KeyValue>(
|
|
58
|
+
data: T[],
|
|
59
|
+
options: {
|
|
60
|
+
checked?: AnyObject[];
|
|
61
|
+
keyProperty: keyof T;
|
|
62
|
+
groupProperty?: keyof T;
|
|
63
|
+
loading?: boolean;
|
|
64
|
+
total: number;
|
|
65
|
+
onLoadMore?: (startIndex: number) => void;
|
|
66
|
+
onCheckChanged?: (checked: AnyObject[]) => void;
|
|
67
|
+
},
|
|
42
68
|
) => {
|
|
43
69
|
// TODO: refactor data into item list, apply grouping if groupColumn provided
|
|
44
70
|
// TODO: when grouping maintain map of group with children, {___GROUP___:true, label, items, open}
|
|
45
71
|
|
|
46
|
-
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
47
|
-
const virtualizer = useVirtualizer({
|
|
48
|
-
count: data.length,
|
|
49
|
-
getScrollElement: () => scrollerRef.current,
|
|
50
|
-
estimateSize: () => 24,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
72
|
const [state, dispatch] = useReducer(
|
|
54
|
-
(state: DataState
|
|
73
|
+
(state: DataState<T>, action: DataAction) => {
|
|
74
|
+
if (action.type === "updateState") {
|
|
75
|
+
state.checked = action.checked;
|
|
76
|
+
}
|
|
55
77
|
if (action.type === "expand") {
|
|
56
78
|
if (state.expanded.includes(action.key))
|
|
57
79
|
state.expanded.splice(state.expanded.indexOf(action.key), 1);
|
|
@@ -61,51 +83,160 @@ export const useVirtualTable = (
|
|
|
61
83
|
if (state.checked.includes(action.key))
|
|
62
84
|
state.checked.splice(state.checked.indexOf(action.key), 1);
|
|
63
85
|
else state.checked.push(action.key);
|
|
64
|
-
onCheckChanged?.(state.checked);
|
|
86
|
+
options.onCheckChanged?.(state.checked);
|
|
65
87
|
}
|
|
66
88
|
if (action.type === "checkall") {
|
|
67
89
|
if (state.allChecked !== 0) {
|
|
68
90
|
state.checked = [];
|
|
69
|
-
} else if (keyProperty) {
|
|
70
|
-
state.checked = data.map((item) => item[keyProperty]);
|
|
91
|
+
} else if (options.keyProperty) {
|
|
92
|
+
state.checked = state.data.map((item) => item[options.keyProperty]);
|
|
93
|
+
}
|
|
94
|
+
options.onCheckChanged?.(state.checked);
|
|
95
|
+
}
|
|
96
|
+
const checkList = state.data.filter((hit) =>
|
|
97
|
+
state.checked.includes(hit[options.keyProperty]),
|
|
98
|
+
);
|
|
99
|
+
if (action.type === "groupExpand") {
|
|
100
|
+
const group = state.groups.find((group) => group.key === action.key);
|
|
101
|
+
if (group) {
|
|
102
|
+
group.open = !group.open;
|
|
103
|
+
state.groups = state.groups.slice();
|
|
104
|
+
state.items = state.groups
|
|
105
|
+
.map((group) => [
|
|
106
|
+
group,
|
|
107
|
+
group.open ? state.grouped[group.key] : undefined,
|
|
108
|
+
])
|
|
109
|
+
.flat(2)
|
|
110
|
+
.filter(Boolean) as any;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (action.type === "reset") {
|
|
114
|
+
state.data = action.data;
|
|
115
|
+
if (action.key) {
|
|
116
|
+
state.grouped = groupBy<T>(state.data, action.key);
|
|
117
|
+
state.groups = Object.keys(state.grouped)
|
|
118
|
+
.sort(compareValues("asc"))
|
|
119
|
+
.map((group) => ({
|
|
120
|
+
__GROUP__: true,
|
|
121
|
+
open: true,
|
|
122
|
+
key: group,
|
|
123
|
+
items: state.grouped[group],
|
|
124
|
+
itemCount: state.grouped[group].length,
|
|
125
|
+
}));
|
|
126
|
+
state.items = state.groups
|
|
127
|
+
.map((group) => [group, ...state.grouped[group.key]])
|
|
128
|
+
.flat() as any;
|
|
129
|
+
} else {
|
|
130
|
+
state.items = state.data;
|
|
71
131
|
}
|
|
72
|
-
onCheckChanged?.(state.checked);
|
|
73
132
|
}
|
|
74
133
|
return {
|
|
75
134
|
...state,
|
|
76
135
|
allChecked:
|
|
77
|
-
|
|
136
|
+
checkList.length === state.data.length
|
|
78
137
|
? 1
|
|
79
138
|
: state.checked.length
|
|
80
139
|
? 2
|
|
81
140
|
: 0,
|
|
82
|
-
} as DataState
|
|
141
|
+
} as DataState<T>;
|
|
83
142
|
},
|
|
84
143
|
{
|
|
144
|
+
data,
|
|
145
|
+
items: [],
|
|
146
|
+
grouped: {},
|
|
147
|
+
groups: [],
|
|
85
148
|
checked: [],
|
|
86
149
|
expanded: [],
|
|
87
150
|
allChecked: 0,
|
|
88
151
|
},
|
|
89
152
|
);
|
|
90
153
|
|
|
154
|
+
const activeStickyIndexRef = useRef(0);
|
|
155
|
+
const stickyIndexes = useMemo<number[]>(
|
|
156
|
+
() =>
|
|
157
|
+
state.items
|
|
158
|
+
.map((hit, idx) => ("__GROUP__" in hit ? idx : undefined))
|
|
159
|
+
.filter(Boolean) as number[],
|
|
160
|
+
[state.items],
|
|
161
|
+
);
|
|
162
|
+
const isActiveSticky = (index: number) =>
|
|
163
|
+
activeStickyIndexRef.current === index;
|
|
164
|
+
|
|
165
|
+
const [hasActiveSticky, setHasActiveSticky] = useState(false);
|
|
166
|
+
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
167
|
+
const virtualizer = useVirtualizer({
|
|
168
|
+
count: state.items.length,
|
|
169
|
+
getScrollElement: () => scrollerRef.current,
|
|
170
|
+
estimateSize: () => 32,
|
|
171
|
+
rangeExtractor: useCallback(
|
|
172
|
+
(range: Range) => {
|
|
173
|
+
activeStickyIndexRef.current =
|
|
174
|
+
[...stickyIndexes]
|
|
175
|
+
.reverse()
|
|
176
|
+
.find((index) => range.startIndex > index) ?? 0;
|
|
177
|
+
|
|
178
|
+
setHasActiveSticky(range.startIndex !== activeStickyIndexRef.current);
|
|
179
|
+
|
|
180
|
+
const next = new Set([
|
|
181
|
+
activeStickyIndexRef.current,
|
|
182
|
+
...defaultRangeExtractor(range),
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
return [...next];
|
|
186
|
+
},
|
|
187
|
+
[stickyIndexes],
|
|
188
|
+
),
|
|
189
|
+
});
|
|
190
|
+
|
|
91
191
|
const virtualItems = virtualizer.getVirtualItems();
|
|
92
192
|
|
|
93
193
|
const getData = useCallback(
|
|
94
194
|
(index: number) => {
|
|
95
|
-
return
|
|
195
|
+
return state.items[index];
|
|
96
196
|
},
|
|
97
|
-
[
|
|
197
|
+
[state.items],
|
|
98
198
|
);
|
|
99
199
|
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
dispatch({ type: "reset", key: options.groupProperty, data });
|
|
202
|
+
}, [data, options.groupProperty]);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
dispatch({ type: "updateState", checked: options.checked ?? [] });
|
|
206
|
+
}, [options.checked]);
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (options.onLoadMore) {
|
|
210
|
+
const firstItem = virtualItems[0]?.index;
|
|
211
|
+
const lastItem = virtualItems?.slice(-1).pop()?.index;
|
|
212
|
+
const canLoadPrevPage =
|
|
213
|
+
firstItem !== undefined && state.items[firstItem] === null;
|
|
214
|
+
const canLoadNextPage =
|
|
215
|
+
lastItem !== undefined &&
|
|
216
|
+
lastItem === state.items.length - 1 &&
|
|
217
|
+
state.items.length < options.total;
|
|
218
|
+
const db = debounce(options.onLoadMore);
|
|
219
|
+
if (canLoadPrevPage) {
|
|
220
|
+
db(firstItem);
|
|
221
|
+
} else if (!options.loading && canLoadNextPage) {
|
|
222
|
+
db(lastItem + 1);
|
|
223
|
+
}
|
|
224
|
+
return () => db.cancel();
|
|
225
|
+
}
|
|
226
|
+
}, [options.onLoadMore, virtualItems]);
|
|
227
|
+
|
|
100
228
|
return {
|
|
101
229
|
scrollerRef,
|
|
102
230
|
items: virtualItems,
|
|
103
231
|
checkState: state,
|
|
104
232
|
getData,
|
|
105
233
|
virtualizer,
|
|
234
|
+
hasActiveSticky,
|
|
235
|
+
isActiveSticky,
|
|
106
236
|
top: virtualizer.range?.startIndex,
|
|
107
237
|
totalSize: virtualizer.getTotalSize,
|
|
108
238
|
measureElement: virtualizer.measureElement,
|
|
239
|
+
toggleGroupExpand: (key: Key) => dispatch({ type: "groupExpand", key }),
|
|
109
240
|
toggleExpand: (key: Key) => dispatch({ type: "expand", key }),
|
|
110
241
|
toggleChecked: (key: Key) => dispatch({ type: "check", key }),
|
|
111
242
|
toggleAllChecked: () => dispatch({ type: "checkall" }),
|
package/src/tree/TreeNode.tsx
CHANGED
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { Badge,
|
|
24
|
+
import { Badge, getBadgeProps, getIconProps, Icon } from "@react-fabric/core";
|
|
25
|
+
import { isEmpty, isString, isTrue } from "@react-fabric/utilities";
|
|
25
26
|
import classNames from "classnames";
|
|
26
|
-
import { useMemo } from "react";
|
|
27
|
+
import { isValidElement, useMemo } from "react";
|
|
28
|
+
import { useTranslation } from "react-i18next";
|
|
27
29
|
import { type TreeNodeProps } from "./types";
|
|
28
30
|
|
|
29
31
|
export const TreeNode = ({
|
|
@@ -35,40 +37,53 @@ export const TreeNode = ({
|
|
|
35
37
|
defaultLeafIcon,
|
|
36
38
|
defaultNodeIcon,
|
|
37
39
|
onToggle,
|
|
40
|
+
onClick,
|
|
38
41
|
onSelect,
|
|
39
42
|
onChecked,
|
|
40
43
|
children,
|
|
44
|
+
leafClassName,
|
|
45
|
+
nodeClassName,
|
|
41
46
|
}: TreeNodeProps) => {
|
|
47
|
+
const { t } = useTranslation("data");
|
|
42
48
|
const badgeProps = useMemo(() => {
|
|
43
49
|
return getBadgeProps(node.badge);
|
|
44
50
|
}, [node.badge]);
|
|
45
51
|
|
|
46
52
|
const filler = useMemo(() => {
|
|
47
|
-
return node.lines.slice(
|
|
48
|
-
<
|
|
53
|
+
return node.lines.slice(1).map((line, idx) => (
|
|
54
|
+
<div
|
|
49
55
|
key={idx}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<path d="M14.989 12.389l0.161-0.389-0.161-0.389-0.389-0.161h-6.050v-13.55h-1.1v14.1l0.161 0.389 0.389 0.161h6.6z" />
|
|
57
|
-
)}
|
|
58
|
-
{!noLines && line === 3 && (
|
|
59
|
-
<path d="M8.55 12.55h6.050l0.389-0.161 0.161-0.389-0.161-0.389-0.389-0.161h-6.050v-13.55h-1.1v28.2h1.1z" />
|
|
56
|
+
className={classNames(
|
|
57
|
+
"flex-initial self-stretch flex items-center w-6 relative",
|
|
58
|
+
!noLines &&
|
|
59
|
+
line > 0 &&
|
|
60
|
+
"before:absolute before:top-0 before:start-[7px] before:bg-tint-200 before:min-h-[50%] before:w-0.5 before:scale-x-[.625]",
|
|
61
|
+
!noLines && line !== 2 && "before:h-full",
|
|
60
62
|
)}
|
|
61
|
-
|
|
63
|
+
>
|
|
64
|
+
<svg viewBox="0 0 24 24" className="fill-tint-200 rtl:-scale-x-100">
|
|
65
|
+
{line === 0 && <path d="" />}
|
|
66
|
+
{!noLines && line === 1 && (
|
|
67
|
+
<path d="M7.45 26.1v-28.2h1.1v28.2h-1.1z" />
|
|
68
|
+
)}
|
|
69
|
+
{!noLines && line === 2 && (
|
|
70
|
+
<path d="M14.989 12.389l0.161-0.389-0.161-0.389-0.389-0.161h-6.050v-13.55h-1.1v14.1l0.161 0.389 0.389 0.161h6.6z" />
|
|
71
|
+
)}
|
|
72
|
+
{!noLines && line === 3 && (
|
|
73
|
+
<path d="M8.55 12.55h6.050l0.389-0.161 0.161-0.389-0.161-0.389-0.389-0.161h-6.050v-13.55h-1.1v28.2h1.1z" />
|
|
74
|
+
)}
|
|
75
|
+
</svg>
|
|
76
|
+
</div>
|
|
62
77
|
));
|
|
63
78
|
}, [noLines, node.lines]);
|
|
64
79
|
|
|
65
80
|
const nodeIcon = useMemo(() => {
|
|
66
81
|
if (node.icon) return node.icon;
|
|
67
|
-
return node.leaf ? defaultLeafIcon : defaultNodeIcon;
|
|
82
|
+
return { icon: node.leaf ? defaultLeafIcon : defaultNodeIcon };
|
|
68
83
|
}, [defaultLeafIcon, defaultNodeIcon, node.icon]);
|
|
69
84
|
|
|
70
85
|
const canCheck = useMemo(() => {
|
|
71
|
-
return checkable
|
|
86
|
+
return isTrue(checkable) || (checkable === "leafOnly" && node.leaf);
|
|
72
87
|
}, [node.leaf, checkable]);
|
|
73
88
|
|
|
74
89
|
const canSelect = useMemo(() => {
|
|
@@ -82,12 +97,18 @@ export const TreeNode = ({
|
|
|
82
97
|
<div
|
|
83
98
|
role="treeitem"
|
|
84
99
|
aria-selected={node.selected}
|
|
85
|
-
className="flex flex-nowrap"
|
|
100
|
+
className={classNames("flex flex-nowrap items-center")}
|
|
86
101
|
data-testid={node["data-testid"]}
|
|
87
102
|
data-test-value={node["data-test-value"]}
|
|
103
|
+
data-id={node.id}
|
|
88
104
|
>
|
|
89
105
|
{filler}
|
|
90
|
-
{
|
|
106
|
+
{node.loading && (
|
|
107
|
+
<div className="flex-initial w-6">
|
|
108
|
+
<Icon icon="icon-[svg-spinners--eclipse]" className="text-muted" />
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
{!node.leaf && !node.childFiltered && !node.loading && (
|
|
91
112
|
<div className="flex-initial w-6">
|
|
92
113
|
<Icon
|
|
93
114
|
rtlFlip
|
|
@@ -103,10 +124,10 @@ export const TreeNode = ({
|
|
|
103
124
|
onClick={() => !node.disabled && onChecked(node.id)}
|
|
104
125
|
icon={
|
|
105
126
|
node.checked === 2
|
|
106
|
-
?
|
|
127
|
+
? "icon-[mdi--checkbox-intermediate]"
|
|
107
128
|
: node.checked === 1
|
|
108
|
-
?
|
|
109
|
-
:
|
|
129
|
+
? "icon-[mdi--checkbox-marked]"
|
|
130
|
+
: "icon-[mdi--checkbox-blank-outline]"
|
|
110
131
|
}
|
|
111
132
|
className={classNames(
|
|
112
133
|
node.disabled
|
|
@@ -116,48 +137,61 @@ export const TreeNode = ({
|
|
|
116
137
|
/>
|
|
117
138
|
</div>
|
|
118
139
|
)}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
{!node.empty && (
|
|
141
|
+
<div
|
|
142
|
+
role="none"
|
|
143
|
+
className={classNames(
|
|
144
|
+
"group/tool data-selected:bg-primary-100 flex flex-nowrap flex-1 overflow-hidden select-none",
|
|
145
|
+
node.childSelected && "font-medium",
|
|
146
|
+
node.disabled && "text-muted opacity-65",
|
|
147
|
+
node.className,
|
|
148
|
+
node.leaf ? leafClassName : nodeClassName,
|
|
149
|
+
!node.disabled &&
|
|
150
|
+
(canSelect || onClick) &&
|
|
151
|
+
"hover:bg-primary-50 cursor-pointer",
|
|
152
|
+
)}
|
|
153
|
+
data-selected={node.selected}
|
|
154
|
+
onClick={
|
|
155
|
+
node.disabled
|
|
156
|
+
? undefined
|
|
157
|
+
: (e) => {
|
|
158
|
+
onClick?.(node.id, node.data);
|
|
159
|
+
canSelect
|
|
160
|
+
? onSelect(node.id, e.shiftKey)
|
|
161
|
+
: canCheck
|
|
162
|
+
? onChecked(node.id)
|
|
163
|
+
: onToggle(node.id);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
>
|
|
167
|
+
{nodeIcon && (
|
|
168
|
+
<div className="flex-content max-w-6 me-1 self-center leading-none">
|
|
169
|
+
{isValidElement(nodeIcon) && nodeIcon}
|
|
170
|
+
{isString(nodeIcon) && <Icon {...getIconProps(nodeIcon)} />}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
{node.childSelected && !node.open && (
|
|
174
|
+
<span className=" flex-content text-primary-500">•</span>
|
|
175
|
+
)}
|
|
176
|
+
<div role="none" className="flex-1 truncate">
|
|
177
|
+
{children}
|
|
143
178
|
</div>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
179
|
+
{!isEmpty(node.badge) && (
|
|
180
|
+
<Badge
|
|
181
|
+
{...badgeProps}
|
|
182
|
+
className={classNames(
|
|
183
|
+
"me-1 self-center flex-content bg-opacity-50",
|
|
184
|
+
badgeProps.className,
|
|
185
|
+
)}
|
|
186
|
+
/>
|
|
187
|
+
)}
|
|
150
188
|
</div>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)}
|
|
158
|
-
/>
|
|
159
|
-
)}
|
|
160
|
-
</div>
|
|
189
|
+
)}
|
|
190
|
+
{node.empty && (
|
|
191
|
+
<div className="text-muted text-sm select-none">
|
|
192
|
+
{t("tree.noItems")}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
161
195
|
</div>
|
|
162
196
|
);
|
|
163
197
|
};
|
package/src/tree/TreePanel.tsx
CHANGED
|
@@ -21,23 +21,57 @@
|
|
|
21
21
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
Button,
|
|
26
|
-
ButtonGroup,
|
|
27
|
-
CoreIcons,
|
|
28
|
-
Header,
|
|
29
|
-
Icon,
|
|
30
|
-
Section,
|
|
31
|
-
} from "@react-fabric/core";
|
|
24
|
+
import { Button, ButtonGroup, Header, Icon, Section } from "@react-fabric/core";
|
|
32
25
|
import { Search } from "@react-fabric/form";
|
|
33
26
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
34
|
-
import { useImperativeHandle, useMemo, useRef } from "react";
|
|
27
|
+
import { useEffect, useImperativeHandle, useMemo, useRef } from "react";
|
|
35
28
|
import { TreeNode } from "./TreeNode";
|
|
36
|
-
import {
|
|
29
|
+
import { type TreePanelProps } from "./types";
|
|
37
30
|
import { useTree } from "./useTree";
|
|
38
31
|
|
|
39
32
|
// TODO: implement keyboard handler for navigating tree
|
|
40
33
|
|
|
34
|
+
/**
|
|
35
|
+
* TreePanel component renders a virtualized tree structure with support for
|
|
36
|
+
* selection, expansion, and filtering. It allows for custom rendering of tree
|
|
37
|
+
* nodes and supports checkable nodes.
|
|
38
|
+
*
|
|
39
|
+
* @param {TreePanelProps<T>} props - The properties for the TreePanel component.
|
|
40
|
+
* @returns {JSX.Element} The rendered TreePanel component.
|
|
41
|
+
* @template T - The type of items in the tree, extending KeyValue.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```jsx
|
|
45
|
+
* <TreePanel
|
|
46
|
+
* items={items}
|
|
47
|
+
* selected={selectedItem}
|
|
48
|
+
* checked={checkedItems}
|
|
49
|
+
* checkable={true}
|
|
50
|
+
* renderer={(item) => <span>{item.name}</span>}
|
|
51
|
+
* searchable={true}
|
|
52
|
+
* selectable={true}
|
|
53
|
+
* matcher={(item, query) => item.name.toLowerCase().includes(query.toLowerCase())}
|
|
54
|
+
* filterPlaceholder="Search..."
|
|
55
|
+
* defaultLeafIcon={<Icon icon="icon-[mdi--file]" />}
|
|
56
|
+
* defaultNodeIcon={<Icon icon="icon-[mdi--folder]" />}
|
|
57
|
+
* expander="caret"
|
|
58
|
+
* onChecked={(checkedItems) => console.log(checkedItems)}
|
|
59
|
+
* onLoad={(id) => console.log(`Load children for ${id}`)}
|
|
60
|
+
* onQuery={(query) => console.log(`Query: ${query}`)}
|
|
61
|
+
* onSelect={(selectedItem) => console.log(selectedItem)}
|
|
62
|
+
* onClick={(item) => console.log(`Clicked on ${item.id}`)}
|
|
63
|
+
* sorter={(a, b) => a.label.localeCompare(b.label)}
|
|
64
|
+
* makeLabel={(item) => item.name}
|
|
65
|
+
* makeIcon={(item) => item.icon ? <Icon icon={item.icon} /> : null}
|
|
66
|
+
* defaultExpanded={["root"]}
|
|
67
|
+
* onExpandToggle={(id, expanded) => console.log(`Toggle expand for ${id}: ${expanded}`)}
|
|
68
|
+
* leafClassName="leaf-node"
|
|
69
|
+
* nodeClassName="tree-node"
|
|
70
|
+
* />
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @see {@link https://adarshpastakia.github.io/react-fabric/?path=/docs/data-treepanel--docs}
|
|
74
|
+
*/
|
|
41
75
|
export const TreePanel = <T extends KeyValue>({
|
|
42
76
|
ref,
|
|
43
77
|
items,
|
|
@@ -45,9 +79,9 @@ export const TreePanel = <T extends KeyValue>({
|
|
|
45
79
|
checked,
|
|
46
80
|
checkable,
|
|
47
81
|
noLines,
|
|
48
|
-
renderer,
|
|
49
82
|
searchable,
|
|
50
83
|
selectable,
|
|
84
|
+
multiple,
|
|
51
85
|
matcher,
|
|
52
86
|
filterPlaceholder,
|
|
53
87
|
defaultLeafIcon,
|
|
@@ -55,15 +89,27 @@ export const TreePanel = <T extends KeyValue>({
|
|
|
55
89
|
expander = "box",
|
|
56
90
|
onChecked,
|
|
57
91
|
onLoad,
|
|
92
|
+
onQuery,
|
|
58
93
|
onSelect,
|
|
94
|
+
onClick,
|
|
95
|
+
sorter,
|
|
96
|
+
makeLabel,
|
|
97
|
+
makeIcon,
|
|
98
|
+
defaultExpanded,
|
|
99
|
+
onExpandToggle,
|
|
100
|
+
leafClassName,
|
|
101
|
+
nodeClassName,
|
|
59
102
|
...aria
|
|
60
103
|
}: TreePanelProps<T>) => {
|
|
61
104
|
const {
|
|
62
105
|
tree,
|
|
106
|
+
query,
|
|
107
|
+
selected: currentSelected,
|
|
63
108
|
toggleExpand,
|
|
64
109
|
toggleCheck,
|
|
65
110
|
expandAll,
|
|
66
111
|
collapseAll,
|
|
112
|
+
expandAndLoad,
|
|
67
113
|
select,
|
|
68
114
|
expand,
|
|
69
115
|
onFilter,
|
|
@@ -71,10 +117,16 @@ export const TreePanel = <T extends KeyValue>({
|
|
|
71
117
|
items,
|
|
72
118
|
checked,
|
|
73
119
|
selected,
|
|
120
|
+
multiple,
|
|
74
121
|
matcher,
|
|
122
|
+
sorter,
|
|
123
|
+
onLoad,
|
|
124
|
+
onQuery,
|
|
75
125
|
onSelect,
|
|
76
126
|
onChecked,
|
|
77
|
-
|
|
127
|
+
defaultExpanded,
|
|
128
|
+
onExpandToggle,
|
|
129
|
+
} as any);
|
|
78
130
|
|
|
79
131
|
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
80
132
|
const virtualizer = useVirtualizer({
|
|
@@ -87,23 +139,29 @@ export const TreePanel = <T extends KeyValue>({
|
|
|
87
139
|
|
|
88
140
|
const expanders = useMemo<[string, string]>(() => {
|
|
89
141
|
if (expander === "caret")
|
|
90
|
-
return [
|
|
142
|
+
return ["icon-[mdi--menu-right]", "icon-[mdi--menu-down]"];
|
|
91
143
|
if (expander === "chevron")
|
|
92
|
-
return [
|
|
144
|
+
return ["icon-[mdi--chevron-right]", "icon-[mdi--chevron-down]"];
|
|
93
145
|
if (expander === "folder")
|
|
94
|
-
return [
|
|
95
|
-
return [
|
|
146
|
+
return ["icon-[mdi--folder-outline]", "icon-[mdi--folder-open-outline]"];
|
|
147
|
+
return ["icon-[mdi--plus-box-outline]", "icon-[mdi--minus-box-outline]"];
|
|
96
148
|
}, [expander]);
|
|
97
149
|
|
|
98
150
|
useImperativeHandle(
|
|
99
151
|
ref,
|
|
100
152
|
() => ({
|
|
101
153
|
open: (id: string) => expand(id),
|
|
102
|
-
|
|
154
|
+
openAndLoad: (list: string[]) => expandAndLoad(list),
|
|
155
|
+
select: (id: string) => select(id),
|
|
103
156
|
}),
|
|
104
157
|
[],
|
|
105
158
|
);
|
|
106
159
|
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const index = tree.findIndex((n) => n.id === currentSelected);
|
|
162
|
+
virtualizer.scrollToIndex(index, { align: "auto" });
|
|
163
|
+
}, [currentSelected]);
|
|
164
|
+
|
|
107
165
|
return (
|
|
108
166
|
<Section>
|
|
109
167
|
<Header
|
|
@@ -112,24 +170,27 @@ export const TreePanel = <T extends KeyValue>({
|
|
|
112
170
|
>
|
|
113
171
|
{searchable && (
|
|
114
172
|
<Search
|
|
173
|
+
value={query}
|
|
115
174
|
onSearch={onFilter}
|
|
116
175
|
allowClear
|
|
117
176
|
placeholder={filterPlaceholder}
|
|
118
177
|
{...{ noBorder: true }}
|
|
119
|
-
decorateStart={
|
|
178
|
+
decorateStart={
|
|
179
|
+
<Icon icon="icon-[mdi--filter-variant]" className="p-1" />
|
|
180
|
+
}
|
|
120
181
|
/>
|
|
121
182
|
)}
|
|
122
183
|
<ButtonGroup variant="link" className="m-0">
|
|
123
184
|
<Button
|
|
124
185
|
size="sm"
|
|
125
186
|
aria-label="expand all"
|
|
126
|
-
icon=
|
|
187
|
+
icon="icon-[mdi--arrow-expand-vertical]"
|
|
127
188
|
onClick={expandAll}
|
|
128
189
|
/>
|
|
129
190
|
<Button
|
|
130
191
|
size="sm"
|
|
131
192
|
aria-label="collapse all"
|
|
132
|
-
icon=
|
|
193
|
+
icon="icon-[mdi--arrow-collapse-vertical]"
|
|
133
194
|
onClick={collapseAll}
|
|
134
195
|
/>
|
|
135
196
|
</ButtonGroup>
|
|
@@ -151,12 +212,17 @@ export const TreePanel = <T extends KeyValue>({
|
|
|
151
212
|
checkable={checkable}
|
|
152
213
|
selectable={selectable}
|
|
153
214
|
defaultLeafIcon={defaultLeafIcon}
|
|
154
|
-
defaultNodeIcon={
|
|
215
|
+
defaultNodeIcon={
|
|
216
|
+
makeIcon?.(tree[index].data) ?? defaultNodeIcon
|
|
217
|
+
}
|
|
155
218
|
onToggle={toggleExpand}
|
|
156
219
|
onChecked={toggleCheck}
|
|
157
220
|
onSelect={select}
|
|
221
|
+
onClick={onClick}
|
|
222
|
+
leafClassName={leafClassName}
|
|
223
|
+
nodeClassName={nodeClassName}
|
|
158
224
|
>
|
|
159
|
-
{(tree[index].data &&
|
|
225
|
+
{(tree[index].data && makeLabel?.(tree[index].data)) ??
|
|
160
226
|
tree[index].label}
|
|
161
227
|
</TreeNode>
|
|
162
228
|
</div>
|