在 HTML5 中添加的众多 JavaScript API 中,拖放(我们在本文中将其称为 DnD)为浏览器带来了原生 DnD 支持,使开发人员更容易将此交互式功能实现到应用程序中。功能实现变得更容易时,会发生一件了不起的事情,那就是人们开始用它制作各种愚蠢、不切实际的东西,就像我们今天要做的:一个停车游戏!
DnD 只需要几件事就能正常工作
- 要拖动的东西
- 要放置的地方
- 目标上的 JavaScript 事件处理程序,用来告诉浏览器它可以放置
我们将从创建可拖动对象开始。
拖动
<img>
和 <a>
(设置了 href
属性)元素默认情况下都是可拖动的。如果您想拖动不同的元素,则需要将可拖动属性设置为 true
。
我们将从设置四辆车图像的 HTML 开始:消防车、救护车、汽车和自行车。
<ul class="vehicles">
<li>
<!-- Fire Truck -->
<!-- <code>img<code> elements don't need a <code>draggable<code> attribute like other elements -->
<img id="fire-truck" alt="fire truck" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Ftruck-clip-art-fire-truck4.png?1519011787956"/>
</li>
<li>
<!-- Ambulance -->
<img id="ambulance" alt="ambulance" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Fambulance5.png?1519011787610">
</li>
<li>
<!-- Car -->
<img id="car" alt="car" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Fcar-20clip-20art-1311497037_Vector_Clipart.png?1519011788408">
</li>
<li>
<!-- Bike -->
<img id="bike" alt="bicycle" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Fbicycle-20clip-20art-bicycle3.png?1519011787816">
</li>
</ul>
由于图像默认情况下是可拖动的,您会看到拖动任何一个图像都会创建一个幻影图像。

只需向不是图像或链接的元素添加可拖动属性,您实际上就可以在大多数浏览器中使元素可拖动。要在所有浏览器中使元素可拖动,您需要定义一些事件处理程序。它们还有助于添加额外功能,例如,如果元素正在拖动,则添加边框,或者如果元素停止拖动,则添加声音。对于这些,您将需要一些拖动事件处理程序,因此让我们看看它们。
拖动事件
您可以监听三个与拖动相关的事件,但我们只使用两个:dragstart
和 dragend
。
dragstart
– 一旦我们开始拖动,就会触发。在这里,我们可以定义拖放数据和拖放效果。dragend
– 可拖动元素被放下时触发。此事件通常在放置区域的放置事件之后立即触发。
我们将很快介绍拖放数据和拖放效果是什么。
let dragged; // Keeps track of what's being dragged - we'll use this later!
function onDragStart(event) {
let target = event.target;
if (target && target.nodeName === 'IMG') { // If target is an image
dragged = target;
event.dataTransfer.setData('text', target.id);
event.dataTransfer.dropEffect = 'move';
// Make it half transparent when it's being dragged
event.target.style.opacity = .3;
}
}
function onDragEnd(event) {
if (event.target && event.target.nodeName === 'IMG') {
// Reset the transparency
event.target.style.opacity = ''; // Reset opacity when dragging ends
dragged = null;
}
}
// Adding event listeners
const vehicles = document.querySelector('.vehicles');
vehicles.addEventListener('dragstart', onDragStart);
vehicles.addEventListener('dragend', onDragEnd);
这段代码中发生了两件事
- 我们正在定义拖放数据。 每个拖放事件都有一个名为
dataTransfer
的属性,用于存储事件的数据。您可以使用setData(type, data)
方法将拖动项目添加到拖放数据中。我们在第 7 行将拖动图像的 ID 存储为类型'text'
。 - 我们正在全局变量中存储正在拖动的元素。 我知道,我知道。全局变量在作用域方面很危险,但这里是我们使用它的原因:尽管您可以使用
setData
存储拖动项目,但您不能在所有浏览器(除了 Firefox)中使用event.dataTransfer.getData()
检索它,因为拖放数据处于受保护模式。您可以在此处阅读有关它的更多信息。我想提一下定义拖放数据,这样您就可以了解它。 - 我们正在将
dropEffect
设置为move
。dropEffect
属性用于控制在拖放操作期间给用户提供的反馈。例如,它会更改浏览器在拖动时显示的鼠标光标。有三种效果:copy、move 和 link。copy
– 表示正在拖动的将从源位置复制到放置位置的数据。move
– 表示正在拖动的将被移动的数据。link
– 表示将创建源位置和放置位置之间的某种关系。
现在我们有了可拖动的车辆,但没有地方放置它们
查看 CodePen 上 Omayeli Arenyeka (@yelly) 的 1 – 你能在这里停车吗?。
放置
默认情况下,当您拖动元素时,只有表单元素(例如 <input>
)才能接受它作为放置目标。我们将把我们的“放置区域”包含在 <section>
元素中,因此我们需要添加放置事件处理程序,以便它像表单元素一样可以接受放置。
首先,由于它是一个空元素,我们将需要为它设置宽度、高度和背景颜色,以便我们可以在屏幕上看到它。

