对于开发者来说,最重要的可用的交互设计模式之一是拖拽和放下。我们使用拖放功能没有考滤太多-特别是它刚完成时。这里有5个简单的步骤来确认一个最佳实践。
定义拖放
一个拖操作,从根本上说,就是一个在UI上的点击事件,同时鼠标停按在上面,并且移动。当一个拖操作结束后,鼠标释放就会产生一个放操作。从一个高级别的层次来看,拖与放的操作流程可以用如下流程图来概括。
为了加快开发,Ext JS为我们提供了Ext.dd类来管理基本的方案。在这篇指南里,我们的讨论将涵盖如下内容:放操作的出现和移除,非法放操作的修复和成功的放操作发生后将会继续发生什么。
组织拖放类
第一眼看上去,Ext.dd的说明文档可能有一点吓人。但如果我们花几分钟会来看一下这些类,我们会发现他们都是起源于拖放类,大多数类都可以被归类到拖操作或放操作。再多花一点时间来研究一下,我们可以将这些类更细分到单节点和多节点的交互操作。
为了学习拖放的基础,我们将重点放在将单个拖放交互操作应用在DOM节点上。为了达到这个目的,我们将使用DD和DDTarget类,这两个类为他们各自的拖放行为分别提供了基础的实现。
手头的任务
让我们讨论一下,如果我们被要求开发一个给汽车租赁公司使用的应用,这个应用可以将汽车或皮卡放在以下三中状态中:可用,已出租或维修状态。汽车和皮卡只允许被放置在他们对应的可用容器内。
作为开始,我们必须让汽车和皮卡元素可拖动(dragable)。我们使用DD来做到这点。我们需要使已租赁,维修和可用容器变为‘drop targets’。我们会使用DDTarget类来实现。最后,我们使用不同的拖放组来使这些汽车和皮卡车只能被放到相应的可用的容器中。现在,我们可以开始为汽车和皮卡车添加拖操作。
第一步:开始拖动
为了配置汽车的div元素为可拖动,我们需要生成一个列表,并循环遍历它为每个元素生成一个DD的新实例。如下是我们的实现方法。
Ext.onReady(function() {
// Create an object that we'll use to implement and override drag behaviors a little later
var overrides = {};
// Configure the cars to be draggable
var carElements = Ext.get('cars').select('div');
Ext.each(carElements.elements, function(el) {
var dd = Ext.create('Ext.dd.DD', el, 'carsDDGroup', {
isTarget : false
});
//Apply the overrides object to the newly created instance of DD
Ext.apply(dd, overrides);
});
var truckElements = Ext.get('trucks').select('div');
Ext.each(truckElements.elements, function(el) {
var dd = Ext.create('Ext.dd.DD', el, 'trucksDDGroup', {
isTarget : false
});
Ext.apply(dd, overrides);
});
});
所有的拖,放类都是通过重写它的方法来实现的。这就是为什么我们在上面的代码段中,我们创建了一个叫做overrides的空的对象,这个对象稍后将会被通过overrides声明的我们需要的方法填充。我们通过使用DomQuery的select方法来查询汽车容器中所有的子div元素得到汽车和皮卡车元素列表 。为了让汽车和皮卡车元素可拖动,我们创建了一个新的DD实例,传入需要拖动的汽车和皮卡车元素和需要参与的拖放组。需要注意的是,车辆类型有它们自己的相应的拖放组。记住这一点对我们稍后的设置已租赁与维修状态的容器作为放入的目标元素十分有用。当然,也要注意到我们正应用overrides对象到我们通过 Ext.apply新建的DD实例对象,这是一个添加属性或方法到一个已经存在的对象里的一个十分方便的方法。在我们继续我们的功能实现之前,我们需要花一小段时间快速地分析当你在屏幕上拖动一个元素时发生了什么。理解了这个之后,剩下的部分将会被逐渐实现。
窥探拖动元素是如何起作用的
第一件你需要注意的事情是,当你拖动汽车或皮卡元素时,不管你将它们放下什么地方,它们就会钉在那里。因为我们刚开始实现功能,这个效果还勉强可以接受。重要的是要理解这些拖动的节点是怎样起作用的。这将帮助我们当我们把它们放在了不合理的目标上时恢复原来的位置,这个被称为“非法放置”(invalid drop)。下面的插图使用FireBug的HTML检测面板 ,并且高亮了由对Camaro( 卡玛洛 — 一款汽车)元素的拖动引起的改变。
当检测到在拖动操作中的拖动元素时,我们可以看到元素上的style属性由三个CSS值构成,style属性会一直随着包含在其中的样式而存在。这就是为什么当我们放置错误时我们可以清除掉它们。除非我们设置了合理的放置目标,否则所有的放置操作会被认为是不合法的。
第二步:修正不合法的放下
修复一个不合法放置操作的最快的方法是重设在拖动操作中的style属性。这意味着拖动元素将会在鼠标下面消失,然后又在它原来的地方出现,这将十分无趣。为了让它变得更流畅,我们会使用Ext.Fx为这个动作加上动画。请记住,拖动和放置类(drag and dropclasses)中是有可以重写的方法的。为了实现位置修复,我们需要重写b45StartDrag,onInvalidDrop和endDrag方法。让我们添加下面的方法到上面的重写对象中,然后讨论这些代码的原理及功能。
var overrides = {
// Called the instance the element is dragged.
b4StartDrag : function() {
// Cache the drag element
if (!this.el) {
this.el = Ext.get(this.getEl());
}
//Cache the original XY Coordinates of the element, we'll use this later.
this.originalXY = this.el.getXY();
},
// Called when element is dropped in a spot without a dropzone, or in a dropzone without matching a ddgroup.
onInvalidDrop : function() {
// Set a flag to invoke the animated repair
this.invalidDrop = true;
},
// Called when the drag operation completes
endDrag : function() {
// Invoke the animation if the invalidDrop flag is set to true
if (this.invalidDrop === true) {
// Remove the drop invitation
this.el.removeCls('dropOK');
// Create the animation configuration object
var animCfgObj = {
easing : 'elasticOut',
duration : 1,
scope : this,
callback : function() {
// Remove the position attribute
this.el.dom.style.position = '';
}
};
// Apply the repair animation
this.el.setXY(this.originalXY, animCfgObj);
delete this.invalidDrop;
}
},
在上面的代码里,我们以重写b4StartDrag方法作为开始,当拖动元素开始在屏幕上被动时被调用,并且这是一个缓存拖放元素和原始XY坐标的理想地方—这些被缓存的东西我们在稍后的处理中会用到。下一步,我们会重写onInvalidDrop方法,当一个拖动节点被放置到任何一个不是它的放置目标上的元素上时该方法被调用,而被放置的元素是位于相同的拖放组。这个重写操作只是将一个本地的invalidDrop属性设置为true,该属性将会在下一个方法里使用。最后一个要重写的方法是endDrag,当拖动元素不在屏幕上被拖动并且拖动元素不再被鼠标运动控制时该方法被调用。这个重写将会使用动画将拖动元素的X, Y坐标设置回它原来的值。我们在动画的末尾,使用elasticOut easing来配置动画以提供一个炫酷的弹性效果。
好了,现在我们已经完成修复操作。为了让它能在放置过程中正常运行合法的放置操作,我们需要设置放置目标。
第三步:配置放置目标
我们的需求是我们允许汽车和皮卡车被放置在相对应的已出租和修复中状态的容器中。为了达到这个目的,我们需要实例化DDTarget类的实例。如下是实现代码:
// Instantiate instances of Ext.dd.DDTarget for the cars and trucks container
var carsDDTarget = Ext.create('Ext.dd.DDTarget', 'cars','carsDDGroup');
var trucksDDTarget = Ext.create('Ext.dd.DDTarget', 'trucks', 'trucksDDGroup');
// Instantiate instances of DDTarget for the rented and repair drop target elements
var rentedDDTarget = Ext.create('Ext.dd.DDTarget', 'rented', 'carsDDGroup');
var repairDDTarget = Ext.create('Ext.dd.DDTarget', 'repair', 'carsDDGroup');
// Ensure that the rented and repair DDTargets will participate in the trucksDDGroup
rentedDDTarget.addToGroup('trucksDDGroup');
repairDDTarget.addToGroup('trucksDDGroup');
在上面的代码片断中,我们已经为汽车,皮卡车,已经出租和修复状态设置了放置目标。请注意汽车容器元素只能参与 ‘carsDDGroup’,皮卡车容器元素只能参与’trucksDDGroup’。这有助于强制汽车和皮卡车只能被放置在它们起源的容器中。下一步,我们为已出租和修理中状态的元素进行实例化DDTarget。起初,他们被配置为只参加’carsDDGroup’。为了让他们能参与‘trucksDDGroup’,我们通过addToGroup方法把它加入到该组中。好了,现在我们已经完成了放置目标的配置。让我们看一下当我们将皮卡车和汽车放置到一个合法的元素上时会发生什么吧。
在练习放置目标时,我们看到拖动元素准确地呆在了它被放置的位置上。那也就是说,图片可以被放置在它的放置目标上的任何位置并且一直呆在那里。这意味着我们的放置实现还没有完成。为了完成它,我们需要亲自为完成放置操作写代码。这需要我们用另一个我们刚刚创建DD实例的重写方法来实现。
第四步:完成放下(drop)
为了完成放下操作,我们需要亲自使用DOM工具从元素的父元素里拖动它,并把它放置到目标元素上。这将由重写DD的onDragDrop方法来完成。如下代码为重写对象的代码。
var overrides = {
...
// Called upon successful drop of an element on a DDTarget with the same
onDragDrop : function(evtObj, targetElId) {
// Wrap the drop target element with Ext.Element
var dropEl = Ext.get(targetElId);
// Perform the node move only if the drag element's
// parent is not the same as the drop target
if (this.el.dom.parentNode.id != targetElId) {
// Move the element
dropEl.appendChild(this.el);
// Remove the drag invitation
this.onDragOut(evtObj, targetElId);
// Clear the styles
this.el.dom.style.position ='';
this.el.dom.style.top = '';
this.el.dom.style.left = '';
}
else {
// This was an invalid drop, initiate a repair
this.onInvalidDrop();
}
},
通过一个成功的放置操作,拖放元素现在将从它们的父元素移动到目标元素上。用户怎样知道他们悬停的是否是一个合法的放置目标呢?通过配置放下邀请(drop invitation)我们将给用户一些可见的回馈。
第五步:增加放下邀请
为了让拖放操作更有用一点,我们需要为用户提供反馈用来判断一个放下操作是否能成功发生。这意味着我们将重写onDragEnter和onDragOut方法,然后将这最后两个方法添加到重写对象里。
var overrides = {
...
// Only called when the drag element is dragged over the a drop target with the
same ddgroup
onDragEnter : function(evtObj, targetElId) {
// Colorize the drag target if the drag node's parent is not the same as the
drop target
if (targetElId != this.el.dom.parentNode.id) {
this.el.addCls('dropOK');
}
else {
// Remove the invitation
this.onDragOut();
}
},
// Only called when element is dragged out of a dropzone with the same ddgroup
onDragOut : function(evtObj, targetElId) {
this.el.removeCls('dropOK');
}
};
在上面的代码中,我们重写了onDragEnter和onDragOut方法。当拖动元素及与之相关的放下目标位于同一个拖放组里时,这两个方法才能被使用。当鼠标指针第一个与放下目标的边界相交时,同时拖动项处理拖动模式,onDragEnter方法才会被调用。同样地,当鼠标指针第一次被拖动出放下目标的四周并且处于拖动模式时,onDragOut被调用。
通过添加onDragEnter方法和onDragOut方法的重写,我们可以看到当鼠标指针第一次与一个合法放下目标接触时,拖动元素的背景色会变为绿色,当它离开放下目标或者被放下时,绿色消失。这就完成了我们对DOM元素的拖放实现。
综述
今天,我们学会了怎样使用第一级的拖放实现类来实现端到端的DOM节点的拖放。从一个高的级别来说,我们定义并讨论了拖放操作是什么,如何思考它在框架中的关系。我们同时学到了拖放类可以通过拖放行为和它们是否支持单个或多个拖放操作被分成不同的组。当实现这种行为时,我们演示了dd类如何帮助决定元素的行为,并且我们写了最后行为的代码。我们希望你通过看一些基础的对DOM节点的拖放操作来享受这个学习过程。我们期望在将来为你们带来更多的关于这个主题的文章。
作者
由Jay Garcia所写。
Gracia先生是Ext JS inAction and Sencha Touch in Action的作者。他从2006年开始就成为一名基于Sencha的JS框架的传播者。Jay也是Modus Create(一家专注于用高端人才开发高质量基于Sencha应用的数码公司)的共同创立者和CTO。Modus Create是SenchaPremier的合作者。