Code HTML 5: glisser-déposer dans un panier

Sans aucun framework, une démo avec code source pour le glisser déposer d'objets.

Cette démonstration offre de choisir un objet dans une boutique et le déplacer dans le panier... Cela permet de voir tous les attributs et méthodes du standard HTML 5 requis pour déplacer des éléments dans une page.

Compatibilité: IE 9, Chrome, Firefox.

La boutique
Mon panier

Le standard HTML 5 pour le glisser déposer

Le navigateur réagit à chaque action de l'utilisateur depuis le moment où il clique sur un objet et maintient le bouton de la souris appuyée, jusqu'au moment où il relâche la souris:

  1. Prendre un objet sur la page: cliquer et maintenir le bouton pressé.
  2. Déplacer l'objet sur la page.
  3. Relâcher le bouton de la souris. Si c'est fait dans une zone destinée au déplacement, l'objet est physiquement copié ou déplacé dans la page.

Cela suppose que l'on ajoute des gestionnaires d'évènements aux deux éléments concernés: l'élément à déplacer et la zone où il peut être déplacé. La spécification a défini une liste d'évènements et d'attributs à assigner aux éléments de la page...

Les nouveaux attributs HTML

draggable
La propriété draggable="true" indique qu'un élément HTML peut être déplacé dans la page. Elle est inutile pour certains élément qui sont déplaçable par défaut, comme les images. Dans l'exemple du panier, on déplace des images.
data-value
La valeur de cet attribut sert à décrire un objet que l'on peut déplacer. Elle sera utilisée par les gestionnaires d'évènements si on veut qu'ils fasse appel à un traitement dépendant d'un objet donné.
dropzone
Attribut d'un conteneur dans lequel on peut déposer un objet. La valeur de cet attribut n'est pas "true" mais varie selon les permissions accordées. Pour accepter un fichier d'image PNG, la valeur sera "file:image/png". Pour un texte ce sera "string:text". D'autres paramètres peuvent être inclus dans la valeur. Par exemple la valeur "move" est ajoutée pour permettre le déplacement.

L'objet dataTransfer

C'est une interface pour un objet actuellement en cours de déplacement dans la page que le code JavaScript peut utiliser. Elle a pour attributs:

DOMString dropEffect
On assigne un type d'opération au déplacement que l'utilisateur a initié: "copy" pour copier, "move" pour déplacer, "link" pour ajouter un lien sur l'élément inital ou "none" pour supprimer. Cela est assigné quand l'évènement dragstart est déclenché.
DOMString effectAllowed.
On assigne le type de l'opération qui est permise à l'utilisateur quand l'objet entre dans la zone de destination, donc quand les évènements dragenter et dragover sont déclenchés. L'assignation est faite lorsque l'utilisateur saisit l'objet pour le déplacer, donc dans la fonction associée à l'évènement dragstart. Aux types d'opérations précédents sont ajoutés 'copyLink", "copyMove, "linkMove", "all" et "unitialized" (voir la spécification pour leur usage).
DataTransferItemList items
Cette liste est associée à tout objet dataTransfer. En lecture seule, elle permet de retourner la liste des éléments déplacés. Elle a l'attribut length, l'attribut items, et les méthodes clear(), add(DOMString data, DOMString type), add(File data), ainsi que la commande delete qui permet de supprimer un élément de la liste à l'index donné.

Et l'interface a pour méthodes:

setDragImage(Element image, x, y)
Pour assigner une image à l'objet en mouvement, en remplacement de l'image qui est saisie par l'utilisateur.

Note: dataTransfer a d'autres méthodes que la spécification classe mais non standard, elles ne sont donc pas reprises ici.

Les nouveaux évènements

Les noms de cette liste sont utilisés tels quels avec addEventListener ou précédés de "on" pour être utilisé comme attribut d'une balise HTML quelconque.

Deux évènements sont associés à chaque objet que l'on peut déplacer.

dragstart
On assigne à cet évènement une fonction qui contient le traitement quand l'utilisateur initie un déplacement.
dragend
Cet évènement est associé à un objet que l'on peut déplacer pour répondre à la fin du déplacement. Contrairement à drop, il est déclenché même si l'objet est relâché hors de la zone de destination, donc dans notre exemple, hors du panier.

Et quatre évènements concernent la cible.

dragenter
Cet évènement est déclenché quand un objet entre sur la surface du conteneur cible.
dragover
On ajoute cet évènement à une balise conteneur, il se déclenche quand la souris passe au dessus.
dragleave
Déclenché quand on lâche l'objet que l'on déplace. S'il y a plusieurs cibles dans la page, il est déclenché quand on quitte une cible pour entrer dans une autre.
drop
Cet évènement est déclenché quand l'utilisateur relâche un objet dans la surface de la balise conteneur.

Exemple de code conforme au standard

La boutique de notre exemple contient quatre objets:

<fieldset id="shop" class="shop">
  <legend>La boutique</legend>
  <img class="product" id="chaise" src="image/chaise.jpg" width="96" height="96">
  <img class="product" id="moniteur" src="image/screen.jpg" width="96" height="96">
  <img class="product" id="valise" src="image/valise.jpg" width="96" height="96">
  <img class="product" id="transat" src="image/transat.jpg" width="96" height="96">
</fieldset>

Il faut savoir que puisque nos objets sont des images, ils sont déplaçables par défaut. S'il s'agissait d'autres éléments, il faudrait ajouter l'attribut draggable statiquement comme ci-dessous ou dynamiquement en JavaScript, comme le fait le code plus loin:

