级别: 中级
Greg Travis , 软件工程师
2008 年 10 月 28 日
通过本文,了解使用拖放功能移动 Web 页面的不同部分的技巧。分别实现交互性的不同方面,然后再将它们组合在一起,这样便于灵活定制页面,也让您的 Web 用户非常满意。
JavaScript是一种功能强大的语言,可用于创建基于 Web的应用程序。它已经足够稳定和成熟,完全可以创建与传统桌面应用程序相抗衡的程序,因为后者在稳定性和特性丰富性方面都要胜出一筹。但JavaScript 最初只是用来向静态 Web 页面添加某些交互性,使它不再是静态页面,它现在还用于此目的。
我将要展示的这个技巧的关键之处是如何恰当地构建页面,使它能与 JavaScript 代码交互。通常,页面都是通过头脑里固有的 JavaScript代码构造的。但是,尽管如此,很多时候您都需要向现有页面内添加新的交互特性。而这往往需要一些技巧,因为 JavaScript代码必须遍历文档结构并在合适的位置添加代码,而且通常还要求不影响现有的结构 — 和页面上已有的 JavaScript代码。总之,要将对系统的影响最小化。
|
本文介绍了一种方法,它通过移动页面的不同部分来 激活 页面。具体来讲,就是通过将一个部分拖放到另一个部分之上从而实现 可切换 部分的切换。
要激活这些部分,只需向其添加
class
参数并加载一个 JavaScript 文件。可以通过向
<body>
标记添加
onload
方法来激活代码,此方法会在页面加载之后立即启动代码。代码会处理随后的事情。
注意: 本文示例所对应的源代码可以从 下载 部分获得。
此外,可以尽量多地使用抽象来构造代码。程序的不同元素通常都不必要地相互缠结,UI 代码更是这样。可切换系统由不同的块构建而成,每个块实现交互性的不同部分。这些块结合起来就能实现简单无缝的界面,该界面对于 UI 的试验和调优都很关键。
|
|
可切换系统很容易使用。先由 Web 页面设计人员将某些部分标志为可切换的。然后就可以在任何一个可切换元素上单击并将该元素拖放到另一个可切换元素。放开鼠标按钮后,这两个元素就完成了交换。
为了能清楚展示所发生的事情,可以使用一些标准的 GUI 操作。
当第一次单击可切换元素时,在光标下面会出现一个透明的矩形。这个矩形由
coveringDiv()
函数创建,它刚好能覆盖这个可切换元素。实际上是将这个矩形拖放到另一个元素。当拖放时,只有这个透明的矩形会移动 — 初始的元素保持不动直到鼠标按钮被松开为止。
另一个重要的操作是清晰标识出要拖动到的目标元素。当拖动透明的矩形四处移动时,光标可以经过多个可切换元素。当光标悬浮于某个可切换元素之上时,该元素就会通过另一个透明矩形突出显示。这种突出显示就能清楚地标示出此元素就是拖放到的目标。当松开鼠标按钮时,被拖动的元素和拖放到的目标元素就会互换位置,而且所有透明矩形也会消失,直到下一次切换。
正如先前提到的,必须要使代码对已有系统影响最小。这就意味着页面设计人员 —工作于 HTML 或 XML— 无需涉及可切换系统。这不是他们的工作。
此页面只需具有如下三项内容:
- JavaScript 标记
-
<body>
标记内的onload
方法 - 标记为 swappable 的可切换区域
必须将以下标记置于页面文件的顶部:
<script src="rearrange-your-page.js"></script> |
此标记在加载过程的早期加载,但它在 body 内的
onload
函数调用之后才会执行。
该方法在整个页面加载时调用这个可切换系统。这一点很重要,因为此代码的第一项功能就是在整个页面内搜索可切换的元素。因而,需要确保这些元素已加载。body 内的
onload
方法应该如清单 1 所示。
<body onload="swappable_start();"> ... rest of page </body> |
必须通过
class
参数这样标记每个想要切换的区域。这是页面作者和设计人员需要多加考虑的事情,因为他们需要将此参数添加给每个部分。参见清单 2。
<div class='swappable'> lorem ipsum lorem ipsum </div> |
代码所需做的首要事情是寻找页面将被激活的部分。正如之前提到的,这只要求包围这个部分的标记具有
class
参数。要寻找这些部分,需要找到所有具有可切换
class
的标记。此函数不是标准 DOM 库的一部分,但它很容易实现。清单 3 展示了一个示例实现。
清单 3. getElementsByClass() 的实现
// By Dustin Diaz function getElementsByClass(searchClass,node,tag) { var classElements = new Array(); if ( node == null ) node = document; if ( tag == null ) tag = '*'; var els = node.getElementsByTagName(tag); var elsLen = els.length; var pattern = new RegExp("(^|////s)"+searchClass+"(////s|$)"); for (i = 0, j = 0; i < elsLen; i++) { if ( pattern.test(els[i].className) ) { classElements[j] = els[i]; j++; } } return classElements; } |
程序一般是通过将各功能块结合在一起而构建起来的。不同的程序员会有不同的实现方式,但作为一种规律,最好是采用多个小的功能块而不是少数几个大的功能块。每个小功能块应该实现一种功能并具有清楚的语义。
不过,在进行 GUI 编程时,这样的构建不太容易。好的 GUI 必须调整很多界面元素并将它们的行为结合起来形成一个能直观工作的整体行为。基于事件的系统通常都是由复杂的交换行为联合起来的回调集合。模块化的交互元素很难创建。
可切换代码就使用了模块化的交互元素。前面,我提到过在可切换系统内有两种主要的交互元素:拖动元素的突出显示和拖动到的目标的突出显示。在代码中,这两个元素的实现是分开的。
本例很好地展示了模块化处理交互性的技巧。正如可切换界面的描述中所提到的,这两个交互性元素常常缠结在一起。突出显示和突出显示的消失都是在一个鼠标操作中发生的,而且它们的发生都对应鼠标输入的不同方面。如果这两个元素是在一个代码片段中实现的,那么代码可能不太容易读懂,因为同时发生的事情很多。
为了使 GUI 的实现模块化,我使用了 拖动处理程序 。这类似于内置在 GUI 系统的事件处理程序。虽然事件处理程序只处理某种单一事件,拖动处理程序却可以处理整个拖放过程。一个拖动处理程序可处理一系列事件而不只一个单一事件。下面是拖动处理程序的示例骨架,如清单 4 所示。
{ start: function( x, y ) { // ... }, move: function( x, y ) { // ... }, done: function() { // ... }, } |
这个拖动处理程序是一个对象,具有三个方法:
start
、
move
和
done
。当初始化一个拖放动作时,调用
start
方法并传递给这次单击的对应坐标。当四处移动光标时,会反复调用
move
方法,然后同样被传递给光标当前对应的坐标。最后,当鼠标按钮释放后,就会调用
done
方法。
可切换系统同时使用了两个不同的拖动处理程序,这也让您能够干净地处理交互的两个不同方面,即便这两个方面具有复杂的关系。让其中的一个交互成为另一个交互的一部分并不合适。相反,应该能同时无缝地使用这两个交互。
这两个拖放处理程序的其中是
rectangle_drag_handler
。此处理程序负责移动代表被拖动元素的透明矩形。清单 5 给出了这个
start
方法。
清单 5. rectangle_drag_handler 处理程序
function rectangle_drag_handler( target ) { this.start = function( x, y ) { this.cover = coveringDiv( target ); make_translucent( this.cover, .6 ); this.cover.style.backgroundColor = "#777"; dea( this.cover ); this.dragger = new dragger( this.cover, x, y ); }; // ... } |
start
方法创建这个透明矩形并将其传递给另一个称为
dragger
的对象。一个
dragger
就是一个对象,它能对应移动的光标移动 DOM 元素。可以将当前的光标的坐标传递给这个 dragger,它会更新所拖动的对象使其跟随光标的移动。
move
方法更新这个 dragger,如清单 6 所示。
this.move = function( x, y ) { this.dragger.update( x, y ); }; |
最后,
done
方法(参见清单 7)删除这个透明矩形,因为拖放过程现在已经结束。
清单 7. rectangle_drag_handler 的 done 方法
this.move = function( x, y ) { this.done = function() { this.cover.parentNode.removeChild( this.cover ); }; } |
现在必须找到一种方法来同时使用这两个拖动处理程序。这可以通过
compose_drag_handlers()
函数轻松实现,该函数接受这两个拖动处理程序并将其结合成一个综合的拖动处理程序。这个综合拖动处理程序的使用与一般的拖动处理程序一样。这样,这两个原始的拖动处理程序的行为就实现了无缝结合。
compose_drag_handlers()
函数很容易编写。它看上去很像是一个拖动处理程序,但每个方法都会调用这两个原始拖动处理程序中相应的方法。这个函数如清单 8 所示。
function compose_drag_handlers( a, b ) { return { start: function( x, y ) { a.start( x, y ); b.start( x, y ); }, move: function( x, y ) { a.move( x, y ); b.move( x, y ); }, done: function() { a.done(); b.done(); }, } } |
正如您所见,拖动处理程序
a
和
b
被组合到一个综合的拖动处理程序内。如果要调用这个综合处理程序的
start()
方法,实际上就是先后调用
a.start()
和
b.start()
。
您需要在名为
prepare_swappable()
的设置函数内调用
compose_drag_handlers
,如清单 9 所示。
function prepare_swappable( o ) { swappables.push( o ); var sdp = new rectangle_drag_handler( o ); var hdp = new highlighting_drag_handler( o ); var both = compose_drag_handlers( sdp, hdp ); install_drag_handler( o, both ); } |
除了其他功能之外,此函数最主要的功能是为可切换元素创建
rectangle_drag_handler
和
highlighting_drag_handler
,然后再将它们组合成一个综合的拖动处理程序。最后,这个综合拖动处理程序再通过调用
install_drag_handler()
来激活,这将在接下来的两个小节中详细介绍。
与常规的事件处理程序不同,一个拖动处理程序可以处理多个事件。尽管如此,它还是需要附加到对象,这与常规的事件处理程序相同。
安装任何一种事件处理程序都是需要技巧的,因为正在修改的元素很可能已经在其内安装了事件处理程序。如果要替换这些事件处理程序,就需要更改页面的行为方式。
为了避免这一问题,可以使用一个名为
install_mouse_handlers()
的实用函数,如清单 10 所示。
清单 10. install_mouse_handlers() 函数
function install_mouse_handlers( target, onmouseup, onmousedown, onmousemove ) { var original_handlers = { onmouseup: target.onmouseup, onmousedown: target.onmousedown, onmousemove: target.onmousemove }; target.onmouseup = onmouseup; target.onmousedown = onmousedown; target.onmousemove = onmousemove; return { restore: function() { target.onmouseup = original_handlers.onmouseup; target.onmousedown = original_handlers.onmousedown; target.onmousemove = original_handlers.onmousemove; } }; } |
install_mouse_handlers()
函数负责向特定的对象添加特定的鼠标处理程序。它返回的是一个对象,可使用该对象恢复原始的处理程序。这样一来,当拖放过程结束后,就可以调用
restore()
函数,恢复到拖放过程开始之前的状态。
针对拖放操作的拖动处理程序使用了这三个鼠标处理程序:
onmousedown
、
onmouseup
和
onmousemove
。不过,开始时,只需安装
mousedown
处理程序,因为此时您尚在等待激发初始化拖放过程的单击。
当单击发生时,就需要安装
mousemove
和
mouseup
处理程序。而且,在此时,不再需要
mousedown
处理程序,因为已经进行单击。该处理程序将被删除,在拖放过程完成后再恢复它。最初的
mousedown
处理程序如清单 11 所示。
var onmousedown = function( e ) { var x = e.clientX; var y = e.clientY; p.start( x, y ); var target_handler_restorer = null; var document_handler_restorer = null; var onmousemove = function( e ) { var x = e.clientX; var y = e.clientY; p.move( x, y ); }; var onmouseup = function( e ) { p.done(); target_handler_restorer.restore(); document_handler_restorer.restore(); }; target_handler_restorer = install_mouse_handlers( target, onmouseup, null, onmousemove ); document_handler_restorer = install_mouse_handlers( document, onmouseup, null, onmousemove ); e.stopPropagation(); return false; }; |
在初始化拖放序列并调用此处理程序时,它会创建
onmousemove
和
onmouseup
处理程序并能在目标元素内安装它们。当然,它还会使用一个
install_mouse_handlers()
,以便以后的卸载。
还有一点需要注意:是在
document
对象内安装这些处理程序的。这一点十分关键,因为在拖放过程中用户可能会将光标拖过整个页面。如果鼠标超出可切换元素的范围 — 您很可能还想收到这些事件。同样地,可以使用
install_mouse_handlers()
以便以后恢复它们。
|
|
至此,我已经介绍了很多不同的类和函数。其中的每一个类或函数本身都十分简单,因此更重要的是要了解它们是如何协同工作的。
下面是对整个拖放过程的一个总结:
- 单击一个可切换元素。
-
此元素的
onmousedown
处理程序将被调用,它安装onmousemove
和onmouseup
处理程序。 - 移动鼠标,这些处理程序将被调用。
- 这些处理程序反过来调用前面安装的拖动处理程序。
- 这个拖动处理程序实际上是一个复合拖动处理程序,综合了两个不同的拖动处理程序的效果。
-
其中的一个拖动处理程序
rectangle_drag_handler
负责向光标附加一个代表被拖动元素的透明矩形。 -
另一个拖动处理程序
highlighting_drag_handler
负责突出显示鼠标移过的那些可切换元素,以显示可以进行元素拖动的地方。 -
当在目标元素之上释放鼠标按钮时,
highlighting_drag_handler
的done()
方法就会切换这两个元素。这个拖动处理程序将被卸载,只留下最初的onmousedown
处理程序,准备好开始下一轮的拖放过程。
|
|
拖放操作相对简单,但它涉及了几个交互过程,用来跟踪整个过程的用户输入和提供即时反馈。本文展示如何将模块化的交互元素组合成统一整体来构建完整的 GUI。
这种做法有很多好处。由于代码是模块化的,因此更容易编写和维护。所需的函数和类的代码没有一个是超过 40 行的,并且它们通常更短。
每一个交互元素都会实现一个典型的 GUI 过程或效果,所以可在其他上下文中重用它们。可以开发这些交互元素的丰富的库,从而通过组合各个部分构建更复杂的 UI。