Dojo Dnd - 拖拽功能实战

来源:互联网 发布:上海人工智能政策 编辑:程序博客网 时间:2024/05/16 07:24

本文翻译自: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最近想出售的二手货:手表、救生衣、玩具推土机、老式手机和一个玩具小飞机。

<div id="store"><div class="wishlistContainer"><h2>Wishlist</h2><ol id="wishlistNode" class="container">    <li>Wrist watch</li>    <li>Life jacket</li>    <li>Toy bulldozer</li>    <li>Vintage microphone</li>    <li>TIE fighter</li></ol></div></div>


拖拽基础类:dojo/dnd/Source

Dojo提供了dojo.dnd.Source类来实现拖拽效果,Source相当于一个容器,包含于其中的对象就有了可以被拖放的能力,下文中都以“内部对象”指代Source中可多拽的子项。下面的代码(本文中的相关代码的dojo版本均为1.7)实现了一个内部对象可拖放的列表:

require([ "dojo/dnd/Source",    "dojo/domReady!" ],    function(Source){    var wishlist =        new Source("wishlistNode");    wishlist.insertNodes(false, [        "Wrist watch",        "Life jacket",        "Toy bulldozer",        "Vintage microphone",        "TIE fighter"    ]);});

这就行了,看这个可排序的列表。如果你更喜欢通过声明的方式创建Dojo控件,那么请参见以下的代码:

<ol data-dojo-type="dojo.dnd.Source" id="wishlistNode" class="container">    <li class="dojoDndItem">Wrist watch</li>    <li class="dojoDndItem">Life jacket</li>    <li class="dojoDndItem">Toy bulldozer</li>    <li class="dojoDndItem">Vintage microphone</li>    <li class="dojoDndItem">TIE fighter</li></ol>

两者的运行结果是一模一样的,请看通过声明方式创建的可排序列表。

通过声明式创建可拖放列表, 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属性:

require([ "dojo/dom-class", "dojo/dnd/Source", "dojo/domReady!" ],        function(domClass, Source){    var catalog = new Source("catalogNode",        { accept: [ "inStock", "outOfStock" ] });    catalog.insertNodes(false, [        { data: "Wrist watch",        type: [ "inStock" ] },        { data: "Life jacket",        type: [ "inStock" ] },        { data: "Toy bulldozer",      type: [ "inStock" ] },        { data: "Vintage microphone", type: [ "outOfStock" ] },        { data: "TIE fighter",        type: [ "outOfStock" ] },        { data: "Apples",             type: [ "inStock" ] },        { data: "Bananas",            type: [ "inStock" ] },        { data: "Tomatoes",           type: [ "outOfStock" ] },        { data: "Bread",              type: [ "inStock" ] }    ]);    catalog.forInItems(function(item, id, map){        domClass.add(id, item.type[0]);    });     var cart = new Source("cartNode", { accept: [ "inStock" ] });    var wishlist = new Source("wishlistNode",        { accept: [ "inStock", "outOfStock" ] });});

通过声明方式创建三个列表的代码如下:

<div class="catalogContainer">    <h2>Catalog</h2>    <ul data-dojo-type="dojo.dnd.Source" id="catalogNode" class="container"        data-dojo-props="accept: [ 'inStock', 'outOfStock' ]" >      <li class="dojoDndItem inStock" dndType="inStock">Wrist watch</li>      <li class="dojoDndItem inStock" dndType="inStock">Life jacket</li>      <li class="dojoDndItem inStock" dndType="inStock">Toy bulldozer</li>      <li class="dojoDndItem outOfStock" dndType="outOfStock">        Vintage microphone</li>      <li class="dojoDndItem outOfStock" dndType="outOfStock">        TIE fighter</li>      <li class="dojoDndItem inStock" dndType="inStock">Apples</li>      <li class="dojoDndItem inStock" dndType="inStock">Bananas</li>      <li class="dojoDndItem outOfStock" dndType="outOfStock">        Tomatoes</li>      <li class="dojoDndItem inStock" dndType="inStock">Bread</li>    </ul></div> <div class="cartContainer">    <h2>Cart</h2>    <ol data-dojo-type="dojo.dnd.Source" id="cartNode" class="container"        data-dojo-props="accept: [ 'inStock' ]" >    </ol></div><div class="wishlistContainer">    <h2>Wishlist</h2>    <ol data-dojo-type="dojo.dnd.Source" id="wishlistNode"        data-dojo-props="accept: [ 'inStock', 'outOfStock' ]"        class="container">    </ol></div>

每个可拖放的对象都能被指定一个或多个type。type列表与容器的accept列表中只要有一对能匹配,那么对象能被放到对应的容器中,反之就不行。type和accept的默认值都是“text”。

这里我们用“inStock”和“outOfStock”来区分商品是否缺货,同时这也决定了商品能否拖放至“购物车”列表内。如果同时拖放“有货”和“缺货”的商品到购物车,会导致整个拖放不成功。

到目前为止Dylan的网上旧货铺看起来还不错。 不过还有几个问题亟待解决:

  1. “目录”中的商品被添加到“购物车”或者“收藏列表”里以后,就从“目录”里消失了。除非用户使用复制操作,不然同一件商品不能被同时被添加到“购物车”和“收藏列表”中。这大大影响了用户体验。
  2. 如果用户使用了复制操作,那么就有可能在同一个列表中出现多个重复商品,这可不妙。
  3. 页面上仅有三个列表,实在有点单调,用户体验还得进一步提升。

