此控件参考aimaraJS优化而来,由原生js实现,注释详细,代码最大幅度简化
不依赖任何库,只有一个js文件和一个css文件

效果展示

treejs 创建仓库 js实现树控件_TreeView

html代码

<html>
    <head>
        <title>TreeView</title>
        <link rel="icon" href="./image/tree.png"/>

        <link rel="stylesheet" type="text/css" href="./css/tree-view.css"/>
        <link rel="stylesheet" type="text/css" href="./home.css"/>
    </head>

    <body>
        <div id="tree"></div>
    </body>

    <script type="module" src="./home.js"></script>
</html>

javascript代码

import * as TreeViewModule from "./js/tree-view.js";

window.onload = function () {

    //创建控制元素折叠展开的子菜单
    let visibilityMenu = {
        text: "Manage Visibility",
        icon: "image/tree.png",
        submenu: []
    };
    visibilityMenu.submenu.push({
        text: "Toggle Node",
        icon: "image/leaf.png",
        action: function (node) {
            node.toggleNode();
        },
        submenu: []
    });
    visibilityMenu.submenu.push({
        text: "Expand Node",
        icon: "image/leaf.png",
        action: function (node) {
            node.expandNode();
        },
        submenu: []
    });
    visibilityMenu.submenu.push({
        text: "Collapse Node",
        icon: "image/leaf.png",
        action: function (node) {
            node.collapseNode();
        },
        submenu: []
    });
    visibilityMenu.submenu.push({
        text: "Expand Subtree",
        icon: "image/leaf.png",
        action: function (node) {
            node.expandSubtree();
        },
        submenu: []
    });
    visibilityMenu.submenu.push({
        text: "Collapse Subtree",
        icon: "image/leaf.png",
        action: function (node) {
            node.collapseSubtree();
        },
        submenu: []
    });

    //创建控制元素删除
    let elementMenu = {
        text: "Manage Element",
        icon: "image/tree.png",
        submenu: []
    };
    elementMenu.submenu.push({
        text: "Create Child Node",
        icon: "images/add.png",
        action: function (node) {
            node.createChildNode("Level N - Created Node", true, "images/file.png", null, null);
            node.expandNode();
        },
        submenu: []
    });
    elementMenu.submenu.push({
        text: "Delete Node",
        icon: "images/delete.png",
        action: function (node) {
            node.removeNode();
        },
        submenu: []
    });
    elementMenu.submenu.push({
        text: "Delete Subtree",
        icon: "images/delete.png",
        action: function (node) {
            node.removeSubTree();
        },
        submenu: []
    });

    //创建主菜单
    let menu = [visibilityMenu, elementMenu];

    //创建树
    let tree = TreeViewModule.createTree("#tree", menu);

    //创建节点
    for (let i = 1; i < 10; i++) {
        let node1 = tree.createNode(null, null, "Level 1 - Node" + i, "image/tree.png", false, menu);
        for (let j = 1; j < 5; j++) {
            let node2 = node1.createChildNode(node1, null, "Level 2 - Node" + j, "image/leaf.png", null, menu);
            for (let k = 1; k < 5; k++)
                node2.createChildNode(node2, null, "Level 3 - Node" + k, "image/leaf.png", null, menu);
        }
    }

    //重绘
    tree.invalidate();
};
let nodeCounter = 0;

