Drag and Drop API

Funcionalidad nativa de arrastrar y soltar elementos


Muchas veces, nos habremos dado cuenta que aunque nuestra página realiza los objetivos que requiere, su utilización no resulta cómoda o intuitiva para los usuarios. Muchas veces, esto puede ocurrir porque la interfaz carece de un sistema de Drag and Drop.

¿Qué es el Drag and Drop?

Se conoce como Drag and Drop al hecho de arrastrar (Drag) y soltar (Drop) un elemento (draggable) sobre otro elemento (drop zone) en una interfaz de usuario. Ejemplos clásicos son cuando puedes arrastrar y soltar una imagen en lugar de pulsar el botón y buscar en las carpetas de tu sistema.

Algunos elementos como el botón de subir imágenes ya tienen drag and drop integrado (sobre el propio botón), pero esta implementación depende del navegador y el sistema operativo, y no es personalizable. Si queremos hacer algo más específico y bonito, tendremos que utilizar esta API.

El atributo draggable

Para empezar, tenemos que saber que existe un atributo HTML draggable que debemos incluir en nuestro elemento HTML para convertirlo en un elemento que pueda arrastrar. El atributo draggable debe tener el valor true:

Atributo Descripción
draggable Con el valor true, convierte el elemento en arrastrable.

Por defecto, algunos elementos como las imágenes <img> ya son arrastrables y no requieren este atributo, sin embargo, otras como un simple <div> no lo son. Observa el siguiente ejemplo:

<div class="container">
  <img src="monoculo.gif" alt="ManzDev Monóculo">
  <div class="box no-draggable"></div>
  <div class="box" draggable="true"></div>
</div>
.container {
  display: flex;
  gap: 1rem;
}

.box {
  width: 128px;
  height: 128px;
  background: indigo;
}

.no-draggable {
  background: darkred;
}

El elemento rojo no es arrastrable, sin embargo el elemento morado si lo es.

Eventos Drag

Para personalizar nuestro sistema Drag and Drop necesitamos utilizar ciertos eventos de Javascript. Podemos empezar con los eventos dragstart, dragend y drag, tres eventos centrados en el elemento que arrastramos, es decir, en la parte en la que hacemos «Drag»:

Evento Descripción
dragstart Evento que se dispara cuando empiezas a arrastrar el elemento.
dragend Evento que se dispara cuando terminas de arrastrar el elemento.
drag Evento que se dispara continuamente mientras arrastras el elemento. ⚠️ Se ejecuta continuamente.

Observa este fragmento de código Javascript, donde utilizamos estos 3 eventos para encender las cajas de la derecha de la demo. Ten en cuenta que los listeners se hacen sobre dragItem, el elemento que arrastras. Al arrastrar la imagen y moverla, comprobarás que se encienden los eventos que se disparan:

const dragItem = document.querySelector(".drag-item");
const [start, move, end] = document.querySelectorAll(".badges div");

const setBlink = (element) => {
  element.classList.add("on");
  setTimeout(() => element.classList.remove("on"), 200);
}

