Una guía exhaustiva sobre la API de Drag and Drop con TypeScript, ejemplos prácticos y explicaciones detalladas.
TypeScript añade tipos estáticos a JavaScript, lo que nos permite escribir código más seguro y mantenible para implementaciones de Drag and Drop.
Nota: TypeScript proporciona interfaces específicas para los eventos de Drag and Drop en lib.dom.d.ts
.
// Interfaces principales para Drag and Drop
interface DragEvent extends MouseEvent {
dataTransfer: DataTransfer;
}
interface DataTransfer {
dropEffect: 'none' | 'copy' | 'link' | 'move';
effectAllowed: 'none' | 'copy' | 'copyLink' | 'copyMove' |
'link' | 'linkMove' | 'move' | 'all' | 'uninitialized';
files: FileList;
items: DataTransferItemList;
types: string[];
clearData(format?: string): void;
getData(format: string): string;
setData(format: string, data: string): void;
setDragImage(image: Element, x: number, y: number): void;
}
<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>
// Definimos tipos para nuestros elementos
type DraggableElement = HTMLElement & {
draggable: true;
id: string;
};
type DropzoneElement = HTMLElement & {
classList: DOMTokenList;
};
// Obtenemos los elementos con aserción de tipo
const draggableItem = document.getElementById('draggable-item') as DraggableElement;
const dropzone = document.getElementById('dropzone') as DropzoneElement;
// Manejador de dragstart con tipo DragEvent
const handleDragStart = (e: DragEvent): void => {
e.dataTransfer.setData('text/plain', draggableItem.id);
setTimeout(() => {
draggableItem.classList.add('hide');
}, 0);
};
// Manejador de dragend
const handleDragEnd = (): void => {
draggableItem.classList.remove('hide');
};
// Manejadores para la zona de destino
const handleDragEnter = (e: DragEvent): void => {
e.preventDefault();
dropzone.classList.add('active');
};
const handleDragOver = (e: DragEvent): void => {
e.preventDefault();
};
const handleDragLeave = (): void => {
dropzone.classList.remove('active');
};
const handleDrop = (e: DragEvent): void => {
e.preventDefault();
dropzone.classList.remove('active');
const data = e.dataTransfer.getData('text/plain');
const draggedItem = document.getElementById(data) as DraggableElement;
if (draggedItem) {
dropzone.appendChild(draggedItem);
draggedItem.classList.remove('hide');
}
};
// Añadimos los event listeners con los tipos correctos
draggableItem.addEventListener('dragstart', handleDragStart);
draggableItem.addEventListener('dragend', handleDragEnd);
dropzone.addEventListener('dragenter', handleDragEnter);
dropzone.addEventListener('dragover', handleDragOver);
dropzone.addEventListener('dragleave', handleDragLeave);
dropzone.addEventListener('drop', handleDrop);
Con TypeScript podemos definir interfaces para los datos transferidos:
// Definimos una interfaz para nuestros datos transferidos
interface DraggableItemData {
id: string;
content: string;
timestamp: number;
metadata?: {
category: string;
priority: number;
};
}
// Manejador de dragstart con datos tipados
const handleDragStartWithTypes = (e: DragEvent, item: DraggableElement): void => {
const itemData: DraggableItemData = {
id: item.id,
content: item.textContent || '',
timestamp: Date.now(),
metadata: {
category: 'demo',
priority: 1
}
};
e.dataTransfer.setData('application/json', JSON.stringify(itemData));
e.dataTransfer.effectAllowed = 'move';
};
// Manejador de drop con datos tipados
const handleDropWithTypes = (e: DragEvent): void => {
e.preventDefault();
const jsonData = e.dataTransfer.getData('application/json');
try {
const data: DraggableItemData = JSON.parse(jsonData);
console.log('Datos recibidos:', data);
// Podemos acceder a las propiedades con autocompletado
if (data.metadata) {
console.log('Categoría:', data.metadata.category);
}
} catch (error) {
console.error('Error parsing JSON:', error);
}
};
// Uso con nuestros elementos
draggableItem.addEventListener('dragstart', (e) =>
handleDragStartWithTypes(e, draggableItem));
dropzone.addEventListener('drop', handleDropWithTypes);
<ul id="sortable-list">
<li draggable="true" data-id="1">Item 1</li>
<li draggable="true" data-id="2">Item 2</li>
<li draggable="true" data-id="3">Item 3</li>
<li draggable="true" data-id="4">Item 4</li>
<li draggable="true" data-id="5">Item 5</li>
</ul>
// Definimos tipos para nuestros elementos de lista
type SortableListItem = HTMLLIElement & {
draggable: true;
dataset: {
id: string;
};
};
// Definimos una interfaz para el estado de la lista
interface ListState {
items: {
id: string;
content: string;
order: number;
}[];
draggedItemId: string | null;
}
// Creamos el estado inicial
const listState: ListState = {
items: [],
draggedItemId: null
};
// Obtenemos la lista y sus elementos
const sortableList = document.getElementById('sortable-list') as HTMLUListElement;
const listItems = Array.from(
document.querySelectorAll('#sortable-list li')
) as SortableListItem[];
// Inicializamos el estado
listState.items = listItems.map((item, index) => ({
id: item.dataset.id,
content: item.textContent || '',
order: index
}));
// Función para encontrar la posición de inserción
const getDragAfterElement = (
container: HTMLUListElement,
y: number
): SortableListItem | null => {
const draggableElements = Array.from(
container.querySelectorAll('li:not(.dragging)')
) as SortableListItem[];
return draggableElements.reduce<{
offset: number;
element: SortableListItem | null;
}>((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: null }).element;
};
// Manejador de dragstart
const handleListDragStart = (e: DragEvent, item: SortableListItem): void => {
listState.draggedItemId = item.dataset.id;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => {
item.classList.add('dragging');
}, 0);
};
// Manejador de dragover
const handleListDragOver = (e: DragEvent): void => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (!listState.draggedItemId) return;
const afterElement = getDragAfterElement(sortableList, e.clientY);
const draggedItem = document.querySelector(
`#sortable-list li[data-id="${listState.draggedItemId}"]`
) as SortableListItem | null;
if (draggedItem) {
if (afterElement) {
sortableList.insertBefore(draggedItem, afterElement);
} else {
sortableList.appendChild(draggedItem);
}
}
};
// Manejador de dragend
const handleListDragEnd = (item: SortableListItem): void => {
item.classList.remove('dragging');
listState.draggedItemId = null;
// Actualizamos el estado con el nuevo orden
listState.items = Array.from(sortableList.children)
.map((child, index) => {
const listItem = child as SortableListItem;
return {
id: listItem.dataset.id,
content: listItem.textContent || '',
order: index
};
});
console.log('Nuevo orden:', listState.items);
};
// Añadimos los event listeners
listItems.forEach(item => {
item.addEventListener('dragstart', (e) => handleListDragStart(e, item));
item.addEventListener('dragend', () => handleListDragEnd(item));
});
sortableList.addEventListener('dragover', handleListDragOver);
// Definimos tipos para nuestros elementos de archivos
type FileDropzoneElement = HTMLElement & {
files: FileList | null;
};
// Manejador de dragover para archivos
const handleFileDragOver = (e: DragEvent): void => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
if (e.currentTarget) {
(e.currentTarget as FileDropzoneElement).classList.add('active');
}
};
// Manejador de drop para archivos
const handleFileDrop = (e: DragEvent): void => {
e.preventDefault();
if (e.currentTarget) {
(e.currentTarget as FileDropzoneElement).classList.remove('active');
}
if (e.dataTransfer.files.length) {
handleFiles(e.dataTransfer.files);
}
};
// Función para procesar archivos con tipos
const handleFiles = (files: FileList): void => {
Array.from(files).forEach((file: File) => {
console.log('Archivo:', file.name, file.type, file.size);
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e: ProgressEvent) => {
if (e.target?.result) {
const img = document.createElement('img');
img.src = e.target.result as string;
document.body.appendChild(img);
}
};
reader.readAsDataURL(file);
}
});
};
// Uso con un elemento dropzone
const fileDropzone = document.createElement('div');
fileDropzone.className = 'dropzone';
fileDropzone.textContent = 'Arrastra archivos aquí';
fileDropzone.addEventListener('dragover', handleFileDragOver);
fileDropzone.addEventListener('drop', handleFileDrop);
interface DraggableFactoryOptions {
id: string;
content: string;
type: 'default' | 'special';
}
class DraggableFactory {
static create(options: DraggableFactoryOptions): HTMLElement {
const element = document.createElement('div');
element.id = options.id;
element.textContent = options.content;
element.draggable = true;
element.classList.add('draggable', options.type);
// Añadir event listeners con tipos
element.addEventListener('dragstart', (e: DragEvent) => {
e.dataTransfer.setData('application/json', JSON.stringify({
id: options.id,
type: options.type
}));
});
return element;
}
}
// Uso
const specialDraggable = DraggableFactory.create({
id: 'special-1',
content: 'Elemento especial',
type: 'special'
});
document.body.appendChild(specialDraggable);
enum DragType {
DEFAULT = 'default',
SPECIAL = 'special',
IMPORTANT = 'important'
}
interface DraggableItem {
id: string;
type: DragType;
content: string;
}
function handleDragStart(e: DragEvent, item: DraggableItem): void {
e.dataTransfer.setData('application/json', JSON.stringify(item));
switch(item.type) {
case DragType.SPECIAL:
e.dataTransfer.effectAllowed = 'copy';
break;
case DragType.IMPORTANT:
e.dataTransfer.effectAllowed = 'move';
break;
default:
e.dataTransfer.effectAllowed = 'all';
}
}