Drag & Drop API e Componenti Interattivi

Guida pratica per implementare dashboard, kanban board e file upload trascinabili utilizzando la Drag & Drop API nativa di HTML5 con vanilla JavaScript.

1. Fondamenti della Drag & Drop API

La Drag & Drop API è uno standard HTML5 che permette di implementare interazioni drag and drop senza dipendenze esterne. Funziona nativamente in tutti i browser moderni ed è perfetta per il nostro stack vanilla.

1.1 Event principali

La Drag & Drop API utilizza una serie di eventi che vengono scatenati durante l'interazione:

1.2 DataTransfer Object

Durante un'operazione drag, l'oggetto DataTransfer permette di trasferire dati tra il source e la destinazione. È fondamentale per passare informazioni sull'elemento trascinato.

Concetto chiave: Per abilitare il drop su un elemento, è necessario prevenire il comportamento default dell'evento dragover con event.preventDefault().

2. File Upload Drag & Drop

L'implementazione di un'area di upload trascinabile è uno dei casi d'uso più comuni e immediati della Drag & Drop API.

2.1 HTML Structure

HTML

<div id="dropzone" style="
    border: 2px dashed #3498db;
    padding: 40px;
    border-radius: 10px;
    text-align: center;
    cursor: pointer;
    transition: all 0.3s ease;
">
    <p>Trascina file qui o clicca per selezionare</p>
    <input type="file" id="fileInput" multiple style="display:none;">
</div>
<ul id="fileList"></ul>

2.2 JavaScript Implementation

JavaScript

const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');

// Previeni default drag behavior
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
    dropzone.addEventListener(eventName, preventDefaults, false);
    document.body.addEventListener(eventName, preventDefaults, false);
});

function preventDefaults(e) {
    e.preventDefault();
    e.stopPropagation();
}

// Highlight dropzone
['dragenter', 'dragover'].forEach(eventName => {
    dropzone.addEventListener(eventName, () => {
        dropzone.style.backgroundColor = '#ecf0f1';
    });
});

['dragleave', 'drop'].forEach(eventName => {
    dropzone.addEventListener(eventName, () => {
        dropzone.style.backgroundColor = 'transparent';
    });
});

// Handle drop
dropzone.addEventListener('drop', handleDrop, false);
fileInput.addEventListener('change', (e) => handleFiles(e.target.files));
dropzone.addEventListener('click', () => fileInput.click());

function handleDrop(e) {
    const dt = e.dataTransfer;
    const files = dt.files;
    handleFiles(files);
}

function handleFiles(files) {
    Array.from(files).forEach(file => {
        const li = document.createElement('li');
        li.textContent = `${file.name} (${(file.size/1024).toFixed(2)} KB)`;
        fileList.appendChild(li);
        
        uploadFile(file);
    });
}

function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);
    
    fetch('upload.php', {
        method: 'POST',
        body: formData
    }).then(response => response.json())
      .catch(error => console.error('Error:', error));
}

3. Kanban Board Interattivo

Uno dei progetti più affascinanti è implementare un kanban board dove le card possono essere trascinate tra colonne diverse.

3.1 Struttura HTML

HTML

<div id="kanban" style="display: flex; gap: 20px; padding: 20px;">
    <div class="column" data-status="todo">
        <h3>To Do</h3>
        <div class="tasks">
            <div class="task" draggable="true">Task 1</div>
            <div class="task" draggable="true">Task 2</div>
        </div>
    </div>
    <div class="column" data-status="inprogress">
        <h3>In Progress</h3>
        <div class="tasks"></div>
    </div>
    <div class="column" data-status="done">
        <h3>Done</h3>
        <div class="tasks"></div>
    </div>
</div>

3.2 CSS Styling

CSS

.column {
    flex: 1;
    background: #ecf0f1;
    padding: 15px;
    border-radius: 10px;
    min-height: 400px;
}

.tasks {
    min-height: 350px;
    border-radius: 5px;
    background: white;
    padding: 10px;
}

.task {
    background: #3498db;
    color: white;
    padding: 15px;
    margin-bottom: 10px;
    border-radius: 5px;
    cursor: move;
    user-select: none;
    transition: opacity 0.2s;
}

.task:hover {
    opacity: 0.8;
}

.task.dragging {
    opacity: 0.5;
}

.tasks.drag-over {
    background: #d5dbdb;
}

3.3 JavaScript Logic

JavaScript

let draggedElement = null;

document.querySelectorAll('.task').forEach(task => {
    task.addEventListener('dragstart', (e) => {
        draggedElement = e.target;
        e.target.classList.add('dragging');
        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData('text/html', e.target.innerHTML);
    });

    task.addEventListener('dragend', () => {
        e.target.classList.remove('dragging');
    });
});