dragItem.addEventListener("dragstart", () => setBlink(start));
dragItem.addEventListener("dragend", () => setBlink(end));
dragItem.addEventListener("drag", () => setBlink(move));
.container {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.badges {
  display: flex;
  gap: 2rem;

  & div {
    display: grid;
    border: 1px solid #aaa;
    padding: 1rem;
    height: fit-content;

    &.on {
      background: orangered;
      border-color: orangered;
      color: #fff;
    }
  }
}
<div class="container">
  <img class="drag-item" src="monoculo.gif" alt="ManzDev Monóculo">
  <div class="badges">
    <div>Start</div>
    <div>Move</div>
    <div>End</div>
  </div>
</div>

El evento drag es un evento que se llama muchas veces por segundo. Intenta siempre que el código ejecutado en drag sea breve, extremandamente rápido y ligero o tenga condicionales para evitar ejecutarse múltiples veces. De lo contrario podrías experimentar lag, bloqueos o retardos.

Eventos Drop

Ahora, centrémonos en la parte en la que hacemos «Drop», y soltamos el elemento en otro elemento. Para ello, vamos a utilizar los eventos dragenter, dragleave, dragover y drop:

Evento Descripción
dragenter Hemos entrado en una zona para soltar arrastrando el elemento.
dragleave Hemos salido de una zona para soltar arrastrando el elemento.
dragover Nos estamos moviendo en una zona para soltar. ⚠️ Se ejecuta continuamente.
drop Hemos soltado el elemento en una zona para soltar.

Ten muy en cuenta que estamos escuchando con los listeners en dropZone, el elemento donde sueltas. En este caso, debemos mover la imagen sobre el bloque que indica «Drop here». Estaremos detectando cuando un elemento es arrastrado hasta esa sección, cuando entras, cuando sales, etc.

const dragItem = document.querySelector(".drag-item");
const dropZone = document.querySelector(".drop-zone");
const [enter, leave, over, drop] = document.querySelectorAll(".badges div");

const setBlink = (element) => {
  element.classList.add("on");
  setTimeout(() => element.classList.remove("on"), 200);
}

dropZone.addEventListener("dragenter", (ev) => {
  dropZone.classList.add("selected");
  setBlink(enter);
});

dropZone.addEventListener("dragleave", () => {
  dropZone.classList.remove("selected");
  setBlink(leave);
});

dropZone.addEventListener("dragover", (ev) => {
  ev.preventDefault();
  setBlink(over);
});

dropZone.addEventListener("drop", (ev) => {
  setBlink(drop);
});
.container {
  display: flex;
  gap: 1rem;
}

.drop-zone {
  border: 2px dotted #777;
  width: 128px;
  height: 128px;
  display: grid;
  place-items: center;

  &.selected {
    border: 2px solid orangered;
    background: #f9dfb7;
  }
}

.badges {
  display: flex;
  gap: 2rem;

  & div {
    display: grid;
    border: 1px solid #aaa;
    padding: 1rem;
    height: fit-content;

    &.on {
      background: indigo;
      border-color: indigo;
      color: #fff;
    }
  }
}
<div class="container">
  <img class="drag-item" src="monoculo.gif" alt="ManzDev Monóculo" draggable="true">
  <div class="drop-zone">Drop here</div>
  <div class="badges">
    <div>Enter</div>
    <div>Leave</div>
    <div>Over</div>
    <div>Drop</div>
  </div>
</div>

Con el evento dragover ocurre lo mismo que con el evento drag. Intenta que sea muy ligero. Además, debes utilizar un .preventDefault() para evitar ocultar la activación del evento drop.

El objeto dataTransfer

Sin embargo, hasta ahora tenemos forma de detectar cuando ocurren los eventos, pero no la información asociada a ellos, como por ejemplo, que elemento estamos arrastrando o donde lo hemos soltado. Para ello, vamos a utilizar la propiedad ev.dataTransfer que ocurre en nuestros eventos de Drag and Drop.

Método Descripción
getData(type) Obtiene el valor guardado con setData(). Tipos: text/plain o text/uri-list.
setData(type, value) Guarda un valor en dataTransfer, pasándole el tipo y el valor a guardar.
clearData(type) Elimina los datos guardados en el dataTransfer.
setDragImage(image, x, y) Muestra una imagen traslúcida al arrastrar el elemento, desplazándola en x e y.

En primer lugar, observa que hemos creado un <div> con clase place que es donde está ubicada la imagen original. Luego, le hemos añadido a la imagen un id para identificarlo de forma única. Ahora, vamos a mover la imagen a la zona punteada:

const dragItem = document.querySelector(".drag-item");
const dropZone = document.querySelector(".drop-zone");
const honkImage = document.createElement("img");
honkImage.src = "honk.gif";

dragItem.addEventListener("dragstart", (ev) => {
  ev.dataTransfer.setData("text/plain", dragItem.id);
  ev.dataTransfer.setDragImage(honkImage, 0, 0);
});

dropZone.addEventListener("dragover", (ev) => ev.preventDefault());

dropZone.addEventListener("drop", (ev) => {
  const id = ev.dataTransfer.getData("text/plain");
  const item = document.querySelector("#" + id);
  dropZone.append(item);
});
.container {
  display: flex;
  gap: 1rem;
}

.place {
  width: 200px;
  height: 200px;
}

.drop-zone {
  border: 2px dotted #777;
  width: 128px;
  height: 128px;
  display: grid;
  place-items: center;

  & img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  &.selected {
    border: 2px solid orangered;
    background: #f9dfb7;
  }
}
<div class="container">
  <div class="place">
    <img class="drag-item" id="item" src="monoculo.gif" alt="ManzDev Monóculo">
  </div>
  <div class="drop-zone"></div>
</div>

En este fragmento de código, en el evento dragstart, utilizamos el método setData() para pasar el id del elemento que hemos empezado a arrastrar. Luego, en el evento drop, obtenemos ese id, lo buscamos en el DOM, y como es único, obtendremos el elemento HTML que hemos soltado. Además, lo que hacemos es añadirlo a la zona drop donde lo hemos soltado, y como es una referencia al original, lo que hará el navegador es moverlo desde su posición inicial.

Observa también, que con el método setDragImage() hemos indicado una imagen para mostrar mientras se arrastra, y dar feedback visual al usuario.

Librerías de Drag and Drop

En algunos casos, nos puede interesar no ir a tan bajo nivel y priorizar nuestro tiempo de desarrollo sobre los inconvenientes de utilizar una librería. Una librería siempre tardará más en descargarse, parsearse y procesarse, y puede que sea más difícil de mantener en el futuro, sin embargo, a veces puede interesar por la velocidad de implementación que representa.

Algunas librerías Javascript interesantes para hacer Drag and Drop serían las siguientes:

Librería Descripción
Drag & Drop formKit Librería moderna, ligera que utiliza la API estándar.
Draggable Desarrollada originalmente por Shopify, mantenida actualmente por la comunidad.
Sortable Crea listas reordenables con esta librería.
Dragula Librería tradicional de Drag and Drop.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev