Creación de un juego de estacionamiento con la API HTML de arrastrar y soltar | Programar Plus

Entre las muchas API de JavaScript que se agregaron en HTML5 se encontraba Arrastrar y soltar (en este artículo nos referiremos a ella como DnD), que trajo compatibilidad nativa con DnD al navegador, lo que facilitó a los desarrolladores la implementación de esta función interactiva en las aplicaciones. Lo increíble que sucede cuando las características se vuelven más fáciles de implementar es que la gente comienza a hacer todo tipo de cosas tontas y poco prácticas con ellas, como la que estamos haciendo hoy: ¡un juego de estacionamiento!

DnD requiere solo unas pocas cosas para funcionar:

  • Algo para arrastrar
  • En algún lugar para caer
  • Controladores de eventos de JavaScript en el destino para decirle al navegador que puede descartar

Vamos a empezar creando nuestros arrastrables.

Arrastrando

Ambos <img> y <a>(con el href conjunto de atributos) los elementos se pueden arrastrar de forma predeterminada. Si desea arrastrar un elemento diferente, deberá establecer el atributo arrastrable en true.

Comenzaremos con el HTML que configura las imágenes de nuestros cuatro vehículos: camión de bomberos, ambulancia, automóvil y bicicleta.

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

Dado que las imágenes se pueden arrastrar de forma predeterminada, verá que arrastrar cualquiera de ellas crea una imagen fantasma.

Simplemente agregar un atributo arrastrable a un elemento que no es una imagen o un enlace es realmente todo lo que necesita para hacer que un elemento se pueda arrastrar en la mayoría de los navegadores. Para hacer que los elementos se puedan arrastrar en todos los navegadores, debe definir algunos controladores de eventos. También son útiles para agregar funciones adicionales, como un borde si se arrastra un elemento o un sonido si deja de arrastrarse. Para estos, necesitará algunos controladores de eventos de arrastre, así que veámoslos.

Eventos de arrastre

Hay tres eventos relacionados con el arrastre que puede escuchar, pero solo usaremos dos: dragstart y dragend.

  • dragstart – Se activa en cuanto empezamos a arrastrar. Aquí es donde podemos definir los datos de arrastre y el efecto de arrastre.
  • dragend – Se activa cuando se suelta un elemento que se puede arrastrar. Este evento generalmente se dispara justo después del evento de soltar de la zona de colocación.

Cubriremos cuáles son los datos de arrastre y el efecto de arrastre en breve.

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);

Hay un par de cosas que suceden en este código:

  • Estamos definiendo los datos de arrastre. Cada evento de arrastre tiene una propiedad llamada dataTransfer que almacena los datos del evento. Puedes usar el setData(type, data) para agregar un elemento arrastrado a los datos de arrastre. Estamos almacenando la identificación de la imagen arrastrada como tipo 'text' en la línea 7.
  • Estamos almacenando el elemento que se está arrastrando en una variable global. Sé que sé. Global es peligroso para el alcance, pero he aquí por qué lo hacemos: aunque puede almacenar el elemento arrastrado usando setData, no puedes recuperarlo usando event.dataTransfer.getData() en todos los navegadores (excepto Firefox) porque los datos de arrastre están en modo protegido. Puedes leer más sobre esto aquí. Quería mencionar la definición de los datos de arrastre solo para que lo sepas.
  • estamos configurando el dropEffect a move. El dropEffect La propiedad se utiliza para controlar los comentarios que se le dan al usuario durante una operación de arrastrar y soltar. Por ejemplo, cambia qué cursor muestra el navegador mientras se arrastra. Hay tres efectos: copiar, mover y vincular.
    • copy – Indica que los datos que se arrastran se copiarán desde su fuente a la ubicación de colocación.
    • move – Indica que los datos que se están arrastrando se moverán.
    • link – Indica que se creará algún tipo de relación entre las ubicaciones de origen y destino.

Ahora tenemos vehículos que se pueden arrastrar, pero no hay dónde soltarlos:

Ver el Pen 1 – ¿Puedes estacionar aquí? por Omayeli Arenyeka (@yelly) en CodePen.

Goteante

De forma predeterminada, cuando arrastra un elemento, solo forma elementos como <input> será capaz de aceptarlo como una gota. Vamos a contener nuestra “zona de salto” en un <section> elemento, por lo que debemos agregar controladores de eventos de caída para que pueda aceptar caídas como un elemento de formulario.

Primero, dado que es un elemento vacío, necesitaremos establecer un ancho, alto y color de fondo para que podamos verlo en la pantalla.