document.querySelectorAll('.tasks').forEach(taskContainer => {
    taskContainer.addEventListener('dragover', (e) => {
        e.preventDefault();
        e.dataTransfer.dropEffect = 'move';
        taskContainer.classList.add('drag-over');
    });

    taskContainer.addEventListener('dragleave', () => {
        taskContainer.classList.remove('drag-over');
    });

    taskContainer.addEventListener('drop', (e) => {
        e.preventDefault();
        taskContainer.classList.remove('drag-over');
        
        if (draggedElement) {
            taskContainer.appendChild(draggedElement);
            draggedElement = null;
        }
    });
});

4. Dashboard con Widget Riposizionabili

Un dashboard con widget che possono essere riposizionati offre un'esperienza utente avanzata e professionale.

4.1 Widget HTML

HTML

<div id="dashboard" style="
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 20px;
    padding: 20px;
">
    <div class="widget" draggable="true" data-id="1">
        <div class="widget-header">Widget 1</div>
        <div class="widget-content">Contenuto...</div>
    </div>
</div>

4.2 CSS per Widget

CSS

.widget {
    background: white;
    border-radius: 10px;
    box-shadow: 0 5px 15px rgba(0,0,0,0.1);
    padding: 20px;
    cursor: move;
    transition: transform 0.2s, box-shadow 0.2s;
}

.widget:hover {
    transform: translateY(-5px);
    box-shadow: 0 10px 25px rgba(0,0,0,0.2);
}

.widget.dragging {
    opacity: 0.5;
    z-index: 1000;
}

.widget-header {
    font-weight: bold;
    margin-bottom: 15px;
    color: #2c3e50;
    border-bottom: 2px solid #3498db;
    padding-bottom: 10px;
}

4.3 JavaScript per Persistenza

JavaScript

class DashboardManager {
    constructor() {
        this.dashboard = document.getElementById('dashboard');
        this.init();
    }

    init() {
        this.loadWidgetOrder();
        this.attachDragListeners();
    }

    attachDragListeners() {
        this.dashboard.querySelectorAll('.widget').forEach(widget => {
            widget.addEventListener('dragstart', (e) => {
                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/html', e.target.innerHTML);
                widget.classList.add('dragging');
            });

            widget.addEventListener('dragend', () => {
                widget.classList.remove('dragging');
                this.saveWidgetOrder();
            });
        });

        this.dashboard.addEventListener('dragover', (e) => {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
        });

        this.dashboard.addEventListener('drop', (e) => {
            e.preventDefault();
            const afterElement = this.getDragAfterElement(e.clientY);
            if (afterElement == null) {
                this.dashboard.appendChild(document.querySelector('.dragging'));
            } else {
                this.dashboard.insertBefore(
                    document.querySelector('.dragging'),
                    afterElement
                );
            }
        });
    }

    getDragAfterElement(y) {
        const draggableElements = [
            ...this.dashboard.querySelectorAll('.widget: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;
    }

    saveWidgetOrder() {
        const order = Array.from(this.dashboard.querySelectorAll('.widget'))
            .map(w => w.dataset.id);
        localStorage.setItem('dashboardOrder', JSON.stringify(order));
    }

    loadWidgetOrder() {
        const order = JSON.parse(localStorage.getItem('dashboardOrder') || '[]');
        if (order.length > 0) {
            const widgets = Array.from(this.dashboard.querySelectorAll('.widget'));
            order.forEach(id => {
                const widget = widgets.find(w => w.dataset.id === id);
                if (widget) this.dashboard.appendChild(widget);
            });
        }
    }
}

const dashboardManager = new DashboardManager();

5. Best Practices e Considerazioni

Pratica Descrizione
Previeni Default Sempre prevenire il comportamento default su dragover con preventDefault()
Visual Feedback Fornisci feedback visuale chiaro durante drag (highlights, cursori, opacità)
Accessibility Offri alternative keyboard per utenti che non possono usare il mouse
Mobile Handling La Drag & Drop API su mobile è limitata; considera touch events come alternativa
Data Persistence Usa localStorage per mantenere l'ordine dei widget tra le sessioni

5.1 Gestione Mobile con Touch Events

JavaScript

// Per mobile, considera di usare touch events come fallback
document.addEventListener('touchstart', (e) => {
    if (e.target.classList.contains('task')) {
        e.target.style.opacity = '0.5';
    }
});

document.addEventListener('touchend', (e) => {
    if (e.target.classList.contains('task')) {
        e.target.style.opacity = '1';
    }
});

Conclusioni

La Drag & Drop API nativa di HTML5 offre tutto ciò che serve per implementare componenti interattivi sofisticati senza dipendenze esterne. Dal semplice file upload al kanban board fino ai dashboard complessi, il vanilla JavaScript è perfettamente adatto.

Le chiavi del successo sono: prevenire i comportamenti default, fornire feedback visuale immediato, salvare lo stato con localStorage, e testare bene su diversi dispositivi. Con queste fondamenta, puoi creare esperienze utente fluide e professionali.

La compatibilità browser è eccellente: tutti i browser moderni supportano pienamente la Drag & Drop API, rendendola una scelta affidabile per qualsiasi progetto web.