zhangyusng 2013-04-07
[b]拖放
在dojo的应用中,拖放是一个比较常见的操作。拖放操作最早的时候比较多的出现在桌面应用中,在传统的Web应用中比较少见。随着Ajax应用的流行,拖放操作越来越多的出现,可以提供与桌面应用类似的用户体验。在Ajax应用中实现拖放操作并不是一件容易的事情,需要考虑很多浏览器兼容性的问题。Dojo核心库的dojo.dnd模块提供了对拖放操作的良好支持。
dojo.dnd模块中包含了对两类拖放相关的操作的支持:一类是一般意义上的拖放,即拖拽页面上的一个元素并把它放到其它位置;另外一类是在页面上自由的移动某个元素。两类操作在实现上有所不同,下面会分别介绍。
拖放操作
从拖放操作本身来说,需要一个源和一个目标。源和目标分别是通过dojo.dnd.Source和dojo.dnd.Target类来表示的。从实现上来说,dojo.dnd.Target继承自dojo.dnd.Source。如果同时是源和目标的话,应该使用dojo.dnd.Source来表示。在拖放的源中可以包含多个能够被拖动的条目,需要对这些条目进行管理。dojo.dnd.Container表示的是包含能被拖动的条目的容器,用来实现对条目的管理。对于dojo.dnd.Container中包含的条目,需要有一种方式允许用户进行选择,比如一次选中一个或多个条目。dojo.dnd.Selector继承自dojo.dnd.Container,并添加了与选择条目相关的功能。dojo.dnd.Source类则是继承自dojo.dnd.Selector的。当用户拖拽一个包含在dojo.dnd.Source中的条目在页面上移动的时候,需要以直观的方式告知用户条目的当前位置。dojo.dnd.Avatar用来实现这样的功能。除了上面提到的四个类之外,dojo.dnd.Manager用来管理整个拖放的过程。
在Ajax应用中使用拖放操作的时候,首先需要根据应用场景定义清楚拖放的源和目标。这些操作要符合用户的使用习惯,以免影响用户体验。比如用拖放操作来改变一个列表中元素的顺序,或是拖放所选择的物品到购物车中,这些都是比较合理的拖放操作的场景。完成这一步之后,就可以从表示拖放源的DOM节点中创建dojo.dnd.Source对象了。dojo.dnd.Source的构造方法有两个参数:第一个参数node表示的是拖放源的DOM节点,第二个参数params是一个包含配置项的JavaScript对象。对于拖放源中所包含的条目,有抽象和具体两种表示方式。抽象的表示方式是一个JavaScript对象,其中包含的内容由应用自己来定义。具体的表示方式则是DOM节点。创建dojo.dnd.Source对象的时候,需要传入一个JavaScript方法用来从抽象的JavaScript对象中创建出具体的DOM节点。该方法由params对象的creator属性来指定。该方法有两个参数,第一个item表示的是代表拖放条目的JavaScript对象,第二个hint用来说明创建出的具体DOM节点的用途,目前只支持一个值avatar,表示创建的DOM节点是给dojo.dnd.Avatar使用的。该方法需要返回一个包含属性node、data和type属性的JavaScript对象。其中属性node表示的是创建出来的DOM节点,data表示的是抽象的JavaScript对象,type表示的是拖放条目的类型,用来判断是否可以被拖放到某个目标上。一般来说,拖放条目的DOM节点是作为拖放源节点的直接子节点的。如果想使用其它节点的话,可以通过params对象的dropParent属性来指定。
接下来就是在拖放源中添加拖放条目。这些条目有可能是固定的,也可能是动态变化的。对于条目固定的情况,可以直接以声明式的方式来增加条目。只需要在拖放源的DOM节点下定义包含CSS类dojoDndItem的节点即可。dojo.dnd提供了默认的creator方法来完成转换。每个节点本身、其属性dndData和dndType分别作为creator方法返回值中的node、data和type属性的值。对于动态的条目,则需要使用dojo.dnd.Source提供的insertNodes(addSelected,data,before,anchor)方法。该方法的参数addSelected表示添加的条目是否为被选中的状态;data表示的是包含条目抽象内容JavaScript对象的数组,会交给creator方法来创建具体的DOM节点;before和anchor用来表示条目节点的插入位置,anchor表示的是参考节点,而before表示是插入的位置在参考节点之前还是之后。
创建了拖放源和其中的条目之后,下一步就是创建拖放目标。创建dojo.dnd.Target的实例就可以表示一个拖放目标。创建目标的时候,通过第二个参数params对象的属性accept可以设置该目标接受的条目类型。只有当accept属性的值与创建条目时creator方法返回值中type属性的值相匹配的时候,拖放才能完成。这样就可以进行基本的拖放操作。代码清单4中给出了使用dojo.dnd实现的简单拖放操作。
<div id="source"> <div class="dojoDndItem" dndType="myItem"> 测试条目 1</div> <div class="dojoDndItem" dndType="myItem"> 测试条目 2</div> <div class="dojoDndItem" dndType="myItem"> 测试条目 3</div> </div> <div id="target"> </div> var source = new dojo.dnd.Source("source"); var target = new dojo.dnd.Target("target", { accept : ["myItem"] });
代码清单4中给出了采用声明式的方式实现的简单拖放操作。默认情况下,被拖拽的条目会从拖放源中删除,添加到拖放目标中。如果希望只是从拖放源复制到目标的话,在创建dojo.dnd.Source的时候,可以指定params的属性copyOnly的值为true。
如果希望定制拖放条目被拖动时的显示方式,需要利用creator方法创建出所需的节点。代码清单5中给出了动态添加拖放条目和定制条目拖动时显示方式的一个示例。
var source = new dojo.dnd.Source("source", { creator : function(item, hint) { var n; if (hint == "avatar") { n = dojo.create("div", { innerHTML : dojo.replace("<span>{title}</span>", item) }); } else { n = dojo.create("div", { innerHTML : dojo.replace("<h4>{title}</h4><div>{description}</div>", item) |-------10--------20--------30--------40--------50--------60--------70--------80--------9| |-------- XML error: The previous line is longer than the max of 90 characters ---------| }); } return { node : n, data : item, type : ["book"] }; }, copyOnly : true }); var target = new dojo.dnd.Target("target", { creator : function(item, hint) { return { node : dojo.create("span", { innerHTML : item.title }), data : item, type : ["book"] }; }, accept : ["book"] }); source.insertNodes(false, [ {title : "图书 1", description : "一本不错的书。"}, {title : "图书 2", description : "这也是一本好书。"} ]);
如代码清单5所示,注意其中insertNodes()方法和creator方法的参数hint的使用。当条目被拖动到目标上方并放下的时候,表示目标的dojo.dnd.Target对象会首先通过checkAcceptance(source,nodes)方法来检查是否允许拖动的条目放下。如果允许的话,目标上的onDrop(source,nodes,copy)方法会被调用,其三个参数source、nodes和copy分别表示拖放源、被拖动的节点以及是否需要进行复制。默认提供的onDrop()方法的实现会根据拖放源和目标是否相同来调用不同的方法:当源和目标不相同的时候,调用的是onDropExternal(source,nodes,copy);相同的时候调用的则是onDropInternal(nodes,copy)。拖动的节点在目标上被放下之后的默认行为取决于在创建目标dojo.dnd.Target对象的时候,是否定义了creator方法。如果定义了creator方法,就通过此方法在拖放目标中创建出新的DOM节点;如果没有的话,就直接进行DOM节点的复制或移动。从代码清单5中可以看到,拖放目标target定义了creator方法,因此当条目从source拖放到其上之后,条目的显示方式发生了变化。
在有些拖放操作中,当条目在目标上放下的时候,并不需要进行DOM节点的复制或移动。拖放行为本身只是作为一种触发的动作。这个时候就需要自定义onDrop()或是onDropExternal()和onDropInternal()方法的实现。代码清单6中给出了一个自定义拖动条目被放下时行为的示例。该示例的场景是用户拖动想要购买的图书到购物车中,购物车会自动更新图书的总价。
var sum = 0; var target = new dojo.dnd.Target("target", { accept : ["book"], onDropExternal : function(source, nodes, copy) { for (var i = 0, n = nodes.length; i < n; i++) { var node = nodes[i]; var item = source.getItem(node.id); sum += item.data.price; } dojo.byId("target").innerHTML = "总价合计为:" + sum + " 元"; } });
如代码清单6中所示,在onDropExternal方法中,通过source.getItem()方法来获取被放下的条目的抽象表示,即creator方法返回的结果。通过得到的条目的抽象表示,就可以获取到所需的数据。有了所需的数据,就可以更新图书的总价。
最后要介绍的是dojo.dnd.Manager的使用。在每个页面上只存在一个dojo.dnd.Manager类的实例。通过dojo.dnd.manager()可以获取该实例。该实例负责协调拖放操作,其中维护了一些与当前正在进行的拖放相关的状态,包括source、target、avatar、nodes和copy等。
当与拖放相关的操作发生时,dojo.dnd会发布一些主题,应用可以监听这些主题来执行额外的逻辑。这些主题包括:
/dnd/start:当开始拖动的时候,发布此主题。
/dnd/drop:当拖动的条目被放下的时候,发布此主题。
/dnd/cancel:当拖放操作被取消的时候,发布此主题。
/dnd/source/over:当正在拖动的条目移动到某个拖放目标的上方时,发布此主题。
/dnd/drop/before:在拖放的条目被放下之前,发布此主题。
移动操作
在Ajax应用中,允许用户自由的移动页面上的元素也是一个常见的操作。比如在画图的应用中允许用户自由的摆放绘制元素。针对这样的需求,dojo.dnd模块中提供了dojo.dnd.Moveable和dojo.dnd.Mover,其中dojo.dnd.Moveable表示的是可以被移动的元素,dojo.dnd.Mover是移动操作的具体执行者。
确定好需要进行移动的DOM节点之后,就可以创建出一个dojo.dnd.Moveable对象来表示它。dojo.dnd.Moveable的构造方法有两个参数:node和params,其中node表示的是DOM节点,params表示的是与配置相关的JavaScript对象。比较重要的配置项有handle,用来表示拖动时的手柄,默认是当前节点。当用户在手柄的区域内点击并拖动的时候,就可以移动此节点。在移动的过程中,dojo.dnd.Moveable的一些方法会被调用。应用可以通过覆写这些方法来添加额外的处理。这些方法包括:
onFirstMove(mover):第一次被移动的时候调用。参数mover表示用来移动节点的dojo.dnd.Mover对象。
onMoveStart(mover):当开始移动的时候调用。
onMoveStop(mover):当停止移动的时候调用。
onMove(mover,leftTop):当每次移动的时候调用。参数leftTop表示将要移动到的位置的坐标。
onMoving(mover,leftTop):由onMove()方法在完成移动之前调用。
onMoved(mover,leftTop):由onMove()方法在完成移动之后调用。
dojo.dnd.Mover的实例只有在移动的时候才会存在,一般由dojo.dnd.Moveable来自动创建。如果希望提供自己的dojo.dnd.Mover的实现,可以通过dojo.dnd.Moveable构造方法的params参数的mover属性来指定。代码清单7中给出了自由移动节点的一个示例。
var position = dojo.cookie("position"); if (position) { var p = dojo.fromJson(position); dojo.style("box", { left : p.l + "px", top : p.t + "px" }); } var moveable = new dojo.dnd.move.parentConstrainedMoveable ("box", { area : "content", within : true }); dojo.connect(moveable, "onMove", null, function(mover, leftTop) { dojo.cookie("position", dojo.toJson(leftTop)); });
在代码清单7中使用了dojo.dnd.move.parentConstrainedMoveable,这是dojo.dnd提供了几个辅助类之一。它用来限制节点移动时不能超出其父节点的范围。与之类似的还有:dojo.dnd.move.boxConstrainedMoveable,用来限制移动范围在某个矩形区域内;dojo.dnd.TimedMoveable用来限制两次移动之间的时间间隔。
关于dojo.dnd模块的更多介绍,见参考资料。在介绍完与拖放操作相关的内容之后,下面介绍与处理常用数据类型相关的内容。
摘选自http://www.360doc.com/content/10/1012/14/3103730_60358757.shtml