YUI3的树实现

在ExtFrame里,我实现了一颗可以自动加载所有节点的树(编程人员无需再为树编写一大堆代码),这颗树是通过继承Ext.TreePanel实现的

但是YUI3的标准版本里,并没有树的相关实现,想做到同样功能有点难了

经过查找,YUI3的Gallery里到是有treeview模块实现(版本3.7),花了几天测试,不过后来发现,原来YUI3 Gallery里有两个treeview实现,一个是Treeview(T索引),另一个是YUI Treeview(Y索引),不过我研究的是前一个,后一个咋看起来好像更好看些

这个treeview的效果网页上有Demo,代码调用方式是直接使用Gallery模块,但是这种方式要求种子使用官方地址,对内部使用的系统不合适

代码是这样的:

var treeview = new Y.CheckBoxTreeView({
            startCollapsed: true,
            toggleOnLabelClick: false,
            children: [ {
                label: "Root",
                checked: "halfchecked",
                children: [ 
                    {
                        label : "sub 1",
                        checked: "checked",
                        children : [
                            { label: "sub 1-1"},
                            { label: "sub 1-2"},
                        ]
                    },
                    {
                        label : "sub 2",
                        children : [
                            { label: "sub 2-1"},
                            {
                                label: "sub 2-2",
                                children: [
                                    { label: "sub 2-2-1" },
                                    { label: "sub 2-2-2" }
                                ]
                            },
                        ]
                    }
                ]
            }]
    });
    treeview.render("#cattree1");

效果么看起来马马虎虎,反正给人一种远不如ExtJS的感觉
地址是http://yuilibrary.com/gallery/show/yui3treeview-ng

这个Demo的问题是要使用必须引用的种子是官方地址,然后模块是gallery-yui3treeview-ng,对内网系统来说有点不太方便(至少种子引用应该是本地服务器吧)

还是下载到本地加入本地模块

下载下来后有些代码要修改,再加上拷贝的代码里还有些错误,研究后做了如下修正:

1、修正了有张背景图片不透明的问题(在灰色背景下有的节点显示正常,有的显示白底)

2、为节点添加了图片,通过icon属性设定CSS可以显示图标,这样效果基本就看起来很象ExtJS的树了

3、修正了些小错误,例如nodeclick事件

4、考虑到框架应用,原本展开、收缩的框是通过isLeaf属性判断的(即节点是否叶子),但实际应用中,有可能需要动态展开,所以改成添加expandable属性,根据该属性判断是否可展开

修改后代码是这样的

