Drag and Drop

28 marzo 2011

La soluzione al Drag and Drop proposta dalle specifiche HTML5 va ben oltre la metafora classica emulata in modo eccelso da librerie come JQurey e similari. Certo, anche quel tipo di comportamento è possibile, ma stiamo solo raschiando la superficie di una feature ben più complessa, articolata e potente. Un assaggio più consistente delle reali capacità del Drag and Drop è offerto dalla versione HTML5 del famoso client di posta Gmail: trascinando un file al di sopra della pagina per la composizione di un nuovo messaggio si evidenza una zona verde, rilasciando il documento al di sopra di tale zona possiamo assistere all’upload dello stesso; ed il tutto funziona anche con più file contemporaneamente! Prima di passare ad una analisi delle specifiche è bene quindi capire che questo meccanismo non è stato studiato solo per riordinare liste o spostare oggetti in un carrello ma deve essere inteso come vero e proprio sistema di input sia per informazioni interne alla pagina sia esterne.

Le specifiche

Gli attori coinvolti in queste API sono essenzialmente tre, e non è necessario che siano tutti presenti ad ogni implementazione. Stiamo parlando dell’oggetto che subisce il drag, della struttura che contiene i dati che intendiamo trasferire attraverso il drop, e dell’oggetto che accetta il drop. Ecco come funziona una sessione completa di Drag and Drop:

<ul id="elementi_da_trascinare" ondragstart="iniziaTrascinamento(event)"> 
  <li draggable="true" data-icona="http://bit.ly/h5Khyt" data-valore="0.5">50 Cents</li> 
  <li draggable="true" data-icona="http://bit.ly/gnyFfh" data-valore="1" > 1 Euro </li> 
  <li draggable="true" data-icona="http://bit.ly/eXq6y5" data-valore="2" > 2 Euro </li> 
  <li draggable="true" data-icona="http://bit.ly/hiftBg" data-valore="10"> 10 Euro </li>
</ul>

In primo luogo è necessario l’utilizzo dell’attributo globale draggable="true" per specificare gli elementi che vogliamo rendere trascinabili; utilizziamo inoltre i campi data-* per salvare informazioni che potranno tornare utili più tardi, come l’url dell’icona della moneta e il suo valore numerico. Il passo successivo consiste nel definire la funzione associata all’evento che notifica l’inizio di un trascinamento (ondragstart), nel nostro caso l’evento è stato associato all’ul, in quanto sottende l’intero elenco degli elementi con il comportamento drag abilitato.

<script>
  iniziaTrascinamento = function(evento){ 
    evento.dataTransfer.setData("text", evento.target.dataset.valore); 
    evento.dataTransfer.effectAllowed = 'copy'; 
    icona = document.createElement('img'); 
    icona.src = evento.target.dataset.icona; 
    evento.dataTransfer.setDragImage(icona, -10, -10);
  } 
</script>

Nell’argomento evento lo user-agent si premura di fornirci tutto il necessario per gestire il trascinamento dell’elemento. Nella prima riga della funzione viene invocata la struttura dataTransfer, atta a contenere i dati che vogliamo siano disponibili all’oggetto incaricato del drop; con il metodo setData richiediamo poi l’inserimento di un nuovo valore all’interno di tale struttura. Stando alle specifiche potremmo identificare questo valore con un’etichetta (usando il primo argomento, ad esempio ‘valuta’), in questo modo saremmo poi in grado di istruire l’area di drop ad accettare solo il rilascio di elementi con tale etichetta; nella realtà questa funzionalità non è ancora supportata in modo sufficiente e funziona solo con etichette text o url. Come secondo argomento alla chiamata specifichiamo invece il contenuto del campo ‘data-valore’ dell’elemento che sta subendo il trascinamento.

Nella riga successiva, con l’istruzione evento.effectAllowed = 'copy' segnaliamo allo user-agent che l’azione di drag è da intendersi come copia e non, ad esempio, come spostamento (in quel caso sarebbe stato ‘move‘). È importante notare che tale segnalazione non implica di per sé nessun cambiamento alla dinamica dello user-agent nei confronti di questo drag and drop (se non qualche accortezza grafica, come vedremo poi) ma ci da ancora una volta la possibilità di filtrare in fase di drop gli elementi che rilasciamo accettando, ad esempio, solamente azioni di tipo ‘copy‘.

