用dojo.dnd实现拖放功能


相信很多人都自己动手写过拖放。DHTML里做拖放的原理很简单,一般有这么三个阶段:mousedown的时候做一些初始化,mousemove的时候更新拖放对象的位置,mouseup的时候再做一些清理工作。讲起来简单,但做起来总要花一些功夫的。Dojodnd模块提供了通用且功能强大的拖放支持,让我们可以不用自己造轮子,而且用起来也很方便。

废话少说,先来看看它到底有多方便。假设页面上有两个ul,我们需要对ul里的li元素实现拖放,让它们可以自由地在两个列表间移动。如果自己手写,虽然不难但也要花点时间吧。用Dojo的话,除了加载模块之外,甚至连一行javascript语句都不需要:


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "://.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta -equiv="Content-Type" content="text/html; charset=iso-8859-1">
<title>Untitled Document</title>
<style type="text/css">
@import "://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/resources/dojo.css";
@import "://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css";

ul{
border: 3px solid #ccc;
padding: 2em;
margin: 5em;
float: left;
cursor: default;
}
.dojoDndItemOver{
background: #ededed;
cursor: pointer;
}
.dojoDndItemSelected {
background: #ccf;
}
.dojoDndItemAnchor{
background: #ccf;
}
</style>
<script type="text/javascript" src="://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js" djConfig="parseOnLoad: true"></script>
<script type="text/javascript">
dojo.require("dojo.dnd.Source");
</script>
</head>
<body class="claro">
<ul dojoType="dojo.dnd.Source">
<li class="dojoDndItem">Item 1</li>
<li class="dojoDndItem">Item 2</li>
<li class="dojoDndItem">Item 3</li>
<li class="dojoDndItem">Item 4</li>
<li class="dojoDndItem">Item 5</li>
</ul>
<ul dojoType="dojo.dnd.Source">
<li class="dojoDndItem">Item A</li>
<li class="dojoDndItem">Item B</li>
<li class="dojoDndItem">Item C</li>
<li class="dojoDndItem">Item D</li>
<li class="dojoDndItem">Item E</li>
</ul>
</body>
</html>

这个例子用了host在google的dojo1.5版本,可以直接运行。这里唯一需要写的javascript语句就是加载dojo.dnd.Source类。剩下的就是在要拖放的对象上做一些标记,用html和CSS class就行了。而且Dojo为拖放对象添加的CSS class非常丰富,让我们能自由定制它们的外观。


Fig.1: Source内部DnD

Fig.2: Source之间DnD

Fig.3: 在无法接受拖放内容的地方改变Avatar的外观


好,现在来仔细看一下dojo.dnd模块到底是怎么一回事。


dojo.dnd包结构

打开dojo/dnd源码文件夹,可以看到里面有很多东西:

Fig.4: dojo.dnd的目录结构


刚才用的dojo.dnd.Source就在Source.js里面。顾名思义,Source就是拖放源,一个存放可拖放对象的容器。相对的还有dojo.dnd.Target(也在Source.js里),它继承了dojo.dnd.Source,不过只能接受从别处拖过来的东西,却不能拖出去。另一个Source的子类是AutoSource,如果你需要在运行时添加可拖放的结点(实时更新可拖放结点列表),那么它就是为你准备的。

Dojo.dnd包中的几个主要类之间的关系大致是这样:

Fig.5: DnD包中主要类的结构


其中Container是顶层基类,它的实例包含有一些子元素,能感知onmouseover/onmouseout事件,并且知道具体over的是哪个元素。SelectorContainer的子类,让容器支持鼠标选择,可以支持单选或多选。Avatar就是在拖放时跟着鼠标跑的那个东西,一般会直接包含拖放对象的dom结点。而Manager(是一个Singleton)则统筹了整个dnd过程,管理拖放的起点和终点,以及负责创建、更新和销毁Avatar

包里剩下的东西其实组成了一个子模块:dojo.dnd.move,如果你只是需要把某个dom结点拖来拖去,就应该用这个模块。这里只介绍dojo.dnd,以后再写dojo.dnd.move


dojo.dnd工作流程

当你在要拖动的对象上按住鼠标左键并开始移动时,Source会调用Manager.startDrag函数,标志拖放过程的开始。这个函数记录当前发起拖放的Source和拖放的结点,然后创建出Avatar,建立起一切必要的事件关联,并发布(dojo.publish)一个“开始拖放”(/dnd/start)的主题(topic)。Dojo.dnd里广泛采用主题广播的方式管理拖放过程,这样页面上所有的Source都能监听这些主题并作出反应。例如这个/dnd/start主题发布后,页面上所有的Source(包括刚才拖出来的那个)都将检查自己是否能够接受那些正在被拖动的结点(通过一个叫checkAcceptance的方法)。

