1 开发目标
实现如下简单树结构组件:
再点击树节点后,会调用客户端传入的回调函数:
2 详细需求
简单树结构组件需根据客户端提供的参数创建,具备动态构建树形结构节点、选项卡切换及自定义内容显示等功能:
(1)树形结构组件的创建与初始化: 类似于 echarts 等知名商业组件的创建与初始化方式,本组件需要根据客户端提供的参数 container 以及 para 进行创建和初始化。
container 是一个已存在的 DOM 元素(一般是 DIV),组件将在此元素内部构建 TAB 区域,包含选项卡以及 TAB 面板。
para 是本组件的配置参数,该对象应包含以下属性:
{
"nodes":[
{
"id":"1",
"name":"node_1",
"children":[
{
"id":"11",
"name":"node_1_1",
},
{
"id":"12",
"name":"node_1_2",
"children":[
{
"id":"121",
"name":"node_1_2_1",
},
{
"id":"122",
"name":"node_1_2_2",
},
]
},
]
},
{
"id":"2",
"name":"node_2",
},
],
"onClickTreeNode":callback_function,
}
(2)树形结构渲染: 组件应能正确渲染树形结构,每个节点应清晰展示。
(3)节点单选: 用户点击树形结构中的某个节点时,该节点应被选中,同时其他节点应取消选中状态。
(4)节点状态反馈: 选中的节点应有明显的视觉反馈,如变色或添加特殊标记。
(5)事件监听: 应提供事件监听机制,允许用户监听节点的选中事件,以便在节点被选中时执行特定操作。
3 代码实现
首先创建一个 neat_treewidget.js 文件,该文件用于本组件的工具类、窗体部件基类以及各个实现类的代码构建。
在具体的业务代码编写之前,先实现一个工具类以及一些工具方法,方便后面调用:
class CommonUtil {
// 设置 DIV 中的文字为水平与垂直居中
static centerTextInDiv(container) {
container.style.display = 'flex';
container.style.textAlign = 'center';
container.style.justifyContent = 'center';
container.style.flexDirection = 'column';
}
}
该工具类中包含一个可以将 DIV 中的文字设置为水平与垂直居中的静态方法。
接下来,定义一个通用的显示窗体的基类:
class NeatBaseWid {
constructor(container, para) {
this.container = container; // 接收调用者传入的 DOM 元素(一般是 DIV)
this.para = para; // 保存调用者传入的 para 对象
}
}
然后开始定义树节点类型:
class NeatTreeNode extends NeatBaseWid {
static LEVEL_OFFSET = 10; // 每个级别的树节点偏移像素
static NODE_HEIGHT = '23px'; // 树节点高度
static NODE_NAME_FONTSIZE = '14px'; // 默认标题字符串的字体大小
static NODE_NAME_COLOR = '#000'; // 默认标题字符串字体颜色
static NODE_OPENCLOSE_ICON_WIDTH = '23px'; // 树节点打开、关闭小箭头图标的宽度
static NODE_OPENCLOSE_ICON_CLASS_OPEN = 'fa fa-angle-down'; //打开
static NODE_OPENCLOSE_ICON_CLASS_CLOSE = 'fa fa-angle-right'; //关闭
constructor(container, para) {
super(container, para);
this.id=this.para.id;
this.name=this.para.name;
this.parent = para.treeNode ?? null; // 父节点
this.level = (para.treeNode && (para.treeNode.level + 1)) ?? 0; // 节点的级别,最高一级是根节点
this.nameContainer = null; // 树节点的标题容器
this.openCloseIconContainer = null; // 树节点的打开、关闭小箭头图标容器
this.children = []; // 子节点
this.childrenContainer = null; // 子节点容器
this.render();
}
上面代码定义了 NeatTreeNode 的一些默认属性与成员变量,并且创建构造函数,该函数接收调用者传入的 DIV 容器,并且调用 render 方法。
在 render 方法,需要渲染当前树节点,并且还要根据是否有子节点的情况,创建子节点容器:
render() {
this.container.innerHTML = ''; // 清空容器
// 渲染当前树节点
let nodeContainer = document.createElement('div');
nodeContainer.style.display = 'flex';
nodeContainer.style.width = '100%';
nodeContainer.style.height = NeatTreeNode.NODE_HEIGHT;
this.container.appendChild(nodeContainer);
上面代码创建了当前树节点的容器,接下来需要根据当前节点的级别,做一个偏移处理:
// 如果当前树节点的级别大于 0,则放一个偏移容器
if (this.level > 0) {
let offsetContainer = document.createElement('div');
offsetContainer.style.width = this.level*NeatTreeNode.LEVEL_OFFSET + 'px';
nodeContainer.appendChild(offsetContainer);
}
接下来,如果当前树节点有子节点,则显示树节点的打开、关闭小箭头图标,如果没有子节点,则做一个简单的偏移处理即可:
// 如果当前树节点有子节点,则显示树节点的打开、关闭小箭头图标
if (this.para.children && this.para.children.length > 0) {
this.openCloseIconContainer = document.createElement('i');
this.openCloseIconContainer.className = NeatTreeNode.NODE_OPENCLOSE_ICON_CLASS_OPEN;
this.openCloseIconContainer.style.width = NeatTreeNode.NODE_OPENCLOSE_ICON_WIDTH;
this.openCloseIconContainer.style.height = '100%';
CommonUtil.centerTextInDiv(this.openCloseIconContainer);
nodeContainer.appendChild(this.openCloseIconContainer);
// 树节点打开或关闭
let that = this;
this.openCloseIconContainer.onclick = function () {
if(NeatTreeNode.NODE_OPENCLOSE_ICON_CLASS_OPEN === that.openCloseIconContainer.className){
that.openCloseIconContainer.className = NeatTreeNode.NODE_OPENCLOSE_ICON_CLASS_CLOSE;
that.childrenContainer.style.display='none';
}else{
that.openCloseIconContainer.className = NeatTreeNode.NODE_OPENCLOSE_ICON_CLASS_OPEN;
that.childrenContainer.style.display='block';
}
}
} else {
let offsetContainer = document.createElement('div');
offsetContainer.style.width = NeatTreeNode.NODE_OPENCLOSE_ICON_WIDTH;
nodeContainer.appendChild(offsetContainer);
}
注意:上面代码中,在创建打开、关闭小箭头图标后,还定义了点击事件的处理函数。
然后,创建当前的树节点标题,并且定义点击树节点标题时的事件处理函数:
// 树节点标题
this.nameContainer = document.createElement('div');
this.nameContainer.style.flexGrow = '1';
this.nameContainer.innerText = this.para.name;
CommonUtil.centerTextInDiv(this.nameContainer);
this.nameContainer.style.textAlign = 'left';
this.nameContainer.style.cursor = 'pointer';
nodeContainer.appendChild(this.nameContainer);
// 点击树节点的触发动作
let that = this;
this.nameContainer.onclick = function () {
that.para.onClickTreeNode(that);
}
最后,根据是否有子节点的情况,创建子节点容器:
// 创建子树节点
if(this.para.children && this.para.children.length > 0){
this.childrenContainer = document.createElement('div');
this.childrenContainer.style.width = '100%';
this.container.appendChild(this.childrenContainer);
this.para.children.forEach(element => {
let nodeContainer = document.createElement('div');
this.childrenContainer.appendChild(nodeContainer);
element.treeNode = this;
element.onClickTreeNode = this.para.onClickTreeNode;
let treeNode = new NeatTreeNode(nodeContainer, element);
this.children.push(treeNode);
});
}
}
至此,完成了整个渲染函数 render 的构建。
下一步,创建树结构类型:
class NeatHeaderTreeWidget extends NeatBaseWid {
constructor(container, para) {
super(container, para);
this.rootTreeNodes = []; // 根节点集合
this.render();
}
render() {
// 清空容器
this.container.innerHTML = '';
// 渲染树结构
this.container.style.width = '100%';
this.container.style.height = '100%';
// 创建树节点
this.para.nodes.forEach(element => {
let nodeContainer = document.createElement('div');
this.container.appendChild(nodeContainer);
element.onClickTreeNode = this.para.onClickTreeNode;
let treeNode = new NeatTreeNode(nodeContainer, element);
this.rootTreeNodes.push(treeNode);
});
}
}
注意:创建树结构类型的渲染函数 render 中的创建树节点实际是创建根节点,至于子节点则通过根节点的渲染函数创建(是一个递归创建的过程)。
完成树结构组件的代码编写后,可以创建 neater_treewidget.html 文件,调用树结构组件:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>header tab</title>
<style>
html {
height: 100%;
}
body {
margin: 0;
height: 100%;
}
</style>
<link rel="stylesheet" href="./font-awesome-4.7.0/css/font-awesome.css">
</head>
<body>
<div id="divMain" style="height: 100%;width: 100%;"></div>
</body>
<script src="./neat_treewidget.js"></script>
<script>
let para = {
"nodes": [
{
"id": "1",
"name": "根节点 node_1",
"children": [
{
"id": "11",
"name": "一级子节点 node_1_1",
},
{
"id": "12",
"name": "一级子节点 node_1_2",
"children": [
{
"id": "121",
"name": "二级子节点 node_1_2_1",
},
{
"id": "122",
"name": "二级子节点 node_1_2_2",
},
]
},
]
},
{
"id": "2",
"name": "根节点 node_2",
},
{
"id": "3",
"name": "根节点 node_3",
"children": [
{
"id": "31",
"name": "一级子节点 node_3_1",
},
]
},
{
"id": "4",
"name": "根节点 node_4",
},
],
"onClickTreeNode":clickTreeNode,
};
function clickTreeNode(treeNode){
alert(treeNode.name);
}
let treeWidget = new NeatHeaderTreeWidget(document.getElementById('divMain'), para);
</script>
</html>