这篇文章来自 Les Orchard,他曾在 Mozilla 的网页开发团队工作。
简介
拖放是图形用户界面中最基本的操作之一。通过一个动作,它允许用户将对象的选中与操作的执行配对,通常包括操作中的第二个对象。这是一个简单但强大的 UI 概念,用于支持复制、列表重新排序、删除(如垃圾箱/回收站),甚至创建链接关系。
由于它是如此基本,因此从浏览器首次在 DHTML 中提供鼠标事件起,在 Web 应用程序中提供拖放就是理所当然的了。但是,尽管mousedown
、mousemove
和mouseup
使其成为可能,但实现仍然局限于浏览器窗口的范围。此外,由于这些事件只涉及被拖动的对象,因此在交互完成时找到放置的目标是一个挑战。
当然,这并不能阻止大多数现代 JavaScript 框架抽象掉大部分问题,并在他们做这件事的时候添加一些修饰。但是,如果浏览器提供对拖放的一流支持,甚至将其扩展到窗口沙箱之外,岂不是很好吗?
事实证明,HTML 5 规范关于新的拖放事件的章节正是对这个愿望的回应,而Firefox 3.5 包含了这些事件的实现。
如果你想直接跳转到代码,我已经整理了一些简单的演示,演示了这些新事件。
我甚至满足了自己的一个愿望,构建了一个大纲编辑器的雏形,其中每个可拖动的元素也是一个放置目标——在一个复杂的文档中可能会有几十到几百个这样的目标,这在我过去试图用普通的鼠标事件来解决这个问题时,给我带来了一些小困扰。
而且,所有这些都可以从我专门为这篇文章创建的GitHub 仓库中下载或克隆。
新的拖放事件
所以,话不多说,以下是新的拖放事件,按照你可能期望它们被触发的顺序排列
dragstart
- 已开始拖动,被拖动的元素是事件目标。
drag
- 鼠标已移动,被拖动的元素是事件目标。
dragenter
- 被拖动的元素已移入放置监听器,放置监听器元素是事件目标。
dragover
- 被拖动的元素已移到放置监听器上,放置监听器元素是事件目标。由于默认行为是取消放置,因此在事件处理程序中返回
false
或调用preventDefault()
表示此处允许放置。 dragleave
- 被拖动的元素已移出放置监听器,放置监听器元素是事件目标。
drop
- 被拖动的元素已成功放置在放置监听器上,放置监听器元素是事件目标。
dragend
- 拖动已结束,无论成功与否,被拖动的元素是事件目标。
就像过去的鼠标事件一样,可以使用addEventListener()
直接或通过你最喜欢的 JS 库将监听器附加到元素。
考虑以下使用 jQuery 的示例,也可以作为实时演示
<div id="newschool">
<div class="dragme">Drag me!</div>
<div class="drophere">Drop here!</div>
</div>
<script type="text/javascript">
$(document).ready(function() {
$('#newschool .dragme')
.attr('draggable', 'true')
.bind('dragstart', function(ev) {
var dt = ev.originalEvent.dataTransfer;
dt.setData("Text", "Dropped in zone!");
return true;
})
.bind('dragend', function(ev) {
return false;
});
$('#newschool .drophere')
.bind('dragenter', function(ev) {
$(ev.target).addClass('dragover');
return false;
})
.bind('dragleave', function(ev) {
$(ev.target).removeClass('dragover');
return false;
})
.bind('dragover', function(ev) {
return false;
})
.bind('drop', function(ev) {
var dt = ev.originalEvent.dataTransfer;
alert(dt.getData('Text'));
return false;
});
});
</script>
由于有了新事件和 jQuery,这个示例既短又简单——但它包含了许多功能,正如本文的其余部分将解释的那样。
在继续之前,关于上面的代码,至少有以下三点值得一提
- 放置目标通过具有放置事件的监听器来启用。但是,根据 HTML 5 规范,可拖动的元素需要一个
draggable="true"
属性,该属性可以在标记或 JavaScript 中设置。因此,
$('#newschool .dragme').attr('draggable', 'true')
。 - 原始 DOM 事件(与 jQuery 的事件包装器相反)提供了一个名为
dataTransfer
的属性。除了操作元素之外,新的拖放事件还允许在交互过程中传输用户定义的数据。 - 由于这些是一流的事件,你可以应用事件委托技术。
那是什么?想象一下,你有一个包含 1000 个列表项的列表——例如,作为深度嵌套的大纲文档的一部分。与其需要附加监听器或以其他方式处理所有 1000 个项目,不如简单地将监听器附加到父节点(例如,
<ul>
元素),所有来自子节点的事件都会传播到单个父监听器。作为一项额外的好处,在页面加载后添加的所有新子元素都将享有相同的优势。
使用 dataTransfer
如上一节所述,新的拖放事件允许你将数据与被拖动的元素一起发送。但它比这更好:你的放置目标可以接收从其他浏览器窗口甚至*其他应用程序*中拖入窗口的内容对象传输的数据。
由于示例有点长,查看实时演示和相关代码,以了解使用dataTransfer
可能实现的功能。
简而言之,这个节目的主角是事件对象公开的dataTransfer
属性的setData()
和getData()
方法。
setData()
方法通常在dragstart
监听器中调用,使用与推荐的 content type相关联的一个或多个内容字符串来加载dataTransfer
。
为了说明,以下是示例代码中的一个快速片段
var dt = ev.originalEvent.dataTransfer;
dt.setData('text/plain', $('#logo').parent().text());
dt.setData('text/html', $('#logo').parent().html());
dt.setData('text/uri-list', $('#logo')[0].src);
另一方面,getData()
允许你按类型查询内容(例如,text/html
,然后是text/plain
)。这反过来让你能够在drop
事件甚至dragover
期间决定可接受的内容类型,以便在拖动期间对不可接受的类型提供反馈。
以下是来自示例代码接收端的另一个示例
var dt = ev.originalEvent.dataTransfer;
$('.content_url .content').text(dt.getData('text/uri-list'));
$('.content_text .content').text(dt.getData('text/plain'));
$('.content_html .content').html(dt.getData('text/html'));
然而,dataTransfer
真正闪耀的地方在于它允许你的放置目标接收来自你定义的可拖动元素之外,甚至来自浏览器本身之外的源的内容。Firefox 接受此类拖动,并尝试使用从外部对象提取的适当 content type 来填充dataTransfer
。
因此,你可以在文字处理器窗口中选择一些文本,并将其拖放到你的其中一个元素中,并且至少期望找到它作为text/plain
内容可用。
你也可以在另一个浏览器窗口中选择内容,并期望看到text/html
出现在你的事件中。查看大纲编辑演示,看看当你尝试将其他窗口中的各种元素(例如,图像、表格和列表)和突出显示的内容拖放到其中的项目时会发生什么。
使用拖动反馈图像
拖放交互的一个重要方面是对被拖动对象的表示。在 Firefox 中,默认情况下,这是一个被拖动元素本身的“幻影”图像。但是,事件对象的原始dataTransfer
属性公开了setDragImage()
方法,用于自定义此表示形式。
这里有一个实时演示,展示了此功能,以及相关的 JS 代码。但是,其要点在这些代码片段中概述
var dt = ev.originalEvent.dataTransfer;
dt.setDragImage( $('#feedback_image h2')[0], 0, 0);
dt.setDragImage( $('#logo')[0], 32, 32);
var canvas = document.createElement("canvas");
canvas.width = canvas.height = 50;
var ctx = canvas.getContext("2d");
ctx.lineWidth = 8;
ctx.moveTo(25,0);
ctx.lineTo(50, 50);
ctx.lineTo(0, 50);
ctx.lineTo(25, 0);
ctx.stroke();
dt.setDragImage(canvas, 25, 25);
你可以提供一个 DOM 节点作为setDragImage()
的第一个参数,其中包括从文本到图像到<canvas>
元素的所有内容。后两个参数指示在拖动时鼠标应该出现在图像中的哪个左偏移量和上偏移量。
例如,由于#logo
图像为 64×64,第二个setDragImage()
方法中的参数将鼠标放置在图像的中心。另一方面,第一个调用将反馈图像定位,使得鼠标位于左上角。
使用放置效果
如本文开头所述,拖放交互已用于支持复制、移动和链接等操作。因此,HTML 5 规范在事件对象公开的effectAllowed
和dropEffect
属性的形式下适应了这些操作。
为了快速解决,查看实时演示,展示了此功能,以及相关的 JS 代码.
基本思想是,dragstart
事件监听器可以像这样为effectAllowed
设置一个值
var dt = ev.originalEvent.dataTransfer;
switch (ev.target.id) {
case 'effectdrag0': dt.effectAllowed = 'copy'; break;
case 'effectdrag1': dt.effectAllowed = 'move'; break;
case 'effectdrag2': dt.effectAllowed = 'link'; break;
case 'effectdrag3': dt.effectAllowed = 'all'; break;
case 'effectdrag4': dt.effectAllowed = 'none'; break;
}
此属性可用的选项包括以下内容
none
- 不允许进行任何操作
copy
- 仅复制
move
- 仅移动
link
- 仅链接
copyMove
- 仅复制或移动
copyLink
- 仅复制或链接
linkMove
- 仅链接或移动
all
- 复制、移动或链接
另一方面,dragover
事件监听器可以设置 dropEffect
属性的值,以指示在成功放下时预期的效果。如果该值与 effectAllowed
不匹配,则放下将在完成时被视为取消。
在 实时演示 中,您应该能够看到只有具有匹配效果的元素才能被放到相应的放置区域中。这是通过以下代码完成的
var dt = ev.originalEvent.dataTransfer;
switch (ev.target.id) {
case 'effectdrop0': dt.dropEffect = 'copy'; break;
case 'effectdrop1': dt.dropEffect = 'move'; break;
case 'effectdrop2': dt.dropEffect = 'link'; break;
case 'effectdrop3': dt.dropEffect = 'all'; break;
case 'effectdrop4': dt.dropEffect = 'none'; break;
}
尽管操作系统本身可以提供一些反馈,但您也可以使用这些属性来更新您自己的可见反馈,无论是在拖动的元素上还是在放置区域本身。
结论
HTML5 和 Firefox 中新的第一类拖放事件使在浏览器中支持这种形式的 UI 交互变得简单、简洁和强大。但除了这些事件的新简便性之外,能够在应用程序之间传输内容为基于 Web 的应用程序开辟了全新的途径,并与一般的桌面软件协作。
30 条评论