La API de arrastrar y soltar de HTML5

En esta publicación, se explican los conceptos básicos de arrastrar y soltar.

En la mayoría de los navegadores, las selecciones de texto, las imágenes y los vínculos se pueden arrastrar de forma predeterminada. Por ejemplo, si arrastras un vínculo en una página web, verás un cuadro pequeño con un título y una URL que puedes soltar en la barra de direcciones o en el escritorio para crear un atajo o navegar al vínculo. Para que otros tipos de contenido sean arrastrables, debes usar las APIs de arrastrar y soltar de HTML5.

Para que un objeto sea arrastrable, establece draggable=true en ese elemento. Casi todo se puede arrastrar, incluidas las imágenes, los archivos, los vínculos o cualquier otro elemento de marcado de tu página.

En el siguiente ejemplo, se crea una interfaz para reorganizar las columnas que se diseñaron con CSS Grid. El marcado básico de las columnas se ve de la siguiente manera, con el atributo draggable de cada columna establecido en true:

<div class="container">
  <div draggable="true" class="box">A</div>
  <div draggable="true" class="box">B</div>
  <div draggable="true" class="box">C</div>
</div>

Este es el CSS para los elementos del contenedor y del cuadro. El único CSS relacionado con la función de arrastre es la propiedad cursor: move. El resto del código controla el diseño y el estilo de los elementos del contenedor y del cuadro.

.container {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 10px;
}

.box {
  border: 3px solid #666;
  background-color: #ddd;
  border-radius: .5em;
  padding: 10px;
  cursor: move;
}

En este punto, puedes arrastrar los elementos, pero no sucede nada más. Para agregar comportamiento, debes usar la API de JavaScript.

Cómo detectar eventos de arrastre

Para supervisar el proceso de arrastre, puedes escuchar cualquiera de los siguientes eventos:

Para controlar el flujo de arrastre, necesitas algún tipo de elemento de origen (donde comienza el arrastre), la carga útil de datos (lo que se arrastra) y un destino (un área para capturar la caída). El elemento de origen puede ser casi cualquier tipo de elemento. El objetivo es la zona de caída o el conjunto de zonas de caída que acepta los datos que el usuario intenta colocar. No todos los elementos pueden ser destinos. Por ejemplo, el objetivo no puede ser una imagen.

Cómo iniciar y finalizar una secuencia de arrastre

Después de definir los atributos draggable="true" en tu contenido, adjunta un controlador de eventos dragstart para iniciar la secuencia de arrastre de cada columna.

Este código establece la opacidad de la columna en un 40% cuando el usuario comienza a arrastrarla y, luego, la vuelve a 100% cuando finaliza el evento de arrastre.

function handleDragStart(e) {
  this.style.opacity = '0.4';
}

function handleDragEnd(e) {
  this.style.opacity = '1';
}

let items = document.querySelectorAll('.container .box');
items.forEach(function (item) {
  item.addEventListener('dragstart', handleDragStart);
  item.addEventListener('dragend', handleDragEnd);
});

Puedes ver el resultado en la siguiente demostración de Glitch. Arrastra un elemento y su opacidad cambiará. Debido a que el elemento de origen tiene el evento dragstart, configurar this.style.opacity en un 40% le brinda al usuario una respuesta visual de que ese elemento es la selección actual que se está moviendo. Cuando sueltas el elemento, el elemento fuente vuelve a tener una opacidad del 100%, aunque aún no hayas definido el comportamiento de soltar.

Agrega indicadores visuales adicionales

Para ayudar al usuario a comprender cómo interactuar con tu interfaz, usa los controladores de eventos dragenter, dragover y dragleave. En este ejemplo, las columnas son destinos de caída, además de ser arrastrables. Para ayudar al usuario a comprender esto, haz que el borde tenga guiones cuando mantenga un elemento arrastrado sobre una columna. Por ejemplo, en tu CSS, puedes crear una clase over para los elementos que son destinos de caída:

.box.over {
  border: 3px dotted #666;
}

Luego, en tu código JavaScript, configura los controladores de eventos, agrega la clase over cuando se arrastre la columna y quítala cuando se quite el elemento arrastrado. En el controlador dragend, también nos aseguramos de quitar las clases al final del arrastre.

document.addEventListener('DOMContentLoaded', (event) => {

  function handleDragStart(e) {
    this.style.opacity = '0.4';
  }

  function handleDragEnd(e) {
    this.style.opacity = '1';

    items.forEach(function (item) {
      item.classList.remove('over');
    });
  }

  function handleDragOver(e) {
    e.preventDefault();
    return false;
  }

  function handleDragEnter(e) {
    this.classList.add('over');
  }

  function handleDragLeave(e) {
    this.classList.remove('over');
  }

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });
});