export function createTree(parentDiv, menu) {

    //清空旧内容
    parentDiv = document.querySelector(parentDiv);
    parentDiv.classList.add("tree");
    parentDiv.innerHTML = "";

    //创建一个顶级节点,作为树节点
    //所有方法绑定在顶级节点上,通过tree对象可以直接调用
    let rootNode = {

        //字段
        id: "tree-node-" + (nodeCounter++),
        level: 1,
        element: null,
        childNodes: [],
        menu: menu,
        selectedNode: null,
        menuContainer: null,
        rendered: false,

        //创建一个树节点
        createNode: function (parentNode, tag, text, icon, expanded, menu) {
            //parentNode如果未指定,默认添加到根节点
            if (!parentNode)
                parentNode = rootNode;

            let node = {
                //字段
                id: "tree-node-" + (nodeCounter++),
                level: parentNode.level + 1,
                element: null,
                text: text,
                icon: icon,
                parentNode: parentNode,
                expanded: expanded,
                childNodes: [],
                menu: menu,

                //移除节点
                removeNode: function () {
                    rootNode.removeNode(this);
                },

                //展开或收起当前节点
                toggleNode: function () {
                    rootNode.toggleNode(this);
                },

                //展开节点
                expandNode: function () {
                    rootNode.expandNode(this);
                },

                //展开所有子节点
                expandSubtree: function () {
                    rootNode.expandSubtree(this);
                },

                //设置节点文本
                setText: function (text) {
                    rootNode.setText(this, text);
                },

                //折叠节点
                collapseNode: function () {
                    rootNode.collapseNode(this);
                },

                //折叠所有子节点
                collapseSubtree: function () {
                    rootNode.collapseSubtree(this);
                },

                //移除所有子节点
                removeSubTree: function () {
                    rootNode.removeSubTree(this);
                },

                //创建子节点
                createChildNode: function (parentNode, tag, text, icon, expanded, menu) {
                    return rootNode.createNode(parentNode, tag, text, icon, expanded, menu);
                }
            };

            //如果是尚未创建完成的树,等创建完毕再统一绘制
            //如果是已经创建完成的树,立刻绘制
            if (rootNode.rendered)
                rootNode.drawNode(parentNode.element, node);

            //加入父节点麾下
            parentNode.childNodes.push(node);

            return node;
        },

        //绘制树
        invalidate: function () {
            //创建对应的元素
            let rootElement = createElement("ul", null, null);
            rootNode.element = rootElement;
            parentDiv.appendChild(rootElement);
            //绘制子节点
            for (let i = 0; i < rootNode.childNodes.length; i++)
                rootNode.drawNode(rootElement, rootNode.childNodes[i]);
            if (rootNode.childNodes.length > 0)
                rootElement.lastChild.classList.add("tree-last-child");
            //标记已经绘制
            rootNode.rendered = true;
        },

        //绘制子节点
        drawNode: function (parent, node) {
            //创建节点根容器
            let nodeElement = document.createElement("li");
            nodeElement.classList.add("tree-node");
            node.element = nodeElement;
            parent.appendChild(nodeElement);

            //创建折叠图标
            let collapseElement = createImgElement(null, null, null);
            collapseElement.className = node.expanded ? "tree-toggle-on" : "tree-toggle-off";
            if (node.childNodes.length == 0)
                collapseElement.style.visibility = "hidden";
            nodeElement.appendChild(collapseElement);

            //创建图标文本容器
            let contentContainer = createElement("span", null, "tree-content");
            nodeElement.appendChild(contentContainer);

            //创建图标元素
            let iconElement = createImgElement(null, "tree-icon", node.icon);
            contentContainer.appendChild(iconElement);

            //创建文本元素
            let textElement = createElement("a", null, "tree-text");
            textElement.innerHTML = node.text;
            contentContainer.appendChild(textElement);

            //创建子节点容器
            let childContainer = createElement("ul", null, "tree-node-parent");
            nodeElement.appendChild(childContainer);

            //递归绘制子节点
            for (let i = 0; i < node.childNodes.length; i++)
                rootNode.drawNode(childContainer, node.childNodes[i]);
            if (node.childNodes.length > 0)
                childContainer.lastChild.classList.add("tree-last-child");
            if (!node.expanded)
                childContainer.style.display = "none";

            //折叠或展开节点
            collapseElement.onclick = function () {
                rootNode.toggleNode(node);
            };

            //选中节点
            contentContainer.onclick = function () {
                rootNode.selectNode(node);
            };

            //双击折叠或展开节点
            contentContainer.ondblclick = function () {
                rootNode.toggleNode(node);
            };

            //右键显示菜单
            contentContainer.oncontextmenu = function (e) {
                //阻止浏览器和其它元素响应事件
                e.preventDefault();
                e.stopPropagation();

                //选中节点,并打开菜单
                rootNode.selectNode(node);
                rootNode.createRootMenu(e, node);
                rootNode.menuContainer.style.display = "block";
            };
        },

        //设置文本
        setText: function (node, text) {
            node.text = text;
            node.element.children[1].children[1].innerHTML = text;
        },

        //展开整颗树
        expandTree: function () {
            for (let i = 0; i < this.childNodes.length; i++)
                if (this.childNodes[i].childNodes.length > 0)
                    rootNode.expandSubtree(this.childNodes[i]);
        },

        //折叠整颗树
        collapseTree: function () {
            for (let i = 0; i < this.childNodes.length; i++)
                if (rootNode.childNodes[i].childNodes.length > 0)
                    rootNode.collapseSubtree(this.childNodes[i]);
        },

        //展开子树
        expandSubtree: function (node) {
            rootNode.expandNode(node);
            for (let i = 0; i < node.childNodes.length; i++)
                rootNode.expandSubtree(node.childNodes[i]);
        },

        //折叠子树
        collapseSubtree: function (node) {
            rootNode.collapseNode(node);
            for (let i = 0; i < node.childNodes.length; i++)
                rootNode.collapseSubtree(node.childNodes[i]);
        },

        //展开节点
        expandNode: function (node) {
            if (node.childNodes.length == 0 || node.expanded)
                return;
            //更改图标
            let collapseImg = node.element.children[0];
            collapseImg.className = "tree-toggle-on";
            //显示子节点
            let childContainer = node.element.children[2];
            childContainer.style.display = "block";
            //更改状态
            node.expanded = true;
        },

        //折叠节点
        collapseNode: function (node) {
            if (node.childNodes.length == 0 || !node.expanded)
                return;
            //更改图标
            let collapseImg = node.element.children[0];
            collapseImg.className = "tree-toggle-off";
            //显示子节点
            let childContainer = node.element.children[2];
            childContainer.style.display = "none";
            //更改状态
            node.expanded = false;
        },

        //展开或折叠节点
        toggleNode: function (node) {
            if (node.childNodes.length == 0)
                return;
            if (node.expanded)
                node.collapseNode();
            else
                node.expandNode();
        },

        //选中节点
        selectNode: function (node) {
            //去除上个选中节点样式
            if (rootNode.selectedNode != null)
                rootNode.selectedNode.element.children[1].classList.remove("tree-content-selected");
            //为当前节点添加选中样式
            let contentContainer = node.element.children[1];
            contentContainer.classList.add("tree-content-selected");
            //更新选中节点
            rootNode.selectedNode = node;
        },

        //删除节点
        removeNode: function (node) {
            let index = node.parentNode.childNodes.indexOf(node);
            //移除节点
            node.element.parentNode.removeChild(node.element);
            node.parentNode.childNodes.splice(index, 1);
            //更新折叠图标
            if (node.parentNode.childNodes.length == 0) {
                let collapseImg = node.parentNode.element.children[0];
                collapseImg.style.visibility = "hidden";
            }
            //更新最后节点的样式
            if (node.parentNode.childNodes.length > 0)
                node.parentNode.childNodes[index - 1].element.classList.add("tree-last-child");
        },

        //删除整个子树
        removeSubTree: function (node) {
            if (node.childNodes.length == 0)
                return;
            //隐藏折叠图标
            let collapseImg = node.element.children[0];
            collapseImg.style.visibility = "hidden";
            //清除子节点
            node.childNodes = [];
            let childContainer = node.element.children[2];
            childContainer.innerHTML = "";
        },

        //设置右键菜单
        createRootMenu: function (event, node) {
            //TODO
            if (!node.menu)
                return;

            //创建菜单容器
            if (rootNode.menuContainer == null) {
                rootNode.menuContainer = createElement("ul", null, null);
                rootNode.menuContainer.classList.add("tree-menu-container");
                rootNode.menuContainer.classList.add("tree-menu-parent");
                document.body.appendChild(rootNode.menuContainer);
            }

            //清除旧的菜单内容
            rootNode.menuContainer.innerHTML = "";

            //设置菜单位置
            let left = event.pageX - 5;
            let right = event.pageY - 5;
            rootNode.menuContainer.style.left = left + "px";
            rootNode.menuContainer.style.top = right + "px";

            //逐个创建菜单节点
            for (let i = 0; i < node.menu.length; i++)
                rootNode.createMenu(rootNode.menuContainer, node.menu[i], node);
        },

        //创建菜单
        createMenu: function (parentElement, menu, node) {
            //创建菜单容器
            let menuContainer = createElement("li", null, null);
            menuContainer.classList.add("tree-menu");
            parentElement.appendChild(menuContainer);
            if (!menu.submenu.length)
                menuContainer.onclick = function () {
                    menu.action(node);
                };

            //创建菜单图标
            let iconImg = createImgElement(null, null, menu.icon);
            menuContainer.appendChild(iconImg);
            if (!menu.icon)
                iconImg.src = "image/menu.png";

            //创建文本标题
            let textElement = createElement("a", null, null);
            textElement.appendChild(document.createTextNode(menu.text));
            menuContainer.appendChild(textElement);

            //创建下级菜单图标
            let subMenuIcon = createImgElement(null, "tree-menu-icon", "image/next.png");
            menuContainer.appendChild(subMenuIcon);
            if (!menu.submenu || !menu.submenu.length)
                subMenuIcon.src = "image/hand.png";

            //创建子菜单容器
            let subMenuContainer = createElement("ul", null, null);
            subMenuContainer.classList.add("tree-menu-parent");
            menuContainer.appendChild(subMenuContainer);

            //递归创建子菜单
            if (menu.submenu)
                for (let i = 0; i < menu.submenu.length; i++)
                    rootNode.createMenu(subMenuContainer, menu.submenu[i], node);

            //无子菜单,则执行菜单事件
            if (!menu.submenu)
                node.element.onclick = () => menu.action(node);
        }
    };

    //隐藏菜单
    window.addEventListener("click", () => {
        if (rootNode.menuContainer != null)
            rootNode.menuContainer.style.display = "none";
    }, false);

    return rootNode;
}

