6.4 复杂的树视图

制作用户控件的目的是提高代码复用率和移植,也因此方便了代码的维护。

在6.2节里实现了一个最简单的树视图,但是这个树视图对于移植和复用都很不方便。如何对它进行控件化呢?

6.4.1 闭包隔离变量污染

众所周知,jQuery强大但是入口单一,没有变量污染,它是如何做到的呢?

        (function( window, undefined ) {
            var jQuery=...
            ....内部代码...
            window.jQuery = window.$ = jQuery;
        })( window );

下面将树控件起名为T,挂载到window对象下,这样才能保证在网页中被自由调用。

        (function(window){
            window.T = window.T || function(){};
        })(window);

6.4.2 省去new关键字调用控件

想一想jQuery,似乎大家在调用的时候就没有用new关键字。我们也来实现一个,另外还需要让控件接受一些配置参数,那么就需要改造一下构造函数:

        window.T = window.T || function(cfg){
            if (! (this instanceof T)) { return new T(cfg) }; //省略new 关键字调用
            this.SET = cfg; //存储起来,让内部可以自由使用
            this.ROOT = null; //记录根节点
        };

6.4.3 丰富控件方法

在编写代码前,应该先打个草稿。由于篇幅限制,我们只完成window资源管理器的模拟,即控件可以自由展开、收缩,并且能发出每个节点的单击事件。其效果如图6-3所示。

图6-3 树控件效果图