Le due righe di codice seguenti servono invece per creare una immagine avente come src il campo data-icona dell’elemento che stiamo trascinando; infine con l’ultima istruzione, evento.dataTransfer.setDragImage(icona, -10, -10), comunichiamo al dataTransfer di usare l’immagine appena generata come oggetto da visualizzare durante il trascinamento.

Se proviamo ad includere i due frammenti di codice in una classica struttura HTML5 e ad eseguirli con Chromium avremo questo effetto (figura 1):

Figura 1

screenshot

Bene, ora creiamo l’oggetto di drop e gestiamo l’azione di rilascio:

... 
</ul>
  <img   id="portamonete" 
            dropzone="copy s:text"
            draggable="false" 
            src="http://bit.ly/gZH4H5" 
            ondragenter="ingressoInZonaDiDrop(event)" 
            ondragleave="uscitaZonaDiDrop(event)" 
            ondragover="trascinamentoInZonaDiDrop(event)" 
            ondrop="rilascioDellOggettoTrascinato(event)"
  >
  <p> Il portamonete contiene: <output id="sommatoria">0</output> euro</p>

Così come ogni elemento HTML può essere impostato come trascinabile grazie all’attributo draggable=true, anche il ruolo di area di drop può essere delegato a qualsiasi tag. In questo caso abbiamo scelto di utilizzare un’immagine di un portamonete nel quale riporremo i nostri risparmi. Diamo una scorsa agli attributi che abbiamo impostato:

  • dropzone: con questo attributo, purtroppo ancora poco supportato, è possibile specificare in modo sintetico ed efficace le varie tipologie di dati per i quali l’area di drop è abilitata. I primi comandi, separati da uno spazio, indicano le tipologie di effectAllowed supportate (in questo caso solamente copy) mentre le restanti istruzioni, nel formato s:testo o f:testo rappresentano in caso di s:testo le etichette supportate, o i mime-type supportati nel caso di f:testo. La particella s indica che il contenuto del dataTransfer deve essere di natura testuale (come nell’esempio che stiamo studiando), mentre la particella f implica che l’area di drop accetti solamente file, come ad esempio succede nel caso dell’aggiunta di allegati alla Gmail. Validi esempi di valorizzazione di questo attributo sono i seguenti:
-- Accetta copy per file di tipo png e jpeg 
dropzone="copy f:image/png f:image/jpeg" 
-- Accetta move e copy per etichette "valuta" 
dropzone="move copy s:valuta"
  • draggable: conosciamo già questo attributo; in ogni caso è importante segnalare che qui è stato utilizzato per informare lo user-agent di non rendere trascinabile questa immagine. Infatti nella maggioranza dei browser le immagini lo sono di default;
  • ondragenter, ondragmove e ondragleave: questi tre eventi vengono invocati quando l’elemento viene trascinato all’interno di un’area di drop (ondragenter), mosso all’interno della stessa (ondragmove) e infine spostato nuovamente all’esterno dell’area (ondragleave);
  • ondrop: l’evento chiave dell’intero processo, viene invocato al rilascio di un elemento al di sopra di un area di drop.

Ottimo! Non ci resta che completare questa panoramica delle specifiche osservando una possibile implementazione degli eventi appena descritti:

...
    ingressoInZonaDiDrop = function(evento){ 
      evento.target.src = "http://bit.ly/gRo6cc";
      evento.preventDefault(); 
    }

    uscitaZonaDiDrop = function(evento){
      evento.target.src = "http://bit.ly/gZH4H5";
    }

    trascinamentoInZonaDiDrop = function(evento){ 
      evento.dataTransfer.dropEffect = 'copy';
      evento.preventDefault();
    }

    rilascioDellOggettoTrascinato = function(evento){ 
      sommatoria = document.getElementById("sommatoria"); 
      sommatoria.innerHTML = parseFloat(sommatoria.innerHTML) + parseFloat

(evento.dataTransfer.getData("text")); 
      evento.target.src = "http://bit.ly/gZH4H5";
    }
...

L’istruzione che merita maggiore attenzione è sicuramente preventDefault() che, se posizionata all’interno di un evento, impedisce al browser di attivare il proprio comportamento di default. In questo caso la funzione in questione è stata utilizzata in ingressoInZonaDiDrop per impedire che il browser negasse l’accesso al drop (comportamento di default) ed in trascinamentoInZonaDiDrop per evitare l’animazione di ‘ritorno’ degli elementi draggati al loro rilascio.

