热门:网页模板.net视频教程JQueryMVCjsonExtJs源码示例三级联动JQuery菜单
您现在的位置:.Net中文社区>> AJAX编程>>正文内容

TabBox与树节点一同支持方向导航

发布时间:2009年01月18日点击数: 彭仁夔

返回深入学习ExtJS 2.2开发系列连载教程目录

4.5扩展TabBox事件
在第二章中,我们已经实现了TabBox组件,即leftMenu类。在那里已经实现nodeClick事件。这个事件的实现就应用了4.4节中的组件事件的编写。这一节我们通过扩展其树的节点的导航来综合应用本章中讲到知识点。

4.5.1使Tree导航能正常运行
Tree组件本身就实现了方向键导航,但该导航实现对于我们的TabBox来说是有点问题的,我们可以先运行第二章的例子看看其上下方向键导航,在IE中,当它其Tab折叠之后展开,其方向键导航失效。在FireFox中其并不失效,这个问题是我们要解决的。

我们先来思考一下键盘按键的事件源(srcElement),也就是说键盘按键事件是由谁来处理?鼠标点击时,它会根据点击坐标点找到页面对应dom元素,如果它注册了监听函数就进行处理,没有就冒泡给其父节点。而键盘按键事件如何找到其对应的dom元素呢?

于是在浏览器出现了焦点(focus)概念,用来指定某个dom元素处在激活状态中。该元素会接收所有键盘事件并根据其注册监听函数进行处理,如果没有监听函数,就会向父节点冒泡。一般来讲如果鼠标点击的元素都能获得焦点,这也就是为什么文本框输入来先采用鼠标把光标定位在文本框元素上。如果页面没有焦点,其接收键盘事件的就是body元素。在开发注册键盘事件时,有时在其元素上不能响应,然后在body元素上却能响应就是因为其没有获得focus焦点。

对于浏览器不能自动去设定指定元素的焦点,我们可以通过手动调用focus函数来为其元素获得焦点,除去焦点则可以通过blur来手动完成。我们TabBox的问题也出现在这里,每次选中Tree节点时,都会调用TreeNodeUI中的focus为其元素集焦。我们看一下其代码:

focus : function(){

if(!this.node.preventHScroll){

   try{this.anchor.focus();}catch(e){}}

else if(!Ext.isIE){

       try{ var noscroll = this.node.getOwnerTree().getTreeEl().dom;

            var l = noscroll.scrollLeft;

             this.anchor.focus();

           noscroll.scrollLeft = l;

       }catch(e){}}

}
,


我们可以看到上面代码中有两个判断处理,只能第一个才能处理IE,然而Tree节点的preventHScroll的值为true且不能通过配置项去改变它,说明了默认情况下IE是不需要通过调用元素的focus函数来手动获得焦点,其默认情况会自动获得。

然而在我们Tabbox的处理就不太一样,IE自动获得焦点的元素必须是在页面初始化时已经生成并显示出来。这里Tab 项折叠之后展开,树节点就不能获得这个焦点。而在FireFox中,它一直都是通过上面focus函数来手动获得。

