Guida pratica per implementare dashboard, kanban board e file upload trascinabili utilizzando la Drag & Drop API nativa di HTML5 con vanilla JavaScript.
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.
La Drag & Drop API utilizza una serie di eventi che vengono scatenati durante l'interazione:
Durante un'operazione drag, l'oggetto DataTransfer permette di trasferire dati tra il source e la destinazione. È fondamentale per passare informazioni sull'elemento trascinato.
event.preventDefault().
L'implementazione di un'area di upload trascinabile è uno dei casi d'uso più comuni e immediati della Drag & Drop API.
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>
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));
}
Uno dei progetti più affascinanti è implementare un kanban board dove le card possono essere trascinate tra colonne diverse.
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>
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;
}
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;
}
});
});
Un dashboard con widget che possono essere riposizionati offre un'esperienza utente avanzata e professionale.
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>
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;
}
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();
| 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 |
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';
}
});
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.