下面继续改进我们的页面。

 

3 列表项的自定义

我们之前已经提到过,可拖拽的列表内部对象可以被自定义。Dylan希望他的商品目录提供商品的图像、介绍和库存数量。根据他的需求,商品的数据结构看起来该是这个样子的:

{    name: "Wrist watch",    image: "watch.jpg",    description: "Tell time with Swiss precision",    quantity: 3}
dojo.dnd提供了自定义内部对象的方法 – creator函数,下面是代码示例:
define(["dojo/string","dojo/dom-construct","dojo/dom-class","dojo/dnd/Source","dojo/text!./itemTemplate.html","dojo/text!./avatarTemplate.html"],function(stringUtil,domConstruct,domClass,Source,template,avatarTemplate){//旧货商品数据var junk = [{ name: "Wrist watch", image: "watch.jpg", description: "Tell time with Swiss precision", quantity: 3 },{ name: "Life jacket", image: "life-jacket.jpg", description: "Stay afloat during your frequent shipwrecks", quantity: 1 },...];//根据传入的item对象构建DOM节点function catalogNodeCreator(item, hint){var node = domConstruct.toDom(stringUtil.substitute(hint === "avatar" ? avatarTemplate : template, {name: item.name || "Product",imageUrl: "images/" + (item.image || "_blank.gif"),quantity: item.quantity || 0,description: item.description ? "<br><span"+ item.description + "</span>" : ""})),type = item.quantity ? ["inStock"] : ["outOfStock"];return {node: node, data: item, type: type};}//创建Source并导入旧货数据var junkCatalog = new Source("junkCatalogContainer", {creator: catalogNodeCreator});junkCatalog.insertNodes(false, junk);});

以下是列表内部对象的Template(即上面代码中的itemTemplate.html):

<tr>    <td class="itemImg dojoDndHandle"><img src="${imageUrl}"></td>    <td class="itemText">${name} ${description}</td>    <td class="itemQty">${quantity}</td></tr>
这个是我们拖拽时的对象的Template(即avatarTemplate.html):
<table>    <tr>        <td class="itemImg"><img src="${imageUrl}"></td>        <td class="itemText">${name}</td>    </tr></table>

下图展示了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

var cart = new Target("cartPaneNode", { accept: [ "inStock" ] });
这里我们引入了一个新的类:dojo.dnd.Target。其实Target就是一个只能放不能拖的Source,相当于把Source类里的isSource属性设为false。有趣的是,isSource属性也可以在运行时被改变,下文中会有示例。

拖放目标容器的更改

cart.parent =dom.byId("cartNode");
原本拖放到cart里的商品会直接在cartPaneNode下创建子节点,不过cart.parent被赋值之后,所有拖放至cartPaneNode里的商品都会在<table id="cartNode">下创建子节点。

下面再基于我们的旧货店铺介绍一些Dojo拖放的额外功能。


4 监听拖放事件

Dojo的拖拽中应用了“订阅/发布”来处理事件响应。这里我们先借助aspect模式来处理onDrop事件。

// sets the count of items in a TitlePanefunction setListCount(){    query(".count", this.node)[0].innerHTML = this.getAllNodes().length;} // update the cart’s displayed item count when dropped onaspect.after(cart, "onDrop", setListCount); // update the wishlist’s displayed item count when dropped onaspect.after(wishlist, "onDrop", setListCount);
在onDrop事件被触发时,更新列表上显示的商品数量。这里需要注意一点,onDrop事件仅在对象被拖放至接受它的容器中才触发,而对象被拖放到页面任何位置都会触发onDndDrop。

 直接监听Topic

下面来借助“订阅/发布”模式来给我们的旧货铺添加一些动态效果:当拖拽开始时,高亮可以接受该拖拽对象的容器。

// 高亮可用的容器function highlightTargets(show, source, nodes){    domClass.toggle("wishlistPaneNode", "highlight", show);    domClass.toggle("cartPaneNode", "highlight",            show && arrayUtil.every(nodes, function(node){        return domClass.contains(node, "inStock");    }));} // 目标容器闪烁一次function glowTarget(source, nodes, copy, target){    domClass.add(target.node, "glow");    setTimeout(lang.hitch(domClass, "remove",        target.node, "glow"), 1000);}//拖拽动作开始// (/dnd/start)topic.subscribe("/dnd/start", lang.partial(highlightTargets, true));// 拖拽动作结束// (/dnd/cancel or /dnd/drop)topic.subscribe("/dnd/cancel, /dnd/drop",    lang.partial(highlightTargets, false));topic.subscribe("/dnd/drop", glowTarget);
避免对象多次复制

在前文中提到的对象多次复制的问题也很容易解决,只要在声明dojo.dnd.Source时设置copyOnly:true,那么在拖拽开始时Source不会移除内部对象,而只是将拷贝进行拖放。另外设置selfAccept:false可以防止被拖拽出去的copy放回源容器造成的重复问题。

 下面是我们的旧货铺的最终版本:



总结

我们建立旧货铺的步骤如下:

  • 搭建页面框架;
  • 单列表拖拽;
  • 多列表拖拽;
  • 可拖拽列表项的自定义;
  • 事件监听和处理;

我们还提供了旧货铺demo的源代码下载, Happy Dragging and Dropping!