rsuite
Version:
A suite of react components
112 lines (108 loc) • 4.15 kB
JavaScript
'use client';
import omit from 'lodash/omit';
import isNil from 'lodash/isNil';
import useForceUpdate from "./useForceUpdate.js";
import { useCallback, useRef, useEffect } from 'react';
import { shallowEqual } from "../../internals/utils/index.js";
import { formatNodeRefKey } from "../utils/index.js";
/**
* Custom hook that flattens a tree data structure into a map of nodes.
*
*/
function useFlattenTree(data, options) {
const {
value,
labelKey,
valueKey,
childrenKey,
uncheckableItemValues = [],
disabledItemValues = [],
cascade,
multiple,
callback
} = options;
const forceUpdate = useForceUpdate();
const flattenedNodes = useRef({});
const seenValues = useRef(new Set());
const updateTreeNodeCheckState = useCallback((value = []) => {
// Reset values to false
Object.keys(flattenedNodes.current).forEach(refKey => {
const node = flattenedNodes.current[refKey];
// Check if this node or any of its parents is disabled
const isNodeDisabled = disabledItemValues.some(disabledValue => shallowEqual(node[valueKey], disabledValue));
let hasDisabledParent = false;
let currentNode = node;
while (currentNode.parent && !hasDisabledParent) {
const parentRefKey = currentNode.parent.refKey;
if (parentRefKey && flattenedNodes.current[parentRefKey]) {
const parentValue = flattenedNodes.current[parentRefKey][valueKey];
if (disabledItemValues.some(disabledValue => shallowEqual(parentValue, disabledValue))) {
hasDisabledParent = true;
}
}
currentNode = currentNode.parent;
}
// Skip disabled nodes - they should not be affected by cascade or value changes
if (isNodeDisabled || hasDisabledParent) {
return;
}
if (cascade && !isNil(node.parent) && !isNil(node.parent.refKey)) {
node.check = flattenedNodes.current[node.parent.refKey].check;
} else {
node.check = false;
}
value.forEach(nodeVal => {
if (shallowEqual(flattenedNodes.current[refKey][valueKey], nodeVal) && !uncheckableItemValues.some(uncheckableValue => shallowEqual(nodeVal, uncheckableValue))) {
flattenedNodes.current[refKey].check = true;
}
});
});
}, [cascade, uncheckableItemValues, disabledItemValues, valueKey]);
const flattenTreeData = useCallback((treeData, parent, layer = 1) => {
if (!Array.isArray(treeData) || treeData.length === 0) {
return [];
}
treeData.map(node => {
const value = node[valueKey];
// Check for duplicate values
if (seenValues.current.has(value)) {
console.error(`[rsuite] The value '${value}' is duplicated. Each node in the tree data must have a unique value.`);
}
seenValues.current.add(value);
const refKey = formatNodeRefKey(value);
node.refKey = refKey;
flattenedNodes.current[refKey] = {
layer,
[labelKey]: node[labelKey],
[valueKey]: node[valueKey],
uncheckable: uncheckableItemValues.some(value => shallowEqual(node[valueKey], value)),
...node
};
if (parent) {
flattenedNodes.current[refKey].parent = omit(parent, 'parent', 'children');
}
flattenTreeData(node[childrenKey], node, layer + 1);
});
callback === null || callback === void 0 || callback(flattenedNodes.current);
forceUpdate();
}, [callback, forceUpdate, valueKey, labelKey, uncheckableItemValues, childrenKey]);
useEffect(() => {
// when data is changed, should clear the flattenedNodes, avoid duplicate keys
flattenedNodes.current = {};
seenValues.current.clear();
flattenTreeData(data);
}, [data]);
useEffect(() => {
if (multiple) {
updateTreeNodeCheckState(value);
forceUpdate();
}
/**
* Add a dependency on data, because when loading data asynchronously through getChildren,
* data may change and the node status needs to be updated.
* @see https://github.com/rsuite/rsuite/issues/3973
*/
}, [value, data]);
return flattenedNodes.current;
}
export default useFlattenTree;