用dojo.dnd实现拖放功能
相信很多人都自己动手写过拖放。DHTML里做拖放的原理很简单,一般有这么三个阶段:mousedown 的时候做一些初始化, mousemove 的时候更新拖放对象的位置, mouseup 的时候再做一些清理工作。讲起来简单,但做起来总要花一些功夫的。 Dojo 的 dnd 模块提供了通用且功能强大的拖放支持,让我们可以不用自己造轮子,而且用起来也很方便。
废话少说,先来看看它到底有多方便。假设页面上有两个ul ,我们需要对 ul 里的 li 元素实现拖放,让它们可以自由地在两个列表间移动。如果自己手写,虽然不难但也要花点时间吧。用 Dojo 的话,除了加载模块之外,甚至连一行 javascript 语句都不需要:
这个例子用了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 属性在这些结点上做标记:
这样,如果你拖的是标记为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 。例如我要在传进来的内容前面加一点东西,可以这样写:
效果如图:
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 的核心组件之一,功能强大且代码优雅,相信你一定能从中学到不少东西。