当前位置 : 主页 > 网页制作 > Dojo >

Dojo Dnd - 拖拽功能实战

来源:互联网 收集:自由互联 发布时间:2021-06-15
本文翻译自:http://www.sitepen.com/blog/2011/12/05/dojo-drag-n-drop-redux/ 原文作者:Colin Snover 译者:Ruan Qi 拖拽(dojo/dnd)作为Dojo的基础功能之一,可视化地支持页面元素或对象在多个容器之间拖

本文翻译自: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 TitlePane
function setListCount(){
    query(".count", this.node)[0].innerHTML = this.getAllNodes().length;
}
 
// update the cart’s displayed item count when dropped on
aspect.after(cart, "onDrop", setListCount);
 
// update the wishlist’s displayed item count when dropped on
aspect.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!

网友评论