这里有必要提一下默认的检查方法。Source有一个属性叫accept,这是一个字符串数组,默认是["text"],表示这个Source能够接受的东西只限于包含文本的结点。你可以自由定义accept里的内容,这将在下一节具体解释。

当这些结点被拖到一个Source上时(onmouseover),将使Manager发布/dojo/source/over主题,更新Avatar上的图标,以反映是否能在这个SourceDrop

当你释放鼠标的时候,首先触发Manageronmouseup事件的响应函数。这个函数将判断当前是否有Source能够接受拖放的内容,如果有,就发布/dnd/drop/before以及/dnd/drop主题;如果没有,就发布/dnd/cancel主题。然后销毁Avatar、事件句柄、以及所有与本次拖放相关的信息。所有的Source都会监听这些主题,并作出相应的应对。

如果某个Source 在响应/dnd/drop主题时发现自己就是Drop的目标,就把这次拖放的结点传给一个叫_normalizedCreator的私有方法,该方法负责把这些结点换成自己可以接受的形式。这里其实有一个定制点,让用户自定义转换的方式,这也将在下一节讲到。最后insertNodes方法把这些新结点插进来。如果做的是“移动”而不是“复制”(拖动时按住CTRL就是复制),还需要通知作为拖放起点的Source删除那些拖出来的子结点。


定制dojo.dnd

定制dojo.dnd的基本方式和dijit类似,就是在构造函数中传入参数对象。如果是声明式创建,就可以直接用html属性的方式写在html元素中。Dojo.dnd具有非常多的定制点,一一列举会过于冗长,这里只挑最常用的几个。(当然,一旦你阅读了源码,完全可以抛开一切约束,通过继承的方式任意扩展dojo.dnd里的内容)

1. 首当其冲是accept数组,刚才已经讲到,只有和这个数组有交集的拖放源才能被接受。例如,一个Sourceaccept数组是["text", "image"],另一个是["image", "video"],那么这两个Source就能接受从对方那里拖过来的东西。你肯定会问:为什么这是一个数组而不是单个字符串?答:对不同的拖放结点可以再定制其拖放类型。例如一个Source里可以既有text类型的结点,也有image类型的结点,你可以通过dndType属性在这些结点上做标记:

<ul dojoType="dojo.dnd.Source" accept="['text', 'image']">
<li class="dojoDndItem" dndType="text"></li>
<li class="dojoDndItem" dndType="image"></li>
</ul>

这样,如果你拖的是标记为textli元素,那么那个accept=["image", "video"]Source就无法接受它了:


Fig.6: 运用accept和dndType精确控制拖放

2. 第二重要的个人感觉就是creator,前面提到,通过这个函数可以任意定制拖进来的东西。这个函数接受两个参数,一个是拖进来的dom结点的innerHTML(注:Container里说这是一个形如{data: data, type: type}的对象,但在Source的实际使用中,传的仅仅是data),另一个叫hint字符串,目前据我所知其唯一的可能值是"avatar",表示创建出的结点是在Avatar中使用的。它需要返回一个形如:{node: node, data: data, type: type}的对象。这里的node可以跟传进来的那个没有半点关系。Data表示拖动的真正内容,一般就是node.innerHTMLType就是这个结点的dndType。例如我要在传进来的内容前面加一点东西,可以这样写:

<ul dojoType="dojo.dnd.Source">
<script type="dojo/method" event="creator" args="data, hint">
var node = dojo.create("div", {
"id": dojo.dnd.getUniqueId(),
"class": "dojoDndItem",
"innerHTML": "<strong style='color: darkred'>Special</strong> " + String(data)
});
return {node: node, data: node.innerHTML, type: ["text"]};
</script>
<li class="dojoDndItem">Item A</li>
<li class="dojoDndItem">Item B</li>
<li class="dojoDndItem">Item C</li>
<li class="dojoDndItem">Item D</li>
<li class="dojoDndItem">Item E</li>
</ul>



3. 一个简单但有用的开关属性:horizontal。如果你的拖放源是一个横向容器,请把它设为true


4. 三个很有用的且互相有关联的开关属性:copyOnly默认false, selfAccept默认true, selfCopy默认false。顾名思义,如果copyOnlytrue,那么这个Source里的东西只能被复制而不能被移走。当copyOnlytrue,且selfAcceptfalse的时候,在容器内dnd也被禁止了。当copyOnlytrueselfAccepttrue,且selfCopytrue的时候,容器内dnd的意思是复制而不是移动。



结语


本文很粗浅地介绍了dojo.dnd包的基本用法,如果要深入了解,强烈建议阅读源码并不断实践。Dojo.dnddojo的核心组件之一,功能强大且代码优雅,相信你一定能从中学到不少东西。