@cloud-ru/ds-tree 1.0.0
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/CHANGELOG.md +8 -0
- package/LICENSE +201 -0
- package/README.md +341 -0
- package/dist/cjs/components/Tree/Tree.d.ts +4 -0
- package/dist/cjs/components/Tree/Tree.js +31 -0
- package/dist/cjs/components/Tree/index.d.ts +1 -0
- package/dist/cjs/components/Tree/index.js +17 -0
- package/dist/cjs/components/Tree/styles.module.css +17 -0
- package/dist/cjs/components/Tree/utils.d.ts +4 -0
- package/dist/cjs/components/Tree/utils.js +15 -0
- package/dist/cjs/components/index.d.ts +1 -0
- package/dist/cjs/components/index.js +17 -0
- package/dist/cjs/constants.d.ts +26 -0
- package/dist/cjs/constants.js +29 -0
- package/dist/cjs/contexts/TreeContext.d.ts +30 -0
- package/dist/cjs/contexts/TreeContext.js +108 -0
- package/dist/cjs/helperComponents/ExpandableTreeNode/ExpandableTreeNode.d.ts +11 -0
- package/dist/cjs/helperComponents/ExpandableTreeNode/ExpandableTreeNode.js +87 -0
- package/dist/cjs/helperComponents/ExpandableTreeNode/index.d.ts +1 -0
- package/dist/cjs/helperComponents/ExpandableTreeNode/index.js +17 -0
- package/dist/cjs/helperComponents/ExpandableTreeNode/styles.module.css +37 -0
- package/dist/cjs/helperComponents/TreeItem/TreeItem.d.ts +8 -0
- package/dist/cjs/helperComponents/TreeItem/TreeItem.js +16 -0
- package/dist/cjs/helperComponents/TreeItem/index.d.ts +1 -0
- package/dist/cjs/helperComponents/TreeItem/index.js +17 -0
- package/dist/cjs/helperComponents/TreeLine/TreeLine.d.ts +10 -0
- package/dist/cjs/helperComponents/TreeLine/TreeLine.js +13 -0
- package/dist/cjs/helperComponents/TreeLine/index.d.ts +1 -0
- package/dist/cjs/helperComponents/TreeLine/index.js +17 -0
- package/dist/cjs/helperComponents/TreeLine/styles.module.css +37 -0
- package/dist/cjs/helperComponents/TreeNode/TreeNode.d.ts +4 -0
- package/dist/cjs/helperComponents/TreeNode/TreeNode.js +187 -0
- package/dist/cjs/helperComponents/TreeNode/components/TreeNodeActions.d.ts +15 -0
- package/dist/cjs/helperComponents/TreeNode/components/TreeNodeActions.js +76 -0
- package/dist/cjs/helperComponents/TreeNode/components/TreeNodeHref.d.ts +7 -0
- package/dist/cjs/helperComponents/TreeNode/components/TreeNodeHref.js +12 -0
- package/dist/cjs/helperComponents/TreeNode/components/index.d.ts +2 -0
- package/dist/cjs/helperComponents/TreeNode/components/index.js +18 -0
- package/dist/cjs/helperComponents/TreeNode/constants.d.ts +5 -0
- package/dist/cjs/helperComponents/TreeNode/constants.js +10 -0
- package/dist/cjs/helperComponents/TreeNode/index.d.ts +1 -0
- package/dist/cjs/helperComponents/TreeNode/index.js +17 -0
- package/dist/cjs/helperComponents/TreeNode/styles.module.css +474 -0
- package/dist/cjs/helperComponents/TreeNode/types.d.ts +9 -0
- package/dist/cjs/helperComponents/TreeNode/types.js +2 -0
- package/dist/cjs/helperComponents/TreeNode/utils.d.ts +3 -0
- package/dist/cjs/helperComponents/TreeNode/utils.js +10 -0
- package/dist/cjs/helperComponents/index.d.ts +4 -0
- package/dist/cjs/helperComponents/index.js +20 -0
- package/dist/cjs/helpers/checkNestedNodesSelection.d.ts +9 -0
- package/dist/cjs/helpers/checkNestedNodesSelection.js +15 -0
- package/dist/cjs/helpers/collectEmptyNestedNodesInExpanded.d.ts +9 -0
- package/dist/cjs/helpers/collectEmptyNestedNodesInExpanded.js +21 -0
- package/dist/cjs/helpers/collectHierarchicalAncestors.d.ts +3 -0
- package/dist/cjs/helpers/collectHierarchicalAncestors.js +18 -0
- package/dist/cjs/helpers/collectIds.d.ts +9 -0
- package/dist/cjs/helpers/collectIds.js +16 -0
- package/dist/cjs/helpers/extractTreeNodeTitle.d.ts +6 -0
- package/dist/cjs/helpers/extractTreeNodeTitle.js +9 -0
- package/dist/cjs/helpers/findAllChildNodeIds.d.ts +9 -0
- package/dist/cjs/helpers/findAllChildNodeIds.js +24 -0
- package/dist/cjs/helpers/findAllExpandedChildNodeIds.d.ts +8 -0
- package/dist/cjs/helpers/findAllExpandedChildNodeIds.js +21 -0
- package/dist/cjs/helpers/getSearchedTreeItems.d.ts +23 -0
- package/dist/cjs/helpers/getSearchedTreeItems.js +41 -0
- package/dist/cjs/helpers/getSearchedTreeNodeById.d.ts +19 -0
- package/dist/cjs/helpers/getSearchedTreeNodeById.js +35 -0
- package/dist/cjs/helpers/index.d.ts +13 -0
- package/dist/cjs/helpers/index.js +29 -0
- package/dist/cjs/helpers/lookupTreeForSelectedNodes.d.ts +15 -0
- package/dist/cjs/helpers/lookupTreeForSelectedNodes.js +21 -0
- package/dist/cjs/helpers/sortTreeItemsByTitle.d.ts +13 -0
- package/dist/cjs/helpers/sortTreeItemsByTitle.js +14 -0
- package/dist/cjs/helpers/traverse.d.ts +11 -0
- package/dist/cjs/helpers/traverse.js +25 -0
- package/dist/cjs/helpers/traverseWithTarget.d.ts +14 -0
- package/dist/cjs/helpers/traverseWithTarget.js +30 -0
- package/dist/cjs/helpers/updateTreeNode.d.ts +13 -0
- package/dist/cjs/helpers/updateTreeNode.js +34 -0
- package/dist/cjs/hooks/index.d.ts +2 -0
- package/dist/cjs/hooks/index.js +18 -0
- package/dist/cjs/hooks/internalHooks.d.ts +4 -0
- package/dist/cjs/hooks/internalHooks.js +29 -0
- package/dist/cjs/hooks/useSearchableTree.d.ts +52 -0
- package/dist/cjs/hooks/useSearchableTree.js +113 -0
- package/dist/cjs/hooks/useTreeMultiSelection.d.ts +33 -0
- package/dist/cjs/hooks/useTreeMultiSelection.js +37 -0
- package/dist/cjs/index.d.ts +17 -0
- package/dist/cjs/index.js +47 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/style.css +565 -0
- package/dist/cjs/types.d.ts +151 -0
- package/dist/cjs/types.js +2 -0
- package/dist/esm/components/Tree/Tree.d.ts +4 -0
- package/dist/esm/components/Tree/Tree.js +25 -0
- package/dist/esm/components/Tree/index.d.ts +1 -0
- package/dist/esm/components/Tree/index.js +1 -0
- package/dist/esm/components/Tree/styles.module.css +17 -0
- package/dist/esm/components/Tree/utils.d.ts +4 -0
- package/dist/esm/components/Tree/utils.js +12 -0
- package/dist/esm/components/index.d.ts +1 -0
- package/dist/esm/components/index.js +1 -0
- package/dist/esm/constants.d.ts +26 -0
- package/dist/esm/constants.js +26 -0
- package/dist/esm/contexts/TreeContext.d.ts +30 -0
- package/dist/esm/contexts/TreeContext.js +103 -0
- package/dist/esm/helperComponents/ExpandableTreeNode/ExpandableTreeNode.d.ts +11 -0
- package/dist/esm/helperComponents/ExpandableTreeNode/ExpandableTreeNode.js +81 -0
- package/dist/esm/helperComponents/ExpandableTreeNode/index.d.ts +1 -0
- package/dist/esm/helperComponents/ExpandableTreeNode/index.js +1 -0
- package/dist/esm/helperComponents/ExpandableTreeNode/styles.module.css +37 -0
- package/dist/esm/helperComponents/TreeItem/TreeItem.d.ts +8 -0
- package/dist/esm/helperComponents/TreeItem/TreeItem.js +13 -0
- package/dist/esm/helperComponents/TreeItem/index.d.ts +1 -0
- package/dist/esm/helperComponents/TreeItem/index.js +1 -0
- package/dist/esm/helperComponents/TreeLine/TreeLine.d.ts +10 -0
- package/dist/esm/helperComponents/TreeLine/TreeLine.js +7 -0
- package/dist/esm/helperComponents/TreeLine/index.d.ts +1 -0
- package/dist/esm/helperComponents/TreeLine/index.js +1 -0
- package/dist/esm/helperComponents/TreeLine/styles.module.css +37 -0
- package/dist/esm/helperComponents/TreeNode/TreeNode.d.ts +4 -0
- package/dist/esm/helperComponents/TreeNode/TreeNode.js +181 -0
- package/dist/esm/helperComponents/TreeNode/components/TreeNodeActions.d.ts +15 -0
- package/dist/esm/helperComponents/TreeNode/components/TreeNodeActions.js +70 -0
- package/dist/esm/helperComponents/TreeNode/components/TreeNodeHref.d.ts +7 -0
- package/dist/esm/helperComponents/TreeNode/components/TreeNodeHref.js +6 -0
- package/dist/esm/helperComponents/TreeNode/components/index.d.ts +2 -0
- package/dist/esm/helperComponents/TreeNode/components/index.js +2 -0
- package/dist/esm/helperComponents/TreeNode/constants.d.ts +5 -0
- package/dist/esm/helperComponents/TreeNode/constants.js +7 -0
- package/dist/esm/helperComponents/TreeNode/index.d.ts +1 -0
- package/dist/esm/helperComponents/TreeNode/index.js +1 -0
- package/dist/esm/helperComponents/TreeNode/styles.module.css +474 -0
- package/dist/esm/helperComponents/TreeNode/types.d.ts +9 -0
- package/dist/esm/helperComponents/TreeNode/types.js +1 -0
- package/dist/esm/helperComponents/TreeNode/utils.d.ts +3 -0
- package/dist/esm/helperComponents/TreeNode/utils.js +6 -0
- package/dist/esm/helperComponents/index.d.ts +4 -0
- package/dist/esm/helperComponents/index.js +4 -0
- package/dist/esm/helpers/checkNestedNodesSelection.d.ts +9 -0
- package/dist/esm/helpers/checkNestedNodesSelection.js +12 -0
- package/dist/esm/helpers/collectEmptyNestedNodesInExpanded.d.ts +9 -0
- package/dist/esm/helpers/collectEmptyNestedNodesInExpanded.js +18 -0
- package/dist/esm/helpers/collectHierarchicalAncestors.d.ts +3 -0
- package/dist/esm/helpers/collectHierarchicalAncestors.js +15 -0
- package/dist/esm/helpers/collectIds.d.ts +9 -0
- package/dist/esm/helpers/collectIds.js +13 -0
- package/dist/esm/helpers/extractTreeNodeTitle.d.ts +6 -0
- package/dist/esm/helpers/extractTreeNodeTitle.js +5 -0
- package/dist/esm/helpers/findAllChildNodeIds.d.ts +9 -0
- package/dist/esm/helpers/findAllChildNodeIds.js +21 -0
- package/dist/esm/helpers/findAllExpandedChildNodeIds.d.ts +8 -0
- package/dist/esm/helpers/findAllExpandedChildNodeIds.js +18 -0
- package/dist/esm/helpers/getSearchedTreeItems.d.ts +23 -0
- package/dist/esm/helpers/getSearchedTreeItems.js +37 -0
- package/dist/esm/helpers/getSearchedTreeNodeById.d.ts +19 -0
- package/dist/esm/helpers/getSearchedTreeNodeById.js +31 -0
- package/dist/esm/helpers/index.d.ts +13 -0
- package/dist/esm/helpers/index.js +13 -0
- package/dist/esm/helpers/lookupTreeForSelectedNodes.d.ts +15 -0
- package/dist/esm/helpers/lookupTreeForSelectedNodes.js +18 -0
- package/dist/esm/helpers/sortTreeItemsByTitle.d.ts +13 -0
- package/dist/esm/helpers/sortTreeItemsByTitle.js +10 -0
- package/dist/esm/helpers/traverse.d.ts +11 -0
- package/dist/esm/helpers/traverse.js +21 -0
- package/dist/esm/helpers/traverseWithTarget.d.ts +14 -0
- package/dist/esm/helpers/traverseWithTarget.js +26 -0
- package/dist/esm/helpers/updateTreeNode.d.ts +13 -0
- package/dist/esm/helpers/updateTreeNode.js +30 -0
- package/dist/esm/hooks/index.d.ts +2 -0
- package/dist/esm/hooks/index.js +2 -0
- package/dist/esm/hooks/internalHooks.d.ts +4 -0
- package/dist/esm/hooks/internalHooks.js +24 -0
- package/dist/esm/hooks/useSearchableTree.d.ts +52 -0
- package/dist/esm/hooks/useSearchableTree.js +110 -0
- package/dist/esm/hooks/useTreeMultiSelection.d.ts +33 -0
- package/dist/esm/hooks/useTreeMultiSelection.js +34 -0
- package/dist/esm/index.d.ts +17 -0
- package/dist/esm/index.js +17 -0
- package/dist/esm/style.css +565 -0
- package/dist/esm/types.d.ts +151 -0
- package/dist/esm/types.js +1 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/package.json +51 -0
- package/src/components/Tree/Tree.tsx +53 -0
- package/src/components/Tree/index.ts +1 -0
- package/src/components/Tree/styles.module.scss +22 -0
- package/src/components/Tree/utils.ts +16 -0
- package/src/components/index.ts +1 -0
- package/src/constants.ts +29 -0
- package/src/contexts/TreeContext.tsx +189 -0
- package/src/helperComponents/ExpandableTreeNode/ExpandableTreeNode.tsx +156 -0
- package/src/helperComponents/ExpandableTreeNode/index.ts +1 -0
- package/src/helperComponents/ExpandableTreeNode/styles.module.scss +55 -0
- package/src/helperComponents/TreeItem/TreeItem.tsx +26 -0
- package/src/helperComponents/TreeItem/index.ts +1 -0
- package/src/helperComponents/TreeLine/TreeLine.tsx +27 -0
- package/src/helperComponents/TreeLine/index.ts +1 -0
- package/src/helperComponents/TreeLine/styles.module.scss +58 -0
- package/src/helperComponents/TreeNode/TreeNode.tsx +389 -0
- package/src/helperComponents/TreeNode/components/TreeNodeActions.tsx +129 -0
- package/src/helperComponents/TreeNode/components/TreeNodeHref.tsx +29 -0
- package/src/helperComponents/TreeNode/components/index.ts +2 -0
- package/src/helperComponents/TreeNode/constants.ts +7 -0
- package/src/helperComponents/TreeNode/index.ts +1 -0
- package/src/helperComponents/TreeNode/styles.module.scss +300 -0
- package/src/helperComponents/TreeNode/types.ts +11 -0
- package/src/helperComponents/TreeNode/utils.ts +9 -0
- package/src/helperComponents/index.ts +4 -0
- package/src/helpers/checkNestedNodesSelection.ts +15 -0
- package/src/helpers/collectEmptyNestedNodesInExpanded.ts +23 -0
- package/src/helpers/collectHierarchicalAncestors.ts +22 -0
- package/src/helpers/collectIds.ts +17 -0
- package/src/helpers/extractTreeNodeTitle.ts +8 -0
- package/src/helpers/findAllChildNodeIds.ts +27 -0
- package/src/helpers/findAllExpandedChildNodeIds.ts +24 -0
- package/src/helpers/getSearchedTreeItems.ts +63 -0
- package/src/helpers/getSearchedTreeNodeById.ts +54 -0
- package/src/helpers/index.ts +13 -0
- package/src/helpers/lookupTreeForSelectedNodes.ts +33 -0
- package/src/helpers/sortTreeItemsByTitle.ts +26 -0
- package/src/helpers/traverse.ts +28 -0
- package/src/helpers/traverseWithTarget.ts +41 -0
- package/src/helpers/updateTreeNode.ts +46 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/internalHooks.ts +30 -0
- package/src/hooks/useSearchableTree.ts +190 -0
- package/src/hooks/useTreeMultiSelection.ts +67 -0
- package/src/index.ts +17 -0
- package/src/types.ts +162 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Собирает id всех недеактивированных узлов в поддереве.
|
|
3
|
+
*
|
|
4
|
+
* @param nodes Стартовые узлы обхода.
|
|
5
|
+
* @returns Массив id всех доступных узлов.
|
|
6
|
+
* @function findAllChildNodeIds
|
|
7
|
+
*/
|
|
8
|
+
export function findAllChildNodeIds(nodes) {
|
|
9
|
+
const stack = [...nodes];
|
|
10
|
+
const ids = [];
|
|
11
|
+
let node;
|
|
12
|
+
while ((node = stack.pop())) {
|
|
13
|
+
if (!node.disabled) {
|
|
14
|
+
ids.push(node.id);
|
|
15
|
+
if (node.nested?.length) {
|
|
16
|
+
stack.push(...node.nested);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return ids;
|
|
21
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TreeNodeId, TreeNodeProps } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Возвращает id узлов в поддереве, проходя только по раскрытым веткам.
|
|
4
|
+
*
|
|
5
|
+
* @param nodes Стартовые узлы обхода.
|
|
6
|
+
* @param expandedNodes Список id раскрытых узлов.
|
|
7
|
+
*/
|
|
8
|
+
export declare function findAllExpandedChildNodeIds(nodes: TreeNodeProps[], expandedNodes: TreeNodeId[]): string[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Возвращает id узлов в поддереве, проходя только по раскрытым веткам.
|
|
3
|
+
*
|
|
4
|
+
* @param nodes Стартовые узлы обхода.
|
|
5
|
+
* @param expandedNodes Список id раскрытых узлов.
|
|
6
|
+
*/
|
|
7
|
+
export function findAllExpandedChildNodeIds(nodes, expandedNodes) {
|
|
8
|
+
const stack = [...nodes];
|
|
9
|
+
const ids = [];
|
|
10
|
+
let node;
|
|
11
|
+
while ((node = stack.shift())) {
|
|
12
|
+
ids.push(node.id);
|
|
13
|
+
if (node.nested?.length && expandedNodes.includes(node.id)) {
|
|
14
|
+
stack.unshift(...node.nested);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return ids;
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ExtendedTreeNodeProps } from '../types.js';
|
|
2
|
+
type SearchParams = {
|
|
3
|
+
/** Корневые узлы дерева, по которым выполняется поиск. */
|
|
4
|
+
tree: ExtendedTreeNodeProps[];
|
|
5
|
+
searchOptions?: Partial<{
|
|
6
|
+
/** Поисковая строка (case-insensitive substring match по `title` узла). */
|
|
7
|
+
query: string;
|
|
8
|
+
/**
|
|
9
|
+
* Если `true` и parent матчит query — в результат попадают **все** его дети
|
|
10
|
+
* без дополнительной фильтрации. По умолчанию `false`: и parent, и дети
|
|
11
|
+
* фильтруются независимо.
|
|
12
|
+
*/
|
|
13
|
+
includeChildrenMatchedParent: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Фильтрует дерево по поисковой строке с сохранением иерархии подходящих узлов.
|
|
18
|
+
*
|
|
19
|
+
* @param params {@link SearchParams} — дерево и опции поиска.
|
|
20
|
+
* @returns Новое дерево, содержащее только совпавшие узлы и их путь в иерархии.
|
|
21
|
+
*/
|
|
22
|
+
export declare const getSearchedTreeItems: ({ tree, searchOptions }: SearchParams) => ExtendedTreeNodeProps[];
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { extractTreeNodeTitle } from './extractTreeNodeTitle.js';
|
|
2
|
+
const isMatchedTreeItem = (search) => {
|
|
3
|
+
const searchLower = search.toLocaleLowerCase();
|
|
4
|
+
return (treeItem) => {
|
|
5
|
+
const currentValue = extractTreeNodeTitle(treeItem);
|
|
6
|
+
return currentValue.toLocaleLowerCase().includes(searchLower);
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Фильтрует дерево по поисковой строке с сохранением иерархии подходящих узлов.
|
|
11
|
+
*
|
|
12
|
+
* @param params {@link SearchParams} — дерево и опции поиска.
|
|
13
|
+
* @returns Новое дерево, содержащее только совпавшие узлы и их путь в иерархии.
|
|
14
|
+
*/
|
|
15
|
+
export const getSearchedTreeItems = ({ tree, searchOptions }) => {
|
|
16
|
+
if (!searchOptions?.query)
|
|
17
|
+
return tree;
|
|
18
|
+
const { query = '', includeChildrenMatchedParent } = searchOptions;
|
|
19
|
+
const matchFunc = isMatchedTreeItem(query);
|
|
20
|
+
const searchItems = (treeItems) => treeItems.reduce((acc, item) => {
|
|
21
|
+
const hasMatchingTitle = matchFunc(item);
|
|
22
|
+
const needDeepSearch = !(hasMatchingTitle && includeChildrenMatchedParent);
|
|
23
|
+
const matchedChildren = item.nested && (needDeepSearch ? searchItems(item.nested) : item.nested);
|
|
24
|
+
if (hasMatchingTitle || matchedChildren?.length) {
|
|
25
|
+
const newItem = { ...item };
|
|
26
|
+
if (matchedChildren?.length) {
|
|
27
|
+
newItem.nested = matchedChildren;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
delete newItem.nested;
|
|
31
|
+
}
|
|
32
|
+
acc.push(newItem);
|
|
33
|
+
}
|
|
34
|
+
return acc;
|
|
35
|
+
}, []);
|
|
36
|
+
return searchItems(tree);
|
|
37
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ExtendedTreeNodeProps } from '../types.js';
|
|
2
|
+
type ReturnSearch<I, T> = I extends string ? T | null : T[];
|
|
3
|
+
type SearchByIdParams<I, T> = {
|
|
4
|
+
tree: T[];
|
|
5
|
+
searchOptions: {
|
|
6
|
+
id: I;
|
|
7
|
+
includeNested?: boolean;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Ищет узлы дерева по одному id или по массиву id.
|
|
12
|
+
*
|
|
13
|
+
* @param params Параметры поиска.
|
|
14
|
+
* @param params.tree Дерево для поиска.
|
|
15
|
+
* @param params.searchOptions Опции поиска (id и includeNested).
|
|
16
|
+
* @returns Для одиночного id возвращает узел или null, для массива id — список узлов.
|
|
17
|
+
*/
|
|
18
|
+
export declare const getSearchedTreeNodeById: <I extends string | string[], T extends ExtendedTreeNodeProps>({ tree, searchOptions: { id, includeNested }, }: SearchByIdParams<I, T>) => ReturnSearch<I, T>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ищет узлы дерева по одному id или по массиву id.
|
|
3
|
+
*
|
|
4
|
+
* @param params Параметры поиска.
|
|
5
|
+
* @param params.tree Дерево для поиска.
|
|
6
|
+
* @param params.searchOptions Опции поиска (id и includeNested).
|
|
7
|
+
* @returns Для одиночного id возвращает узел или null, для массива id — список узлов.
|
|
8
|
+
*/
|
|
9
|
+
export const getSearchedTreeNodeById = ({ tree, searchOptions: { id, includeNested = true }, }) => {
|
|
10
|
+
const isArrayIds = Array.isArray(id);
|
|
11
|
+
if (!id || (isArrayIds && id.length === 0)) {
|
|
12
|
+
return (isArrayIds ? [] : null);
|
|
13
|
+
}
|
|
14
|
+
const foundItems = [];
|
|
15
|
+
const searchNodes = (nodes) => {
|
|
16
|
+
for (const node of nodes) {
|
|
17
|
+
const matchCondition = isArrayIds ? id.includes(node.id) : node.id === id;
|
|
18
|
+
if (matchCondition) {
|
|
19
|
+
foundItems.push(includeNested ? node : { ...node, nested: undefined });
|
|
20
|
+
if (!isArrayIds) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (node.nested) {
|
|
25
|
+
searchNodes(node.nested);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
searchNodes(tree);
|
|
30
|
+
return (isArrayIds ? foundItems : foundItems[0] || null);
|
|
31
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './checkNestedNodesSelection.js';
|
|
2
|
+
export * from './collectEmptyNestedNodesInExpanded.js';
|
|
3
|
+
export * from './collectIds.js';
|
|
4
|
+
export * from './extractTreeNodeTitle.js';
|
|
5
|
+
export * from './findAllChildNodeIds.js';
|
|
6
|
+
export * from './findAllExpandedChildNodeIds.js';
|
|
7
|
+
export * from './getSearchedTreeItems.js';
|
|
8
|
+
export * from './getSearchedTreeNodeById.js';
|
|
9
|
+
export * from './lookupTreeForSelectedNodes.js';
|
|
10
|
+
export * from './sortTreeItemsByTitle.js';
|
|
11
|
+
export * from './traverse.js';
|
|
12
|
+
export * from './traverseWithTarget.js';
|
|
13
|
+
export * from './updateTreeNode.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './checkNestedNodesSelection.js';
|
|
2
|
+
export * from './collectEmptyNestedNodesInExpanded.js';
|
|
3
|
+
export * from './collectIds.js';
|
|
4
|
+
export * from './extractTreeNodeTitle.js';
|
|
5
|
+
export * from './findAllChildNodeIds.js';
|
|
6
|
+
export * from './findAllExpandedChildNodeIds.js';
|
|
7
|
+
export * from './getSearchedTreeItems.js';
|
|
8
|
+
export * from './getSearchedTreeNodeById.js';
|
|
9
|
+
export * from './lookupTreeForSelectedNodes.js';
|
|
10
|
+
export * from './sortTreeItemsByTitle.js';
|
|
11
|
+
export * from './traverse.js';
|
|
12
|
+
export * from './traverseWithTarget.js';
|
|
13
|
+
export * from './updateTreeNode.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HierarchicalSelectionHandlers } from '@cloud-ru/ds-utils';
|
|
2
|
+
import { ParentNode, TreeNodeId, TreeNodeProps } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Пересчитывает выбранные узлы для режима мультивыбора в дереве.
|
|
5
|
+
* Учитывает потомков текущего узла и корректирует состояние предков.
|
|
6
|
+
*
|
|
7
|
+
* @param params Параметры пересчета выделения.
|
|
8
|
+
* @returns Обновленный список выбранных id.
|
|
9
|
+
*/
|
|
10
|
+
export declare function lookupTreeForSelectedNodes({ node, selectedNodes, parentNode, toggleSelection, }: {
|
|
11
|
+
node: Pick<TreeNodeProps, 'id' | 'nested' | 'disabled'>;
|
|
12
|
+
selectedNodes: TreeNodeId[];
|
|
13
|
+
parentNode?: ParentNode;
|
|
14
|
+
toggleSelection: HierarchicalSelectionHandlers['toggleSelection'];
|
|
15
|
+
}): string[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { collectHierarchicalAncestors } from './collectHierarchicalAncestors.js';
|
|
2
|
+
import { findAllChildNodeIds } from './findAllChildNodeIds.js';
|
|
3
|
+
/**
|
|
4
|
+
* Пересчитывает выбранные узлы для режима мультивыбора в дереве.
|
|
5
|
+
* Учитывает потомков текущего узла и корректирует состояние предков.
|
|
6
|
+
*
|
|
7
|
+
* @param params Параметры пересчета выделения.
|
|
8
|
+
* @returns Обновленный список выбранных id.
|
|
9
|
+
*/
|
|
10
|
+
export function lookupTreeForSelectedNodes({ node, selectedNodes, parentNode, toggleSelection, }) {
|
|
11
|
+
const descendantIds = node.nested?.length ? findAllChildNodeIds(node.nested) : [];
|
|
12
|
+
return toggleSelection({
|
|
13
|
+
nodeId: node.id,
|
|
14
|
+
descendantIds,
|
|
15
|
+
selectedIds: selectedNodes,
|
|
16
|
+
ancestors: collectHierarchicalAncestors(parentNode),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ExtendedTreeNodeProps } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Сортирует узлы дерева по заголовку в алфавитном порядке.
|
|
4
|
+
*
|
|
5
|
+
* @param items Список узлов для сортировки.
|
|
6
|
+
* @param options Опции сортировки. По умолчанию сортировка чувствительна к регистру.
|
|
7
|
+
* @returns Новый отсортированный массив.
|
|
8
|
+
*/
|
|
9
|
+
type SortTreeItemsByTitleOptions = {
|
|
10
|
+
caseSensitive?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare const sortTreeItemsByTitle: (items: ExtendedTreeNodeProps[], options?: SortTreeItemsByTitleOptions) => ExtendedTreeNodeProps[];
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { extractTreeNodeTitle } from './extractTreeNodeTitle.js';
|
|
2
|
+
export const sortTreeItemsByTitle = (items, options) => items &&
|
|
3
|
+
[...items].sort((itemA, itemB) => {
|
|
4
|
+
const valueA = extractTreeNodeTitle(itemA);
|
|
5
|
+
const valueB = extractTreeNodeTitle(itemB);
|
|
6
|
+
if (options?.caseSensitive ?? true) {
|
|
7
|
+
return valueA.localeCompare(valueB);
|
|
8
|
+
}
|
|
9
|
+
return valueA.toLowerCase().localeCompare(valueB.toLowerCase());
|
|
10
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TreeNodeProps } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Обходит дерево в ширину (BFS) и вызывает callback для каждого узла.
|
|
4
|
+
*
|
|
5
|
+
* Очередь — массив с курсором `head`: `shift()` на массиве переиндексирует весь хвост,
|
|
6
|
+
* а сдвиг курсора — O(1).
|
|
7
|
+
*
|
|
8
|
+
* @param nodes Корневые узлы дерева.
|
|
9
|
+
* @param callback Функция, вызываемая для каждого найденного узла.
|
|
10
|
+
*/
|
|
11
|
+
export declare const traverse: <T extends TreeNodeProps>(nodes: T[], callback: (node: T, depth: number) => void) => void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Обходит дерево в ширину (BFS) и вызывает callback для каждого узла.
|
|
3
|
+
*
|
|
4
|
+
* Очередь — массив с курсором `head`: `shift()` на массиве переиндексирует весь хвост,
|
|
5
|
+
* а сдвиг курсора — O(1).
|
|
6
|
+
*
|
|
7
|
+
* @param nodes Корневые узлы дерева.
|
|
8
|
+
* @param callback Функция, вызываемая для каждого найденного узла.
|
|
9
|
+
*/
|
|
10
|
+
export const traverse = (nodes, callback) => {
|
|
11
|
+
const queue = nodes.map(node => ({ node, depth: 0 }));
|
|
12
|
+
let head = 0;
|
|
13
|
+
while (head < queue.length) {
|
|
14
|
+
const { node, depth } = queue[head];
|
|
15
|
+
head += 1;
|
|
16
|
+
callback(node, depth);
|
|
17
|
+
for (const child of node.nested ?? []) {
|
|
18
|
+
queue.push({ node: child, depth: depth + 1 });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { TreeNodeProps } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Обходит дерево в ширину (BFS), передавая каждому узлу список,
|
|
4
|
+
* в который должен быть добавлен его трансформированный результат.
|
|
5
|
+
*
|
|
6
|
+
* Очередь — массив с курсором `head`: `shift()` на массиве переиндексирует весь хвост,
|
|
7
|
+
* а сдвиг курсора — O(1).
|
|
8
|
+
*
|
|
9
|
+
* @param nodes Корневые узлы дерева.
|
|
10
|
+
* @param rootTargetList Корневой список-приемник для преобразованных узлов.
|
|
11
|
+
* @param callback Функция обработки узла. Возвращает список для детей
|
|
12
|
+
* или undefined, если вложенные узлы обходить не нужно.
|
|
13
|
+
*/
|
|
14
|
+
export declare const traverseWithTarget: <T extends TreeNodeProps>(nodes: T[], rootTargetList: T[], callback: (node: T, depth: number, targetList: T[]) => T[] | undefined) => void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Обходит дерево в ширину (BFS), передавая каждому узлу список,
|
|
3
|
+
* в который должен быть добавлен его трансформированный результат.
|
|
4
|
+
*
|
|
5
|
+
* Очередь — массив с курсором `head`: `shift()` на массиве переиндексирует весь хвост,
|
|
6
|
+
* а сдвиг курсора — O(1).
|
|
7
|
+
*
|
|
8
|
+
* @param nodes Корневые узлы дерева.
|
|
9
|
+
* @param rootTargetList Корневой список-приемник для преобразованных узлов.
|
|
10
|
+
* @param callback Функция обработки узла. Возвращает список для детей
|
|
11
|
+
* или undefined, если вложенные узлы обходить не нужно.
|
|
12
|
+
*/
|
|
13
|
+
export const traverseWithTarget = (nodes, rootTargetList, callback) => {
|
|
14
|
+
const queue = nodes.map(node => ({ node, depth: 0, targetList: rootTargetList }));
|
|
15
|
+
let head = 0;
|
|
16
|
+
while (head < queue.length) {
|
|
17
|
+
const { node, depth, targetList } = queue[head];
|
|
18
|
+
head += 1;
|
|
19
|
+
const childTargetList = callback(node, depth, targetList);
|
|
20
|
+
if (childTargetList !== undefined && node.nested?.length) {
|
|
21
|
+
for (const child of node.nested) {
|
|
22
|
+
queue.push({ node: child, depth: depth + 1, targetList: childTargetList });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TreeNodeProps } from '../types.js';
|
|
2
|
+
type UpdateTreeNodeData<Node extends TreeNodeProps> = Partial<TreeNodeProps> & {
|
|
3
|
+
nested?: Node[];
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Возвращает копию дерева, где у узла с указанным id обновляются переданные поля.
|
|
7
|
+
*
|
|
8
|
+
* @param tree Исходное дерево.
|
|
9
|
+
* @param nodeId Идентификатор узла, который нужно обновить.
|
|
10
|
+
* @param data Частичные данные узла, которые будут применены к найденному элементу.
|
|
11
|
+
*/
|
|
12
|
+
export declare const updateTreeNode: <Node extends TreeNodeProps>(tree: Node[], nodeId: string, data: UpdateTreeNodeData<Node>) => Node[];
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { traverseWithTarget } from './traverseWithTarget.js';
|
|
2
|
+
/**
|
|
3
|
+
* Возвращает копию дерева, где у узла с указанным id обновляются переданные поля.
|
|
4
|
+
*
|
|
5
|
+
* @param tree Исходное дерево.
|
|
6
|
+
* @param nodeId Идентификатор узла, который нужно обновить.
|
|
7
|
+
* @param data Частичные данные узла, которые будут применены к найденному элементу.
|
|
8
|
+
*/
|
|
9
|
+
export const updateTreeNode = (tree, nodeId, data) => {
|
|
10
|
+
const result = [];
|
|
11
|
+
traverseWithTarget(tree, result, (source, _depth, targetList) => {
|
|
12
|
+
const isTarget = source.id === nodeId;
|
|
13
|
+
const hasNested = Array.isArray(source.nested);
|
|
14
|
+
let newNested;
|
|
15
|
+
if (isTarget) {
|
|
16
|
+
newNested = data.nested;
|
|
17
|
+
}
|
|
18
|
+
else if (hasNested) {
|
|
19
|
+
newNested = [];
|
|
20
|
+
}
|
|
21
|
+
const newNode = {
|
|
22
|
+
...source,
|
|
23
|
+
...(isTarget ? data : {}),
|
|
24
|
+
...(newNested !== undefined ? { nested: newNested } : {}),
|
|
25
|
+
};
|
|
26
|
+
targetList.push(newNode);
|
|
27
|
+
return !isTarget && hasNested && source.nested?.length ? newNested : undefined;
|
|
28
|
+
});
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { DependencyList, EffectCallback, MutableRefObject } from 'react';
|
|
2
|
+
export declare function useRefState<T>(initial: T): MutableRefObject<T>;
|
|
3
|
+
export declare function useDebounceValue<T>(value: T, delay: number): T;
|
|
4
|
+
export declare function useDidUpdate(effect: EffectCallback, deps?: DependencyList): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
export function useRefState(initial) {
|
|
3
|
+
const ref = useRef(initial);
|
|
4
|
+
return ref;
|
|
5
|
+
}
|
|
6
|
+
export function useDebounceValue(value, delay) {
|
|
7
|
+
const [debounced, setDebounced] = useState(value);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const id = setTimeout(() => setDebounced(value), delay);
|
|
10
|
+
return () => clearTimeout(id);
|
|
11
|
+
}, [value, delay]);
|
|
12
|
+
return debounced;
|
|
13
|
+
}
|
|
14
|
+
export function useDidUpdate(effect, deps) {
|
|
15
|
+
const mounted = useRef(false);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!mounted.current) {
|
|
18
|
+
mounted.current = true;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
return effect();
|
|
22
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
23
|
+
}, deps);
|
|
24
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SearchableTreeDataLoadResult, TreeNodeProps } from '../types.js';
|
|
2
|
+
export type SearchResult<TTreeNode extends TreeNodeProps> = {
|
|
3
|
+
tree: TTreeNode[];
|
|
4
|
+
needPreloadNodes: string[];
|
|
5
|
+
};
|
|
6
|
+
export type SearchParams = {
|
|
7
|
+
search: string;
|
|
8
|
+
expandedNodes: string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Параметры {@link useSearchableTree}.
|
|
12
|
+
*
|
|
13
|
+
* @typeParam TRecordValue — тип записи в `treeItemsRecord` (по одному элементу на узел).
|
|
14
|
+
* @typeParam TTreeNode — тип узла дерева.
|
|
15
|
+
*/
|
|
16
|
+
type UseSearchableTreeParams<TRecordValue, TTreeNode extends TreeNodeProps> = {
|
|
17
|
+
/** Начальное дерево узлов. */
|
|
18
|
+
initTree: TTreeNode[];
|
|
19
|
+
/** Подгрузка дочерних узлов для одного узла. */
|
|
20
|
+
onPreloadNode: (node: TreeNodeProps) => Promise<TTreeNode[]>;
|
|
21
|
+
/** Пакетная подгрузка детей по id родителей. */
|
|
22
|
+
onPreloadNodes: (nodes: string[], signal?: AbortSignal) => Promise<Record<string, TTreeNode[]>>;
|
|
23
|
+
/** Поиск по дереву. */
|
|
24
|
+
onSearch: (params: SearchParams, signal?: AbortSignal) => Promise<SearchResult<TTreeNode>>;
|
|
25
|
+
/** Сопоставление узла с элементом записи `treeItemsRecord` (по `node.id`). */
|
|
26
|
+
mapNodeToRecordItem: (node: TTreeNode) => TRecordValue;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Хук для работы с деревом, поддерживающим поиск и асинхронную подгрузку узлов.
|
|
30
|
+
*
|
|
31
|
+
* Возможности:
|
|
32
|
+
* - хранение актуального дерева и записи элементов (`treeItemsRecord`);
|
|
33
|
+
* - управление раскрытыми узлами;
|
|
34
|
+
* - дебаунс-поиск с отменой предыдущего запроса;
|
|
35
|
+
* - дозагрузка дочерних узлов после поиска или раскрытия.
|
|
36
|
+
*
|
|
37
|
+
* @param params Параметры и обработчики для работы с деревом.
|
|
38
|
+
* @returns Ссылки на состояние дерева, контролы поиска и колбэки для `expand`/`dataLoad`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function useSearchableTree<TRecordValue, TTreeNode extends TreeNodeProps>({ initTree, onPreloadNode, onPreloadNodes, onSearch, mapNodeToRecordItem, }: UseSearchableTreeParams<TRecordValue, TTreeNode>): {
|
|
41
|
+
tree: import("react").MutableRefObject<TTreeNode[]>;
|
|
42
|
+
expandedNodes: import("react").MutableRefObject<string[]>;
|
|
43
|
+
loading: boolean;
|
|
44
|
+
treeItemsRecord: import("react").MutableRefObject<Record<string, TRecordValue>>;
|
|
45
|
+
search: {
|
|
46
|
+
value: string;
|
|
47
|
+
onChange: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
48
|
+
};
|
|
49
|
+
onExpand: (nodes: string[]) => void;
|
|
50
|
+
onDataLoad: (node: TreeNodeProps) => Promise<SearchableTreeDataLoadResult<TTreeNode, TRecordValue>>;
|
|
51
|
+
};
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { collectEmptyNestedNodesInExpanded, traverse, updateTreeNode } from '../helpers/index.js';
|
|
3
|
+
import { useDebounceValue, useDidUpdate, useRefState } from './internalHooks.js';
|
|
4
|
+
/**
|
|
5
|
+
* Хук для работы с деревом, поддерживающим поиск и асинхронную подгрузку узлов.
|
|
6
|
+
*
|
|
7
|
+
* Возможности:
|
|
8
|
+
* - хранение актуального дерева и записи элементов (`treeItemsRecord`);
|
|
9
|
+
* - управление раскрытыми узлами;
|
|
10
|
+
* - дебаунс-поиск с отменой предыдущего запроса;
|
|
11
|
+
* - дозагрузка дочерних узлов после поиска или раскрытия.
|
|
12
|
+
*
|
|
13
|
+
* @param params Параметры и обработчики для работы с деревом.
|
|
14
|
+
* @returns Ссылки на состояние дерева, контролы поиска и колбэки для `expand`/`dataLoad`.
|
|
15
|
+
*/
|
|
16
|
+
export function useSearchableTree({ initTree, onPreloadNode, onPreloadNodes, onSearch, mapNodeToRecordItem, }) {
|
|
17
|
+
const tree = useRefState(initTree);
|
|
18
|
+
const treeItemsRecord = useRefState({});
|
|
19
|
+
const expandedNodes = useRefState([]);
|
|
20
|
+
const [search, setSearch] = useState('');
|
|
21
|
+
const debouncedSearch = useDebounceValue(search, 500);
|
|
22
|
+
const [loading, setLoading] = useState(false);
|
|
23
|
+
// Актуальный запрос поиска. Он же маркер «этот вызов ещё не вытеснен следующим»:
|
|
24
|
+
// после каждого await сверяемся по идентичности, а не по отдельному флагу отмены.
|
|
25
|
+
const searchAbortControllerRef = useRef(null);
|
|
26
|
+
const buildTreeItemsRecord = useCallback((nodes) => {
|
|
27
|
+
const record = {};
|
|
28
|
+
traverse(nodes, node => {
|
|
29
|
+
record[node.id] = mapNodeToRecordItem(node);
|
|
30
|
+
});
|
|
31
|
+
return record;
|
|
32
|
+
}, [mapNodeToRecordItem]);
|
|
33
|
+
const onExpand = useCallback((nodes) => {
|
|
34
|
+
expandedNodes.current = nodes;
|
|
35
|
+
}, [expandedNodes]);
|
|
36
|
+
const onDataLoad = useCallback(async (node) => {
|
|
37
|
+
const preloadedChildren = await onPreloadNode(node);
|
|
38
|
+
const updatedTree = updateTreeNode(tree.current, node.id, { nested: preloadedChildren });
|
|
39
|
+
tree.current = updatedTree;
|
|
40
|
+
const newTreeItemsRecord = { ...treeItemsRecord.current };
|
|
41
|
+
traverse(preloadedChildren, child => {
|
|
42
|
+
newTreeItemsRecord[child.id] = mapNodeToRecordItem(child);
|
|
43
|
+
});
|
|
44
|
+
treeItemsRecord.current = newTreeItemsRecord;
|
|
45
|
+
return { preloadedChildren, updatedTree, newTreeItemsRecord };
|
|
46
|
+
}, [mapNodeToRecordItem, onPreloadNode, tree, treeItemsRecord]);
|
|
47
|
+
const handleSearch = useCallback(async (searchQuery) => {
|
|
48
|
+
searchAbortControllerRef.current?.abort();
|
|
49
|
+
setLoading(true);
|
|
50
|
+
const abortController = new AbortController();
|
|
51
|
+
searchAbortControllerRef.current = abortController;
|
|
52
|
+
try {
|
|
53
|
+
const { tree: searchedTree, needPreloadNodes } = await onSearch({ search: searchQuery, expandedNodes: expandedNodes.current }, abortController.signal);
|
|
54
|
+
if (!abortController.signal.aborted) {
|
|
55
|
+
tree.current = searchedTree;
|
|
56
|
+
treeItemsRecord.current = buildTreeItemsRecord(searchedTree);
|
|
57
|
+
const expandedSet = new Set(expandedNodes.current);
|
|
58
|
+
const toPreloadExpandableNodes = collectEmptyNestedNodesInExpanded(searchedTree, expandedSet);
|
|
59
|
+
const collectedNodesForPreload = Array.from(new Set([...toPreloadExpandableNodes.map(node => node.id), ...needPreloadNodes]));
|
|
60
|
+
if (!collectedNodesForPreload.length) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const preloadedNodes = await onPreloadNodes(collectedNodesForPreload, abortController.signal);
|
|
64
|
+
if (searchAbortControllerRef.current !== abortController) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let tmpTree = [...searchedTree];
|
|
68
|
+
for (const [nodeId, children] of Object.entries(preloadedNodes)) {
|
|
69
|
+
tmpTree = updateTreeNode(tmpTree, nodeId, { nested: children });
|
|
70
|
+
}
|
|
71
|
+
tree.current = tmpTree;
|
|
72
|
+
treeItemsRecord.current = buildTreeItemsRecord(tmpTree);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// Прерванный запрос — штатный путь: следующий ввод уже вытеснил этот вызов.
|
|
77
|
+
if (!abortController.signal.aborted) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
if (searchAbortControllerRef.current === abortController) {
|
|
83
|
+
setLoading(false);
|
|
84
|
+
searchAbortControllerRef.current = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}, [buildTreeItemsRecord, expandedNodes, onPreloadNodes, onSearch, tree, treeItemsRecord]);
|
|
88
|
+
useDidUpdate(() => {
|
|
89
|
+
handleSearch(debouncedSearch);
|
|
90
|
+
}, [debouncedSearch, handleSearch]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
tree.current = initTree;
|
|
93
|
+
treeItemsRecord.current = buildTreeItemsRecord(initTree);
|
|
94
|
+
}, [buildTreeItemsRecord, initTree, tree, treeItemsRecord]);
|
|
95
|
+
useEffect(() => () => {
|
|
96
|
+
searchAbortControllerRef.current?.abort();
|
|
97
|
+
}, []);
|
|
98
|
+
return {
|
|
99
|
+
tree,
|
|
100
|
+
expandedNodes,
|
|
101
|
+
loading,
|
|
102
|
+
treeItemsRecord,
|
|
103
|
+
search: {
|
|
104
|
+
value: search,
|
|
105
|
+
onChange: setSearch,
|
|
106
|
+
},
|
|
107
|
+
onExpand,
|
|
108
|
+
onDataLoad,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PreloadNodeHandler, SelectHandler, TreeNodeProps } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Параметры {@link useTreeMultiSelection}.
|
|
4
|
+
*
|
|
5
|
+
* @typeParam TTreeNode — тип узла дерева.
|
|
6
|
+
*/
|
|
7
|
+
type UseTreeMultiSelectionParams<TTreeNode extends TreeNodeProps> = {
|
|
8
|
+
/**
|
|
9
|
+
* Подгрузка дочерних узлов, если у узла есть `nested`, но массив пуст (ленивая загрузка перед выбором).
|
|
10
|
+
*/
|
|
11
|
+
onDataLoad: PreloadNodeHandler<TTreeNode>;
|
|
12
|
+
/** Логика мультивыбора: по клику возвращает, какие id добавить и снять. */
|
|
13
|
+
onSelect: SelectHandler;
|
|
14
|
+
/** Controlled: текущие выбранные id; без пропа — внутреннее состояние хука. */
|
|
15
|
+
selected?: string[];
|
|
16
|
+
/** Вызывается при изменении набора выбранных id (controlled-сценарий). */
|
|
17
|
+
onChangeSelected?: (newSelected: string[]) => void;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Хук мультивыбора для дерева.
|
|
21
|
+
*
|
|
22
|
+
* Выполняет пересчет выбранных id после клика по узлу, поддерживает
|
|
23
|
+
* controlled/uncontrolled режим (`selected` + `onChangeSelected`) и при необходимости
|
|
24
|
+
* подгружает детей узла перед вычислением итогового выбора.
|
|
25
|
+
*
|
|
26
|
+
* @param params Параметры управления выбором и подгрузкой данных.
|
|
27
|
+
* @returns Текущий список выбранных id и обработчик выбора узла.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useTreeMultiSelection<TTreeNode extends TreeNodeProps>({ onDataLoad, onSelect: onSelectProp, selected, onChangeSelected, }: UseTreeMultiSelectionParams<TTreeNode>): {
|
|
30
|
+
selected: string[];
|
|
31
|
+
onSelect: (selectedKeys: string[], node: TreeNodeProps) => Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
export {};
|