我们要让tab项折叠/展开之后也能获得焦点,可以重写覆盖上面的focus函数,还可以设定TreeNode的preventHScroll为false。这两个方法都要修改其源码。如preventHScroll是为了防止节点出现滚动条,不过其设定除了在上面focus函数中进行控制之后好像还没有用到其它地方。我们可以通过Ext.override(Ext.tree.TreeNode, {preventHScroll : false}来实现。

如果不去修改源码,那么我们可以把在tree元素注册的keydown键盘处理注册到其没有隐藏的父元素之上,如body元素。因为keydown事件会进行冒泡到其父元素,这样也能达到所要的效果。

Tree的默认导航处理处理是Ext.tree.DefaultSelectionModel类,我们可以通过继承它来实现一个导航处理类,如取名为Ext.TabEvent。这时只要把DefaultSelectionModel中init函数中代码都拷到Ext.TabEvent中init中去,同时把对于tree.el注册的keydown修改成Ext.getBody.on("keydown",…)就可以了。
initTabsel : function() {        

for (var i = 0;i < this.items.length; i++) {

    Ext.TabEvent = Ext.extend(Ext.tree.DefaultSelectionModel, {

       init : function(tree) {

   this.tree = tree;

           Ext.getBody().on("keydown", this.onKeyDown, this);

           tree.on("click", this.onNodeClick, this);}
    

}
);        

t.selModel=new Ext.TabEvent();      

}}
,


在这里讲一下,因为很多事件是和dom元素相关,也就是说一定要在其render之后才能为其注册事件,在initComponent或其类中构建函数注册,很有可能会出现dom元素找不到错误,为了避免类似问题, panel类在afterRender之后加上了一个initEvents来专门初始化事件。我们把第二章的leftMenu的事件改动一下,把其初始化都放在initEvents中,其代码如下:
initEvents : function() {

       Morik.Office.LeftMenu.superclass.initEvents.call(this);

       this.initTabsel();

this.initTreeEvent();

},


同时规范代码,我们把initTreeEvent拆分为成两个函数,对于树的click的处理代码单独出来,并将其命名为onNodeClick。在ExtJS中一般是把其分来处理,同时采用on前缀命名,分开来之后,其它函数也可以调用它,代码不列出,见光盘或自定实现。

现在我们的tree导航就可以正常运行。
4.5.2 在Tree事件基础上扩展Tabbox导航
我们的需求并不是停留在能够正常运行Tree事件,而是要去扩展其事件,使导航不只限制在Tab项的树中节点,而是要让它们支持所有的Tab项导航。即如果向下导航在第一个Tab项的Tree中最后一个节点,那么它就应导航到第二个Tab项中第一个节点。向上导航相同。如果导航到最上或最下Tab项Tree中最上或最下位置节点时,继续还能实现循环导航。

如何去实现呢?首先我们要想到这个导航是与树节点导航相关连的,不同的地方在就是该树中最上或最下节点位置还将继续导航。这个导航处理要折叠该树所在Tab项,同时还展开接下来Tab项,并将选择定位到其Tab项树中的节点上去。

接下要做的就是结合事件原理去分析Tree节点导航是如何实现的,并在其上面进行改造。在DefaultSelectionModel类中,它是通过onKeyDown来实现导航处理,其向下导航是通过selectNext函数,向上函数是通过selectPrevious函数来实现的。我们先看一下selectNext:
selectNext : function(){

var s = this.selNode || this.lastSelNode;

if(!s){   return null; }                   ①

if(s.firstChild&&s.isExpanded()){return this.select(s.firstChild);}

else if(s.nextSibling){ return this.select(s.nextSibling); }       ③

else if(s.parentNode){

var newS = null;

   s.parentNode.bubble(function(){                ④

      if(this.nextSibling){

         newS = this.getOwnerTree().selModel.select(this.nextSibling);

           return false; }


else{ //这是要扩展的地方   } }

   return newS;}
);            ⑤


return null; }
,


我们先来分析一下这个代码,看看如何把一颗树的导航扩展到多颗树中向下导航。在①是判断是否有选中的节点,当通过上下导航和点击时都把节点存在selNode属性中。②处是看看该节点有没有处在展开之中的子节点,有的话就导航到其第一个子节点,没有就进行③处来判断其是否有下一个兄弟节点,有的话就导航到其兄弟节点,没有的话,就执行④处看看其父亲节点有没有下一个兄弟节点,有的话就导航到其父亲的下一个兄弟节点上。这里是实现Tree节点层次导航,在遍历子节点之后又能回到父亲节点的导航层次上来。

我们在⑤加了一个else,用来处理Tabbox项的导航,为什么加在这里呢?那是因为在Tabbo都有一个隐藏的root根节点,导航时只需要处理是其所有子节点,这样在根节点第一层子节点导航完成之后如果继续就应该找下一个Tab项中树的第一个节点。

怎么找到下一颗中的节点呢?当然可以利用其Dom元素的特征(如class)通过CSS S查询器来查找,最简单的方式是在构建其选择模式类(如Ext.TabEvent)时把需要的信息传入,其实也不需要什么,只要知道该树在哪个Tab项中,其Tabbox的引用等就可以。我们可以把代码3.25中改成如下:
initTabsel : function() {

var th = this, c = this.items;

for (var i = 0;i < this.items.length; i++) {

    var p = c.itemAt(i), t = p.items.itemAt(0);

    p.tree = t;

    t.selModel = new Ext.TabEvent( {tabBox : this,tabPanel : p});

}}
,

这里把Ext.TabEvent类的实现移到LeftMenu类的所在文件的前面位置。
var tb = t.tabBox, tp = t.tabPanel; ①

var titems = tb.items, tlen = titems.length;

if (!tb.ativeTab) tb.ativeTab = tp; ②

var index = titems.indexOf(tb.ativeTab);

index = index < 0 ? 0 : index;

tb.ativeTab = titems.itemAt((index + tlen + 1) % tlen);  ③

tb.ativeTab.expand(true);

var atree = tb.ativeTab.tree;

atree.getSelectionModel().select(atree.getRootNode().firstChild);

tp.collapse(true);                                   ④

tb.doLayout();

在这段代码中,①处t是在selectNext函数开始处加上的var t=this;让它指向当前Ext.TabEvent对象。②的ativeTab用来指引正处在展开状态的Tab项。③处是根据当前Tab项所在顺序号找到其下一个Tab项。④处是把当前的Tab项折叠,下一个Tab项展开,同时选中(为其高亮,focus焦点)其树中第一个节点。

好像这样的实现能满足要求。运行一下,大失所望。在IE中不但导航的顺序混乱,而且有时还不能折叠非激活状态中的Tab项。通过加上alert(s.text)来观测一下其运行的节点的顺序,发现当导航到下一个tab中,每按下导航键,就会出现多个alert的弹出信息。说明了该导航事件触发了多个事件处理函数。

这就是我们采用了body做为其监听事件处理元素,因为我们为body注册了四个事件监听处理,每颗树都有一个监听处理。每按下一个导航键,它就会触发这四个事件监听处理,而监听处理中是采用selNode是否存在做为判断,当导航在第一个Tab中,那么其它树选择模式的selNode,当从第一个Tab导航到第二个Tab时,第二个树选择模式的selNode就不是null,同时第一个Tab也不是null。那么这样按下一个键就会弹出两次处理的节点文本。当到了第三个Tab项时,就会弹出三个。

现在如何去解决呢?既然是selNode出了问题,那么找到它来进行处理,只要在运行完成之后将前面的selNode设为null。这个可以在加在代码④处,这样还是会有问题,因为清除其选中的高亮也是依赖于这个selNode,当运行完成之后设定为null。在第二次循环重复这个节点时,它原来的高亮没有去除,现在选中了变成了去除高亮,与我们的需求不合。这个只要在selectNext函数中判断selNode是否为空的代码之后加上this.clearSelections();用来清除所有的选择就可以了。

如果不采用这种方式,我们可以在地init中为每颗树注册onkeydown事件,这个几个事件处理都是隔离的,就不会出现这样的问题。如下代码:
if (Ext.isIE) Ext.override(Ext.tree.TreeNode,{preventHScroll:false});

tree.el.on("keydown", this.onKeyDown, this);

这样的缺点就是修改了TreeNode的preventHScroll属性。

现在我们看一下其兼容性怎么样?在FireFox中能展开第二项,却导航不了,但是加上alert调试又能导航,通过alert调试能成功又没有却不能正常运行的问题一般都是时间上先后顺序的问题。这里是什么原因?这是因为在元素的展开来还要生成树,是要花相对多的时间,更不用说加上动画的效果。如果在展开的过程中,而树还没有生成,那又是如何实现选中聚焦导航呢?

怎么解决?我们要等待Tab项展开之后才去进行选择。想到等待,我们就会想到Java等中的sleep。JS中没有sleep函数,怎么办?我们可以采用事件方式实现,每个panel都有一个expand事件来扩展panel展开之后的处理。在这里只要把选中放在其expand事件就可以。
tb.ativeTab.on('expand', function() {

    var atree = tb.ativeTab.tree;

    atree.getSelectionModel().select(atree.getRootNode().firstChild);

    tb.items.each(function() {

           if (this != tb.ativeTab) {this.collapse(true); }  

});}
, this);

tb.ativeTab.expand(true);


ok!大功告成,接下来就是读者的任务了,对着该实例去完成selectPrevious向上导航改造。其实现代码在光盘中。

4.5.3自定义实现Tabbox事件
上一节中,我们是在selectModel中进行了改造了来实现TabBox的导航事件,但是这样做并不是很好。因为有多少Tab项,就会为其中每颗树注册一个事件监听,这些导航事件之间又是相互切换的,不好理解。

为了演示Event的技术,我们在这一节中采用另外的方法,抛开单独自树的导航事件,而是采用一个Tabbox一个导航事件。把其selNode统一控制起来。即然不用Tree选择导航事件,那么就是取消它的默认导航。

取消有二种方法,一种是通过为Tree的selModel注册一个未实现的类,这个函数中一定要有init函数名。如t.selModel = new function(){this.init =function(tree){}};第二方法采用前面讲到beforeMethod拦截取消,它的selModel的init调用是initEvents函数中,那么我们就可以通过t.beforeMethod('initEvents', function() {this.getSelectionModel().init = function() { };}, t);来取消其导航事件。

取消了之后,我们要另外写一个导航处理类,如Ext.TabSel。我们可以在initTabsel中加上this.tabsel = new Ext.TabSel( { tabBox : this });来完成导航事件的注册。当然要运行起来,像上一节一样,我们还得为每颗树建立tree与tabpanel之间的关系,就是在3.26代码中加上t.tabPanel = p;。这样就建立两者之间的对应关系。

对于Ext.TabSel的实现,首先是要选择树的节点,那么还得继承树的defaultSelectionModel,但是这对每颗树,而现在是对多颗树怎么办?因为现在的多颗树在每个时刻都只能是选择一颗中的节点导航,也是说可以动态改变defaultSelectionModel中this.tree的引用就可以达到要求。这个动态修改最好是每次选择时都进行修改才不会出错,于是应该是在其select函数中。
select : function(node) {

       this.msg = this.msg + " " + node.text;

       this.tree = node.getOwnerTree();

       this.tabPanel = this.tree.tabPanel;

       return Ext.TabSel.superclass.select.call(this, node);

    }

因为每次选择都会调用该函数,所以这里能动态地把tree和tree所在tabpanel存到当前类的属性中共用。对于选择(如高亮,聚焦)都是在其父类中同名函数中实现。这里的msg是为了调试用的。

在这一章,我们还有keyNav和keyMap没有用到,那么这里我们不采用defaultSelectionModel中的onkeydown的实现,而是通过keyNav和keyMap来实现,
Ext.TabSel = function(config) {

    Ext.TabSel.superclass.constructor.call(this);

    Ext.apply(this, config);

    this.init();};

Ext.extend(Ext.TabSel, Ext.tree.DefaultSelectionModel, { msg : '',

init : function(tree) {

       this.tlen = this.tabBox.items.length;

       this.keynav = new Ext.KeyNav(Ext.getBody(), {    ①

           'enter' : this.enter,'left' : this.left,

           'right' : this.right, 'up' : this.up,

           'down' : this.down,  scope : this}
);

for (var i = 0;i < this.tabs.length; i++) {    

          this.tabs.itemAt(i).tree.on("click",this.onNodeClick,this);}


           new Ext.KeyMap(Ext.getBody(), [{             ②

  key : 'n', alt : true,

               fn : function() {    alert(this.msg);  },

              scope : this}]);   }
,

.. .. ..}
);

在①处,我们采用了keyNav来实现方向键导航,这里还实现了回键选择中的节点就会在触发其节点关联的动作。②处的KeyMap是用来调试导航时所经过的节点顺序,我们只要通过ctrl+N就可以获取其信息。

其它的right,left,down,up实现都比较简单,可以参考上一小节的实现。这部分代码可以参考光盘中。光盘中实现把down,up中切换Tab项的实现抽象到一个函数中,它们down和up能共用。


本站热点业务

更多模板/案例展示

关于我们 | 联系我们 | 团队日志 | 网站地图 | 网站合作