
Crear un juego de estacionamiento con la API HTML de arrastrar y soltar
Entre las muchas API de JavaScript agregadas en HTML5 se encuentra Arrastrar y soltar (lo llamaremos DnD en este artículo), que brinda soporte nativo de DnD al navegador, lo que facilitó a los desarrolladores implementar esta característica interactiva en las aplicaciones. Lo sorprendente que sucede cuando las funciones 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!
Para que DnD funcione sólo se necesitan unas pocas cosas:
- algo para arrastrar
- Algún lugar para dejar
- Eventos de JavaScript con el objetivo de indicarle al navegador que puede descartar
Comenzaremos creando nuestros elementos arrastrables.
Arrastrando
Ambos elementos img
y a
(con el href
conjunto de atributos) 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, coche y bicicleta.
ul li !-- Fire Truck -- !-- codeimgcode elements don't need a codedraggablecode attribute like other elements -- img alt="fire truck" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Ftruck-clip-art-fire-truck4.png?1519011787956"/ /li li !-- Ambulance -- img alt="ambulance" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Fambulance5.png?1519011787610" /li li !-- Car -- img alt="car" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Fcar-20clip-20art-1311497037_Vector_Clipart.png?1519011788408" /li li !-- Bike -- img 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ás que al arrastrar cualquiera de ellas se crea una imagen fantasma.
Simplemente agregar un atributo arrastrable a un elemento que no sea una imagen o un enlace es realmente todo lo que se necesita para hacer que un elemento se pueda arrastrar en la mayoría de los navegadores. Para que los elementos se puedan arrastrar en todos los navegadores, es necesario 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án algunos controladores de eventos de arrastre, así que veamoslos.
Arrastrar eventos
Hay tres eventos relacionados con el arrastre que puedes escuchar, pero solo usaremos dos: dragstart
y dragend
.
dragstart
– Se activa tan pronto como comenzamos a arrastrar. Aquí es donde podemos definir los datos de arrastre y el efecto de arrastre.dragend
– Se activa cuando se suelta un elemento arrastrable. Este evento generalmente se activa justo después del evento de lanzamiento de la zona de lanzamiento.
En breve cubriremos cuáles son los datos de arrastre y el efecto de arrastre.
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 listenersconst vehicles = document.querySelector('.vehicles');vehicles.addEventListener('dragstart', onDragStart);vehicles.addEventListener('dragend', onDragEnd);
En este código suceden un par de cosas:
- Estamos definiendo los datos de arrastre. Cada evento de arrastre tiene una propiedad llamada
dataTransfer
que almacena los datos del evento. Puede utilizar elsetData(type, data)
método para agregar un elemento arrastrado a los datos de arrastre. Estamos almacenando el ID de la imagen arrastrada como tipo'text'
en la línea 7. - Estamos almacenando el elemento que se arrastra en una variable global. Sé que sé. Global es peligroso para el alcance, pero he aquí por qué lo hacemos: aunque puedes almacenar el elemento arrastrado usando
setData
, no puedes recuperarlo usandoevent.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 para que lo sepas. - Estamos configurando el
dropEffect
enmove
. LadropEffect
propiedad se utiliza para controlar la retroalimentación que recibe el usuario durante una operación de arrastrar y soltar. Por ejemplo, cambia el cursor que muestra el navegador mientras arrastra. Hay tres efectos: copiar, mover y vincular.copy
– Indica que los datos que se arrastran se copiarán desde su origen 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 ningún lugar donde dejarlos:
Goteante
De forma predeterminada, cuando arrastra un elemento, solo los elementos del formulario como input
podrán aceptarlo como una caída. Vamos a contener nuestra “zona de caída” en un section
elemento, por lo que necesitamos 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 poder verlo en la pantalla.
Estos son los parámetros que tenemos disponibles para eventos de entrega:
dragenter
– Se activa en el momento en que un elemento que se puede arrastrar ingresa a un área que se puede soltar. Al menos el 50% del elemento arrastrable debe estar dentro de la zona de colocación.dragover
– Lo mismo quedragenter
pero se llama repetidamente mientras el elemento que se puede arrastrar está dentro de la zona de colocación.dragleave
– Se activa una vez que un elemento arrastrable se aleja de una zona de colocación.drop
– Se activa cuando se suelta el elemento arrastrable y el área de colocación acepta 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 se pregunta por qué seguimos llamando, event.preventDefault()
es porque de forma predeterminada el navegador asume que cualquier destino no es un destino de colocación válido. Esto no es cierto siempre para todos los navegadores, ¡pero es mejor evitar que lamentar! Al llamar preventDefault()
a los eventos dragenter
y dragover
drop, se informa al navegador que el destino actual es un destino de drop válido.
¡Ahora una aplicación sencilla de arrastrar y soltar!
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 aleatorias y te animo a que creas algunas propias. Las señales de estacionamiento generalmente indican los 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 coche normal y una bicicleta. Entonces, vamos a crear reglas para ellos.
- Estacionamiento para ambulancias únicamente: de lunes a viernes, de 9:00 pm a 3:00 am
- Estacionamiento exclusivo para camiones de bomberos: todo el día durante el fin de semana.
- Estacionamiento regular: de lunes a viernes de 3:00 a 15:00 horas.
- Aparcamiento de bicicletas: de lunes a viernes, de 15 a 21 horas.
Ahora, traducimos estas reglas al código. Usaremos dos bibliotecas para manejar el tiempo y los rangos: Momento y Rango-momento.
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"/scriptscript 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 weekdaysconst 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 una days
propiedad con una variedad de días que puede estacionar y una times
propiedad que es un rango de tiempo. Para obtener la hora actual usando Moment, llame al moment()
. Para crear un rango usando Moment-range, pase una hora de inicio y finalización a la moment.range
función.
Ahora, en los controladores de eventos onDragEnter
y onDrop
que definimos anteriormente, agregamos algunas comprobaciones para asegurarnos de que un vehículo pueda estacionarse. Nuestro alt
atributo en la img
etiqueta almacena el tipo de vehículo, por lo que lo pasamos a un canPark
método que devolverá si el automóvil se puede estacionar. También agregamos señales visuales (cambio de fondo) para indicarle al usuario si un vehículo puede estacionarse 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 tienen permiso para estacionar pueden hacerlo. Por último, agregamos las reglas a la pantalla y le damos estilo.
Aquí está el resultado final:
Hay muchas maneras en que esto podría mejorarse:
- ¡Genere automáticamente el HTML para la lista de reglas a partir del
parkingRules
objeto! - ¡Añade algunos efectos de sonido!
- Agregue la capacidad de arrastrar vehículos al punto original sin actualizar la página.
- Todas esas molestas variables globales.
Pero te dejaré encargarte de eso.
Si está interesado en aprender más sobre la API de DnD y algunas críticas sobre ella, aquí tiene una buena lectura:
- Especificación WHATWG
- Cómo trabajar con arrastrar y soltar HTML5: programación profesional en HTML5, capítulo 9, por Jen Simmons
- Arrastrar y soltar accesible usando WAI-ARIA: consideraciones de accesibilidad de Dev.Opera
- Arrastrar y soltar HTML5 nativo: tutorial de HTML5 Rocks
- El desastre de arrastrar y soltar en HTML5: publicación de QuirksMode con contexto útil sobre la implementación del módulo DnD
Deja una respuesta