@rainbow-oh/yee-c 0.4.2 → 0.4.4
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/cjs/Cascader/cascader.js +53 -62
- package/dist/cjs/Cascader/interface.d.ts +19 -2
- package/dist/cjs/Cascader/node.js +11 -6
- package/dist/cjs/Cascader/search-list.js +2 -1
- package/dist/cjs/Cascader/utils/index.d.ts +58 -14
- package/dist/cjs/Cascader/utils/index.js +57 -73
- package/dist/cjs/Form/form-store.js +3 -0
- package/dist/cjs/Form/hooks/useWatch.js +4 -2
- package/dist/cjs/Input/style/index.less +19 -0
- package/dist/cjs/Popover/style/index.less +1 -0
- package/dist/cjs/Select/interface.d.ts +77 -0
- package/dist/cjs/Select/option.d.ts +2 -1
- package/dist/cjs/Select/option.js +6 -3
- package/dist/cjs/Select/options.d.ts +3 -2
- package/dist/cjs/Select/options.js +53 -47
- package/dist/cjs/Select/select.js +1 -1
- package/dist/cjs/Select/style/index.less +8 -0
- package/dist/cjs/Selector/interface.d.ts +4 -0
- package/dist/cjs/Selector/selector.js +3 -0
- package/dist/cjs/Selector/style/index.less +135 -116
- package/dist/cjs/Table/hooks/useFilter.js +1 -1
- package/dist/cjs/Table/interface.d.ts +4 -0
- package/dist/cjs/Table/style/index.less +2 -0
- package/dist/cjs/Table/theader/filter.js +2 -1
- package/dist/cjs/Tabs/demo/basic.js +8 -1
- package/dist/cjs/Tabs/tabs.js +3 -1
- package/dist/cjs/Tag/demo/checkable.js +1 -0
- package/dist/cjs/Tag/tag.js +1 -1
- package/dist/cjs/TextArea/textarea.js +0 -1
- package/dist/cjs/Trigger/trigger.js +1 -1
- package/dist/cjs/style/color.css +1 -1
- package/dist/cjs/style/size.css +10 -2
- package/dist/cjs/style/themes.css +1 -1
- package/dist/cjs/style/variables.css +12 -4
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/Cascader/cascader.js +49 -56
- package/dist/esm/Cascader/interface.d.ts +19 -2
- package/dist/esm/Cascader/node.js +8 -11
- package/dist/esm/Cascader/search-list.js +2 -1
- package/dist/esm/Cascader/utils/index.d.ts +58 -14
- package/dist/esm/Cascader/utils/index.js +124 -75
- package/dist/esm/Form/form-store.js +4 -0
- package/dist/esm/Form/hooks/useWatch.js +7 -2
- package/dist/esm/Input/style/index.less +19 -0
- package/dist/esm/Popover/style/index.less +1 -0
- package/dist/esm/Select/interface.d.ts +77 -0
- package/dist/esm/Select/option.d.ts +2 -1
- package/dist/esm/Select/option.js +11 -2
- package/dist/esm/Select/options.d.ts +3 -2
- package/dist/esm/Select/options.js +3 -1
- package/dist/esm/Select/select.js +1 -3
- package/dist/esm/Select/style/index.less +8 -0
- package/dist/esm/Selector/interface.d.ts +4 -0
- package/dist/esm/Selector/selector.js +4 -2
- package/dist/esm/Selector/style/index.less +135 -116
- package/dist/esm/Table/hooks/useFilter.js +1 -1
- package/dist/esm/Table/interface.d.ts +4 -0
- package/dist/esm/Table/style/index.less +2 -0
- package/dist/esm/Table/theader/filter.js +3 -4
- package/dist/esm/Tabs/demo/basic.js +4 -1
- package/dist/esm/Tabs/tabs.js +3 -1
- package/dist/esm/Tag/demo/checkable.js +1 -0
- package/dist/esm/Tag/tag.js +1 -1
- package/dist/esm/TextArea/textarea.js +0 -1
- package/dist/esm/Trigger/trigger.js +1 -1
- package/dist/esm/style/color.css +1 -1
- package/dist/esm/style/size.css +10 -2
- package/dist/esm/style/themes.css +1 -1
- package/dist/esm/style/variables.css +12 -4
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/yee-c.min.css +2 -2
- package/dist/umd/yee-c.min.js +1 -1
- package/package.json +1 -1
- package/src/Cascader/cascader.tsx +65 -62
- package/src/Cascader/interface.ts +19 -2
- package/src/Cascader/node.tsx +12 -7
- package/src/Cascader/search-list.tsx +1 -1
- package/src/Cascader/utils/index.ts +130 -68
- package/src/Form/form-store.ts +4 -0
- package/src/Form/hooks/useWatch.ts +9 -2
- package/src/Input/style/index.less +19 -0
- package/src/Popover/style/index.less +1 -0
- package/src/Select/interface.ts +87 -0
- package/src/Select/option.tsx +13 -4
- package/src/Select/options.tsx +49 -43
- package/src/Select/select.tsx +5 -2
- package/src/Select/style/index.less +8 -0
- package/src/Selector/interface.ts +4 -0
- package/src/Selector/selector.tsx +2 -0
- package/src/Selector/style/index.less +135 -116
- package/src/Table/hooks/useFilter.ts +1 -1
- package/src/Table/interface.ts +4 -0
- package/src/Table/style/index.less +2 -0
- package/src/Table/theader/filter.tsx +2 -2
- package/src/Tabs/demo/basic.tsx +7 -1
- package/src/Tabs/tabs.tsx +3 -2
- package/src/Tag/demo/checkable.tsx +1 -0
- package/src/Tag/tag.tsx +1 -1
- package/src/TextArea/textarea.tsx +0 -2
- package/src/Trigger/trigger.tsx +1 -1
- package/src/style/color.css +1 -1
- package/src/style/size.css +10 -2
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -17,7 +17,13 @@ import isEqual from '../utils/isEqual';
|
|
|
17
17
|
import mergeContextToProps from '../utils/mergeContextToProps';
|
|
18
18
|
import Column from './column';
|
|
19
19
|
import SearchList from './search-list';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
changeToView,
|
|
22
|
+
flattenOptions,
|
|
23
|
+
getChildren,
|
|
24
|
+
isSamePrefixPath,
|
|
25
|
+
pathToUid,
|
|
26
|
+
} from './utils';
|
|
21
27
|
|
|
22
28
|
import type {
|
|
23
29
|
CascaderContextValue,
|
|
@@ -41,7 +47,7 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
41
47
|
options,
|
|
42
48
|
placement = 'bottomLeft',
|
|
43
49
|
popupClassName,
|
|
44
|
-
searchable,
|
|
50
|
+
searchable = true,
|
|
45
51
|
fieldNames = {
|
|
46
52
|
label: 'label',
|
|
47
53
|
value: 'value',
|
|
@@ -97,11 +103,10 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
97
103
|
currentValue || [],
|
|
98
104
|
flatOptions,
|
|
99
105
|
{
|
|
100
|
-
needNext: true, // Set to true to expand to full path
|
|
101
106
|
multiple,
|
|
102
107
|
},
|
|
103
108
|
);
|
|
104
|
-
setColumns(newQueue
|
|
109
|
+
setColumns(newQueue);
|
|
105
110
|
setExpandedPath(newPath);
|
|
106
111
|
},
|
|
107
112
|
[flatOptions, multiple],
|
|
@@ -126,9 +131,9 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
126
131
|
|
|
127
132
|
// node mouse evt
|
|
128
133
|
const onNodeMouseEnter = (item: FlattenOption, level: number) => {
|
|
129
|
-
if (expandTrigger === 'hover' &&
|
|
134
|
+
if (expandTrigger === 'hover' && !item.isLeaf) {
|
|
130
135
|
const queue = columns.slice(0, level + 1);
|
|
131
|
-
queue.push(item.
|
|
136
|
+
queue.push(getChildren(flatOptions, item.uid));
|
|
132
137
|
setColumns(queue);
|
|
133
138
|
// Also update expandedPath to maintain highlight state
|
|
134
139
|
setExpandedPath((prev) => [
|
|
@@ -139,7 +144,6 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
139
144
|
};
|
|
140
145
|
|
|
141
146
|
// Helper function to get all descendant paths of a node
|
|
142
|
-
// Note: For very deep trees, consider adding maxDepth parameter or prevent stack overflow
|
|
143
147
|
const getAllDescendantPaths = useCallback(
|
|
144
148
|
(
|
|
145
149
|
node: FlattenOption,
|
|
@@ -148,17 +152,17 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
148
152
|
const paths: Array<Array<string | number>> = [];
|
|
149
153
|
const traverse = (currentNode: FlattenOption, depth: number) => {
|
|
150
154
|
if (depth > maxDepth) return; // Prevent stack overflow
|
|
151
|
-
if (
|
|
155
|
+
if (currentNode.isLeaf) {
|
|
152
156
|
paths.push(currentNode.path);
|
|
153
157
|
return;
|
|
154
158
|
}
|
|
155
|
-
const children = currentNode.
|
|
159
|
+
const children = getChildren(flatOptions, currentNode.uid);
|
|
156
160
|
children.forEach((child) => traverse(child, depth + 1));
|
|
157
161
|
};
|
|
158
162
|
traverse(node, 0);
|
|
159
163
|
return paths;
|
|
160
164
|
},
|
|
161
|
-
[],
|
|
165
|
+
[flatOptions],
|
|
162
166
|
);
|
|
163
167
|
|
|
164
168
|
// cascader node click
|
|
@@ -177,18 +181,17 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
177
181
|
// Check if current path is already selected
|
|
178
182
|
const isSelected = currentValue.some((v) => isEqual(v, item.path));
|
|
179
183
|
|
|
180
|
-
if (
|
|
184
|
+
if (!item.isLeaf) {
|
|
181
185
|
// Parent node: select/deselect all descendants
|
|
182
186
|
const descendantPaths = getAllDescendantPaths(item, 50); // Limit to depth 50 to prevent performance issues
|
|
183
|
-
const descendantPathStrs = descendantPaths.map((p) => p.join('-'));
|
|
184
187
|
|
|
185
188
|
// Filter out all descendant paths from current value
|
|
186
|
-
let filteredValue = currentValue.filter(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
let filteredValue = currentValue.filter(
|
|
190
|
+
(v) =>
|
|
191
|
+
!descendantPaths.some((descPath) =>
|
|
192
|
+
isSamePrefixPath(descPath, v),
|
|
193
|
+
),
|
|
194
|
+
);
|
|
192
195
|
|
|
193
196
|
if (!isSelected) {
|
|
194
197
|
// Add all descendant paths
|
|
@@ -196,12 +199,9 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
newValue = filteredValue;
|
|
199
|
-
newOpts = filteredValue.map((path) =>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return nodeMap.get(uid);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
202
|
+
newOpts = filteredValue.map((path) =>
|
|
203
|
+
path.map((_, i) => nodeMap.get(pathToUid(path.slice(0, i + 1)))),
|
|
204
|
+
);
|
|
205
205
|
} else {
|
|
206
206
|
// Leaf node: toggle individual selection
|
|
207
207
|
if (isSelected) {
|
|
@@ -209,12 +209,9 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
209
209
|
} else {
|
|
210
210
|
newValue = [...currentValue, item.path];
|
|
211
211
|
}
|
|
212
|
-
newOpts = (newValue as Array<Array<string | number>>).map((path) =>
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return nodeMap.get(uid);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
212
|
+
newOpts = (newValue as Array<Array<string | number>>).map((path) =>
|
|
213
|
+
path.map((_, i) => nodeMap.get(pathToUid(path.slice(0, i + 1)))),
|
|
214
|
+
);
|
|
218
215
|
}
|
|
219
216
|
changed = true;
|
|
220
217
|
} else if (shouldSelect) {
|
|
@@ -222,23 +219,21 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
222
219
|
newValue = multiple ? [...mergedValue, item.path] : item.path;
|
|
223
220
|
newOpts = multiple
|
|
224
221
|
? [
|
|
225
|
-
...(mergedValue as Array<Array<string | number>>).map((
|
|
226
|
-
const vs =
|
|
227
|
-
|
|
228
|
-
|
|
222
|
+
...(mergedValue as Array<Array<string | number>>).map((path) => {
|
|
223
|
+
const vs = path as Array<string | number>;
|
|
224
|
+
return vs.map((_, i) =>
|
|
225
|
+
nodeMap.get(pathToUid(vs.slice(0, i + 1))),
|
|
226
|
+
);
|
|
229
227
|
}),
|
|
230
|
-
|
|
231
|
-
item.path.
|
|
232
|
-
|
|
233
|
-
return nodeMap.get(uid);
|
|
234
|
-
}),
|
|
235
|
-
],
|
|
228
|
+
item.path.map((_, i) =>
|
|
229
|
+
nodeMap.get(pathToUid(item.path.slice(0, i + 1))),
|
|
230
|
+
),
|
|
236
231
|
]
|
|
237
232
|
: flatOptions.filter((opt: FlattenOption) => {
|
|
238
|
-
const
|
|
239
|
-
item.path.slice(0, i + 1)
|
|
233
|
+
const ancestors = item.path.map((_, i) =>
|
|
234
|
+
pathToUid(item.path.slice(0, i + 1)),
|
|
240
235
|
);
|
|
241
|
-
return
|
|
236
|
+
return ancestors.includes(opt.uid);
|
|
242
237
|
});
|
|
243
238
|
changed = true;
|
|
244
239
|
} else {
|
|
@@ -289,7 +284,7 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
289
284
|
return { options: [], selectedKeys: [] };
|
|
290
285
|
|
|
291
286
|
const { fullNode = false, onlyParentNode = false } = props;
|
|
292
|
-
const formatKey = (val: Array<string | number>) => val
|
|
287
|
+
const formatKey = (val: Array<string | number>) => pathToUid(val);
|
|
293
288
|
|
|
294
289
|
let rawValues: (string | number)[][] = [];
|
|
295
290
|
|
|
@@ -308,30 +303,27 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
308
303
|
// Filter values based on onlyParentNode option
|
|
309
304
|
let displayValues = rawValues;
|
|
310
305
|
if (multiple && onlyParentNode) {
|
|
311
|
-
// Find parent nodes that have all children selected
|
|
312
|
-
const
|
|
313
|
-
const
|
|
306
|
+
// Find parent nodes that have all (direct) children selected
|
|
307
|
+
const selectedPathKeys = new Set(rawValues.map(pathToUid));
|
|
308
|
+
const parentPaths: Array<Array<string | number>> = [];
|
|
314
309
|
|
|
315
310
|
// Check each node to see if it's a fully selected parent
|
|
316
|
-
nodeMap.forEach((node
|
|
317
|
-
if (
|
|
318
|
-
const allChildrenSelected = (node.
|
|
319
|
-
(child) =>
|
|
311
|
+
nodeMap.forEach((node) => {
|
|
312
|
+
if (!node.isLeaf) {
|
|
313
|
+
const allChildrenSelected = getChildren(flatOptions, node.uid).every(
|
|
314
|
+
(child) => selectedPathKeys.has(pathToUid(child.path)),
|
|
320
315
|
);
|
|
321
316
|
if (allChildrenSelected) {
|
|
322
|
-
|
|
317
|
+
parentPaths.push(node.path);
|
|
323
318
|
}
|
|
324
319
|
}
|
|
325
320
|
});
|
|
326
321
|
|
|
327
322
|
// Filter out child nodes whose parent is fully selected
|
|
328
|
-
displayValues = rawValues.filter(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
pathStr.startsWith(parentId + '-'),
|
|
333
|
-
);
|
|
334
|
-
});
|
|
323
|
+
displayValues = rawValues.filter(
|
|
324
|
+
(path) =>
|
|
325
|
+
!parentPaths.some((parentPath) => isSamePrefixPath(parentPath, path)),
|
|
326
|
+
);
|
|
335
327
|
}
|
|
336
328
|
|
|
337
329
|
const displayOptions = displayValues.map((valPath) => {
|
|
@@ -348,8 +340,7 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
348
340
|
options: opts,
|
|
349
341
|
});
|
|
350
342
|
} else {
|
|
351
|
-
const source =
|
|
352
|
-
opts[opts.length - 1]?.$source ?? opts[opts.length - 1];
|
|
343
|
+
const source = opts[opts.length - 1]?.$source;
|
|
353
344
|
label = (source?.[optionLabelProp as string] ??
|
|
354
345
|
'') as React.ReactNode;
|
|
355
346
|
}
|
|
@@ -367,6 +358,7 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
367
358
|
multiple,
|
|
368
359
|
mergedValue,
|
|
369
360
|
nodeMap,
|
|
361
|
+
flatOptions,
|
|
370
362
|
optionLabelProp,
|
|
371
363
|
props.fullNode,
|
|
372
364
|
props.onlyParentNode,
|
|
@@ -377,6 +369,12 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
377
369
|
popupVisible.current = newVisible;
|
|
378
370
|
};
|
|
379
371
|
|
|
372
|
+
const handleClear = () => {
|
|
373
|
+
setSearchValue('');
|
|
374
|
+
setMergedValue([]);
|
|
375
|
+
onChange?.([], []);
|
|
376
|
+
};
|
|
377
|
+
|
|
380
378
|
// Performance Note: For large datasets (1000+ options), consider:
|
|
381
379
|
// 1. Adding debounce to the search input
|
|
382
380
|
// 2. Implementing virtual scrolling for search results
|
|
@@ -390,7 +388,11 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
390
388
|
}> = [];
|
|
391
389
|
|
|
392
390
|
for (const item of flatOptions) {
|
|
393
|
-
|
|
391
|
+
// Match against the full breadcrumb (e.g. "A / B / C") so that:
|
|
392
|
+
// - searching a parent label surfaces its descendants,
|
|
393
|
+
// - the result row shows the full path to disambiguate duplicate leaf
|
|
394
|
+
// labels across different parents.
|
|
395
|
+
const text = item.labelPath.join(' / ');
|
|
394
396
|
const textLower = text.toLowerCase();
|
|
395
397
|
if (textLower.includes(lowStr)) {
|
|
396
398
|
const matches: Array<{ text: string; highlight: boolean }> = [];
|
|
@@ -435,6 +437,7 @@ const Cascader = (baseprops: CascaderProps) => {
|
|
|
435
437
|
mode={multiple ? 'multiple' : undefined}
|
|
436
438
|
value={searchValue}
|
|
437
439
|
onSearch={handleSearch}
|
|
440
|
+
onClear={handleClear}
|
|
438
441
|
/>
|
|
439
442
|
);
|
|
440
443
|
};
|
|
@@ -24,11 +24,27 @@ export type FieldNames = {
|
|
|
24
24
|
children: string;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Flattened representation of a tree node.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately a concrete type (not `Option & ...`) so that:
|
|
31
|
+
* - `.label` / `.value` / `.isLeaf` resolve to their precise types (an index
|
|
32
|
+
* signature on `Option` would otherwise widen them to `unknown`),
|
|
33
|
+
* - `.children` is a compile error — flattened nodes intentionally do not
|
|
34
|
+
* carry children; use `getChildren(data, uid)` instead.
|
|
35
|
+
* Dynamic/extra-field access should go through `$source` (the raw node).
|
|
36
|
+
*/
|
|
37
|
+
export type FlattenOption = {
|
|
38
|
+
$source: Option;
|
|
39
|
+
label: string;
|
|
40
|
+
value: string | number;
|
|
28
41
|
pid: string;
|
|
29
42
|
uid: string;
|
|
30
43
|
path: Array<string | number>;
|
|
31
|
-
|
|
44
|
+
labelPath: string[];
|
|
45
|
+
level: number;
|
|
46
|
+
isLeaf: boolean;
|
|
47
|
+
disabled?: boolean;
|
|
32
48
|
};
|
|
33
49
|
|
|
34
50
|
export type NodeProps = FlattenOption & {
|
|
@@ -104,6 +120,7 @@ export interface CascaderProps extends Omit<
|
|
|
104
120
|
multiple?: boolean;
|
|
105
121
|
/**
|
|
106
122
|
* Is it searchable
|
|
123
|
+
* @default true
|
|
107
124
|
*/
|
|
108
125
|
searchable?: boolean;
|
|
109
126
|
/**
|
package/src/Cascader/node.tsx
CHANGED
|
@@ -5,12 +5,12 @@ import Checkbox from '../Checkbox';
|
|
|
5
5
|
import Spin from '../Spin';
|
|
6
6
|
import isEqual from '../utils/isEqual';
|
|
7
7
|
import { CascaderCtx } from './cascader';
|
|
8
|
-
import {
|
|
8
|
+
import { getChildren, isHalfChecked } from './utils';
|
|
9
9
|
|
|
10
10
|
import type { NodeProps } from './interface';
|
|
11
11
|
|
|
12
12
|
const Node = (props: NodeProps) => {
|
|
13
|
-
const { uid, value, disabled, label,
|
|
13
|
+
const { uid, value, disabled, label, $source, isLeaf, path, level } = props;
|
|
14
14
|
|
|
15
15
|
const {
|
|
16
16
|
prefixCls,
|
|
@@ -19,11 +19,12 @@ const Node = (props: NodeProps) => {
|
|
|
19
19
|
mergedValue,
|
|
20
20
|
loadData,
|
|
21
21
|
nodeMap,
|
|
22
|
+
flatOptions,
|
|
22
23
|
onNodeMouseEnter,
|
|
23
24
|
onItemClick,
|
|
24
25
|
} = useContext(CascaderCtx);
|
|
25
26
|
|
|
26
|
-
const isParent =
|
|
27
|
+
const isParent = !isLeaf;
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* 0: not load
|
|
@@ -50,7 +51,7 @@ const Node = (props: NodeProps) => {
|
|
|
50
51
|
const handleLoadData = () => {
|
|
51
52
|
if (loadData && isLeaf === false) {
|
|
52
53
|
setLoad(1);
|
|
53
|
-
loadData(
|
|
54
|
+
loadData($source)
|
|
54
55
|
?.then(() => {
|
|
55
56
|
setLoad(2);
|
|
56
57
|
})
|
|
@@ -79,13 +80,17 @@ const Node = (props: NodeProps) => {
|
|
|
79
80
|
const [checkedAll, indeterminate] = useMemo(() => {
|
|
80
81
|
const currentNode = nodeMap.get(uid);
|
|
81
82
|
if (!multiple || !currentNode || !isParent) return [false, false];
|
|
82
|
-
return isHalfChecked(
|
|
83
|
-
|
|
83
|
+
return isHalfChecked(
|
|
84
|
+
mergedValue as (string | number)[][],
|
|
85
|
+
currentNode,
|
|
86
|
+
getChildren(flatOptions, uid),
|
|
87
|
+
);
|
|
88
|
+
}, [nodeMap, uid, mergedValue, flatOptions, isParent, multiple]);
|
|
84
89
|
|
|
85
90
|
const clsName = clsx(
|
|
86
91
|
`${prefixCls}-menu-item`,
|
|
87
92
|
{
|
|
88
|
-
[`${prefixCls}-menu-item-expand`]:
|
|
93
|
+
[`${prefixCls}-menu-item-expand`]: !isLeaf,
|
|
89
94
|
[`${prefixCls}-menu-item-disabled`]: disabled,
|
|
90
95
|
active: expandedPath[level]?.key === uid,
|
|
91
96
|
},
|
|
@@ -35,7 +35,7 @@ const SearchList = (props: SearchListProps) => {
|
|
|
35
35
|
<ul className={`${prefixCls}-filter-menu`}>
|
|
36
36
|
{items.length ? (
|
|
37
37
|
items.map((item) => {
|
|
38
|
-
const title = item.data
|
|
38
|
+
const title = item.data.$source?.title || item.data.labelPath.join(' / ');
|
|
39
39
|
|
|
40
40
|
return (
|
|
41
41
|
<li
|
|
@@ -16,70 +16,104 @@ export function hasChild(
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* Separator used to build a node's `uid` from its value path. A control
|
|
20
|
+
* character is used on purpose so it can never appear inside a user value —
|
|
21
|
+
* this prevents collisions like value `"a-1"` vs path `["a", "1"]` that a plain
|
|
22
|
+
* `'-'` separator would produce.
|
|
23
|
+
*/
|
|
24
|
+
export const UID_SEP = '';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the canonical uid for a value path. Use this everywhere a path needs
|
|
28
|
+
* to be turned into a key (nodeMap lookups, selected keys, comparisons) so the
|
|
29
|
+
* format stays consistent with the `uid`/`pid` produced by `flattenOptions`.
|
|
30
|
+
* @param path - The value path
|
|
31
|
+
*/
|
|
32
|
+
export const pathToUid = (path: Array<string | number>): string =>
|
|
33
|
+
path.map(String).join(UID_SEP);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the direct flattened children of the node identified by `uid`.
|
|
37
|
+
*
|
|
38
|
+
* Flattened nodes intentionally do not carry a `children` reference (it used to
|
|
39
|
+
* point at the raw, un-flattened subtree and caused type lies). Derive children
|
|
40
|
+
* from the flat list via `pid` instead — single source of truth.
|
|
41
|
+
* @param data - The flattened options data
|
|
42
|
+
* @param uid - The parent node's uid
|
|
43
|
+
*/
|
|
44
|
+
export const getChildren = (
|
|
45
|
+
data: Array<FlattenOption>,
|
|
46
|
+
uid: string,
|
|
47
|
+
): FlattenOption[] => data.filter((item) => item.pid === uid);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Find the node in `data` whose parent is `pid` and whose value is `value`.
|
|
20
51
|
* @internal
|
|
21
52
|
*/
|
|
22
|
-
const
|
|
53
|
+
const findInColumn = (
|
|
23
54
|
data: Array<FlattenOption>,
|
|
55
|
+
pid: string,
|
|
24
56
|
value: string | number,
|
|
25
|
-
):
|
|
26
|
-
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
for (let i = 0; i < data.length; i++) {
|
|
31
|
-
const item = data[i];
|
|
32
|
-
if (item.uid === value) {
|
|
33
|
-
const nexts = hasChild(item)
|
|
34
|
-
? (item.children as Array<FlattenOption>).filter((c) => c.pid === value)
|
|
35
|
-
: [];
|
|
36
|
-
return [
|
|
37
|
-
nexts,
|
|
38
|
-
{ id: item.value as string | number, text: item.label as string },
|
|
39
|
-
];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return [];
|
|
43
|
-
};
|
|
57
|
+
): FlattenOption | undefined =>
|
|
58
|
+
data.find((item) => item.pid === pid && item.value === value);
|
|
44
59
|
|
|
45
60
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
61
|
+
* Reconstruct cascade columns and the highlight path from a value path.
|
|
62
|
+
*
|
|
63
|
+
* Walks the flattened options level by level, matching each value segment
|
|
64
|
+
* against the correct parent column via `pid` + `value`. Returns:
|
|
65
|
+
* - `queue`: the columns to render (column 0 = root, then each matched
|
|
66
|
+
* node's children column).
|
|
67
|
+
* - `path`: the highlight breadcrumb shaped as `{ key: uid, label }`, so it
|
|
68
|
+
* aligns with `expandedPath[level].key === uid` in the node view.
|
|
69
|
+
*
|
|
70
|
+
* Stops safely at the last valid segment if the value path does not exist in
|
|
71
|
+
* the data source.
|
|
72
|
+
*
|
|
73
|
+
* @param arr - The selected path. In `multiple` mode the value is an array of
|
|
74
|
+
* paths; the first path is used to drive which columns get expanded.
|
|
48
75
|
* @param data - The flattened options data
|
|
49
76
|
* @param options - Optional configuration
|
|
50
|
-
* @returns A tuple of [queue, path]
|
|
77
|
+
* @returns A tuple of `[queue, path]`
|
|
51
78
|
*/
|
|
52
79
|
export const changeToView = (
|
|
53
|
-
arr: Array<string | number
|
|
80
|
+
arr: Array<string | number> | Array<Array<string | number>>,
|
|
54
81
|
data: Array<FlattenOption>,
|
|
55
82
|
options?: {
|
|
56
|
-
needNext?: boolean;
|
|
57
83
|
multiple?: boolean;
|
|
58
84
|
},
|
|
59
|
-
) => {
|
|
60
|
-
const {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
85
|
+
): [FlattenOption[][], Array<{ key: string; label: string }>] => {
|
|
86
|
+
const { multiple } = options || {};
|
|
87
|
+
const queue: FlattenOption[][] = [];
|
|
88
|
+
const path: Array<{ key: string; label: string }> = [];
|
|
89
|
+
|
|
90
|
+
// Column 0 (root level) is always present so the popup can open even with
|
|
91
|
+
// no selection.
|
|
92
|
+
queue.push(data.filter((item) => item.level === 0));
|
|
93
|
+
|
|
94
|
+
if (!Array.isArray(arr) || arr.length === 0) {
|
|
95
|
+
return [queue, path];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// In multiple mode the value is an array of paths; use the first one to
|
|
99
|
+
// drive column expansion.
|
|
100
|
+
const segments: Array<string | number> =
|
|
101
|
+
multiple && Array.isArray(arr[0])
|
|
102
|
+
? (arr[0] as Array<string | number>)
|
|
103
|
+
: (arr as Array<string | number>);
|
|
63
104
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
105
|
+
let currentPid = '';
|
|
106
|
+
for (let i = 0; i < segments.length; i++) {
|
|
107
|
+
const matched = findInColumn(data, currentPid, segments[i]);
|
|
108
|
+
if (!matched) break; // value path doesn't exist in the data source
|
|
109
|
+
path.push({ key: matched.uid, label: matched.label as string });
|
|
110
|
+
if (!matched.isLeaf) {
|
|
111
|
+
queue.push(getChildren(data, matched.uid));
|
|
69
112
|
}
|
|
70
|
-
|
|
71
|
-
do {
|
|
72
|
-
_queue.push(_data);
|
|
73
|
-
const result = getNextQueueElement(_data, _arr[i]);
|
|
74
|
-
if (result.length) {
|
|
75
|
-
const [_dataNext, _pathItem] = result;
|
|
76
|
-
_data = _dataNext;
|
|
77
|
-
_path.push(_pathItem);
|
|
78
|
-
}
|
|
79
|
-
i++;
|
|
80
|
-
} while (needNext ? i <= _arr.length : i < _arr.length);
|
|
113
|
+
currentPid = matched.uid;
|
|
81
114
|
}
|
|
82
|
-
|
|
115
|
+
|
|
116
|
+
return [queue, path];
|
|
83
117
|
};
|
|
84
118
|
|
|
85
119
|
// Assemble Tags Options
|
|
@@ -302,11 +336,11 @@ export const changeToView = (
|
|
|
302
336
|
// }
|
|
303
337
|
|
|
304
338
|
/**
|
|
305
|
-
* Format a path
|
|
339
|
+
* Format a value path into a key string. Alias of `pathToUid` so all path→key
|
|
340
|
+
* conversions share one collision-safe format.
|
|
306
341
|
* @param val - The path array
|
|
307
|
-
* @returns A string key joined by '-'
|
|
308
342
|
*/
|
|
309
|
-
export const formatKey = (val: Array<string | number>) => val
|
|
343
|
+
export const formatKey = (val: Array<string | number>): string => pathToUid(val);
|
|
310
344
|
|
|
311
345
|
/**
|
|
312
346
|
* Check if a path has the same prefix as another path
|
|
@@ -330,14 +364,16 @@ export const isSamePrefixPath = (
|
|
|
330
364
|
* Determine if a node is fully selected or half selected (indeterminate)
|
|
331
365
|
* @param value - The selected value paths
|
|
332
366
|
* @param item - The flatten option node to check
|
|
367
|
+
* @param children - The node's direct flattened children (derive via `getChildren`)
|
|
333
368
|
* @returns A tuple [checkedAll, indeterminate]
|
|
334
369
|
*/
|
|
335
370
|
export function isHalfChecked(
|
|
336
371
|
value: Array<Array<string | number>>,
|
|
337
372
|
item: FlattenOption,
|
|
373
|
+
children: FlattenOption[],
|
|
338
374
|
) {
|
|
339
|
-
if (!
|
|
340
|
-
const total =
|
|
375
|
+
if (!children.length) return [false, false];
|
|
376
|
+
const total = children.length;
|
|
341
377
|
const prefixPath = item.path;
|
|
342
378
|
|
|
343
379
|
const filtered = value.filter((path) => isSamePrefixPath(prefixPath, path));
|
|
@@ -348,14 +384,29 @@ export function isHalfChecked(
|
|
|
348
384
|
}
|
|
349
385
|
|
|
350
386
|
/**
|
|
351
|
-
*
|
|
387
|
+
* Default maximum tree depth accepted by `flattenOptions`. Guards against
|
|
388
|
+
* stack overflow on pathological/deeply-nested input.
|
|
389
|
+
*/
|
|
390
|
+
const DEFAULT_MAX_DEPTH = 100;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Flatten a tree structure into an array for easier searching and traversal.
|
|
394
|
+
*
|
|
395
|
+
* The flattened nodes intentionally do NOT carry a `children` field — use
|
|
396
|
+
* `getChildren(data, uid)` to read a node's children. `uid`/`pid` are built
|
|
397
|
+
* via `pathToUid` (collision-safe). Includes a depth cap and a cycle guard so
|
|
398
|
+
* malformed input (very deep or self-referencing trees) cannot overflow the
|
|
399
|
+
* stack.
|
|
400
|
+
*
|
|
352
401
|
* @param tree - The tree data to flatten
|
|
353
402
|
* @param fieldNames - Custom field names for label, value, and children
|
|
403
|
+
* @param maxDepth - Maximum nesting depth (default 100)
|
|
354
404
|
* @returns An array of flattened options with additional metadata
|
|
355
405
|
*/
|
|
356
406
|
export const flattenOptions = (
|
|
357
407
|
tree: Array<Record<string, unknown>>,
|
|
358
|
-
fieldNames
|
|
408
|
+
fieldNames?: FieldNames,
|
|
409
|
+
maxDepth: number = DEFAULT_MAX_DEPTH,
|
|
359
410
|
): FlattenOption[] => {
|
|
360
411
|
const res: FlattenOption[] = [];
|
|
361
412
|
const {
|
|
@@ -363,29 +414,40 @@ export const flattenOptions = (
|
|
|
363
414
|
value = 'value',
|
|
364
415
|
children = 'children',
|
|
365
416
|
} = fieldNames || {};
|
|
417
|
+
// Object-identity set of nodes on the current DFS path, for cycle detection.
|
|
418
|
+
const seen = new Set<Record<string, unknown>>();
|
|
366
419
|
const dfs = (
|
|
367
420
|
nodes: Array<Record<string, unknown>>,
|
|
368
421
|
parents: Array<string | number> = [],
|
|
422
|
+
labelParents: string[] = [],
|
|
369
423
|
) => {
|
|
370
424
|
nodes.forEach((node: Record<string, unknown>) => {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const path = [...parents, wrappered['value']] as Array<string | number>;
|
|
378
|
-
const uid = path.join('-');
|
|
379
|
-
const pid = parents.join('-');
|
|
425
|
+
if (parents.length >= maxDepth) return; // depth guard
|
|
426
|
+
if (seen.has(node)) return; // cycle guard (back-edge to an ancestor)
|
|
427
|
+
const nodeLabel = node[label] as string;
|
|
428
|
+
const path = [...parents, node[value]] as Array<string | number>;
|
|
429
|
+
const labelPath = [...labelParents, nodeLabel];
|
|
430
|
+
const uid = pathToUid(path);
|
|
380
431
|
res.push({
|
|
381
|
-
|
|
382
|
-
|
|
432
|
+
$source: node as Option,
|
|
433
|
+
label: nodeLabel,
|
|
434
|
+
value: node[value] as string | number,
|
|
435
|
+
pid: pathToUid(parents),
|
|
383
436
|
uid,
|
|
384
437
|
path,
|
|
438
|
+
labelPath,
|
|
385
439
|
level: parents.length,
|
|
386
|
-
isLeaf: !hasChild(
|
|
387
|
-
}
|
|
388
|
-
if (
|
|
440
|
+
isLeaf: !hasChild(node, children),
|
|
441
|
+
});
|
|
442
|
+
if (hasChild(node, children)) {
|
|
443
|
+
seen.add(node);
|
|
444
|
+
dfs(
|
|
445
|
+
node[children] as Array<Record<string, unknown>>,
|
|
446
|
+
path,
|
|
447
|
+
labelPath,
|
|
448
|
+
);
|
|
449
|
+
seen.delete(node);
|
|
450
|
+
}
|
|
389
451
|
});
|
|
390
452
|
};
|
|
391
453
|
dfs(tree);
|
package/src/Form/form-store.ts
CHANGED
|
@@ -494,6 +494,10 @@ export class FormStore {
|
|
|
494
494
|
} else {
|
|
495
495
|
this.store = { ...this.store, [normalizedName]: value };
|
|
496
496
|
}
|
|
497
|
+
// When value is not undefined, notify watchers to trigger re-rendering of useWatch
|
|
498
|
+
if (value !== undefined) {
|
|
499
|
+
this.notifyWatchers(normalizedName);
|
|
500
|
+
}
|
|
497
501
|
}
|
|
498
502
|
}
|
|
499
503
|
entity.onStoreChange?.();
|