Una guía exhaustiva sobre la API de Drag and Drop del navegador con ejemplos prácticos y explicaciones detalladas.
La API de Drag and Drop (arrastrar y soltar) es una característica poderosa de HTML5 que permite a los usuarios seleccionar elementos, arrastrarlos a una ubicación diferente y soltarlos. Esta funcionalidad es nativa del navegador y está soportada por la mayoría de los navegadores modernos.
Nota: Aunque existen librerías como interact.js o Draggable.js que simplifican el proceso, entender la API nativa te dará mayor control y flexibilidad.
El sistema de Drag and Drop se basa en varios eventos y atributos que trabajan juntos:
Atributo | Descripción |
---|---|
draggable |
Hace que un elemento sea arrastrable (valores: true, false, auto) |
dataTransfer |
Objeto que contiene los datos que se transfieren durante el arrastre |
El proceso de arrastrar y soltar involucra múltiples eventos que se disparan en diferentes momentos:
Evento | Descripción |
---|---|
dragstart |
Se dispara cuando el usuario comienza a arrastrar el elemento |
drag |
Se dispara continuamente mientras el elemento se está arrastrando |
dragend |
Se dispara cuando el usuario termina de arrastrar el elemento |
Evento | Descripción |
---|---|
dragenter |
Se dispara cuando el elemento arrastrado entra en la zona de destino |
dragover |
Se dispara continuamente mientras el elemento está sobre la zona de destino |
dragleave |
Se dispara cuando el elemento arrastrado sale de la zona de destino |
drop |
Se dispara cuando el elemento arrastrado se suelta en la zona de destino |
Importante: Para que el evento drop
funcione, debes prevenir el comportamiento por defecto en los eventos dragover
y dragenter
.
Veamos cómo implementar un sistema básico de drag and drop:
<div class="demo-container">
<div class="demo-box">
<div id="draggable-item" class="draggable" draggable="true">
Arrástrame
</div>
</div>
<div class="demo-box">
<div id="dropzone" class="dropzone">
Suéltame aquí
</div>
</div>
</div>
const draggableItem = document.getElementById('draggable-item');
const dropzone = document.getElementById('dropzone');
// Eventos para el elemento arrastrable
draggableItem.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', draggableItem.id);
setTimeout(() => {
draggableItem.classList.add('hide');
}, 0);
});
draggableItem.addEventListener('dragend', () => {
draggableItem.classList.remove('hide');
});
// Eventos para la zona de destino
dropzone.addEventListener('dragenter', (e) => {
e.preventDefault();
dropzone.classList.add('active');
});
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('active');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('active');
const data = e.dataTransfer.getData('text/plain');
const draggedItem = document.getElementById(data);
dropzone.appendChild(draggedItem);
draggedItem.classList.remove('hide');
});
.hide {
display: none;
}
/* Los estilos restantes están en la sección de estilos principal */
Consejo: El uso de setTimeout
en el evento dragstart
es un truco común para asegurar que el elemento se oculte correctamente durante el arrastre.
El objeto dataTransfer
es fundamental para pasar información entre el elemento arrastrado y la zona de destino.
Método | Descripción |
---|---|
setData(format, data) |
Establece los datos a transferir con un formato específico |
getData(format) |
Obtiene los datos transferidos con el formato especificado |
clearData() |
Limpia todos los datos transferidos |
Los formatos más comunes son:
text/plain
- Texto simpletext/html
- Datos HTMLtext/uri-list
- URLsapplication/json
- Datos JSON// Ejemplo de transferencia de múltiples tipos de datos
draggableItem.addEventListener('dragstart', (e) => {
// Datos simples
e.dataTransfer.setData('text/plain', draggableItem.id);
// Datos complejos como JSON
const itemData = {
id: draggableItem.id,
content: draggableItem.textContent,
timestamp: Date.now()
};
e.dataTransfer.setData('application/json', JSON.stringify(itemData));
// Establecer el efecto de arrastre
e.dataTransfer.effectAllowed = 'move';
});
dropzone.addEventListener('drop', (e) => {
// Obtener datos simples
const plainData = e.dataTransfer.getData('text/plain');
// Obtener datos JSON
const jsonData = JSON.parse(e.dataTransfer.getData('application/json'));
console.log('Datos simples:', plainData);
console.log('Datos JSON:', jsonData);
});
Puedes controlar el tipo de operación de arrastre y el cursor que se muestra mediante las propiedades effectAllowed
y dropEffect
.
Se establece en el elemento arrastrable (dragstart) y define qué operaciones están permitidas:
none
- No se permite ninguna operacióncopy
- Solo copiarmove
- Solo moverlink
- Solo enlazarcopyLink
- Copiar o enlazarcopyMove
- Copiar o moverlinkMove
- Enlazar o moverall
- Todas las operacionesSe establece en la zona de destino (dragover) y define qué tipo de operación se realizará:
none
- No se permite soltarcopy
- Se copiará el datomove
- Se moverá el datolink
- Se creará un enlace al dato// Configurar efectos en el elemento arrastrable
draggableItem.addEventListener('dragstart', (e) => {
e.dataTransfer.effectAllowed = 'copyMove'; // Permite copiar o mover
});
// Configurar efecto en la zona de destino
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
// Comprobar si se está presionando la tecla Ctrl para decidir el efecto
if (e.ctrlKey) {
e.dataTransfer.dropEffect = 'copy'; // Copiar si Ctrl está presionada
} else {
e.dataTransfer.dropEffect = 'move'; // Mover por defecto
}
});
Puedes implementar la selección y arrastre de múltiples elementos:
const draggableItems = document.querySelectorAll('.draggable');
draggableItems.forEach(item => {
item.addEventListener('dragstart', (e) => {
// Marcar el elemento como seleccionado
item.classList.add('selected');
// Obtener todos los elementos seleccionados
const selectedItems = document.querySelectorAll('.draggable.selected');
const itemsData = Array.from(selectedItems).map(el => el.id);
e.dataTransfer.setData('application/json', JSON.stringify(itemsData));
e.dataTransfer.effectAllowed = 'move';
});
item.addEventListener('click', (e) => {
// Selección múltiple con Ctrl/Cmd
if (e.ctrlKey || e.metaKey) {
item.classList.toggle('selected');
}
});
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
const itemsData = JSON.parse(e.dataTransfer.getData('application/json'));
itemsData.forEach(id => {
const item = document.getElementById(id);
dropzone.appendChild(item);
item.classList.remove('selected');
});
});
La API de Drag and Drop también permite arrastrar archivos desde el sistema:
const fileDropzone = document.getElementById('file-dropzone');
fileDropzone.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
fileDropzone.classList.add('active');
});
fileDropzone.addEventListener('dragleave', () => {
fileDropzone.classList.remove('active');
});
fileDropzone.addEventListener('drop', (e) => {
e.preventDefault();
fileDropzone.classList.remove('active');
const files = e.dataTransfer.files;
if (files.length) {
handleFiles(files);
}
});
function handleFiles(files) {
Array.from(files).forEach(file => {
console.log('Archivo:', file.name, file.type, file.size);
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement('img');
img.src = e.target.result;
fileDropzone.appendChild(img);
};
reader.readAsDataURL(file);
}
});
}
Para hacer tu implementación accesible:
aria-grabbed
, aria-dropeffect
)Para optimizar el rendimiento:
drag
y dragover
requestAnimationFrame
para animacionesAunque la API es ampliamente soportada, hay diferencias:
Un ejemplo práctico de una lista que se puede reordenar mediante drag and drop:
<ul id="sortable-list">
<li draggable="true">Item 1</li>
<li draggable="true">Item 2</li>
<li draggable="true">Item 3</li>
<li draggable="true">Item 4</li>
<li draggable="true">Item 5</li>
</ul>
const list = document.getElementById('sortable-list');
let draggedItem = null;
list.addEventListener('dragstart', (e) => {
draggedItem = e.target;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => {
e.target.classList.add('dragging');
}, 0);
});
list.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const afterElement = getDragAfterElement(list, e.clientY);
if (afterElement) {
list.insertBefore(draggedItem, afterElement);
} else {
list.appendChild(draggedItem);
}
});
list.addEventListener('dragend', (e) => {
e.target.classList.remove('dragging');
});
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('li:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
#sortable-list {
list-style: none;
padding: 0;
}
#sortable-list li {
padding: 1rem;
background-color: var(--bg-secondary);
margin-bottom: 0.5rem;
border-radius: var(--border-radius);
cursor: move;
transition: var(--transition);
}
#sortable-list li.dragging {
opacity: 0.5;
background-color: var(--accent);
color: var(--bg-primary);
}
#sortable-list li:hover {
background-color: var(--bg-tertiary);
}