这些是我们可用于放置事件的参数
dragenter
– 可拖动项目进入可放置区域时触发。可拖动元素至少有 50% 必须位于放置区域内。dragover
– 与dragenter
相同,但它在可拖动项目位于放置区域内时会反复调用。dragleave
– 可拖动项目从放置区域移开时触发。drop
– 可拖动项目被释放且放置区域同意接受放置时触发。
function onDragOver(event) {
// Prevent default to allow drop
event.preventDefault();
}
function onDragLeave(event) {
event.target.style.background = '';
}
function onDragEnter(event) {
const target = event.target;
if (target) {
event.preventDefault();
// Set the dropEffect to move
event.dataTransfer.dropEffect = 'move'
target.style.background = '#1f904e';
}
}
function onDrop(event) {
const target = event.target;
if ( target) {
target.style.backgroundColor = '';
event.preventDefault();
// Get the id of the target and add the moved element to the target's DOM
dragged.parentNode.removeChild(dragged);
dragged.style.opacity = '';
target.appendChild(dragged);
}
}
const dropZone = document.querySelector('.drop-zone');
dropZone.addEventListener('drop', onDrop);
dropZone.addEventListener('dragenter', onDragEnter);
dropZone.addEventListener('dragleave', onDragLeave);
dropZone.addEventListener('dragover', onDragOver);
如果您想知道为什么我们一直调用 event.preventDefault()
,这是因为默认情况下,浏览器假设任何目标都不是有效的放置目标。这并非一直适用于所有浏览器,但小心驶得万年船!在 dragenter
、dragover
和放置事件上调用 preventDefault()
会通知浏览器当前目标是有效的放置目标。
现在,我们有一个简单的拖放应用程序!
查看 CodePen 上 Omayeli Arenyeka (@yelly) 的 2 – 你能在这里停车吗?。
很有趣,但并不像停车那样令人沮丧。我们必须创建一些规则才能实现这一点。
规则和验证
我想出了一些随机的停车规则,我鼓励您创建一些自己的规则。停车标志通常会标明您允许停车的时间和日期,以及允许在那个特定时间停放的车辆类型。在创建可拖动对象时,我们有四辆车:救护车、消防车、普通汽车和自行车。因此,我们将为它们创建规则。
- 仅允许救护车停车:周一至周五,晚上 9 点至凌晨 3 点。
- 仅允许消防车停车:周末全天。
- 普通汽车停车:周一至周五,凌晨 3 点至下午 3 点。
- 自行车停车:周一至周五,下午 3 点至晚上 9 点。
现在,我们将这些规则转换为代码。我们将使用两个库来处理时间和范围:Moment 和 Moment-range。
这些脚本已在 CodePen 中提供,可以添加到任何新的演示中,但如果您在 CodePen 之外进行开发,可以从此处复制或链接它们
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/moment-range/3.1.1/moment-range.js"></script>
然后,我们创建一个对象来存储所有停车规则。
window['moment-range'].extendMoment(moment);
// The array of weekdays
const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
const parkingRules = {
ambulance: {
// The ambulance can only park on weekdays...
days: weekdays,
// ...from 9pm to 3am (the next day)
times: createRange(moment().set('hour', 21), moment().add(1, 'day').set('hour', 3))
},
'fire truck': {
// The fire truck can obnly park on Saturdays and Sundays, but all day
days: ['Saturday', 'Sunday']
},
car: {
// The car can only park on weekdays...
days: weekdays,
// ...from 3am - 3pm (the same day)
times: createRange(moment().set('hour', 3), moment().set('hour', 15))
},
bicycle: {
// The car can only park on weekdays...
days: weekdays,
// ...from 3pm - 9pm (the same day)
times: createRange(moment().set('hour', 15), moment().set('hour', 21))
}
};
function createRange(start, end) {
if (start && end) {
return moment.range(start, end);
}
}
parkingRules
对象中的每辆车都有一个 days
属性,其中包含一个数组,表示它可以停放的日期,还有一个 times
属性,它是一个时间范围。要使用 Moment 获取当前时间,请调用 moment()
。要使用 Moment-range 创建范围,请将开始时间和结束时间传递给 moment.range
函数。
现在,在我们之前定义的 onDragEnter
和 onDrop
事件处理程序中,我们添加了一些检查,以确保车辆可以停放。我们 img
标签上的 alt
属性存储了车辆类型,因此我们将它传递给 canPark
方法,该方法将返回汽车是否可以停放。我们还添加了视觉提示(背景变化),以告诉用户车辆是否可以停放。
function onDragEnter(event) {
const target = event.target;
if (dragged && target) {
const vehicleType = dragged.alt; // e.g bicycle, ambulance
if (canPark(vehicleType)) {
event.preventDefault();
// Set the dropEffect to move
event.dataTransfer.dropEffect = 'move';
/* Change color to green to show it can be dropped /*
target.style.background = '#1f904e';
}
else {
/* Change color to red to show it can't be dropped. Notice we
* don't call event.preventDefault() here so the browser won't
* allow a drop by default
*/
target.style.backgroundColor = '#d51c00';
}
}
}
function onDrop(event) {
const target = event.target;
if (target) {
const data = event.dataTransfer.getData('text');
const dragged = document.getElementById(data);
const vehicleType = dragged.alt;
target.style.backgroundColor = '';
if (canPark(vehicleType)) {
event.preventDefault();
// Get the ID of the target and add the moved element to the target's DOM
dragged.style.opacity = '';
target.appendChild(dragged);
}
}
}
然后,我们创建 canPark
方法。
function getDay() {
return moment().format('dddd'); // format as 'monday' not 1
}
function getHours() {
return moment().hour();
}
function canPark(vehicle) {
/* Check the time and the type of vehicle being dragged
* to see if it can park at this time
*/
if (vehicle && parkingRules[vehicle]) {
const rules = parkingRules[vehicle];
const validDays = rules.days;
const validTimes = rules.times;
const curDay = getDay();
if (validDays) {
/* If the current day is included on the parking days for the vehicle
* And if the current time is within the range
*/
return validDays.includes(curDay) && (validTimes ? validTimes.contains(moment()) : true);
/* Moment.range has a contains function that checks
* to see if your range contains a moment.
https://github.com/rotaready/moment-range#contains
*/
}
}
return false;
}
现在,只有允许停放的汽车才能停放。最后,我们将规则添加到屏幕上并设置样式。
这是最终结果
查看 CodePen 上 Omayeli Arenyeka (@yelly) 的 3 – 你能在这里停车吗?。
有很多方法可以改进它
- 从
parkingRules
对象自动生成规则列表的 HTML! - 添加一些音效!
- 添加将车辆拖回原始点而不刷新页面的功能。
- 所有那些讨厌的全局变量。
但我让你来处理这些。
如果您有兴趣了解更多关于 DnD API 以及它的一些评价,这里有一些不错的读物
- WHATWG 规范
- 使用 HTML5 拖放 – 专业 HTML5 编程,第 9 章,作者:Jen Simmons
- 使用 WAI-ARIA 实现无障碍拖放 – 来自 Dev.Opera 的无障碍考虑因素
- 原生 HTML5 拖放 – HTML5 Rocks 教程
- HTML5 拖放灾难 – QuirksMode 帖子,提供有关 DnD 模块实现的有用背景信息
这是一个很棒的教程,谢谢您。
谢谢!
您好,Yeli 和其他人:
确实,偶尔读一些你以前不知道的东西总是好的。而且,正如阅读而不实践就像游泳没有水一样,我测试了一些可能性。直截了当的(只将一辆车停放在停车场):没问题。
当我试图将更多车辆停放在停放区域时,情况变得更加复杂。
在 PC 上的 Win7 系统中,我在 CodePen #2 中遇到了一些浏览器差异和(对我来说)奇怪的现象。放置的图像的大小和比例(成比例/不成比例)会有所不同。 - 在已经放置的车辆上方放置会导致其中一个车辆消失。 - 消失的车辆(根据开发人员工具:也从 html-DOM 中删除)可以通过将现有的车辆再次拖放到停放区域的另一个位置来恢复 (!) 。这种奇技淫巧应该存在于脚本中;由于我不是 JavaScript 专家,所以我不知道它是如何做到的。
为了改进示例,我创建了一些扩展的 Pen,允许重复放置,消除浏览器差异,并且汽车和自行车不再消失。当然,还有许多其他 CSS/JS 可能性可以获得类似的结果 - 游戏场是开放的。 :-)
完整的探索之旅(包含一些屏幕录制说明和指向分叉的新 Pen 的链接)可以在这里阅读:clba.nl/testing/parking
致敬:
Francky
您好,Francky,我目前只能浏览一下,但非常感谢您的分享!我已经学到了很多东西。尝试找出不同浏览器和设备之间的差异非常有趣。我很快就会浏览整个内容 :)