<div class="product" draggable="true">
      <img src="images/chaise.jpg" width="96" height="96">
</div> 

Le panier de l'utilisateur peut être tout type de balise conteneur. Comme on utilise un fieldset contenant une balise legend, on ajoute ici un calque pour avoir un conteneur vide au départ:

<fieldset id="mycart" class="cart"><legend>Mon panier</legend>  
  <div id="cartArea"></div>
</fieldset>

Voici le code JavaScript complet, dont on va expliquer le fonctionnement plus bas:

<script>
var cartArea = document.querySelector('#cartArea'); 

var prods = document.querySelectorAll('.product');
for(var i = 0; i < prods.length; i++)
{
  prods[i].setAttribute('draggable', 'true');  // optionnel avec des images
  prods[i].addEventListener('dragstart', function(evnt) {
  this.className = 'itemchoosen';
  evnt.dataTransfer.effectAllowed = 'copy';
  evnt.dataTransfer.setData('text', this.id);
  return false;
  }, false);
}

cartArea.addEventListener('dragover', function(evnt) {
   if (evnt.preventDefault) evnt.preventDefault();
   evnt.dataTransfer.dropEffect = 'copy';
   return false;
}, false);
   
cartArea.addEventListener('dragenter', function(evnt) {
    return false;
}, false);

cartArea.addEventListener('dragleave', function(evnt) {
   return false;
}, false);

cartArea.addEventListener('drop', function(evnt) {
  if (evnt.stopPropagation) evnt.stopPropagation();

  var id = evnt.dataTransfer.getData('text');
  var theitem = document.getElementById(id); 
  // theitem.parentNode.removeChild(theitem);   // une option non retenue ici
  theitem.className='itemblurred';
  var y  = document.createElement('img');
  y.src = theitem.src;
  cartArea.appendChild(y);
  evnt.preventDefault(); // pour Firefox
  return false;
}, false);
</script>

Détail du code

On obtient la liste des objets HTML des produits avec la méthode querySelectorAll. C'est un choix parmi d'autre. Puis on ajoute à chaque balise représentant un produit la propriété draggable, à titre de démonstration (cela fonctionne sans, car on déplace des images). Ensuite à chaque balise on associe un évènement dragstart et une fonction qui répond à cet évènement.
Dans notre exemple, quand on initie un déplacement dans la page, l'image du produit prend une bordure rouge.
Le contenu à transférer est définit par dataTransfer.setData. On lui donne le type text et l'id de la balise représentant un produit.

var prods = document.querySelectorAll('.product');
for(var i = 0; i < prods.length; i++)
{
  prods[i].setAttribute('draggable', 'true');  // optionnel avec des images
  prods[i].addEventListener('dragstart', function(evnt) {
  this.className = 'itemchoosen';
  evnt.dataTransfer.effectAllowed = 'copy';
  evnt.dataTransfer.setData('text', this.id);
  return false;
  }, false);
}

On associe l'évènement dragEnter et dragLeave à la balise dans laquelle on veut déposer les objets. Dans notre exemple, aucun traitement n'est ajouté.

On associe aussi dragOver. C'est l'occasion d'assigner un effet.

cartArea.addEventListener('dragover', function(evnt) {
   if (evnt.preventDefault) evnt.preventDefault();
   evnt.dataTransfer.dropEffect = 'copy';
   return false;
}, false); 

Et finalement on ajoute l'évènement drop qui permet d'assigner un traitement a exécuter lorsque l'objet est déposé dans le panier.

cartArea.addEventListener('drop', function(evnt) {
  if (evnt.stopPropagation) evnt.stopPropagation();
  var id = evnt.dataTransfer.getData('text');
  var theitem = document.getElementById(id); 
  // theitem.parentNode.removeChild(theitem);
  theitem.className='itemblurred';
  var y  = document.createElement('img');
  y.src = theitem.src;
  cartArea.appendChild(y);
  evnt.preventDefault(); // for Firefox
  return false;
}, false);

C'est la fonction la plus complexe (dans notre exemple) et la plus importante et donc, elle mérite d'être explicitée en détails.

  1. L'appel à stopPropagation évite de déclencher les évènements qui pourraient découler de ce que nous modifions dans la page.
  2. On récupère le contenu de dataTransfer, en l'occurence l'id du produit.
  3. Cela permet de placer l'élément HTML correspondant dans la variable theitem.
  4. On peut à ce stade supprimer l'objet dans la boutique avec la méthode DOM removeChild. Cela conviendrait à certains traitements où il s'agit de déplacer des objets dans la page, mais pas à notre exemple, donc j'ai ajouté cette instruction pour mémoire en commentaire.
  5. Pour insérer l'image dans le panier, on crée une balise image, on assigne comme source celle de l'objet déplacé, et on ajoute cette balise comme descendant du conteneur cartArea.
  6. On annule l'exécution des évènements par défaut avec preventDefault. C'est necéssaire pour Firefox.

Ce traitement est une démonstration. On peut reprendre la structure du code et associer un traitement différent aux évènements, selon l'application à réaliser. Dans le cas d'une boutique en ligne, les objets ajoutés au panier ne sont pas supprimés de la liste originale car on peut en ajouter plusieurs au panier. Dans d'autres cas, le déplacement est matériel, on enlève l'objet en un point pour le placer dans un autre.

Le code CSS est également propre à notre exemple et répond à des choix. On peut le voir dans le source de la page de démonstration.

Cette page de démonstration donne un accès plus simple au code source JavaScript et CSS. Son utilisation est totalement libre (sauf comme tutoriel sur le web).

Références