//创建一个标签
function createElement(type, id, clazz) {
    let element = document.createElement(type);
    if (id) element.id = id;
    if (clazz) element.className = clazz;
    return element;
}

//创建一个img标签
function createImgElement(id, clazz, src) {
    let element = document.createElement("img");
    if (id) element.id = id;
    if (clazz) element.className = clazz;
    if (src) element.src = src;
    return element;
}

css代码

* {
    margin: 0;
    padding: 0;
}

.tree {
    color: rgba(0, 0, 0, 0.8);
    font-size: 12px;
}

.tree-node-parent {
    background: url(../image/line-vertical.png) repeat-y;
    background-position-x: 8px;
    list-style-type: none;
}

.tree-last-child > .tree-node-parent {
    background: none;
}

.tree-node {
    background: url(../image/line-tri.png) no-repeat;
    background-position-x: 28px;
    background-position-y: -2px;
    padding-left: 20px;
    user-select: none;
    list-style-type: none;
    position: relative;
}

.tree-toggle-off {
    position: absolute;
    top: 2px;
    content: url(../image/toggle-off.png);
}

.tree-toggle-on {
    position: absolute;
    top: 2px;
    content: url(../image/toggle-on.png);
}

.tree-content {
    width: 150px;
    margin-left: 18px;
    padding-left: 5px;
    padding-right: 10px;
    box-sizing: border-box;
    display: inline-block;
    border: 1px transparent solid;
    border-radius: 2px;
}

.tree-content-selected {
    background: lightblue;
    border: 1px lightskyblue solid;
}

.tree-icon {
    position: relative;
    top: 1px;
}

.tree-text {
    padding-left: 4px;
    position: relative;
    top: -2px;
}

.tree-menu-container {
    position: absolute;
    display: block;
    font-size: 12px;
    color: rgba(0, 0, 0, 0.7);
}

.tree-menu-parent {
    background: white;
    border-radius: 2px;
    padding: 5px 0px 5px 0px;
    box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.3);
    list-style: none;
}

.tree-menu {
    height: 25px;
    padding-left: 5px;
    padding-right: 5px;
    user-select: none;
    display: grid;
    position: relative;
    grid-template-columns: 16px 150px 16px;
    grid-column-gap: 6px;
    align-items: center;
    justify-content: center;
}

.tree-menu:hover {
    background: lightskyblue;
}

.tree-menu > .tree-menu-parent {
    display: none;
    position: absolute;
    left: 204px;
    top: -5px;
}

.tree-menu:hover > .tree-menu-parent {
    display: block;
}