Hay algunos puntos que vale la pena abordar en este código:

  • La acción predeterminada para el evento dragover es establecer la propiedad dataTransfer.dropEffect en "none". La propiedad dropEffect se trata más adelante en esta página. Por ahora, solo debes saber que evita que se active el evento drop. Para anular este comportamiento, llama a e.preventDefault(). Otra práctica recomendada es mostrar false en ese mismo controlador.

  • El controlador de eventos dragenter se usa para activar o desactivar la clase over en lugar de dragover. Si usas dragover, el evento se activa de forma reiterada mientras el usuario mantiene el elemento arrastrado sobre una columna, lo que hace que la clase CSS se active de forma reiterada. Esto hace que el navegador realice mucho trabajo de renderización innecesario, lo que puede afectar la experiencia del usuario. Te recomendamos que minimices los reenganches y, si necesitas usar dragover, considera limitar o anular el bloqueo de tu objeto de escucha de eventos.

Completa la bajada

Para procesar la acción de soltar, agrega un objeto de escucha de eventos para el evento drop. En el controlador drop, deberás evitar el comportamiento predeterminado del navegador para las caídas, que suele ser algún tipo de redireccionamiento molesto. Para ello, llama a e.stopPropagation().

function handleDrop(e) {
  e.stopPropagation(); // stops the browser from redirecting.
  return false;
}

Asegúrate de registrar el controlador nuevo junto con los demás:

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });

Si ejecutas el código en este punto, el elemento no se colocará en la nueva ubicación. Para ello, usa el objeto DataTransfer.

La propiedad dataTransfer contiene los datos enviados en una acción de arrastre. dataTransfer se establece en el evento dragstart y se lee o controla en el evento de soltar. Llamar a e.dataTransfer.setData(mimeType, dataPayload) te permite establecer el tipo de MIME y la carga útil de datos del objeto.

En este ejemplo, permitiremos que los usuarios reordenen el orden de las columnas. Para ello, primero debes almacenar el HTML del elemento fuente cuando comienza el arrastre:

function handleDragStart(e) {
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

En el evento drop, procesas la caída de la columna configurando el HTML de la columna de origen en el HTML de la columna de destino en la que colocaste los datos. Esto incluye verificar que el usuario no vuelva a soltar el elemento en la misma columna desde la que lo arrastró.

function handleDrop(e) {
  e.stopPropagation();

  if (dragSrcEl !== this) {
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

Puedes ver el resultado en la siguiente demostración. Para que esto funcione, necesitarás un navegador para computadoras. La API de arrastrar y soltar no es compatible con dispositivos móviles. Arrastra y suelta la columna A sobre la columna B y observa cómo cambian de lugar:

Más propiedades de arrastre

El objeto dataTransfer expone propiedades para proporcionar comentarios visuales al usuario durante el proceso de arrastre y controlar cómo cada destino de caída responde a un tipo de datos en particular.

  • dataTransfer.effectAllowed limita el "tipo de arrastre" que el usuario puede realizar en el elemento. Se usa en el modelo de procesamiento de arrastrar y soltar para inicializar dropEffect durante los eventos dragenter y dragover. La propiedad puede tener los siguientes valores: none, copy, copyLink, copyMove, link, linkMove, move, all y uninitialized.
  • dataTransfer.dropEffect controla los comentarios que recibe el usuario durante los eventos dragenter y dragover. Cuando el usuario coloca el puntero sobre un elemento de destino, el cursor del navegador indica qué tipo de operación se llevará a cabo, como una copia o un movimiento. El efecto puede tener uno de los siguientes valores: none, copy, link o move.
  • e.dataTransfer.setDragImage(imgElement, x, y) significa que, en lugar de usar los comentarios predeterminados de "imagen fantasma" del navegador, puedes establecer un ícono de arrastre.

Subir archivo

En este ejemplo simple, se usa una columna como fuente y destino de arrastre. Esto puede ocurrir en una IU que le pide al usuario que reordene los elementos. En algunas situaciones, el destino y la fuente de arrastre pueden ser diferentes tipos de elementos, como en una interfaz en la que el usuario debe arrastrar una imagen seleccionada a un destino para seleccionarla como imagen principal de un producto.

El método de arrastrar y soltar se usa con frecuencia para permitir que los usuarios arrastren elementos de su escritorio a una aplicación. La diferencia principal está en el controlador drop. En lugar de usar dataTransfer.getData() para acceder a los archivos, sus datos se contienen en la propiedad dataTransfer.files:

function handleDrop(e) {
  e.stopPropagation(); // Stops some browsers from redirecting.
  e.preventDefault();

  var files = e.dataTransfer.files;
  for (var i = 0, f; (f = files[i]); i++) {
    // Read the File objects in this FileList.
  }
}

Puedes encontrar más información sobre esto en Arrastrar y soltar personalizado.

Más recursos