拖拽

HTML 拖放(Drag and Drop)接口使应用程序能够在浏览器中使用拖放功能。例如,用户可使用鼠标选择可拖拽(draggable)元素,将元素拖拽到可放置(droppable)元素,并释放鼠标按钮以放置这些元素。拖拽操作期间,会有一个可拖拽元素的半透明快照跟随着鼠标指针。

拖拽事件

HTML 的 drag & drop 使用了 DOM event model 以及从 mouse events 继承而来的 drag events。一个典型的拖拽操作是这样的:用户选中一个可拖拽的(draggable)元素,并将其拖拽(鼠标不放开)到一个可放置的(droppable)元素,然后释放鼠标。

在操作期间,会触发一些事件类型,有一些事件类型可能会被多次触发(比如drag 和 dragover 事件类型)。

下面的表格提供了一个简短的事件类型描述,以及一个相关文档的链接。

事件 事件处理程序 触发时刻
drag ondrag 当拖拽元素或选中的文本时触发
dragend ondragend 当拖拽操作结束时触发 (比如松开鼠标按键或敲“Esc”键)
dragenter ondragenter 当拖拽元素或选中的文本到一个可释放目标时触发
dragleave ondragleave 当拖拽元素或选中的文本离开一个可释放目标时触发
dragover ondragover 当元素或选中的文本被拖到一个可释放目标上时触发(每 100 毫秒触发一次)
dragstart ondragstart 当用户开始拖拽一个元素或选中的文本时触发
drop ondrop 当元素或选中的文本在可释放目标上被释放时触发

注意:当从操作系统向浏览器中拖拽文件时,不会触发 dragstart 和dragend 事件。

基础

确定什么是可拖拽的