Estos son los parámetros que tenemos disponibles para eventos drop:

  • dragenter – Se activa en el momento en que un elemento arrastrable ingresa a un área desplegable. Al menos el 50 % del elemento arrastrable debe estar dentro de la zona de colocación.
  • dragover – Lo mismo que dragenter pero se llama repetidamente mientras el elemento arrastrable está dentro de la zona de colocación.
  • dragleave – Se activa una vez que un elemento que se puede arrastrar se ha alejado de una zona de colocación.
  • drop – Se activa cuando el elemento que se puede arrastrar se ha soltado y el área de colocación accede a aceptar la colocación.
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);

Si te preguntas por qué seguimos llamando event.preventDefault() es porque, de manera predeterminada, el navegador asume que cualquier destino no es un destino de colocación válido. Esto no es cierto todo el tiempo para todos los navegadores, ¡pero es mejor prevenir que lamentar! Vocación preventDefault() sobre el dragenter, dragover y colocar eventos, informa al navegador que el destino actual es un destino de colocación válido.

¡Ahora tenemos una sencilla aplicación de arrastrar y soltar!

Ver el Pen 2 – ¿Puedes aparcar aquí? por Omayeli Arenyeka (@yelly) en CodePen.

Es divertido, pero no tan frustrante como estacionar. Tenemos que crear algunas reglas para que eso suceda.

Reglas y Validación

Se me ocurrieron algunas reglas de estacionamiento al azar, y te animo a que crees algunas propias. Los letreros de estacionamiento generalmente tienen días y horarios en los que puede estacionar, así como qué tipos de vehículos pueden estacionar en ese momento. Cuando estábamos creando nuestros objetos arrastrables, teníamos cuatro vehículos: una ambulancia, un camión de bomberos, un auto normal y una bicicleta. Entonces, vamos a crear reglas para ellos.

  1. Estacionamiento de ambulancias únicamente: de lunes a viernes, de 9 p. m. a 3 a. m.
  2. Solo estacionamiento para camiones de bomberos: Todo el día durante el fin de semana.
  3. Estacionamiento de vehículos regulares: de lunes a viernes, de 3:00 a 15:00 horas.
  4. Aparcamiento de bicicletas: de lunes a viernes de 15 a 21 h.

Ahora, traducimos estas reglas a código. Vamos a usar dos bibliotecas para manejar el tiempo y los rangos: Moment y Moment-range.

Los scripts ya están disponibles en Codepen para agregarlos a cualquier demostración nueva, pero si está desarrollando fuera de Codepen, puede copiarlos o vincularlos desde aquí:

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

Luego, creamos un objeto para almacenar todas las reglas de estacionamiento.

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);
  }
}

Cada vehículo en el parkingRules objeto tiene un days propiedad con una variedad de días que puede estacionar y un times propiedad que es un intervalo de tiempo. Para obtener la hora actual usando Moment, llame moment(). Para crear un rango usando Moment-range, pase un tiempo de inicio y finalización al moment.range función.

Ahora, en el onDragEnter y onDrop controladores de eventos que definimos anteriormente, agregamos algunas comprobaciones para asegurarnos de que un vehículo pueda estacionar. Nuestra alt atributo en el img la etiqueta está almacenando el tipo de vehículo, así que lo pasamos a un canPark método que devolverá si el coche se puede aparcar. También agregamos señales visuales (cambio en el fondo) para decirle al usuario si un vehículo se puede estacionar o no.

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);
    }
  }
}

Luego, creamos el canPark método.

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;
}

Ahora, solo los autos que pueden estacionar pueden estacionar. Por último, agregamos las reglas a la pantalla y le damos estilo.

Aquí está el resultado final:

Ver el Pen 3 – ¿Puedes aparcar aquí? por Omayeli Arenyeka (@yelly) en CodePen.

Hay muchas maneras en que esto podría mejorarse:

  • Genere automáticamente el HTML para la lista de reglas desde el parkingRules ¡objeto!
  • ¡Agrega algunos efectos de sonido!
  • Agregue la capacidad de arrastrar vehículos hacia atrás al punto original sin actualizar la página.
  • Todas esas molestas variables globales.

Pero te dejaré manejar eso.

Si está interesado en obtener más información sobre la API de DnD y algunas críticas, aquí tiene una buena lectura:

  • Especificación WHATWG
  • Trabajar con HTML5 Arrastrar y soltar: Programación Pro HTML5, Capítulo 9, por Jen Simmons
  • Arrastrar y soltar accesible mediante WAI-ARIA: consideraciones de accesibilidad de Dev.Opera
  • HTML5 nativo Arrastrar y soltar: tutorial HTML5 Rocks
  • El desastre de arrastrar y soltar de HTML5: publicación de QuirksMode con contexto útil sobre la implementación del módulo DnD
(Visited 6 times, 1 visits today)