import React, { useState, useRef, useEffect, useMemo, forwardRef } from "react"; import classNames from "classnames"; import { TreeNode, TreeNodeProps } from "./TreeNode"; import { CheckTree, CheckTreeRelation } from "../../checktree"; import { TreeContext, TreeContextValue } from "./TreeContext"; import { useDefault } from "../../_util/use-default"; import { isChildOfType } from "../../_util/is-child-of-type"; import { StyledProps, Omit } from "../../_type"; import { useConfig } from "../../_util/config-context"; import { warn } from "../../_util/warn"; export interface TreeData extends Omit { /** * 子节点数据 */ children?: TreeData[]; } export interface TreeProps extends StyledProps { /** * 树节点数据,如果设置则无需手动构造 */ data?: TreeData[]; /** * 树节点是否支持选择 * * @default false */ selectable?: boolean; /** * 节点选择是否完全受控(父子节点状态取消关联) * * @default false */ selectStrictly?: boolean; /** * 默认选中的节点 */ defaultSelectedIds?: string[]; /** * 选中的节点(受控) */ selectedIds?: string[]; /** * 选择事件回调 */ onSelect?: ( selectedIds: string[], context?: { selected: boolean; nodeId: string } ) => void; /** * 树节点是否支持点击高亮 * * @default false */ activable?: boolean; /** * 默认高亮的节点 */ defaultActiveIds?: string[]; /** * 高亮的节点(受控) */ activeIds?: string[]; /** * 点击高亮事件回调 */ onActive?: ( activeIds: string[], context?: { active: boolean; nodeId: string } ) => void; /** * 默认展开的节点 */ defaultExpandedIds?: string[]; /** * 展开的节点(受控) */ expandedIds?: string[]; /** * 展开/收起事件回调 */ onExpand?: ( expandedIds: string[], context?: { expanded: boolean; nodeId: string } ) => void; /** * 是否点击整个节点范围可控制展开 * * *默认点击范围为图标区域* * * @default false * @since 2.4.0 */ fullExpandable?: boolean; /** * 异步加载数据 * * **需要返回 Promise** * * 节点展开时在 onExpand 前被调用,直到 Promise resolve 后调用 onExpand */ onLoad?: TreeContextValue["onLoad"]; /** * 异步加载数据失败回调 * * onLoad Promise reject 后调用 */ onLoadError?: TreeContextValue["onLoadError"]; /** * 树节点的展开/收起图标 * * @docType React.ReactNode | (context: { expanded: boolean }) => React.ReactNode */ switcherIcon?: TreeContextValue["switcherIcon"]; /** * 包含的树节点 */ children?: React.ReactNode; } /** * CheckTree 转换 * * 叶节点 -> 全部选中节点 */ function getAllIds( selectedIds: string[], relations: CheckTreeRelation, childrenMap: Map> ): string[] { const nodes = [...selectedIds]; // 插入缺失的父节点 for (const id of nodes) { const pid = relations[id]; const childrenSet = childrenMap.get(pid); // 加入子节点全部选中的父节点 if ( pid && childrenSet && !nodes.includes(pid) && ![...childrenSet].find(cid => !nodes.includes(cid)) ) { nodes.push(pid); } } // 插入缺失的子节点 for (const id of nodes) { const children = [...(childrenMap.get(id) || [])]; children.forEach(cid => { if (!nodes.includes(cid)) { nodes.push(cid); } }); } return nodes; } /** * CheckTree 转换 * * 全部选中节点 -> 叶节点 */ function getLeafIds( selectedIds: string[], relations: CheckTreeRelation, childrenMap: Map> ): string[] { const all = getAllIds(selectedIds, relations, childrenMap); const nodes = []; all.forEach(id => { if (!childrenMap.get(id)) { nodes.push(id); } }); return nodes; } export function isTreeNode( node: React.ReactNode ): node is React.ReactComponentElement { return isChildOfType(node, TreeNode); } /** * 遍历子节点 */ function traverse( rootId: string, children: React.ReactNode, tree: { relations: CheckTreeRelation; childrenMap: Map>; disabledIds: string[]; } ) { if (!children) { return; } const { relations, childrenMap, disabledIds } = tree; React.Children.forEach(children, child => { if ( isTreeNode(child) || (React.isValidElement(child) && typeof child.props.id !== "undefined") ) { const { id, selectable, disableSelect } = child.props; if (selectable !== false) { // 非最外层根 if (rootId) { relations[id] = rootId; if (!childrenMap.has(rootId)) { childrenMap.set(rootId, new Set()); } childrenMap.get(rootId).add(id); } // 禁用 if (disableSelect) { disabledIds.push(id); } } // 当前结点不可选时其子结点 rootId 为当前 rootId traverse(selectable !== false ? id : rootId, child.props.children, tree); } else if (React.isValidElement(child)) { // eslint-disable-next-line dot-notation traverse(rootId, child.props["children"], tree); } }); } /** * 构造树节点 */ function renderTreeNodes(data: TreeData[]) { return data.map(({ children, id, ...props }) => { if (children && children.length > 0) { return ( {renderTreeNodes(children)} ); } return ; }); } export const TreeView = React.forwardRef(function TreeView( { selectable, selectStrictly, selectedIds, defaultSelectedIds = [], onSelect, activable, activeIds, defaultActiveIds = [], onActive, expandedIds, defaultExpandedIds = [], onExpand = () => null, fullExpandable, onLoad, onLoadError, switcherIcon, className, children, ...props }: Omit, ref: React.Ref ) { // 开启选择时内容区不能用于展开 if (selectable) { // eslint-disable-next-line no-param-reassign fullExpandable = false; } const { classPrefix } = useConfig(); const childrenMap = useRef>>(new Map()); const [relations, setRelations] = useState({}); const [disabledIds, setDisabledIds] = useState([]); const [selectedNodes, setSelectedNodes] = useDefault( selectedIds, defaultSelectedIds, onSelect ); const [activeNodes, setActiveNodes] = useDefault( activeIds, defaultActiveIds, onActive ); const [expandedNodes, setExpandedNodes] = useDefault( expandedIds, defaultExpandedIds, onExpand ); useEffect(() => { childrenMap.current = new Map(); if (selectable) { const disabledIds = []; if (!selectStrictly) { const relations = {}; traverse(undefined, children, { relations, childrenMap: childrenMap.current, disabledIds, }); setRelations(relations); } else { traverse(undefined, children, { relations: {}, childrenMap: new Map(), disabledIds, }); } setDisabledIds(disabledIds); } }, [children, selectStrictly, selectable]); return (
setActiveNodes([activeId], { active: true, nodeId: activeId }), onExpand: (nodeId, expanded) => setExpandedNodes( expanded ? [...expandedNodes, nodeId] : expandedNodes.filter(id => id !== nodeId), { nodeId, expanded } ), onLoad, onLoadError, switcherIcon, fullExpandable, }} >
    {selectable ? ( setSelectedNodes( getAllIds(value, relations, childrenMap.current), { nodeId: check.name, selected: check.value } ) } disabledNames={disabledIds} > {children} ) : ( children )}
); }); TreeView.displayName = "TreeView"; export const Tree = forwardRef(function Tree( { data, children, ...props }: TreeProps, ref: React.Ref ) { useEffect(() => { warn("请不要在 Tree 内嵌套使用自定义组件,该用法将在后续版本中被废弃。"); }, []); return ( {children} ); }); Tree.displayName = "Tree";