其中“本书目录”的第二和第三节点是收缩的,“控件说明”的第三节点是展开的。因为代码是复用的,为了便于移植,笔者把前面base.js中积累的常用方法挂载到控件里。

        /*静态方法*/
        T.extend = function(){/*合并对象*/
            var len = arguments.length
                  ,obj = arguments[0]
                  ,tmp
            if(! obj || typeof obj === "number" || obj.constructor ! == Object){
                  obj = {};
            }
            for (var i = 1; i < len; i++){
                  tmp = arguments[i];
                  if(tmp){
                          for (var o in tmp){
                          obj[o] = tmp[o];
                  }
                  }
            }
            return obj;
        };
        T.$ = function(id){//取得DOM 元素
            return document.getElementById(id);
        }
        T.hasClass = function(el, cls){//判断是否包含某个class
            return el.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'));
        };
        T.addClass = function(el, cls){//增加class
            if (! this.hasClass(el, cls)) el.className += " "+cls;
        };
        T.removeClass = function(el, cls) {//移除某个class
            if (this.hasClass(el, cls)) {
                var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)');
                el.className = el.className.replace(reg, ' ');
            }
        };
        T.find = function(el, target){//根据ClassName, tagName, ID 查找
            var target = target.replace(/#|\./g, "");
            var cd = el.children; //获取元素子元素集合
            for(var i=0; i<cd.length; i++){
                  var p = cd[i];
                  if(p.tagName.toLowerCase() === target.toLowerCase() || p.id
                      === target || T.hasClass(p, target)) return p;
            }
            return null;
        };
        T.addListener = function(target, type, handler){     //绑定事件
            if(target.addEventListener){
                  target.addEventListener(type, handler, false);
            }else if(target.attachEvent){
                  target.attachEvent("on"+type, handler);
            }else{
                  target["on"+type]=handler;
            }
        };

同时把它们作为静态方法,无须实例化就可以调用,也便于其他控件使用。

作为一个控件总会有一些独有的动态方法,也就是实例化后才可使用的,将这样的方法挂载到prototype属性下:

        var P = T.prototype;

通过转接,将省略不少字节,代码也便于阅读。

控件常见的一个方法就是init()初始化方法:

        P.init = function(cfg){//模板和配置文件的处理
            T.extend(this.SET, cfg||{});
            var set = this.SET, dic = set.data
            for(var i in dic){//用来处理所属关系
                  if(dic[i].pid ! ==undefined){       //判断是指定的pid 才处理
                          var pid = dic[i].pid;
                          if(dic[pid]){ //判断父类是否存在
                                dic[pid].child || (dic[pid].child = []);
                                        //判断父类有无child,无则初始化
                                    dic[pid].child.push(i); //登记到父类child 中
                              }
                      }
                }
                this.addNode(T.$(this.SET.id), -1);
            };

在init()里首先用静态方法extend()合并处理配置信息,并且格式化JSON数据,最后调用添加节点函数addNode()。

        P.addNode = function(el, pid){ //在某个父节点下增加子节点
            if(this.ROOT === null) this.ROOT = pid; //记录根id
            var ul = document.createElement("ul"); //创建一个ul 元素
            var dic = this.SET.data;
            for(var i in dic){//遍历数据
                  if(dic[i].pid == pid){
                      //判断节点是否都是同一个父节点,即是否是当前需要显示的节点
                          var dl = dic[i]; //取得一个节点的信息
                          var child = dl.child && dl.child.length>0; //判断是否还有子类
                          var li  = document.createElement("li"); //创建一个li 元素
                          li.innerHTML = '<span id="s'+i+'"></span><a href="'+
                            dl.url+'">'+dl.cn+'</a>'; //拼接html
                          if(child){
                                this.addNode(li, i); //递归下去
                                this.setParentNodeEvent(li); //设置父节点事件
                          }
                          this.setNodeClass(li, pid, child); //设置节点样式
                          this.setNodeEvent(li); //设置节点事件
                          ul.appendChild(li); //把拼装好的li 追加到ul 中去
                  }else{
                          continue; //继续下一个循环
                  }
            }
            el.appendChild(ul);                          //插入到给定的元素中
        };

addNode()方法是主要的方法,这里需要增加一些更丰富的操作。比如设置父节点的展开和关闭事件、设置节点样式、设置节点事件等,而且每个节点除<a>标签之外还有<span>标签。

        P.setNodeClass = function(el, pid, child){
              var cls = "page"; //默认子节点样式
              if(this.ROOT === pid){
                  cls = "root"; //设置根节点样式
              }else if(child>0){
                  cls = "open"; //设置父节点样式
              }
              T.addClass(el, cls);
        };

根节点的样式和其他节点样式均不同:

        P.setParentNodeEvent = function(el){
            var span = el.firstChild;          //找到第一个子元素
            T.addListener(span, "click", function(){
                  if(T.hasClass(el, "open")){
                          T.removeClass(el, "open");
                          T.addClass(el, "close");
                  }else{
                          T.removeClass(el, "close");
                          T.addClass(el, "open");
                  }
            });
        };

父节点的展开和关闭都是靠单击事件触发的:

        P.setNodeEvent = function(el){
            var a = T.find(el, "a");
            var self = this; //存储this 对象
            T.addListener(a, "click", function(event){
                  if(typeof self.SET.onclick === "function"){
                          self.SET.onclick(event.srcElement||this);
                                  //这里的this 和上面的this 指向不同的对象
                  }
            });
        };

对节点单击的处理也是绑定在单击事件上的,只是元素不同。这里还接受配置参数里传递过来的onclick回调函数,由于function在JavaScript里可以作为参数传递,因此配置灵活度会得到极大的提高,不过这些回调函数都需要在内部作为验证并调用,包括回调函数能使用的参数都可以控制。

在网页中如何设置回调函数呢?请看下面的代码。

        var myTree = T({id:"mytree", data:dic
                  ,onclick:function(node){
                        alert(node.text); //弹出节点文本
                  }
          });
        myTree.init();
        var myTree2 = T({id:"mytree2", data:dic2});
        myTree2.init();

回调函数就是一个参数而已。整个HTML结构如【范例6-3】所示。

【范例6-3 控件的HTML结构及其调用】

    1.      <! DOCTYPE html>
    2.      <html>
    3.      <head>
    4.      <title>javascript tree</title>
    5.      <link rel="stylesheet" href="T.css" type="text/css" />
    6.      </head>
    7.      <body>
    8.      <div id="mytree" class="T"></div>
    9.      <div id="mytree2" class="T"></div>
    10.     </body>
    11.     </html>
    12.     <script src="base.js"></script>
    13.     <script src="z3fTree.js"></script>
    14.     <script>
    15.     var dic = {
    16.                     "0" : {pid:-1, cn:’本书目录’, url:'/'}
    17.                     , "1" : {pid:0, cn:’第1 章 JavaScript 概述’, url:'/01'}
    18.                     , "2" : {pid:0, cn:’第2 章 用JavaScript 验证表单’, url:'/02'}
    19.                     , "11" : {pid:1, cn:'1.1 认识JavaScript', url:'javascript:; '}
    20.                    , "12" : {pid:1, cn:'1.2 配置JavaScript 开发环境’, url:
                          'javascript:; '}
    21.                     , "3" : {pid:0, cn:’第3 章 JavaScript 实现的照片展示’, url:'/03'}
    22.                     , "21" : {pid:2, cn:'2.1  最简单的表单验证 - 禁止空白的必填项目’,
                          url:'javascript:; '}
    23.                     , "22" : {pid:2, cn:'2.2  处理各种类型的表单元素’, url:
                        'javascript:; '}
    24.                     , "23" : {pid:2, cn:'2.3  输入的邮箱地址正确吗?用正则来校验复杂的
                        格式要求’, url:'javascript:; '}
    25.                     , "24" : {pid:2, cn:'2.4  改善用户体验’, url:'javascript:; '}
    26.                     , "31" : {pid:3, cn:'3.1  功能设计’, url:'javascript:; '}
    27.                     , "32" : {pid:3, cn:'3.2  照片加载与定位’, url:'javascript:; '}
    28.                     , "33" : {pid:3, cn:'3.3  响应鼠标动作’, url:'javascript:; '}
    29.             };
    30.     var dic2 = {
    31.                     "0" : {pid:-1, cn:’控件说明’, url:'/'}
    32.                     , "1" : {pid:0, cn:’构造器’, url:'/01'}
    33.                     , "11" : {pid:1, cn:’参数:cfg', url:'javascript:; '}
    34.                     , "2" : {pid:0, cn:’静态方法’, url:'/02'}
    35.                      , "21" : {pid:2, cn:'extend(obj[, obj]...[, obj])',
                          url:'javascript:; '}
    36.                      , "22" : {pid:2, cn:'$(id)', url:'javascript:; '}
    37.                      , "23" : {pid:2, cn:'hasClass(el, cls)', url:'javascript:; '}
    38.                      , "24" : {pid:2, cn:'addClass(el, cls)', url:'javascript:; '}
    39.                     , "25"  :  {pid:2, cn:'removeClass(el, cls)', url:'javascript:; '}
    40.                    , "26" : {pid:2, cn:'addListener(target, type, handler)',
                        url:'javascript:; '}
    41.                     , "3" : {pid:0, cn:’动态方法’, url:'/03'}
    42.                      , "31" : {pid:3, cn:'init()', url:'javascript:; '}
    43.                      , "32" : {pid:3, cn:'addNode(el, pid)', url:'javascript:; '}
    44.                      , "33" : {pid:3, cn:'setNodeClass(el, pid, child)',
                          url:'javascript:; '}
    45.                      , "34" : {pid:3, cn:'setNodeEvent(el)', url:'javascript:; '}
    46.             };
    47.     var myTree = T({id:"mytree", data:dic
    48.                      , onclick:function(node){
    49.                              alert(node.innerText||node.text);
                                //输出文字(兼容IE 和其他浏览器)
    50.                      }
    51.             });
    52.     myTree.init();
    53.     var myTree2 = T({id:"mytree2", data:dic2});
    54.     myTree2.init();
    55.     </script>

读者可能注意到了,一个漂亮的控件不可能完全没有CSS, 【范例6-3】中的T.css代码如下:

        ul, li{ list-style: none; }/*去掉自带的样式*/
        .T .root{
            background: url("img/base.gif") no-repeat scroll 0 0 transparent;
            padding-left: 20px; /*把图标用背景的方式显示,不重复,然后内容向右位移*/
        }
        .T .open{
            background: url("img/folderopen.gif") no-repeat scroll 0 0 transparent;
        }
        .T .page{
            background:   url("img/page.gif")   no-repeat   scroll   0   0   transparent;
            padding-left: 20px;
        }
        .T .open span{display:inline-block; width:20px; height:20px; }
        .T .page span{display:none; }
        .T .close{
            background: url("img/folder.gif") no-repeat scroll 0 0 transparent;
        }
        .T .close span{display:inline-block; width:20px; height:20px; }
        .T .close ul{display:none; }/*当父节点切换到关闭状态时,其子节点自动隐藏*/

到此为止,基本上完成了一个相对6.2节更为复杂的树视图,当然应用于实际项目时,还需要补充一些更为丰富的操作。本例仅仅讲了基本的流程和步骤,以便引导读者入门,相信读者能够以此举一反三,编写出更多优秀的控件。

由于兼容性问题,类似这样甚至更为复杂的用户控件都是基于jQuery的,这样做一方面是因为节省开发成本,另一方面是因为jQuery的广泛应用。