当前位置: 首页 > 工具软件 > Folder Tree > 使用案例 >

easyui之tree控件分析

庄元龙
2023-12-01
/**
 * tree - jQuery EasyUI
 *
 * Licensed under the GPL:
 *   http://www.gnu.org/licenses/gpl.txt
 *
 * Copyright 2010 stworthy [ stworthy@gmail.com ]
 *
 * Node is a javascript object which contains following properties:
 * 1 id: An identity value bind to the node.
 * 2 text: Text to be showed.
 * 3 checked: Indicate whether the node is checked selected.
 * 3 attributes: Custom attributes bind to the node.
 * 4 target: Target DOM object.
 */
/**
 * 首先要明白,tree控件包装的思想,tree包装结构是:
 * 节点(<li><div></div><ul>其他节点(li)</ul>)
 * 所以说一个节点就是一个li,一个li里面会有两块内容,第一个是由div包装的这个节点的基本信息,另一个是由ul标签包装的子节点的信息
 * 树级别之间的缩进是用一个个小方格划分的,每个节点的高度是18px,每个小方格的尺寸是16*18
 */
(function ($) {
    /**
     * wrap the <ul> tag as a tree and then return it.
     * 包装tree外层标签,默认tree的外层标签是一个ul,其实不使用tree控件并不依赖于ul
     */
    function wrapTree(target) {
        //targetdom对象
        var tree = $(target);
        //首先给外层标签添加类tree
        tree.addClass('tree');
        //包装这个最外层的dom对象,并说明深度为0
        wrapNode(tree, 0);

        /**
         * 包装指定的节点,如果depth0表示包装的是一个树节点的外层,这个ul很像是节点的ul标签
         */
        function wrapNode(ul, depth) {
            //找到这个最外层节点的下级li标签,如果depth0那么就是在寻找所有的根节点
            $('>li', ul).each(function () {
                //如果找到了根节点,就直接向这个根节点里面加入一个div标签(这个标签封装了这个节点的信息)
                var node = $('<div class="tree-node"></div>').prependTo($(this));
                //找到这个根节点下的下级span标签,将这些span标签添加到新创建的div标签后获取其内容
                var text = $('>span', this).addClass('tree-title').appendTo(node).text();
                //给这个节点(div标签)绑定'tree-node'数据,绑定的数据中记录了这个节点的值
                $.data(node[0], 'tree-node', {
                    text: text
                });
                //找到这个根节点下的所有ul标签
                var subTree = $('>ul', this);

                if (subTree.length) {
                    //如果找到了ul标签,那么就认为这个节点有子节点,然后就把文件打开的span标签添加到这个节点的最前方
                    $('<span class="tree-folder tree-folder-open"></span>').prependTo(node);
                    //把这个节点的展开信息添加到这个节点的最前方
                    $('<span class="tree-hit tree-expanded"></span>').prependTo(node);
                    //递归调用,将这个ul标签传递进去,并说明深度加了1
                    wrapNode(subTree, depth + 1);
                } else {
                    $('<span class="tree-file"></span>').prependTo(node);
                    $('<span class="tree-indent"></span>').prependTo(node);
                }
                //最后处理由于深度问题引起的行缩进问题
                for (var i = 0; i < depth; i++) {
                    $('<span class="tree-indent"></span>').prependTo(node);
                }
            });
        }

        return tree;
    }

    function expandNode(target, node) {
        //展开节点
        var opts = $.data(target, 'tree').options;

        var hit = $('>span.tree-hit', node);
        if (hit.length == 0) return;   // is a leaf node

        if (hit.hasClass('tree-collapsed')) {
            hit.removeClass('tree-collapsed tree-collapsed-hover').addClass('tree-expanded');
            hit.next().addClass('tree-folder-open');
            var ul = $(node).next();
            if (ul.length) {
                if (opts.animate) {
                    ul.slideDown();
                } else {
                    ul.css('display', 'block');
                }
            } else {
                var id = $.data($(node)[0], 'tree-node').id;
                var subul = $('<ul></ul>').insertAfter(node);
                request(target, subul, {id: id});  // request children nodes data
            }
        }
    }

    function collapseNode(target, node) {
        //折叠节点
        var opts = $.data(target, 'tree').options;

        var hit = $('>span.tree-hit', node);
        if (hit.length == 0) return;   // is a leaf node

        if (hit.hasClass('tree-expanded')) {
            hit.removeClass('tree-expanded tree-expanded-hover').addClass('tree-collapsed');
            hit.next().removeClass('tree-folder-open');
            if (opts.animate) {
                $(node).next().slideUp();
            } else {
                $(node).next().css('display', 'none');
            }
        }
    }

    function toggleNode(target, node) {
        //展开或折叠节点
        var hit = $('>span.tree-hit', node);
        //如果没有加减号就代表是子节点
        if (hit.length == 0) return;   // is a leaf node

        if (hit.hasClass('tree-expanded')) {
            //如果是展开的就关闭子节点
            collapseNode(target, node);
        } else {
            //如果是折叠的就展开子节点
            expandNode(target, node);
        }
    }

    /**
     * 给树控件绑定事件,包括节点的鼠标划入划出,单击、双击,奇怪的是这里没有看到右击事件
     * @param target
     */
    function bindTreeEvents(target) {
        var opts = $.data(target, 'tree').options;
        var tree = $.data(target, 'tree').tree;

        //给整体节点绑定单击、双击、鼠标划入划出事件,注意绑定事件前先把同类型的事件移除掉,注意这些绑定的事件的返回值大部分都是false,这是要阻止事件冒泡
        $('.tree-node', tree).unbind('.tree').bind('dblclick.tree', function () {
            //双击树节点,从源代码上看,单击和双击都是选中节点,然后再去调用用户绑定的方法,其他的并没有什么不同
            $('.tree-node-selected', tree).removeClass('tree-node-selected');
            $(this).addClass('tree-node-selected');

            if (opts.onDblClick) {
                var target = this; // the target HTML DIV element
                var data = $.data(this, 'tree-node');
                opts.onDblClick.call(this, {
                    id: data.id,
                    text: data.text,
                    attributes: data.attributes,
                    target: target
                });
            }
        }).bind('click.tree', function () {
            //点击树节点,代表选中该节点
            $('.tree-node-selected', tree).removeClass('tree-node-selected');
            $(this).addClass('tree-node-selected');

            if (opts.onClick) {
                var target = this; // the target HTML DIV element
                var data = $.data(this, 'tree-node');
                opts.onClick.call(this, {
                    id: data.id,
                    text: data.text,
                    attributes: data.attributes,
                    target: target
                });
            }
//       return false;
        }).bind('mouseenter.tree', function () {
            //鼠标滑进去节点(说的是div标签,不是li标签)
            $(this).addClass('tree-node-hover');
            return false;
        }).bind('mouseleave.tree', function () {
            //鼠标滑出来
            $(this).removeClass('tree-node-hover');
            return false;
        });

        //给节点的加减号绑定事件
        $('.tree-hit', tree).unbind('.tree').bind('click.tree', function () {
            //单击加减号,切换展开或折叠效果
            var node = $(this).parent();
            toggleNode(target, node);
            return false;
        }).bind('mouseenter.tree', function () {
            if ($(this).hasClass('tree-expanded')) {
                $(this).addClass('tree-expanded-hover');
            } else {
                $(this).addClass('tree-collapsed-hover');
            }
        }).bind('mouseleave.tree', function () {
            if ($(this).hasClass('tree-expanded')) {
                $(this).removeClass('tree-expanded-hover');
            } else {
                $(this).removeClass('tree-collapsed-hover');
            }
        });

        //给复选框绑定事件
        $('.tree-checkbox', tree).unbind('.tree').bind('click.tree', function () {
            if ($(this).hasClass('tree-checkbox0')) {
                $(this).removeClass('tree-checkbox0').addClass('tree-checkbox1');
            } else if ($(this).hasClass('tree-checkbox1')) {
                $(this).removeClass('tree-checkbox1').addClass('tree-checkbox0');
            } else if ($(this).hasClass('tree-checkbox2')) {
                $(this).removeClass('tree-checkbox2').addClass('tree-checkbox1');
            }
            //设置父节点是否选中
            setParentCheckbox($(this).parent());
            //设置子节点是否选中
            setChildCheckbox($(this).parent());
            return false;
        });

        function setChildCheckbox(node) {
            //选中子节点
            var childck = node.next().find('.tree-checkbox');
            childck.removeClass('tree-checkbox0 tree-checkbox1 tree-checkbox2')
            if (node.find('.tree-checkbox').hasClass('tree-checkbox1')) {
                childck.addClass('tree-checkbox1');
            } else {
                childck.addClass('tree-checkbox0');
            }
        }

        function setParentCheckbox(node) {
            //设置父节点是否展开,需要递归调用
            var pnode = getParentNode(target, node[0]);
            if (pnode) {
                var ck = $(pnode.target).find('.tree-checkbox');
                ck.removeClass('tree-checkbox0 tree-checkbox1 tree-checkbox2');
                if (isAllSelected(node)) {
                    ck.addClass('tree-checkbox1');
                } else if (isAllNull(node)) {
                    ck.addClass('tree-checkbox0');
                } else {
                    ck.addClass('tree-checkbox2');
                }
                setParentCheckbox($(pnode.target));
            }

            //是否全部被选中
            function isAllSelected(n) {
                var ck = n.find('.tree-checkbox');
                if (ck.hasClass('tree-checkbox0') || ck.hasClass('tree-checkbox2')) return false;
                var b = true;
                n.parent().siblings().each(function () {
                    if (!$(this).find('.tree-checkbox').hasClass('tree-checkbox1')) {
                        b = false;
                    }
                });
                return b;
            }

            function isAllNull(n) {
                var ck = n.find('.tree-checkbox');
                if (ck.hasClass('tree-checkbox1') || ck.hasClass('tree-checkbox2')) return false;
                var b = true;
                n.parent().siblings().each(function () {
                    if (!$(this).find('.tree-checkbox').hasClass('tree-checkbox0')) {
                        b = false;
                    }
                });
                return b;
            }
        }
    }

    //加载数据
    function loadData(target, ul, data) {
        // clear the tree when loading to the root
        //加载之前,直接将树的根节点的html标签全部清空
        if (target == ul) {
            $(target).empty();
        }

        var opts = $.data(target, 'tree').options;

        function appendNodes(ul, children, depth) {
            for (var i = 0; i < children.length; i++) {
                //首先给每一个节点先创建li标签
                var li = $('<li></li>').appendTo(ul);
                var item = children[i];

                // the node state has only 'open' or 'closed' attribute
                if (item.state != 'open' && item.state != 'closed') {
                    item.state = 'open';
                }

                //给每个节点创建主要展示区div标签,并设置node-id属性值
                var node = $('<div class="tree-node"></div>').appendTo(li);
                node.attr('node-id', item.id);

                //存储这个节点绑定的数据模型,方面后面事件的调用
                // store node attributes
                $.data(node[0], 'tree-node', {
                    id: item.id,
                    text: item.text,
                    attributes: item.attributes
                });

                //这里先把节点显示的文本信息添加进去,其他的内容"加减号""复选框""文件夹"后面根据条件插入到前面去
                $('<span class="tree-title"></span>').html(item.text).appendTo(node);
                //如果设置了复选框,就将现将复选框插入到最左
                if (opts.checkbox) {
                    if (item.checked) {
                        $('<span class="tree-checkbox tree-checkbox1"></span>').prependTo(node);
                    } else {
                        $('<span class="tree-checkbox tree-checkbox0"></span>').prependTo(node);
                    }
                }
                //如果这个节点有子节点,就添加子节点的容器ul标签
                if (item.children) {
                    var subul = $('<ul></ul>').appendTo(li);
                    //如果当前节点是展开的,就将文件夹打开的图标和减号添加到树节点的展示区
                    if (item.state == 'open') {
                        $('<span class="tree-folder tree-folder-open"></span>').addClass(item.iconCls).prependTo(node);
                        $('<span class="tree-hit tree-expanded"></span>').prependTo(node);
                    } else {
                        $('<span class="tree-folder"></span>').addClass(item.iconCls).prependTo(node);
                        $('<span class="tree-hit tree-collapsed"></span>').prependTo(node);
                        subul.css('display', 'none');
                    }
                    //如果存在子节点就递归调用
                    appendNodes(subul, item.children, depth + 1);
                } else {
                    //如果没有子节点
                    if (item.state == 'closed') {
                        $('<span class="tree-folder"></span>').addClass(item.iconCls).prependTo(node);
                        $('<span class="tree-hit tree-collapsed"></span>').prependTo(node);
                    } else {
//                $('<input type="checkbox" style="vertical-align:bottom;margin:0;height:18px;">').prependTo(node);
                        $('<span class="tree-file"></span>').addClass(item.iconCls).prependTo(node);
                        $('<span class="tree-indent"></span>').prependTo(node);
                    }
                }
                for (var j = 0; j < depth; j++) {
                    $('<span class="tree-indent"></span>').prependTo(node);
                }
            }
        }

        var depth = $(ul).prev().find('>span.tree-indent,>span.tree-hit').length;
        appendNodes(ul, data, depth);

    }

    /**
     * request remote data and then load nodes in the <ul> tag.
     */
    function request(target, ul, param) {
        //首先取出来这个树dom对象绑定好的参数数据
        var opts = $.data(target, 'tree').options;
        //如果没有设置url属性就直接返回
        if (!opts.url) return;

        param = param || {};
        //主要是针对子节点中的url,找到这个子节点的div标签,将里面的span表示树文件夹的节点打开状添加加载中的类
        var folder = $(ul).prev().find('>span.tree-folder');
        folder.addClass('tree-loading');
        $.ajax({
            type: 'post',
            url: opts.url,
            data: param,
            dataType: 'json',
            success: function (data) {
                //加载成功后,首先将加载中去掉,然后加载数据到这个子节点
                folder.removeClass('tree-loading');
                loadData(target, ul, data);
                //然后为这个节点绑定事件
                bindTreeEvents(target);
                if (opts.onLoadSuccess) {
                    //如果指定了加载成功的事件函数,就调用用户定义的函数
                    opts.onLoadSuccess.apply(this, arguments);
                }
            },
            error: function () {
                //加载失败,就移除加载中,如果用户定义了加载失败的处理函数,就调用用户定义的加载失败的函数
                folder.removeClass('tree-loading');
                if (opts.onLoadError) {
                    opts.onLoadError.apply(this, arguments);
                }
            }
        });
    }

    /**
     * get the parent node
     * param: DOM object, from which to search it's parent node
     */
    function getParentNode(target, param) {
        //这里只是获取父节点对象,name所指的父节点对象以及param应该都是div标签
        var node = $(param).parent().parent().prev();
        if (node.length) {
            return $.extend({}, $.data(node[0], 'tree-node'), {
                target: node[0],
                checked: node.find('.tree-checkbox').hasClass('tree-checkbox1')
            });
        } else {
            return null;
        }
    }

    function getCheckedNode(target) {
        //找到所有选中了的节点对象,这里的target应该指的的根节点dom对象,但是用户仍然可以自己制定target,这就需要$("...").tree("getCheckedNode"):这样去掉用了
        var nodes = [];
        $(target).find('.tree-checkbox1').each(function () {
            var node = $(this).parent();
            nodes.push($.extend({}, $.data(node[0], 'tree-node'), {
                target: node[0],
                checked: node.find('.tree-checkbox').hasClass('tree-checkbox1')
            }));
        });
        return nodes;
    }

    /**
     * Get the selected node data which contains following properties: id,text,attributes,target
     */
    function getSelectedNode(target) {
        //获取选中了的子节点对象,这里的选中说的是节点,不是复选框
        var node = $(target).find('div.tree-node-selected');
        if (node.length) {
            return $.extend({}, $.data(node[0], 'tree-node'), {
                target: node[0],
                checked: node.find('.tree-checkbox').hasClass('tree-checkbox1')
            });
        } else {
            return null;
        }
    }

    /**
     * Append nodes to tree.
     * The param parameter has two properties:
     * 1 parent: DOM object, the parent node to append to.
     * 2 data: array, the nodes data.
     */
    function appendNodes(target, param) {
        var node = $(param.parent);
        var ul = node.next();
        if (ul.length == 0) {
            ul = $('<ul></ul>').insertAfter(node);
        }

        // ensure the node is a folder node
        if (param.data && param.data.length) {
            var nodeIcon = node.find('span.tree-file');
            if (nodeIcon.length) {
                nodeIcon.removeClass('tree-file').addClass('tree-folder');
                var hit = $('<span class="tree-hit tree-expanded"></span>').insertBefore(nodeIcon);
                if (hit.prev().length) {
                    hit.prev().remove();
                }
            }
        }

        loadData(target, ul, param.data);
        bindTreeEvents(target);
    }

    /**
     * Remove node from tree.
     * param: DOM object, indicate the node to be removed.
     */
    function removeNode(target, param) {
        var node = $(param);
        var li = node.parent();
        var ul = li.parent();
        li.remove();
        if (ul.find('li').length == 0) {
            var node = ul.prev();
            node.find('.tree-folder').removeClass('tree-folder').addClass('tree-file');
            node.find('.tree-hit').remove();
            $('<span class="tree-indent"></span>').prependTo(node);
            if (ul[0] != target) {
                ul.remove();
            }
        }
    }

    /**
     * select the specified node.
     * param: DOM object, indicate the node to be selected.
     */
    function selectNode(target, param) {
        //选中给定的父节点下的子节点,首先清空这个父节点下所有的子节点,然后再去选中给的子节点
        $('div.tree-node-selected', target).removeClass('tree-node-selected');
        $(param).addClass('tree-node-selected');
    }

    /**
     * Check if the specified node is leaf.
     * param: DOM object, indicate the node to be checked.
     */
    function isLeaf(target, param) {
        //判断某个节点是不是叶子节点,只需要判断是不是有加减号就行了
        var node = $(param);
        var hit = $('>span.tree-hit', node);
        return hit.length == 0;
    }


    $.fn.tree = function (options, param) {
        //如果第一个参数是字符串,name调用的是方法
        if (typeof options == 'string') {
            switch (options) {
                case 'options':
                    //options方法从树控件的绑定的tree属性上取得数据
                    return $.data(this[0], 'tree').options;
                case 'reload':
                    //如果是reload方法,就清空树形控件的内容,重新发送请求
                    return this.each(function () {
                        $(this).empty();
                        request(this, this);
                    });
                case 'getParent':
                    //如果是getParent方法,就获取当前树的父节点
                    return getParentNode(this[0], param);
                case 'getChecked':
                    //获取选中了的节点(是复选框选中)
                    return getCheckedNode(this[0]);
                case 'getSelected':
                    //获取选中了的节点(是选中节点,不是复选框)
                    return getSelectedNode(this[0]);
                case 'isLeaf':
                    //判断是不是叶子节点,实现上也就是去判断有没有加减号
                    return isLeaf(this[0], param); // param is the node object
                case 'select':
                    //选中给的节点节点
                    return this.each(function () {
                        selectNode(this, param);
                    });
                case 'collapse':
                    //折叠给定的节点
                    return this.each(function () {
                        collapseNode(this, $(param));  // param is the node object
                    });
                case 'expand':
                    //展开节点
                    return this.each(function () {
                        expandNode(this, $(param));       // param is the node object
                    });
                case 'append':
                    //给指定的节点,加入子节点
                    return this.each(function () {
                        appendNodes(this, param);
                    });
                case 'toggle':
                    //切换节点的展开与关闭
                    return this.each(function () {
                        toggleNode(this, $(param));
                    });
                case 'remove':
                    //移除给的节点
                    return this.each(function () {
                        removeNode(this, param);
                    });
            }
        }

        var options = options || {};
        return this.each(function () {
            var state = $.data(this, 'tree');
            var opts;
            if (state) {
                //之前在这个dom节点上绑定过'tree'属性的数据,说明不是第一次调用,就要用现在的设置去覆盖以前的设置
                opts = $.extend(state.options, options);
                state.options = opts;
            } else {
                //没有发现这个节点上之前绑定过'tree'属性的数据,说明是第一次加载
                //可能是这一个版本的树控件不支持data-options的加载方式,自己扩展了下面的代码段
                var optex = {};
                try {
                    optex = eval("({" + $(this).attr("data-options") + "})");
                } catch (ex) {
                }
                //将用户设置的参数对象或节点的特殊属性信息叠加到系统默认参数对象上
                opts = $.extend({}, $.fn.tree.defaults, {
                    url: $(this).attr('url'),
                    animate: ($(this).attr('animate') ? $(this).attr('animate') == 'true' : undefined)
                }, options, optex);
                //组装好了当前有效的参数模型后就将组装好的参数对象以及包装一下当前的节点然后以'tree'属性绑定到当前元素上
                $.data(this, 'tree', {
                    options: opts,
                    tree: wrapTree(this)
                });
            }
            //如果用户指定了加载数据的url地址那么就去加载远程的数据
            if (opts.url) {
                request(this, this);
            }
            //给这个数绑定事件
            bindTreeEvents(this);
        });
    };

    //树控件的一些默认值
    $.fn.tree.defaults = {
        url: null,
        animate: false,
        checkbox: false,

        onLoadSuccess: function () {
        },
        onLoadError: function () {
        },
        onClick: function (node) {
        }, // node: id,text,attributes,target
        onDblClick: function (node) {
        }  // node: id,text,attributes,target
    };
})(jQuery);
 类似资料: