使用 HTML 拖放 API 创建停车游戏

Avatar of Omayeli Arenyeka
Omayeli Arenyeka

DigitalOcean 为您的旅程各个阶段提供云产品。立即开始使用 200 美元的免费积分!

在 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>

由于图像默认情况下是可拖动的,您会看到拖动任何一个图像都会创建一个幻影图像。

只需向不是图像或链接的元素添加可拖动属性,您实际上就可以在大多数浏览器中使元素可拖动。要在所有浏览器中使元素可拖动,您需要定义一些事件处理程序。它们还有助于添加额外功能,例如,如果元素正在拖动,则添加边框,或者如果元素停止拖动,则添加声音。对于这些,您将需要一些拖动事件处理程序,因此让我们看看它们。

拖动事件

您可以监听三个与拖动相关的事件,但我们只使用两个:dragstartdragend

  • 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 设置为 movedropEffect 属性用于控制在拖放操作期间给用户提供的反馈。例如,它会更改浏览器在拖动时显示的鼠标光标。有三种效果: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(),这是因为默认情况下,浏览器假设任何目标都不是有效的放置目标。这并非一直适用于所有浏览器,但小心驶得万年船!在 dragenterdragover 和放置事件上调用 preventDefault() 会通知浏览器当前目标是有效的放置目标。

现在,我们有一个简单的拖放应用程序!

查看 CodePen 上 Omayeli Arenyeka (@yelly) 的 2 – 你能在这里停车吗?

很有趣,但并不像停车那样令人沮丧。我们必须创建一些规则才能实现这一点。

规则和验证

我想出了一些随机的停车规则,我鼓励您创建一些自己的规则。停车标志通常会标明您允许停车的时间和日期,以及允许在那个特定时间停放的车辆类型。在创建可拖动对象时,我们有四辆车:救护车、消防车、普通汽车和自行车。因此,我们将为它们创建规则。

  1. 仅允许救护车停车:周一至周五,晚上 9 点至凌晨 3 点。
  2. 仅允许消防车停车:周末全天。
  3. 普通汽车停车:周一至周五,凌晨 3 点至下午 3 点。
  4. 自行车停车:周一至周五,下午 3 点至晚上 9 点。

现在,我们将这些规则转换为代码。我们将使用两个库来处理时间和范围:MomentMoment-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 函数。

现在,在我们之前定义的 onDragEnteronDrop 事件处理程序中,我们添加了一些检查,以确保车辆可以停放。我们 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 以及它的一些评价,这里有一些不错的读物