supce's blog

IFE Task 12


任务目的

  • 熟练JavaScript
  • 学习树这种数据结构的基本知识

任务描述

  • 基于上几个任务,实现一个树形组件,样式自定义,不做限制
  • 要求有以下功能:
  • 节点的折叠与展开
  • 允许增加节点与删除节点
  • 按照内容进行节点查找,并且把找到的节点进行特殊样式呈现,如果找到的节点处于被父节点折叠隐藏的状态,则需要做对应的展开

整体思路

印象中最早遇见的树形组件是在xp系统中的文件系统,能够在窗口左侧显示文件夹的展开树。
这里,我们用一个列表来实现一个树形组件,并且增加一个查询按钮和文本输入框。

<ul id="menu">
    <li><label>无可奉告</label>
        <ul>
            <li><label>闷声大发财</label></li>
            <li><label>无可奉告</label></li>
            <li><label>爱慕安格瑞</label></li>
        </ul>
    </li>
    <li><label>听风是雨</label></li>
    <li><label>天气晴朗</label>
        <ul>
            <li><label>跑得快</label>
                <ul>
                    <li><label>弄个大新闻</label></li>
                    <li><label>硬点</label></li>
                </ul>
            </li>
            <li><label>作为长者</label></li>
            <li><label>人生经验</label></li>
        </ul>
    </li>
</ul>
<div id="searchContainer">
        <input type="text" id="searchText">
        <button id="btnSearch">搜索</button>
</div>

然后设置基本的样式

ul{
    list-style: none;
}
li{
    margin-top: 8px;
}
label{
    margin: 0;
}
.glyphicon{
    margin-right: 5px;
}
.hidden{
    display: none;
}
#menu label span{
    margin: 5px;
    display: none;
}
#menu label:hover span{
    display: inline;
}
.searched{
    color: #fb3;
}

如果节点存在孩子节点,则在该节点前面添加一个箭头icon,当箭头朝下时展开该节点下的子节点。当箭头朝右时,隐藏该节点下的所有子节点。通过给箭头添加点击事件来控制这两个状态。
下面是给列表添加icon和icon点击事件的函数

//添加icon
function initIcon(){
    var lists = document.getElementsByTagName("li");
    for(var i=0,len=lists.length;i<len;i++){
        if(lists[i].getElementsByTagName("ul").length){
            var node = document.createElement("span");
            node.className = "glyphicon glyphicon-chevron-down";
            lists[i].insertBefore(node,lists[i].firstChild);
        }
    }
}

//给Icon添加点击事件
function clickEvent(){
    var node = event.target;
    if(node){
        var nodeClass = node.classList;
        if(nodeClass.contains("glyphicon-chevron-down")){
            nodeClass.remove("glyphicon-chevron-down");
            nodeClass.add("glyphicon-chevron-right");
            node.parentNode.lastElementChild.classList.add("hidden");
        }else if(nodeClass.contains("glyphicon-chevron-right")){
            nodeClass.remove("glyphicon-chevron-right");
            nodeClass.add("glyphicon-chevron-down");
            node.parentNode.lastElementChild.classList.remove("hidden");
        }
    }
}

然后,我们给列表添加增加和删除按钮,当鼠标hover某个列表时,会显示这两个按钮,增加的节点会成为鼠标正在hover节点的子节点

//渲染添加和删除按钮
function render(label){
    var addSpan = document.createElement("span");
    addSpan.classList.add("addSpan");
    addSpan.innerHTML = "+";
    addHandler(addSpan,"click",addNode);
    var delSpan = document.createElement("span");
    delSpan.classList.add("delSpan");
    delSpan.innerHTML = "x";
    addHandler(delSpan,"click",delNode)
    label.appendChild(addSpan);
    label.appendChild(delSpan);
}

在删除节点的时候,需要进行判断,如果该节点存在兄弟节点,可以直接删除,如果该节点没有兄弟节点,则需要将父节点前面的icon一起删除

//返回指定元素的子元素节点的个数
function elementChildCount(node){
    var num = 0;
    var children = node.childNodes;
    for(var i=0,len=children.length;i<len;i++){
        if(children[i].nodeType == 1){
            num ++;
        }
    }
    return num;
}

//删除指定节点
function delNode(){
    node = event.target.parentNode.parentNode;
    // console.log(node);
    //用了children不返回文本节点,可能有兼容问题,最好自己写一个返回
    // if(node.parentNode.children.length>1){
    //  node.parentNode.removeChild(node);
    // }
    if(elementChildCount(node.parentNode)>1){
        node.parentNode.removeChild(node);
    }else{
        node = node.parentNode;
        node.parentNode.removeChild(node.parentNode.firstElementChild);
        node.parentNode.removeChild(node);
    }
}

添加节点也分两种情况,如果该节点下已经存在子节点,直接添加即可,如果不存在子节点,需要在添加子节点的同时,给父元素添加一个icon,不能忘记给icon添加点击事件以及新节点的添加和删除功能

//添加节点
function addNode(){
    var node = event.target.parentNode.parentNode;
    var childUl = node.getElementsByTagName("ul")[0];
    if(childUl){
        var name = prompt("请输入名称","");
        if(name && name != ""){
            var li = document.createElement("li");
            var label = document.createElement("label");
            label.innerHTML = name;
            //添加删除和增加按钮
            render(label);
            li.appendChild(label);
            childUl.appendChild(li);
        }
    }else{
        var name = prompt("请输入名称","");
        if(name && name != ""){
            var ul = document.createElement("ul");
            var li = document.createElement("li");
            var label = document.createElement("label");
            label.innerHTML = name;
            render(label);
            li.appendChild(label);
            ul.appendChild(li)
            node.appendChild(ul);
            var span = document.createElement("span");
            span.className = "glyphicon glyphicon-chevron-down";  //setAttribute
            addHandler(span,"click",clickEvent);
            node.insertBefore(span,node.firstChild);
        }
    }
}

最后,还需要给整个树添加查询功能。如果查到该节点,并且该节点的长辈节点有折叠的状态,则需要将其展开。

//查询node函数
function searchNode(){
    var text = document.getElementById('searchText').value;
    var menu = document.getElementById('menu');
    if(text){
        var label = document.getElementsByTagName('label');
        for(var i=0,len=label.length;i<len;i++){
            if(text == label[i].childNodes[0].nodeValue){
                //设置特殊样式
                label[i].setAttribute("class","searched");
                //如果父节点折叠则展开
                var ul = label[i].parentNode.parentNode;
                while(ul != menu){
                    // console.log(ul);
                    if(ul.classList.contains("hidden")){
                        ul.parentNode.firstElementChild.classList.remove("glyphicon-chevron-right");
                        ul.parentNode.firstElementChild.classList.add("glyphicon-chevron-down");
                        ul.classList.remove("hidden");
                    }
                    ul = ul.parentNode.parentNode;
                }
            }
        }
    }   
}

最终代码

最终代码
demo