Dojo Dnd - 拖拽功能实战
本文翻译自:http://www.sitepen.com/blog/2011/12/05/dojo-drag-n-drop-redux/
原文作者:Colin Snover
译者:Ruan Qi
拖拽(dojo/dnd)作为Dojo的基础功能之一,可视化地支持页面元素或对象在多个容器之间拖放。Dojo/dnd还支持同时拖拽多个对象;另外还可以制定规则过滤拖放对象的目标容器,比如“桌子”应该被放在“家具”容器内,而不该放在“家电”容器中。下面通过一个有趣的故事,开始我们的Dojo拖拽功能实践。
1 单个容器内的拖拽
首先来介绍一下Dylan,Dylan这家伙最大爱好就是收集二手旧货。刚才他决定把一部分旧货处理掉,腾出地方来放新的破烂货。这不,他在当地租了个店铺开始经营旧货铺的小生意。和所有野心勃勃的实体店老板一样,Dylan也决定开个网店兜售他的旧货。Dylan有个正在攻读市场经济学位的弟弟,他建议为了让Dylan的网上旧货铺与众不同,得搞个自己的品牌。两兄弟思来想去,决定用Dylan’sOriginal做为他们铺子的商标。 Dylan决定给自己的网上店铺加上有着酷到一塌糊涂的用户体验的功能,顾客不由自主地就会买他家的二手旧货。所以Dylan就把我们请来了,我们给他做了个叫Dylan’s Original JunkOutlet的Demo来演示页面上的拖拽效果。
我们以最基础的拖拽功能进行演示,目标是一个可以由用户来动态排序的列表。首先得完成页面的总体UI框架,导入了Dojo工具包以及一点点CSS。请看最初的页面。
可以看到该页面包含一个简单的列表,列表里是Dylan最近想出售的二手货:手表、救生衣、玩具推土机、老式手机和一个玩具小飞机。
拖拽基础类:dojo/dnd/Source
Dojo提供了dojo.dnd.Source类来实现拖拽效果,Source相当于一个容器,包含于其中的对象就有了可以被拖放的能力,下文中都以“内部对象”指代Source中可多拽的子项。下面的代码(本文中的相关代码的dojo版本均为1.7)实现了一个内部对象可拖放的列表:
这就行了,看这个可排序的列表。如果你更喜欢通过声明的方式创建Dojo控件,那么请参见以下的代码:
两者的运行结果是一模一样的,请看通过声明方式创建的可排序列表。
通过声明式创建可拖放列表, dojo.dnd.Source会根据容器的HTML标签创建不同的子节点,主要有以下几种:
- 如果容器是<div>或者<p>,子节点是<div>.
- 如果容器是<ul>或者<ol>,子节点是<li>.
- 如果容器是<table>,那么首先创建<tbody>,然后在<tbody>下添加子节点<tr><td>.
- 其他情况下,子节点形势都是<span>.
下面介绍下dojo.dnd.Source一些常用的功能:
- 多选。同时拖拽多个对象是很基本的需求,dojo.dnd.Source当然是支持这项功能的。操作方式符合标准:ctrl+鼠标点击,或是shift+鼠标点击。
- 内部对象管理。除了上文出现过的inserNodes方法,dojo.dnd.Source还提供了不少方法来操作内部对象:
- getAllNodes()– 以dojo.NodeList形势返回所有内部对象。
- forInItems(fn,ctx) – 类似于dojo.forEach遍历所有内部对象。
- selectNone()、selectAll()、getSelectedNodes()、deleteSelectedNodes() – 功能与命名相同:全不选、全选、返回选中的对象、删除选中的对象。
- 另外更多方法请参见dojoreference guide。
- 复制内部对象。通常选中一个对象并移动鼠标时,选中对象就开始被拖拽,如果在选中前按下ctrl键不放,那么之前的操作就会复制选中的对象并进行拖拽。
取消拖拽。点击ESC键取消拖拽。
自定义拖放图标。可以自定义拖拽时自动生成的图标,下文会有详细介绍。
2 多个容器间的拖拽
当然,如果页面只提供在单个列表中拖拽对象的功能,那么还不足以打动用户。于是我们给Dylan的网店UI做了一点升级:请看新的网店页面。
我们添加了哪些内容呢?首先,页面上有了三个列表:Catalog(目录)、Cart(购物车)和Wishlist(就是收藏列表)。现在你可以在这三个列表间拖放商品了,有些被标记为“缺货”(out)的商品是不能被拖放到购物车里的。
拖放对象类型
新版本的页面引入了可拖放对象的类型。注意下面代码中新出现的accept和type属性:
通过声明方式创建三个列表的代码如下:
每个可拖放的对象都能被指定一个或多个type。type列表与容器的accept列表中只要有一对能匹配,那么对象能被放到对应的容器中,反之就不行。type和accept的默认值都是“text”。
这里我们用“inStock”和“outOfStock”来区分商品是否缺货,同时这也决定了商品能否拖放至“购物车”列表内。如果同时拖放“有货”和“缺货”的商品到购物车,会导致整个拖放不成功。
到目前为止Dylan的网上旧货铺看起来还不错。 不过还有几个问题亟待解决:
- “目录”中的商品被添加到“购物车”或者“收藏列表”里以后,就从“目录”里消失了。除非用户使用复制操作,不然同一件商品不能被同时被添加到“购物车”和“收藏列表”中。这大大影响了用户体验。
- 如果用户使用了复制操作,那么就有可能在同一个列表中出现多个重复商品,这可不妙。
- 页面上仅有三个列表,实在有点单调,用户体验还得进一步提升。
下面继续改进我们的页面。
3 列表项的自定义
我们之前已经提到过,可拖拽的列表内部对象可以被自定义。Dylan希望他的商品目录提供商品的图像、介绍和库存数量。根据他的需求,商品的数据结构看起来该是这个样子的:
dojo.dnd提供了自定义内部对象的方法 – creator函数,下面是代码示例:以下是列表内部对象的Template(即上面代码中的itemTemplate.html):
这个是我们拖拽时的对象的Template(即avatarTemplate.html):下图展示了item与avatar间的区别:
现在来说明对商品目录做的一些修改:
- 为了方便布局,我们使用table作为商品列表的容器,可以看到上面的itemTemplate也相应的修改为<tr><td>.
- 商品以库存数量动态的标记自己的type值.
- dojo.dnd.Source构造函数中的creator函数还接受hint参数。当hint被设为“avatar”时,creator函数构造的是被拖拽的对象的DOM结构。
来看看更新后的Dylan’s Original Outlet Store吧。
新版本的页面看起来有很大的改进,除了商品列表分为旧货商品列表和食品列表以外,收藏列表和购物车列表被放到了dijit.TitlePane里,省出了很大的页面空间。
dojo.dnd.Target
这里我们引入了一个新的类:dojo.dnd.Target。其实Target就是一个只能放不能拖的Source,相当于把Source类里的isSource属性设为false。有趣的是,isSource属性也可以在运行时被改变,下文中会有示例。拖放目标容器的更改
原本拖放到cart里的商品会直接在cartPaneNode下创建子节点,不过cart.parent被赋值之后,所有拖放至cartPaneNode里的商品都会在<table id="cartNode">
下创建子节点。
下面再基于我们的旧货店铺介绍一些Dojo拖放的额外功能。
4 监听拖放事件
Dojo的拖拽中应用了“订阅/发布”来处理事件响应。这里我们先借助aspect模式来处理onDrop事件。
在onDrop事件被触发时,更新列表上显示的商品数量。这里需要注意一点,onDrop事件仅在对象被拖放至接受它的容器中才触发,而对象被拖放到页面任何位置都会触发onDndDrop。直接监听Topic
下面来借助“订阅/发布”模式来给我们的旧货铺添加一些动态效果:当拖拽开始时,高亮可以接受该拖拽对象的容器。
避免对象多次复制在前文中提到的对象多次复制的问题也很容易解决,只要在声明dojo.dnd.Source时设置copyOnly:true,那么在拖拽开始时Source不会移除内部对象,而只是将拷贝进行拖放。另外设置selfAccept:false可以防止被拖拽出去的copy放回源容器造成的重复问题。
下面是我们的旧货铺的最终版本:
总结
我们建立旧货铺的步骤如下:
- 搭建页面框架;
- 单列表拖拽;
- 多列表拖拽;
- 可拖拽列表项的自定义;
- 事件监听和处理;
我们还提供了旧货铺demo的源代码下载, Happy Dragging and Dropping!