Per il resto le due funzioni associate a ondragenter e ondragleave si preoccupano solamente di alternare, tra chiusa e aperta, l’immagine del portamonete all’ingresso (o uscita) di un elemento trascinato al di sopra di esso. trascinamentoInZonaDiDrop deve invece preoccuparsi nuovamente di segnalare il supporto al solo effetto copy; questo accorgimento non sarebbe necessario con un pieno supporto dell’attributo dropzone.

Infine la funzione legata all’evento ondrop contiene l’importante metodo getData(etichetta), indispensabile per recuperare dal dataTransfer l’ultimo oggetto inserito con l’etichetta richiesta. Eseguiamo il tutto in Chromium notando anche come l’icona del mouse si modifichi durante l’azione di drag and drop ad indicare la copia (figura 2):

Figura 2 (click per ingrandire)

screenshot

Ecco la demo.

Un esempio

Visto che non abbiamo ancora sperimento il drag and drop di file, utilizziamo il progetto guida per fare alcune prove; in particolare implementiamo la possibilità di rilasciare un file di testo al di sopra di una textarea per fare in modo che in questa vi si riversi il contenuto. Questo esempio ci darà anche l’occasione di toccare con mano la potenza delle nuove File API, in particolare approfondiremo gli oggetti Javascript File, FileList e FileReader: ottimi compagni nel caso sia necessario accedere agli attributi o al contenuto di un file selezionato (o trascinato) per l’upload.

Incominciamo modificando il markup del file index.html per attivare una drop area sulla textarea:

<label>Cosa hai in mente? 
  <textarea name="testo_da_ricordare" required autofocus 
  placeholder="La lista della spesa, il teatro di questa sera ..." 
  dropzone="copy f:text/plain" 
  ondragover="consentiIlDrop(event)" 
  ondrop="caricaIlContenutoTestuale(event,this)">
  </textarea>
</label>

Fin qui niente di strano, la parte divertente è tutta racchiusa nella definizione delle due funzioni. Vediamole insieme nel file ‘application.js’:

consentiIlDrop = function(evento){ 
  evento.dataTransfer.dropEffect = 'copy'; 
  evento.preventDefault();
}

caricaIlContenutoTestuale = function(evento,element){ 
  var files = evento.dataTransfer.files; 
  var reader = new FileReader(); 
  reader.onload = function(evento){
    element.value = element.value + evento.target.result;
  } 
  for(file in files){
    reader.readAsBinaryString(files[file]);
  }
}

La funzione caricaIlContenutoTestuale merita un approfondimento: il metodo files chiamato su dataTransfer ritorna un elenco di File racchiusi in un oggetto di tipo FileList, che per i nostri scopi è assimilabile ad un array.

Successivamente, nella variabile reader, viene caricata un istanza di FileReader, una classe predisposta esclusivamente alla lettura del contenuto degli oggetti di tipo File. Nelle righe successive viene impostata l’azione associata all’evento onload di reader, che viene invocato al completamento dell’azione di lettura di un file; all’interno di questa azione il contenuto testuale del file (evento.target.result) è concatenato al testo già presente nella textarea. L’ultimo frammento di questa funzione cicla su tutti i file presenti in files; successivamente su ognuno di essi viene richiesta la lettura al reader attraverso il metodo readAsBinaryString.

Anche per questo esempio abbiamo preparato una demo. Funziona al momento solo su Chromium. Per il test basta creare un file di testo, salvarlo (magari sul desktop) e trascinarlo nella textarea.

Tabella del supporto sui browser

API e Web Applications Internet Explorer Firefox Safari Google Chrome Opera
Drag and Drop No 3.5+ 3.2+ 2.0+ No

Tutte le lezioni

1 ... 40 41 42 ... 51

Se vuoi aggiornamenti su Drag and Drop inserisci la tua e-mail nel box qui sotto:
Tags:
 
X
Se vuoi aggiornamenti su Drag and Drop

inserisci la tua e-mail nel box qui sotto:

Ho letto e acconsento l'informativa sulla privacy

Acconsento al trattamento di cui al punto 3 dell'informativa sulla privacy