Il Drag & Drop è quella operazione, diventata ormai familiare a chiunque usi il computer, che permette di trascinare (drag) uno o più elementi all'interno di un elemento target (drop). Questo comportamento ha funzionalità multiple: può permettere per esempio di spostare elementi (per esempio il classico spostamento di file) o di associare elementi tra di loro semplicemente creando un "link" tra essi.
Molte applicazioni web implementano già il Drag & Drop da parecchio tempo sfruttando funzionalità, spesso abbastanza complesse, realizzate ad hoc tramite JavaScript. Questa sorta di adattamento evidenzia un'assenza che si fa sempre più importante nel mondo delle applicazioni web.
Con l'avvento di HTML5 e delle nuove API JavaScript avremo a disposizione un ambiente nativo (quindi anche più performante) per sfruttare e personalizzare per le nostre applicazione questa operazione ormai diventata standard nelle applicazioni desktop. Grazie a queste API infatti potremmo trascinare qualsiasi elemento verso un qualsiasi target e sfruttare i numerosi eventi messi a disposizione dal browser.
Compatibilità
Ecco l'elenco dei principali browser e dei relativi rendering engine con la specifica relativa all'implementazione dello standard Drag & Drop:
- Internet Explorer/Trident: supporta parzialmente il Drag & Drop;
- Firefox/Gecko: supporta il Drag & Drop;
- Safari/WebKit: supporta parzialmente il Drag & Drop;
- Chrome/WebKit: supporta parzialmente il Drag & Drop;
- Opera/Presto: non supporta il Drag & Drop.
Gli esempi realizzati in questo articolo funzioneranno perfettamente solo su Firefox.
Drag & Drop in generale
Prima di iniziare a parlare delle API esposte dalla nuova specifica è necessario fare un introduzione teorica a come viene implementato "sul campo" questa particolare modalità di interazione.
Per realizzare un'applicazione basata sul Drag & Drop sono necessari almeno due componenti: un elemento da trascinare o "draggare" (che dovrà essere marcato con draggable = "true"
) e un target all'interno del quale l'elemento precedente potrà essere rilasciato. Ovviamente ciascun target dovrà essere a sua volta marcato come droppable
ed eventualmente si potrà specificare un determinato controllo per permettere di rilasciare solo particolari elementi.
Oltre a questi aspetti è possibile anche sfruttare la possibilità di "allegare" all'elemento draggato una serie di informazioni (per esempio un id
) che potranno essere recuperati dall'elemento target per eseguire una determinata azione sulla base dell'oggetto.
Per ultimo si potrà anche configurare l'oggetto proxy
ovvero il componente che effettivamente si muoverà nella nostra pagina; è possibile utilizzare l'oggetto stesso (spostandolo quindi dal luogo di partenza) o creare un oggetto o un'icona da utilizzare solo per lo spostamento (lasciando di fatto l'oggetto alla sua posizione di partenza). Non esiste una soluzione migliore, dipende tutto dalla nostra creatività e dalla tipologia di d&d che si intende realizzare.
Le API
Le specifiche del W3C relative alla componente Drag & Drop sono abbastanza corpose ed espongono l'argomento in maniera completa ma anche un po' fuorviante. Cerchiamo qua di fare ordine puntando l'attenzione sulle tematiche principali.
I primi passi
Attraverso lo studio delle API realizzeremo un piccola applicazione in maniera incrementale, aggiungendo passo passo le funzionalità incontrate nell'articolo. L'applicazione finale permetterà di selezionare, da un elenco di giocatori della nostra nazionale di calcio, quelli da schierare in campo cercando, almeno noi, di non fare una pessima figura.
Innanzitutto è necessario, come detto in precedenza, determinare quali sono gli elementi HTML che potranno essere trascinati. Questo è possibile grazie all'attributo draggable
: impostando la proprietà a true
è possibile spostare quegli elementi all'interno del browser.
Gli elementi <img>
e <a>
(con attributo href
valido) hanno impostata questa proprietà a true
di default.
Il secondo step è quello di definire delle aree target dove sarà possibile rilasciare i nostri elementi draggable. L'elemento droppable
espone alcuni eventi tra i quali utilizzeremo ondragenter
, che scatterà quando un elemento draggable entrerà nell'area "calda" e ondragleave
che, specularmente, scatterà quando l'elemento verrà rimosso. Questi eventi sono utili per modificare lo stile dell'elemento target per far capire all'utente che è disponibile un'azione rilasciando l'oggetto proprio in questo punto.
Iniziamo quindi con queste righe di codice:
<img src="res/italia.png" draggable="false" class="no-drag"/>
<img src="res/buffon.jpg" title="buffon" class="goalkeeper"/>
<img src="res/cannavaro.jpg" title="cannavaro" class="defender"/>
<img src="res/zambrotta.jpg" title="zambrotta" class="defender middlefield"/>
<img src="res/pirlo.jpg" title="pirlo" class="middlefield striker"/>
<img src="res/gilardino.jpg" title="gilardino" class="striker"/>
<div id="field"></div>
La pagina presenta una serie di immagini di calciatori, un'immagine della bandiera italiana non draggabile (notare l'attributo draggable="false"
: di default le immagini sono considerate trascinabili) e un <div>
che rappresenta il campo di gioco. Oltre a questo un minimo di CSS:
#field { height: 200px; width: 200px; background: green; color: snow }
img { border: 3px solid black; cursor: move }
img.no-drag { cursor: default }
Passiamo ora alle poche funzionalità per ora implementate, il cambiamento di colore di sfondo nel momento in cui un giocatore viene trascinato sul campo di gioco:
var init = function() {
var field = document.getElementById("field");
field.ondragenter = function() {
this.className = "active";
}
field.ondragleave = function() {
this.className = "inactive";
}
}
onload = init;
Gestiamo il drop
Fino a questo momento abbiamo gestito la transizione dragover
/dragleave
ma non abbiamo ancora sfruttato in pieno le funzionalità del Drag & Drop. Il prossimo passo sarà quindi quello di aggiungere una sorta di persistenza che permetta di scrivere i giocatori convocati all'interno del campo di gioco.
Per realizzare il drag&dorp è necessario marcare il div field
come target sul quale vogliamo avere il controllo. Per fare questo basterà disattivare il comportamento di default per l'evento ondragover
:
[...]
field.ondragover = function(event) {
return false;
}
[...]
Ora basterà gestire l'evento ondrop
implementando lo script che vogliamo eseguire nel momento in cui l'utente rilasci il tasto del mouse:
[...]
field.ondrop = function(event) {
alert(“Player dropped”);
}
[...]
Tutto questo però non basta! Essendo l'evento ondrop
centralizzato sul div field
non abbiamo attualmente nessuno strumento che ci permetta di identificare quale giocatore sia stato effettivamente scelto. Per risolvere questo problema dobbiamo sfruttare l'oggetto dataTransfer
incapsulato automaticamente dal browser all'interno dell'oggetto event
.
Innanzitutto è necessario decidere quale informazione vogliamo avere a disposizione al drop e appenderla all'interno dell'oggetto dataTransfer
per recuperarla successivamente. Modifichiamo l'HTML in questo modo:
[...]
<img src="res/buffon.jpg" title="buffon" />
<img src="res/cannavaro.jpg" title="cannavaro" />
<img src="res/zambrotta.jpg" title="zambrotta" />
<img src="res/pirlo.jpg" title="pirlo" />
<img src="res/gilardino.jpg" title="gilardino" />
[...]
Ora grazie all'attributo title
e all'evento ondragstart
possiamo salvarci temporaneamente il nome del calciatore selezionato:
[...]
var players = document.getElementsByTagName("IMG");
for(var i = 0;i<players.length; i++) {
players[i].ondragstart = function(event) {
event.dataTransfer.setData("player", this.title);
}
}
[...]
Ora che l'oggetto dataTransfer
è stato popolato, possiamo riscrivere il nostro ondrop
recuperando il nome del giocatore per scriverlo all'interno del campo di gioco:
[...]
field.ondrop = function(event) {
event.preventDefault();
var title = event.dataTransfer.getData("player");
this.innerHTML += title+"<br>";
}
[...]
Grazie a questo accorgimento l'applicazione sta prendendo sempre più forma.
Modifichiamo la dragImage
In fase di drag, il comportamento di default dei browser è quello di creare un elemento clonato a partire dal draggable (nel nostro caso un'immagine) e utilizzarlo come proxy posizionato vicino al cursore del mouse per dare questa sensazione di spostamento.
Ovviamente questa immagini può essere personalizzata sempre tramite l'oggetto dataTransfer
grazie al metodo setDragImage
. Modifichiamo in questo modo la callback dell'evento ondragstart
:
[...]
var players = document.getElementsByTagName("IMG");
var icon = document.createElement('img');
icon.src = 'res/ball.gif';
for(var i = 0;i<players.length; i++) {
players[i].ondragstart = function(event) {
event.dataTransfer.setData("player", this.title);
event.dataTransfer.setDragImage(icon, -5, -5);
}
}
[...]
Personalizziamo i target
L'ultimo aspetto da analizzare della nostra applicazione, è la possibilità di creare diverse aree target e di creare filtri particolari per identificare quale elemento draggable può essere rilasciato in una determinata area droppable.
Nel nostro caso creeremo 4 aree ognuna differenziata sulla base del ruolo (portiere, difensore, centrocampista e attaccante). Sulla base di questo i giocatori potranno essere rilasciati solamente sull'area di loro competenza. Per complicare le cose ho supposto che alcuni giocatori (Pirlo e Zambrotta) abbiano caratteristiche tecniche particolare che gli permettono di giocare tranquillamente in due posizioni differenti.
Per quest'ultimo esempio è necessario rivedere completamente la pagina per cui l'approccio incrementale verrà sostituito da un nuovo file HTML che implementerà le nuove feature. I punti chiave sono i seguenti. Il markup è stato modificato inserendo all'interno del nostro div field
quattro nuovi div ognuno relativo ad un ruolo specifico. Sulla base dell'id sarà poi possibile identificare il ruolo “accettato”:
<div id="field">
<div id="field-goalkeeper" class="zone"></div>
<div id="field-defender" class="zone"></div>
<div id="field-middlefield" class="zone"></div>
<div id="field-striker" class="zone"></div>
</div>
Parallelamente a questo anche le immagini dei giocatori dovranno acquisire in qualche modo l'informazione del ruolo. Nel nostro esempio ho utilizzato il l'attributo class
(da notare i doppi ruoli per Zambrotta e Pirlo):
<img src="res/buffon.jpg" title="buffon" class="goalkeeper"/>
<img src="res/cannavaro.jpg" title="cannavaro" class="defender"/>
<img src="res/zambrotta.jpg" title="zambrotta" class="defender middlefield"/>
<img src="res/pirlo.jpg" title="pirlo" class="middlefield striker"/>
<img src="res/gilardino.jpg" title="gilardino" class="striker"/>
Le modifiche allo script sono molte e per comodità lo riporterò completo:
var init = function() {
var field = document.getElementById("field");
var zones = field.childNodes;
var isAcceptedRole = function(role, roles) {
for(var i = 0; i<roles.length; i++) {
if(roles[i] == role) {
return true
}
}
return false;
}
for(var i = 0; i<zones.length; i++) {
if(zones[i].tagName && zones[i].tagName.toLowerCase() == "div") {
zones[i].ondragenter = function(event) {
var accceptedRole = this.id.substring(6);
var roles = event.dataTransfer.getData("roles").split(" ");
if(isAcceptedRole(accceptedRole, roles)) {
this.className = "valid";
} else {
this.className = "invalid";
}
}
zones[i].ondragleave = function(event) {
this.className = "zone";
}
zones[i].ondragover = function(event) {
return false;
}
zones[i].ondrop = function(event) {
event.preventDefault();
var accceptedRole = this.id.substring(6);
var roles = event.dataTransfer.getData("roles").split(" ");
if(isAcceptedRole(accceptedRole, roles)) {
this.innerHTML += event.dataTransfer.getData("player")+"<br>";
}
}
}
}
var players = document.getElementsByTagName("IMG");
var icon = document.createElement('img');
icon.src = 'res/ball.gif';
for(var i = 0;i<players.length; i++) {
players[i].ondragstart = function(event) {
event.dataTransfer.setData("player", this.title);
event.dataTransfer.setData("roles", this.className);
event.dataTransfer.setDragImage(icon, -5, -5);
}
}
}
onload = init;
Il codice dell'esempio è disponibile per il download.
Ulteriori sviluppi
L'applicazione, nonostante tutti i nostri sforzi, non è ancora molto utilizzabile. Con le nozioni apprese in questo articolo potete provare ad aggiungere ulteriori funzionalità come ad esempio:
- selezione singola di un giocatore (ora può essere draggato più volte lo stesso);
- eliminazione dal campo di gioco (una volta selezionato non è più possibile rimuovere un giocatore);
- prevedere una sorta di meccanismo di logging server-side utilizzando AJAX;
- differenziare la dragImage sulla base del ruolo e modificarla a run-time quando l'area target non è valida.
Buon lavoro!