YUI.add('treeview', function(Y) {
    var getClassName = Y.ClassNameManager.getClassName,
        BOUNDING_BOX = "boundingBox",
        CONTENT_BOX = "contentBox",
        TREEVIEW = "treeview",
        TREENODE = "treenode",
        CHECKBOXTREEVIEW = "checkboxtreeview",
        CHECKBOXTREENODE = "checkboxtreenode",
        classNames = {
            tree: getClassName(TREENODE),
            content: getClassName(TREENODE, "content"),
            label: getClassName(TREENODE, "label"),
            labelContent: getClassName(TREENODE, "label-content"),
            toggle: getClassName(TREENODE, "toggle-control"),
            collapsed: getClassName(TREENODE, "collapsed"),
            leaf: getClassName(TREENODE, "leaf"),
            lastnode: getClassName(TREENODE, "last"),
            checkbox: getClassName(CHECKBOXTREENODE, "checkbox")
        },
        checkStates = {
            // Check states for checkbox tree            
            unchecked: 10,
            halfchecked: 20,
            checked: 30
        },
        checkStatesClasses = {
            10: getClassName(CHECKBOXTREENODE, "checkbox-unchecked"),
            20: getClassName(CHECKBOXTREENODE, "checkbox-halfchecked"),
            30: getClassName(CHECKBOXTREENODE, "checkbox-checked")
        },
        findChildren;

    /* 
    * Used in HTML_PARSERs to find children of the current widget 
    */
    findChildren = function(srcNode, selector) {
        var descendants = srcNode.all(selector),
            children = Array(),
            child;
        descendants.each(function(node) {
            child = {
                srcNode: node,
                boundingBox: node,
                contentBox: node.one("> ul")
            };
            children.push(child);
        });
        return children;
    };

    /** 
    * TreeView widget. Provides a tree style widget, with a hierachical representation of it's components. 
    * It extends WidgetParent and WidgetChild, please refer to it's documentation for more info.    
    * This widget represents the root cotainer for TreeNode objects that build the actual tree structure.  
    * Therefore this widget will not usually have any visual representation. Its also responsible for handling node events.
    * @class TreeView * @constructor * @uses WidgetParent * @extends Widget * @param {Object} config User configuration object. 
    */
    Y.TreeView = Y.Base.create(TREEVIEW, Y.Widget, [Y.WidgetParent], {
        CONTENT_TEMPLATE: "<ul></ul>",
        initializer: function(config) {
            /**             
            * Fires when node is expanded / collapsed             
            * @event nodeToggle             
            * @param {TreeNode} treenode tree node that is expanding / collapsing.             
            * Use this event to listed for nodes being clicked.              
            */
            this.publish("nodeToggle", {
                defaultFn: this._nodeToggleDefaultFn
            });
            /**             
            * Fires when node is collapsed             
            * @event nodeCollapse             
            * @param {TreeNode} treenode tree node that is collapsing             
            */
            this.publish("nodeCollapse", {
                defaultFn: this._nodeCollapseDefaultFn
            });
            /**             
            * Fires when node is expanded             
            * @event nodeExpand             
            * @param {TreeNode} treenode tree node that is expanding             
            */
            this.publish("nodeExpand", {
                defaultFn: this._nodeExpandDefaultFn
            });
            /**             
            * Fires when node is clicked             
            * @event nodeClick             
            * @param {TreeNode} treenode tree node that is being clicked             
            */
            this.publish("nodeClick", {
                defaultFn: this._nodeClickDefaultFn
            });
        },
        /**            
        * Default event handler for "nodeclick" event            
        * @method _nodeClickDefaultFn            
        * @protected            
        */
        _nodeClickDefaultFn: function(e) {
        },
        /**            
        * Default event handler for "toggleTreeState" event            
        * @method _nodeToggleDefaultFn            
        * @protected            
        */
        _nodeToggleDefaultFn: function(e) {
            if (e.treenode.get("collapsed")) {
                this.fire("nodeExpand", { treenode: e.treenode });
            } else {
                this.fire("nodeCollapse", { treenode: e.treenode });
            }
        },
        /**            
        * Default event handler for "collapse" event            
        * @method _nodeCollapseDefaultFn            
        * @protected            
        */
        _nodeCollapseDefaultFn: function(e) {
            e.treenode.collapse();
        },
        /**            
        * Default event handler for "expand" event            
        * @method _expandStateDefaultFn            
        * @protected            
        */
        _nodeExpandDefaultFn: function(e) {
            e.treenode.expand();
        },
        /**         
        * Sets child event handlers         
        * @method _setChildEventHandlers         
        * @protected         
        */
        _setChildEventHandlers: function() {
            var parent;
            this.after("addChild", function(e) {
                parent = e.child.get("parent");
                if (e.child.get("isLast") && parent.size() > 1) {
                    parent.item(e.child.get("index") - 1)._unmarkLast();
                }
            });
            this.on("removeChild", function(e) {
                parent = e.child.get("parent");
                if ((parent.size() == 1) || e.child.get("index") === 0) {
                    return;
                }
                if (e.child.get("isLast")) {
                    parent.item(e.child.get("index") - 1)._markLast();
                }
            });
        },
        /**            
        * Handles internal tree click events            
        * @method _onClickEvents            
        * @protected            
        */
        _onClickEvents: function(event) {
            var target = event.target,
                twidget = Y.Widget.getByNode(target),
                toggle = false;
            event.preventDefault();
            twidget = Y.Widget.getByNode(target);
            if (!twidget instanceof Y.TreeNode) {
                return;
            }
//            if (!twidget.get("expandable")) {
//                return;
//            }
            Y.Array.each(target.get("className").split(" "), function(className) {
                switch (className) {
                    case classNames.toggle:
                        toggle = true;
                        break;
                    case classNames.labelContent:
                        if (this.get("toggleOnLabelClick")) {
                            toggle = true;
                        }
                        break;
                }
            }, this);
            if (toggle) {
                this.fire("nodeToggle", { treenode: twidget });
            }
            else {
                this.fire("nodeClick", { treenode: twidget });
            }
        },
        /**         
        * Handles internal tree keyboard interaction         
        * @method _onKeyEvents         
        * @protected        
        */
        _onKeyEvents: function(event) {
            var target = event.target,
                twidget = Y.Widget.getByNode(target),
                keycode = event.keyCode,
                collapsed = twidget.get("collapsed");
            if (twidget.get("isLeaf")) {
                return;
            }
            if (((keycode == 39) && collapsed) || ((keycode == 37) && !collapsed)) {
                this.fire("nodeToggle", { treenode: twidget });
            }
        },
        bindUI: function() {
            var boundingBox = this.get(BOUNDING_BOX);
            boundingBox.on("click", this._onClickEvents, this);
            boundingBox.on("keypress", this._onKeyEvents, this);
            /*            boundingBox.delegate("click", Y.bind(function(e) {
            var twidget = Y.Widget.getByNode(e.target);
            if (twidget instanceof Y.TreeNode) {
            this.fire("nodeClick", { treenode: twidget });
            }
            }, this), "." + classNames.label);*/
            this._setChildEventHandlers();
            boundingBox.plug(Y.Plugin.NodeFocusManager, {
                descendants: ".yui3-treenode-label",
                keys: {
                    next: "down:40",
                    // Down arrow                    
                    previous: "down:38"
                    // Up arrow                 
                },
                circular: false
            });
        }
    }, {
        NAME: TREEVIEW,
        ATTRS: {
            /**             
            * @attribute defaultChildType             
            * @type String             
            * @readOnly            
            * @description default child type definition             
            */
            defaultChildType: {
                value: "TreeNode",
                readOnly: true
            },
            /**             
            * @attribute toggleOnLabelClick             
            * @type Boolean             
            * @description whether to toogle tree state on label clicks with addition to toggle control clicks             
            */
            toggleOnLabelClick: {
                value: true,
                validator: Y.Lang.isBoolean
            },
            /**             
            * @attribute startCollapsed             
            * @type Boolean             
            * @description Whether to render tree nodes expanded or collapsed by default             
            */
            startCollapsed: {
                value: true,
                validator: Y.Lang.isBoolean
            },
            /**             
            * @attribute loadOnDemand             
            * @type boolean             
            *             
            * @description Whether children of this node can be loaded on demand             
            * (when this tree node is expanded, for example).             
            * Use with gallery-yui3treeview-ng-datasource.             
            */
            loadOnDemand: {
                value: false,
                validator: Y.Lang.isBoolean
            }
        },
        HTML_PARSER: {
            children: function(srcNode) {
                return findChildren(srcNode, "> li");
            }
        }
    });
    /** 
    * TreeNode widget. Provides a tree style node widget. 
    * It extends WidgetParent and WidgetChild, please refer to it's documentation for more info.    
    * @class TreeNode 
    * @constructor 
    * @uses WidgetParent, WidgetChild 
    * @extends Widget 
    * @param {Object} config User configuration object. 
    */
    Y.TreeNode = Y.Base.create(TREENODE, Y.Widget, [Y.WidgetParent, Y.WidgetChild], {
        /**         
        * Flag to determine if the tree is being rendered from markup or not         
        * @property _renderFromMarkup         
        * @protected         
        */
        _renderFromMarkup: false,
        CONTENT_TEMPLATE: "<ul></ul>",
        BOUNDING_TEMPLATE: "<li></li>",
        TREENODELABEL_TEMPLATE: "<a class={labelClassName} role='treeitem' href='#'></a>",
        TREENODELABELCONTENT_TEMPLATE: "<span class={labelContentClassName}><a class={iconClassName}>{label}</a></span>",
        TOGGLECONTROL_TEMPLATE: "<span class={toggleClassName}></span>",
        bindUI: function() {
            // Both TreeVew and TreeNode share the same child event handling            
            Y.TreeView.prototype._setChildEventHandlers.apply(this, arguments);
        },
        /**         
        * Renders TreeNode         
        * @method renderUI         
        * @protected        
        */
        renderUI: function() {
            var boundingBox = this.get(BOUNDING_BOX),
    treeLabel,
    treeLabelHTML,
    labelContent,
    labelContentHTML,
    toggleControlHTML,
    label,
    isLeaf;
            toggleControlHTML = Y.substitute(this.TOGGLECONTROL_TEMPLATE, { toggleClassName: classNames.toggle });
            isLeaf = this.get("isLeaf");
            if (this._renderFromMarkup) {
                treeLabel = boundingBox.one(":first-child");
                treeLabel.set("role", "treeitem");
                treeLabel.addClass(classNames.label);
                labelContent = treeLabel.removeChild(treeLabel.one(":first-child"));
                labelContent.addClass(classNames.labelContent);
            } else {
                label = this.get("label");
                treeLabelHTML = Y.substitute(this.TREENODELABEL_TEMPLATE, { labelClassName: classNames.label });
                labelContentHTML = Y.substitute(this.TREENODELABELCONTENT_TEMPLATE, { labelContentClassName: classNames.labelContent, iconClassName: this.get('icon') == '' ? '' : ' ' + this.get('icon'), label: label });
                labelContent = labelContentHTML;
                treeLabel = Y.Node.create(treeLabelHTML);
                boundingBox.prepend(treeLabel);
            }
            if (this.get('expandable')) {
                treeLabel.appendChild(toggleControlHTML).appendChild(labelContent);
            } else {
                treeLabel.append(labelContent);
            }
            boundingBox.set("role", "presentation");
            if (this.get('expandable')) {
                if (this.get("root").get("startCollapsed")) {
                    boundingBox.addClass(classNames.collapsed);
                } else {
                    if (this.size() === 0) {
                        // Nodes (not leafs) without children should start in collapsed mode                        
                        boundingBox.addClass(classNames.collapsed);
                    }
                }
            }
            if (!this.get('expandable')) {
                boundingBox.addClass(classNames.leaf);
            }
            if (this.get("isLast")) {
                this._markLast();
            }
        },
        /**         
        * Marks this node as the last one in list         
        * @method _markLast         
        * @protected         
        */
        _markLast: function() {
            this.get(BOUNDING_BOX).addClass(classNames.lastnode);
        },
        /**         
        * Unmarks this node as the last one in list         
        * @method _markLast         
        * @protected         
        */
        _unmarkLast: function() {
            this.get(BOUNDING_BOX).removeClass(classNames.lastnode);
        },
        /**         
        * Collapse the tree         
        * @method collapse         
        */
        collapse: function() {
            var boundingBox = this.get(BOUNDING_BOX);
            if (!boundingBox.hasClass(classNames.collapsed)) {
                boundingBox.toggleClass(classNames.collapsed);
            }
        },
        /**         
        * Expands the tree         
        * @method expand         
        */
        expand: function() {
            var boundingBox = this.get(BOUNDING_BOX);
            if (boundingBox.hasClass(classNames.collapsed)) {
                boundingBox.toggleClass(classNames.collapsed);
            }
        },
        /**         
        * Toggle current expaned/collapsed tree state         
        * @method toggleState         
        */
        toggleState: function() {
            this.get(BOUNDING_BOX).toggleClass(classNames.collapsed);
        },
        /**         
        * Returns breadcrumbs path of labels from root of the tree to this node (inclusive)         
        * @method path         
        * @param cfg {Object} An object literal with the following properties:         
        *     <dl>         
        *     <dt><code>labelAttr</code></dt>         
        *     <dd>Attribute name to use for node representation. Can be any attribute of TreeNode</dd>         
        *     <dt><code>reverse</code></dt>         
        *     <dd>Return breadcrumbs from the node to root instead of root to the node</dd>         
        *     </dl>         * @return {Array} array of node labels         
        */
        path: function(cfg) {
            var bc = Array(),
            node = this;
            if (!cfg) {
                cfg = {};
            }
            if (!cfg.labelAttr) {
                cfg.labelAttr = "label";
            }
            while (node && (node instanceof Y.TreeNode)) {
                bc.unshift(node.get(cfg.labelAttr));
                node = node.get("parent");
            }
            if (cfg.reverse) {
                bc = bc.reverse();
            }
            return bc;
        },
        /**         
        * Returns toggle control node         
        * @method _getToggleControlNode         
        * @protected        
        */
        _getToggleControlNode: function() {
            return this.get(BOUNDING_BOX).one("." + classNames.toggle);
        },
        /**         
        * Returns label content node         
        * @method _getLabelContentNode         
        * @protected         
        */
        _getLabelContentNode: function() {
            return this.get(BOUNDING_BOX).one("." + classNames.labelContent);
        }
    }, {
        NAME: TREENODE,
        ATTRS: {
            /**             
            * @attribute defaultChildType             
            * @type String             
            * @readOnly             
            * @description default child type definition             
            */
            defaultChildType: {
                value: "TreeNode",
                readOnly: true
            },
            /**             
            * @attribute label             
            * @type String             
            *             
            * @description TreeNode node label              
            */
            label: {
                validator: Y.Lang.isString,
                value: ""
            },
            /**             
            * @attribute loadOnDemand             
            * @type boolean             
            *             
            * @description Whether children of this node can be loaded on demand             
            * (when this tree node is expanded, for example).             
            * Use with gallery-yui3treeview-ng-datasource.             
            */
            loadOnDemand: {
                value: false,
                validator: Y.Lang.isBoolean
            },
            /**             
            * @attribute collapsed             
            * @type Boolean             
            * @readOnly             
            *             
            * @description Represents current treenode state - whether its collapsed or extended             
            */
            collapsed: {
                value: null,
                getter: function() {
                    return this.get(BOUNDING_BOX).hasClass(classNames.collapsed);
                },
                readOnly: true
            },
            /**             
            * @attribute clabel            
            * @type String             
            *             
            * @description Canonical label for the node.              
            * You can set it to anything you like and use later with your external tools.             
            */
            clabel: {
                value: "",
                validator: Y.Lang.isString
            },
            /**             
            * @attribute nodeId             
            * @type String             
            *             
            * @description Signifies id of this node.             
            * You can set it to anything you like and use later with your external tools.             
            */
            nodeId: {
                value: "",
                validator: Y.Lang.isString
            },
            /**             
            * @attribute isLeaf             
            * @type Boolean             
            *             
            * @description Signifies whether this node is a leaf node.             
            * Nodes with loadOnDemand set to true are not considered leafs.             
            */
            isLeaf: {
                value: null,
                getter: function() {
                    return (this.size() > 0 ? false : true) && (!this.get("loadOnDemand"));
                },
                readOnly: true
            },
            /**             
            * @attribute isLast             
            * @type Boolean             
            *             
            * @description Signifies whether this node is the last child of its parent.             
            */
            isLast: {
                value: null,
                getter: function() {
                    return (this.get("index") + 1 == this.get("parent").size());
                },
                readOnly: true
            },
            icon: {
                value: ''
            },
            expandable: {
                value: false
            }
        },
        HTML_PARSER: {
            children: function(srcNode) {
                return findChildren(srcNode, "> ul > li");
            },
            label: function(srcNode) {
                var labelContentNode = srcNode.one("> a > span");
                if (labelContentNode !== null) {
                    this._renderFromMarkup = true;
                    return labelContentNode.getContent();
                }
            }
        }
    });
}, '3.6.0', { requires: ["substitute", "widget", "widget-parent", "widget-child", "node-focusmanager", "array-extras"] });

创建树及操作的代码可以和demo一样,也可以看我下一篇实现自动创建树的代码及效果图

 

posted @ 2012-12-04 10:43  Zux  阅读(730)  评论(0编辑  收藏  举报