用dojo.dnd实现拖放功能

<!-- [if gte mso 9]><xml><w:WordDocument><w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel><w:DisplayHorizontalDrawingGridEvery>0</w:DisplayHorizontalDrawingGridEvery><w:DisplayVerticalDrawingGridEvery>2</w:DisplayVerticalDrawingGridEvery><w:DocumentKind>DocumentNotSpecified</w:DocumentKind><w:DrawingGridVerticalSpacing>7.8</w:DrawingGridVerticalSpacing><w:View>Normal</w:View><w:Compatibility></w:Compatibility><w:Zoom>0</w:Zoom></w:WordDocument></xml><![endif]-->

用dojo.dnd实现拖放功能

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

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

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <title>Untitled Document</title> <style type="text/css"> @import "http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/resources/dojo.css"; @import "http://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="http://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和 CSSclass 就行了。而且 Dojo 为拖放对象添加的 CSSclass 非常丰富,让我们能自由定制它们的外观。

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 的是哪个元素。 Selector Container 的子类,让容器支持鼠标选择,可以支持单选或多选。 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 上的图标,以反映是否能在这个 Source Drop

当你释放鼠标的时候,首先触发Manager onmouseup 事件的响应函数。这个函数将判断当前是否有 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 数组,刚才已经讲到,只有和这个数组有交集的拖放源才能被接受。例如,一个 Source accept 数组是 ["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>

这样,如果你拖的是标记为text li 元素,那么那个 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.innerHTML Type 就是这个结点的 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 。顾名思义,如果 copyOnly true ,那么这个 Source 里的东西只能被复制而不能被移走。当 copyOnly true ,且 selfAccept false 的时候,在容器内 dnd 也被禁止了。当 copyOnly true selfAccept true ,且 selfCopy true 的时候,容器内 dnd 的意思是复制而不是移动。


结语


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