Manual Completo de Drag and Drop en JavaScript

Una guía exhaustiva sobre la API de Drag and Drop del navegador con ejemplos prácticos y explicaciones detalladas.

Introducción a Drag and Drop

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.

Conceptos Básicos

El sistema de Drag and Drop se basa en varios eventos y atributos que trabajan juntos:

Elementos Clave

Atributos Importantes

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

Eventos de Drag and Drop

El proceso de arrastrar y soltar involucra múltiples eventos que se disparan en diferentes momentos:

Eventos en el Elemento Arrastrable

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

Eventos en la Zona de Destino

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.

Implementación Básica

Veamos cómo implementar un sistema básico de drag and drop:

HTML

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

JavaScript

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

CSS

.hide {
    display: none;
}

/* Los estilos restantes están en la sección de estilos principal */
Arrástrame
Suéltame aquí

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.

Transferencia de Datos

El objeto dataTransfer es fundamental para pasar información entre el elemento arrastrado y la zona de destino.

Métodos Principales

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

Formatos de Datos

Los formatos más comunes son:

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

Efectos de Arrastre

Puedes controlar el tipo de operación de arrastre y el cursor que se muestra mediante las propiedades effectAllowed y dropEffect.

effectAllowed

Se establece en el elemento arrastrable (dragstart) y define qué operaciones están permitidas:

dropEffect

Se establece en la zona de destino (dragover) y define qué tipo de operación se realizará:

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

Drag and Drop Avanzado

Arrastrar Múltiples Elementos

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

Arrastrar Archivos

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

Mejores Prácticas y Consideraciones

Accesibilidad

Para hacer tu implementación accesible:

Rendimiento

Para optimizar el rendimiento:

Compatibilidad entre Navegadores

Aunque la API es ampliamente soportada, hay diferencias:

Ejemplo Completo: Lista Reordenable

Un ejemplo práctico de una lista que se puede reordenar mediante drag and drop:

HTML

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

JavaScript

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

CSS

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