上一篇文章实现了Tree选择器,本文将介绍TreeView树形控件的实现。
其实在笔者进行TreeData的配置项,原本一直在纠结于树结构的递归,进行封装,但仔细想想,其实可以把他改造成一个双向链表,每个节点在children的基础上(子节点),加入prev(父节点),并且当前节点和prev指向同一个内存地址,进行更快速的中间查询和修改,所以树形控件其实是设计了一个链表的数据结构,如图: 在组件内部进行二次封装Tree结构,变成了这样: 对于二次封装,多了prev这一部分,具体的实现如图所示:
TreeView的切换菜单、递归渲染所有层级和Tree选择器的实现是一样的,本文就不再介绍,但是TreeView实际上是多选,而TreeSelect是进行单层选择,并且与其他层节点没有关联;而TreeView在特定情况,如:三个儿子节点都选中,父节点也被选中,多了这一块的交互,具体的业务判断如图: 链表对于这部分处理有了很大的帮助。
对于拖拽,主要用到的是js的drag和drop两个事件,记录拖拽和目标节点,对TreeData进行整体处理,代码如图: 上图,是获取拖拽节点并处理TreeData的流程。 上图是基于拖拽节点,查找到释放节点,并将拖拽节点插入到TreeData的流程。
组件源码如下:
import React, {
memo,
FC,
Fragment,
useState,
useEffect,
useCallback,
useMemo,
Children,
} from 'react';
import { CaretRightOutlined, CaretDownOutlined, CheckOutlined } from '@ant-design/icons';
import './index.module.less';
type treeViewProps = {
treeData: Array<treeData>;
defaultOpen?: boolean;
disabled?: boolean;
avaDrop?: boolean;
checkCallback?: Function;
dropCallback?: Function;
};
interface treeData {
title: string;
value: string;
group: number;
level: number;
prev: treeData | null;
height?: string;
disabled?: boolean;
checked: boolean;
children?: Array<treeData>;
}
const TreeView: FC<treeViewProps> = (props) => {
const { treeData, defaultOpen, avaDrop, checkCallback, dropCallback } = props;
const [stateTreeData, setStateTreeData] = useState<Array<treeData>>(treeData);
const [hoverTreeNode, setHoverTreeNode] = useState('');
useEffect(() => {
resolveTreeData(stateTreeData as Array<treeData>, 1, null);
}, []);
const resolveTreeData = (
treeData: Array<treeData>,
nowIndexLevel: number,
prev: treeData | null,
) => {
const newTreeData = [...treeData];
newTreeData.forEach((treeNode: treeData, index) => {
treeNode.level = nowIndexLevel;
if (defaultOpen) {
treeNode.height = '30px';
} else {
treeNode.height = treeNode.level == 1 ? '30px' : '0';
}
treeNode.checked = false;
treeNode.prev = prev;
if (treeNode?.children?.length) {
resolveTreeData(treeNode.children, nowIndexLevel + 1, treeNode);
} else {
nowIndexLevel = treeNode.level;
}
});
setStateTreeData(newTreeData);
};
const toggleTreeMenu = (clickTreeNode: treeData) => {
if (clickTreeNode?.children?.length) {
const oldStateTree = [...stateTreeData];
const editTreeNode = (treeNode: Array<treeData>) => {
treeNode.forEach((child) => {
if (child?.children?.length) {
child.height = '0';
editTreeNode(child.children);
} else {
child.height = '0';
}
});
};
const mapFn = (treeNode: Array<treeData>) => {
treeNode.forEach((t: treeData, i: number) => {
if (t.title == clickTreeNode.title && t.value == clickTreeNode.value) {
if (t?.children?.length) {
if (t.children[0].height == '0') {
t.children = t.children.map((child: treeData) => {
return {
...child,
height: child.height == '0' ? '30px' : '0',
};
});
} else {
editTreeNode(t.children);
}
}
} else if (t?.children?.length) {
mapFn(t.children);
}
});
};
mapFn(oldStateTree);
setStateTreeData(oldStateTree);
} else {
}
};
const checkTreeNode = (clickTreeNode: treeData) => {
if (clickTreeNode.disabled) {
return;
}
const oldStateTree = [...stateTreeData];
const editTreeNode = (treeNode: Array<treeData>, status: boolean) => {
treeNode.forEach((child) => {
if (child?.children?.length) {
child.checked = status;
editTreeNode(child.children, status);
} else {
child.checked = status;
}
});
};
const mapFn = (treeNode: Array<treeData>, prevNode: treeData | null) => {
treeNode.forEach((t: treeData, i: number) => {
if (t.title == clickTreeNode.title && t.value == clickTreeNode.value) {
t.checked = !t.checked;
if (prevNode) {
let nowTreeList = treeNode;
while (prevNode !== null) {
if (nowTreeList.every((c) => c.checked)) {
prevNode.checked = true;
nowTreeList.map((c) => (c.prev = prevNode));
nowTreeList = prevNode.children as Array<treeData>;
prevNode = prevNode.prev as treeData;
} else {
break;
}
}
}
if (t?.children?.length) {
editTreeNode(t.children, t.checked);
}
} else if (t?.children?.length) {
mapFn(t.children, t);
}
});
};
mapFn(oldStateTree, null);
setStateTreeData(oldStateTree);
checkCallback && checkCallback(oldStateTree);
};
const checkBoxRender = useCallback(
(treeData: treeData) => {
if (treeData.disabled) {
return <div className="disblaed-checkBox"></div>;
}
if (!treeData?.children?.length) {
if (treeData.checked) {
return (
<div className="checkBox-actived" onClick={() => checkTreeNode(treeData)}>
<CheckOutlined />
</div>
);
} else {
return <div className="checkBox-noActived" onClick={() => checkTreeNode(treeData)}></div>;
}
} else if (treeData.children && treeData.children.length) {
let treeList: Array<number> = [];
const mapFn = (treeNode: treeData): any => {
for (let i = 0; i < (treeNode.children as Array<treeData>).length; i++) {
const child: treeData = (treeNode.children as Array<treeData>)[i];
treeList.push(child.checked ? 1 : 0);
if (child.children && child.children.length) {
return mapFn(child);
} else {
if (i == (treeNode.children as Array<treeData>).length - 1) {
if (treeList.every((c) => c == 0)) {
return (
<div
className="checkBox-noActived"
onClick={() => checkTreeNode(treeData)}
></div>
);
} else if (treeList.every((c) => c == 1)) {
return (
<div className="checkBox-actived" onClick={() => checkTreeNode(treeData)}>
<CheckOutlined />
</div>
);
} else {
return (
<div className="checkBox-activedLess" onClick={() => checkTreeNode(treeData)}>
<div className="less-box"></div>
</div>
);
}
}
}
}
};
return mapFn(treeData);
}
},
[stateTreeData],
);
const dragStartTree = (e: any, treeData: treeData) => {
if (!avaDrop) return;
if (stateTreeData.length == 1 && treeData.level == 1) {
const oldTree = [...stateTreeData];
const mapTree = (tree: treeData) => {
if (tree.level !== 1) {
tree.height = '0';
}
if (tree?.children?.length) {
tree.children.forEach((c) => {
c.height = '0';
if (c?.children?.length) {
c.children.forEach((child) => {
mapTree(child);
});
}
});
}
};
mapTree(oldTree[0]);
setStateTreeData(oldTree);
} else {
e.nativeEvent.dataTransfer.setData('dargTree', treeData.title);
}
};
const dropOver = (e: any, treeNode: treeData) => {
if (!avaDrop) return;
e.nativeEvent.preventDefault();
if (treeNode.title && treeNode.title !== hoverTreeNode) {
setHoverTreeNode(treeNode.title);
const oldTree = [...stateTreeData];
const mapFn = (tree: treeData) => {
tree?.children?.forEach((c) => {
if (c.title == treeNode.title) {
c.height = '30px';
c?.children?.forEach((childNode) => {
childNode.height = '30px';
});
} else if (c?.children?.length) {
mapFn(c);
}
});
};
oldTree.forEach((c) => {
mapFn(c);
});
setStateTreeData(oldTree);
}
};
const drop = (e: any, treeNode: treeData) => {
if (!avaDrop) return;
e.nativeEvent.preventDefault();
var dragTreeNode = e.nativeEvent.dataTransfer.getData('dargTree');
if (!dragTreeNode) {
return;
}
const oldStateTree = [...stateTreeData];
const findDragNode = (treeList: treeData) => {
if (treeList.title == dragTreeNode && treeNode !== treeList) {
dragTreeNode = treeList;
if (treeList.level == 1) {
oldStateTree.splice(treeList.group, 1);
} else {
treeList?.children?.splice(0, 1);
}
if (treeList?.children?.length == 0) {
delete treeList.children;
}
return;
}
if (treeList?.children?.length) {
treeList.children.forEach((c, i) => {
if (c.title == dragTreeNode) {
dragTreeNode = c;
treeList?.children?.splice(i, 1);
if (treeList?.children?.length == 0) {
delete treeList.children;
}
}
if (c.children) {
findDragNode(c);
}
});
}
};
oldStateTree.forEach((c) => {
findDragNode(c);
});
const mapFn = (treeList: treeData) => {
if (treeList.title == treeNode.title) {
dragTreeNode.level = treeList.level + 1;
dragTreeNode.prev = treeList;
if (treeList.children) {
treeList.children.splice(0, 0, dragTreeNode);
} else {
treeList.children = [dragTreeNode];
}
return;
} else if (treeList?.children?.length) {
treeList.children.forEach((child: treeData, index) => {
if (child.title == treeNode.title) {
dragTreeNode.level = child.level;
dragTreeNode.prev = treeList;
if (treeList.children) {
treeList.children.splice(index + 1, 0, dragTreeNode);
if (treeList.children[index + 1].children) {
treeList.children[index + 1].children = (
treeList?.children[index + 1]?.children as Array<treeData>
).map((c) => {
return {
...c,
level: (treeList?.children as Array<treeData>)[index + 1].level + 1,
};
});
}
} else {
treeList.children = [dragTreeNode];
}
} else if (child?.children?.length) {
mapFn(child);
}
});
}
};
if (typeof dragTreeNode == 'object')
oldStateTree.forEach((c) => {
mapFn(c);
});
if (dragTreeNode.group == treeNode.group && dragTreeNode.level < treeNode.level) {
return;
}
setStateTreeData(oldStateTree);
dropCallback && dropCallback(oldStateTree);
};
const render = useCallback(
(data: Array<treeData> = stateTreeData) => {
return data.map((treeNode: treeData, index) => {
return (
<Fragment key={index}>
<div
className="treeNode"
style={{
marginLeft: `${treeNode.level * 10}px`,
height: `${treeNode.height}`,
}}
draggable={avaDrop}
onDragStart={(e) => dragStartTree(e, treeNode)}
onDrop={(e) => drop(e, treeNode)}
onDragOver={(e) => dropOver(e, treeNode)}
>
{
treeNode?.children?.length ? (
treeNode.children[0].height == '0' ? (
<CaretDownOutlined onClick={() => toggleTreeMenu(treeNode)} />
) : (
<CaretRightOutlined onClick={() => toggleTreeMenu(treeNode)} />
)
) : (
<div style={{ width: '14px', height: '14px' }}></div>
)
}
{checkBoxRender(treeNode)}
<span
className="text"
onClick={() => toggleTreeMenu(treeNode)}
style={treeNode.disabled ? { color: '#00000040' } : { color: '#000000' }}
>
{treeNode.title}
</span>
</div>
{treeNode?.children?.length && render(treeNode.children)}
</Fragment>
);
});
},
[stateTreeData],
);
return (
<Fragment>
<div className="tree-select-dialog">{render(stateTreeData)}</div>
</Fragment>
);
};
export default memo(TreeView);
组件的文档如下图: API: React View UI线上链接:戳戳戳~~ 组件库github:https://github.com/fengxinhhh/React-View-UI-fs
如果在线上文档体验满意的话,欢迎npm下载呀,详细教程在github中~~
|