让一个元素被拖拽需要添加 draggable 属性,再加上事件处理函数 ondragstart;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
function dragstart_handler(ev) {
// Add the target element's id to the data transfer object
ev.dataTransfer.setData("text/plain", ev.target.id);
ev.dataTransfer.setData(
"text/uri-list",
ev.target.ownerDocument.location.href,
}

window.addEventListener("DOMContentLoaded", () => {
// Get the element by id
const element = document.getElementById("p1");
// Add the ondragstart event listener
element.addEventListener("dragstart", dragstart_handler);
});
</script>

<p id="p1" draggable="true">This element is draggable.</p>

定义拖拽数据

每个 dragevent 都有一个dataTransfer 属性,其中保存着事件的数据。这个属性也有管理拖拽数据的方法。setData() 方法为拖拽数据添加一个项,如下面的示例代码所示:

1
2
3
4
5
6
// 添加拖拽数据
ev.dataTransfer.setData("text/plain", ev.target.innerText);
ev.dataTransfer.setData("text/html", ev.target.outerHTML);
ev.dataTransfer.setData(
"text/uri-list",
ev.target.ownerDocument.location.href,

如果你试图以相同的格式添加两次数据,那么新的数据将替换旧的数据。你可以使用 clearData() 方法清除这些数据,该方法接收一个参数,即要删除的数据类型。

1
event.dataTransfer.clearData("text/uri-list");

clearData()方法的 type 参数是可选的。如果没有声明type,则所有类型的数据都会被删除。如果拖拽不包含拖拽数据项,或者所有的数据项都被清除,那么就不会出现拖拽行为。

定义拖拽图像

拖拽过程中,浏览器会在鼠标旁显示一张默认图片。当然,应用程序也可以通过 setDragImage() 方法自定义一张图片

1
2
3
var img = new Image();
img.src = "example.gif";
ev.dataTransfer.setDragImage(img, 10, 10);

定义拖拽效果

dropEffect 属性用来控制拖放操作中用户给予的反馈。它会影响到拖拽过程中浏览器显示的鼠标样式。比如,当用户悬停在目标元素上的时候,浏览器鼠标也许要反映拖放操作的类型。

有以下效果可以定义:

  1. copy 表明被拖拽的数据将从它原本的位置拷贝到目标的位置。
  2. move 表明被拖拽的数据将被移动。
  3. link 表明在拖拽源位置和目标位置之间将会创建一些关系表格或是连接。
  4. none 不允许操作

属性默认允许以上所有的操作(all),所以你不需要调整这个属性,除非你想要排除某个特定操作。在拖拽过程中,拖拽效果也许会被修改以用于表明在具体位置上具体效果是否被允许,如果允许,在该位置则被允许放置。

可以使用 none 表示在这个位置不允许任何放置

ev.dataTransfer.dropEffect = "copy";

指定放置目标

当拖拽一个项目到 HTML 元素中时,浏览器默认不会有任何响应。想要让一个元素变成可释放区域,该元素必须设置 ondragover和ondrop事件处理程序属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
function dragover_handler(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = "move";
}
function drop_handler(ev) {
ev.preventDefault();
// Get the id of the target and add the moved element to the target's DOM
var data = ev.dataTransfer.getData("text/plain");
ev.target.appendChild(document.getElementById(data));
}
</script>

<p
id="target"
ondrop="drop_handler(event)"
ondragover="dragover_handler(event)">
Drop Zone
</p>

如果你想要允许放置,你必须取消 dragenter 和 dragover 事件来阻止默认的处理。你可以在属性定义的事件监听程序返回 false,或者调用事件的 preventDefault() 方法来实现这一点。

在 dragenter 和 dragover 事件中调用 preventDefault() 方法将表明在该位置允许放置。但是,你通常希望只在某些情况下调用 preventDefault() 方法(如只当拖拽的是链接时)。

要做到这一点,调用一个函数以检查条件,并且只在满足条件时取消事件。如果条件未满足,则不取消事件,此时用户释放鼠标按钮不会执行放置。

最常见的是根据数据传输中拖拽数据的类型来接受或拒绝放置——例如,允许放置图像或链接,或者都允许。你可以检查 dataTransfer 属性的 types 属性来查看哪些类型允许放置。types 属性返回一个字符串类型的数组,这些字符串类型是在拖拽开始时添加的,顺序是从最重要到最不重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
function contains(list, value) {
for (var i = 0; i < list.length; ++i) {
if (list[i] === value) return true;
}
return false;
}

function doDragOver(event) {
var isLink = contains(event.dataTransfer.types, "text/uri-list");
if (isLink) {
event.preventDefault();
}
}

如果你希望更具体地限制操作类型,你可能还需要设置 effectAllowed 或 dropEffect 属性,或者两者都设置。当然,如果你不取消这个事件,改变这两个属性不会有任何效果。

放置反馈

你还可以根据需要更新用户界面,如添加一个插入标记或使用高亮显示。对于简单的高亮显示,你可以在放置目标上使用 -moz-drag-over CSS 伪类。

1
2
3
.droparea:-moz-drag-over {
border: 1px solid black;
}

在这个例子中,当带有 droparea 类的元素是一个有效的放置目标时,即在该元素的 dragenter 事件中调用 preventDefault() 方法时,元素会出现一个 1 像素的黑色轮廓。

要使这个伪类生效,你必须在 dragenter 事件中调用 preventDefault() 方法,因为这个伪类状态不会检查 dragover 事件(译者注:即在 dragover 事件中调用 preventDefault() 方法也不会使伪类生效,尽管这个伪类叫做“-moz-drag-over”)。

你可以在 dragenter 事件中执行其他操作。例如在放置位置插入一个元素,这样的元素可以表示一个插入标记,或表示被拖拽的元素移动到了新位置。为此你可以在 dragenter 事件中创建一个新元素,然后将其插入到文档中。

dragover 事件在鼠标指向的元素上触发。自然,你可能需要将插入标记移动到事件发生的位置附近。你可以使用事件的 clientX 和 clientY 属性,还有其他鼠标事件的属性来确定鼠标的位置。

最后,dragleave 事件会在拖拽离开元素时在该元素上触发。这是移除插入标记或高亮的好时机。你不需要取消这个事件(译者注:即不需要使用 preventDefault())。使用 -moz-drag-over 伪类设置的高亮或其他视觉效果会被自动移除。即使拖拽被取消了,dragleave 事件也会照常触发,所以你可以确保在这个事件中对任何插入标记的清除操作都一定可以完成。

执行放置

如果在有效的放置目标元素上放开鼠标,放置会成功实现,drop 事件在目标元素上被触发。否则,拖拽会被取消,不会触发 drop 事件。

在所有拖拽操作相关的事件中,事件的 dataTransfer 属性会一直保存着拖拽数据。可使用 getData() 方法来取回数据。

1
2
3
4
5
function onDrop(event) {
const data = event.dataTransfer.getData("text/plain");
event.target.textContent = data;
event.preventDefault();
}

完成拖拽

拖拽操作结束时,在源元素(开始拖拽时的目标元素)上触发 dragend 事件。不管拖拽是完成还是被取消这个事件都会被触发。dragend 事件处理程序可以检查dropEffect 属性的值来确认拖拽成功与否。

如果在 dragend 事件中,dropEffect 属性值为 none,则拖拽会被取消。