Le Drag & Drop

Le drag and drop (plus communément écrit drag & drop, voire drag'n'drop) est l'un des principaux éléments d'une interface fonctionnelle. Cela se nomme le « glisser-déposer » en français, il s'agit d'une manière de gérer une interface en permettant le déplacement de certains éléments vers d'autres conteneurs. Ainsi, dans l'explorateur de fichiers d'un système d'exploitation quelconque, vous pouvez très bien faire glisser un fichier d'un dossier à un autre d'un simple déplacement de souris, ceci est possible grâce au concept du drag & drop.

Bien que le drag & drop ait longtemps existé sur les sites Web grâce au JavaScript, jamais un vrai système standard n'avait encore vu le jour jusqu'à ce que le HTML5 n'arrive. Grâce au HTML5, il est maintenant possible de permettre un déplacement de texte, de fichier ou d'autres éléments depuis n'importe quelle application jusqu'à votre navigateur. Tout au long de ce chapitre nous allons tâcher de voir comment utiliser au mieux cette nouvelle API.

Aperçu de l'API

Rendre un élément déplaçable

En temps normal, un élément d'une page Web ne peut pas être déplacé. Vous pouvez toujours essayer, vous ne pourrez faire qu'une sélection du contenu. Certains éléments, comme les liens ou les images, peuvent être déplacés nativement, mais vous ne pouvez pas interagir avec ce mécanisme en JavaScript sans passer par la nouvelle API disponible dans la spécification HTML5.

Afin de rendre un élément déplaçable, il vous suffit d'utiliser son attributdraggableet de le mettre àtrue(que ce soit en HTML ou en JavaScript). À partir de là, vous pouvez essayer de déplacer l'élément sans problème.

Essayer un exemple

Parmi les huit événements que l'API Drag & Drop fournit, l'élément déplaçable peut en utiliser deux :dragstartetdragend.

L'événementdragstartse déclenche, comme son nom l'indique, lorsque l'élément ciblé commence à être déplacé. Cet événement est particulièrement utile pour initialiser certains détails utilisés tout au long du processus de déplacement. Pour cela, il nous faudra utiliser l'objetdataTransferque nous étudierons plus loin.

Quant à l'événementdragend, celui-ci permet de signaler à l'objet déplacé que son déplacement est terminé, que le résultat soit un succès ou non.

Initialiser un déplacement avec l'objetdataTransfer

L'objetdataTransferest généralement utilisé au travers de deux événements :dragstartetdrop. Il peut toutefois être utilisé avec d'autres événements spécifiques au Drag & Drop.
Cet objet permet de définir et de récupérer les informations relatives au déplacement en cours d'exécution. Ici, nous n'allons aborder l'objetdataTransferque dans le cadre de l'initialisation d'un déplacement, son utilisation pour la fin d'un processus de drag & drop sera étudiée plus tard.

L'objetdataTransferpermet de réaliser trois actions (toutes facultatives) :

  • Sauvegarder une chaîne de caractères qui sera transmise à l'élément HTML qui accueillera l'élément déplacé. La méthode à utiliser estsetData().

  • Définir une image utilisée lors du déplacement. La méthode concernée estsetDragImage().

  • Spécifier le type de déplacement autorisé avec la propriétéeffectAllowed. Cette propriété ayant un usage assez restreint, nous vous laissons vous documenter par vous-mêmes sur la manière dont elle doit être utilisée, nous ne l'aborderons pas et cela est aussi valable pour sa congénèredropEffect.

La méthodesetData()prend deux arguments en paramètres. Le premier est le type MIME des données (sous forme de chaîne de caractères) que vous allez spécifier dans le deuxième argument. Précisons que le deuxième argument est obligatoirement une chaîne de caractères, ce qui signifie que le type MIME qui sera spécifié n'a que peu d'intérêt, vous utiliserez généralement le typetext/plainpour des raisons de simplicité :

draggableElement.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', "Ce texte sera transmis à l'élément HTML de réception");
});

En temps normal, vous nous diriez probablement que cette méthode est inutile puisqu'il suffirait de stocker les données dans une variable plutôt que par le biais desetData(). Eh bien, en travaillant sur la même page oui, cependant le Drag & Drop en HTML5 possède la faculté de s'étendre bien au-delà de votre page Web actuelle et donc de faire un glisser-déposer d'une page à une autre, que ce soit d'un onglet à un autre ou bien même d'un navigateur à un autre ! Le transfert de données entre les pages Web n'étant pas possible (tout du moins pas sans « tricher »), il est utile d'utiliser la méthodesetData().

La méthodesetDragImage()est extrêmement utile pour qui souhaite personnaliser l'affichage de sa page Web ! Elle permet de définir une image qui se placera sous le curseur pendant le déplacement de l'élément concerné. La méthode prend trois arguments en paramètres. Le premier est un élément<img>contenant l'image souhaitée, le deuxième est la position horizontale de l'image et le troisième est la position verticale :

var dragImg = new Image(); // Il est conseillé de précharger l'image, sinon elle risque de ne pas s'afficher pendant le déplacement
dragImg.src = 'drag_img.png';
document.querySelector('*[draggable="true"]').addEventListener('dragstart', function(e) {
e.dataTransfer.setDragImage(dragImg, 40, 40); // Une position de 40x40 pixels centrera l'image (de 80x80 pixels) sous le curseur
});

Essayer le code

Définir une zone de « drop »

Un élément en cours de déplacement ne peut pas être déposé n'importe où, il faut pour cela définir une zone de « drop » (zone qui va permettre de déposer des éléments) qui ne sera, au final, qu'un simple élément HTML.

Les zones de drop prennent généralement en charge quatre événements :

  • dragenter, qui se déclenche lorsqu'un élément en cours de déplacement entre dans la zone de drop.

  • dragover, qui se déclenche lorsqu'un élément en cours de déplacement se déplace dans la zone de drop.

  • dragleave, qui se déclenche lorsqu'un élément en cours de déplacement quitte la zone de drop.

  • drop, qui se déclenche lorsqu'un élément en cours de déplacement est déposé dans la zone de drop.

Par défaut, le navigateur interdit de déposer un quelconque élément où que ce soit dans la page Web. Notre but est donc d'annuler cette action par défaut, et qui dit « annulation d'une action par défaut », ditpreventDefault()! Cette méthode va devoir être utilisée au travers de l'événementdragover.

Prenons un exemple simple :

<div id="draggable" draggable="true">Je peux être déplacé !</div>
<div id="dropper">Je n'accepte pas les éléments déplacés !</div>

Essayer le code

Comme vous pouvez le constater, cet exemple ne fonctionne pas, le navigateur affiche un curseur montrant une interdiction lorsque vous survolez le deuxième<div>. Afin d'autoriser cette action, il va vous falloir ajouter un code JavaScript très simple :

document.querySelector('#dropper').addEventListener('dragover', function(e) {
e.preventDefault(); // Annule l'interdiction de drop
});

Essayer le code

Avec ce code, le curseur n'affiche plus d'interdiction en survolant la zone de drop, cependant il ne se passe rien si nous relâchons notre élément sur la zone de drop. Cela est parfaitement normal, car c'est à nous de définir la manière dont la zone de drop doit gérer les éléments qu'elle reçoit.

Avant toute chose, pour agir suite à un drop d'élément, il nous faut détecter ce fameux drop, nous allons donc devoir utiliser l'événementdrop(logique, n'est-ce pas ? :p ), cela se fait de manière enfantine :

document.querySelector('#dropper').addEventListener('drop', function(e) {
e.preventDefault(); // Cette méthode est toujours nécessaire pour éviter une éventuelle redirection inattendue
alert('Vous avez bien déposé votre élément !');
});

Essayer le code

Tant que nous y sommes, essayons les événementsdragenter,dragleaveet un petit oublié qui se nommedragend:

var dropper = document.querySelector('#dropper');
dropper.addEventListener('dragenter', function() {
dropper.style.borderStyle = 'dashed';
});
dropper.addEventListener('dragleave', function() {
dropper.style.borderStyle = 'solid';
});
// Cet événement détecte n'importe quel drag & drop qui se termine, autant le mettre sur « document » :
document.addEventListener('dragend', function() {
alert("Un Drag & Drop vient de se terminer mais l'événement dragend ne sait pas si c'est un succès ou non.");
});

Avant d'essayer ce code, il nous faut réfléchir à une chose : nous appliquons un style lorsque l'élément déplacé entre dans la zone de drop puis nous le retirons lorsqu'il en sort. Cependant, que se passe-t-il si nous relâchons notre élément dans la zone de drop ? Eh bien le style reste en place, car l'élément n'a pas déclenché l'événementdragleave. Il nous faut donc retirer le style en modifiant notre événementdrop:

dropper.addEventListener('drop', function(e) {
e.preventDefault(); // Cette méthode est toujours nécessaire pour éviter une éventuelle redirection inattendue
alert('Vous avez bien déposé votre élément !');
// Il est nécessaire d'ajouter cela car sinon le style appliqué par l'événement « dragenter » restera en place même après un drop :
dropper.style.borderStyle = 'solid';
});

Voilà tout, essayez donc maintenant de déplacer l'élément approprié à la fois dans la zone de drop et en-dehors de cette dernière :

Essayer le code

Terminer un déplacement avec l'objetdataTransfer

L'objetdataTransfera deux rôles importants lors de la fin d'un drag & drop. Le premier consiste à récupérer, grâce à la méthodegetData(), le texte sauvegardé parsetData()lors de l'initialisation du drag & drop.

Ici donc, rien de bien compliqué :

dropZone.addEventListener('drop', function(e) {
alert(e.dataTransfer.getData('text/plain')); // Affiche le contenu du type MIME « text/plain »
});

Quant au deuxième rôle, celui-ci consiste à récupérer les éventuels fichiers qui ont été déposés par l'utilisateur, car, oui, le drag & drop de fichiers est maintenant possible en HTML5 ! Cela fonctionne plus ou moins de la même manière qu'avec une balise<input type="file" />, il nous faut toujours accéder à une propriétéfiles, sauf que celle-ci est accessible dans l'objetdataTransferdans le cadre d'un drag & drop. Exemple :

dropZone.addEventListener('drop', function(e) {
e.preventDefault();
var files = e.dataTransfer.files,
filesLen = files.length,
filenames = "";
for (var i = 0 ; i < filesLen ; i++) {
filenames += '\n' + files[i].name;
}
alert(files.length + ' fichier(s) :\n' + filenames);
});

Essayer une adaptation de ce code

Imaginez maintenant ce qu'il est possible de faire avec ce que vous avez appris dans ce chapitre et le précédent ! Vous pouvez très bien créer un hébergeur de fichiers avec support du drag & drop, prévisualisation des images, upload des fichiers avec une barre de progression, etc. Les possibilités deviennent maintenant extrêmement nombreuses et ne sont pas forcément bien compliquées à mettre en place !

Mise en pratique

Nous allons faire une petite mise en pratique avant de terminer ce chapitre. Notre but ici est de créer une page Web avec deux zones de drop et quelques éléments que l'on peut déplacer d'une zone à l'autre.

Afin de vous éviter de perdre du temps pour pas grand-chose, voici le code HTML à utiliser et le CSS associé :

<div class="dropper">
<div class="draggable">#1</div>
<div class="draggable">#2</div>
</div>
<div class="dropper">
<div class="draggable">#3</div>
<div class="draggable">#4</div>
</div>
.dropper {
margin: 50px 10px 10px 50px;
width: 400px;
height: 250px;
background-color: #555;
border: 1px solid #111;
border-radius: 10px;
transition: all 200ms linear;
}
.drop_hover {
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8) inset;
}
.draggable {
display: inline-block;
margin: 20px 10px 10px 20px;
padding-top: 20px;
width: 80px;
height: 60px;
color: #3D110F;
background-color: #822520;
border: 4px solid #3D110F;
text-align: center;
font-size: 2em;
cursor: move;
transition: all 200ms linear;
user-select: none;
}

Rien de bien compliqué, le code HTML est extrêmement simple et la seule chose à comprendre au niveau du CSS est que la classe.drop_hoversera appliquée à une zone de drop lorsque celle-ci sera survolée par un élément HTML déplaçable.

Alors, par où commencer ? Il nous faut, avant toute chose, une structure pour notre code. Nous avons décidé de partir sur un code basé sur cette forme :

(function() {
var dndHandler = {
// Cet objet est conçu pour être un namespace et va contenir les méthodes que nous allons créer pour notre système de drag & drop
};
// Ici se trouvera le code qui utilisera les méthodes de notre namespace « dndHandler »
})();

Pour commencer à exploiter notre structure, il nous faut une méthode capable de donner la possibilité aux éléments concernés d'être déplacés. Les éléments concernés sont ceux qui possèdent une classe.draggable. Afin de les paramétrer, nous allons créer une méthodeapplyDragEvents()dans notre objetdndHandler:

var dndHandler = {
applyDragEvents: function(element) {
element.draggable = true;
}
};

Ici, notre méthode s'occupe de rendre déplaçables tous les objets qui lui seront passés en paramètres. Cependant, cela ne suffit pas pour deux raisons :

  • Nos zones de drop devront savoir quel est l'élément qui sera déposé, nous allons utiliser une propriétédraggedElementpour sauvegarder ça.

  • Firefox nécessite l'envoi de données avecsetData()pour autoriser le déplacement d'éléments.

Ces deux ajouts ne sont pas bien compliqués à mettre en place :

var dndHandler = {
draggedElement: null, // Propriété pointant vers l'élément en cours de déplacement
applyDragEvents: function(element) {
element.draggable = true;
var dndHandler = this; // Cette variable est nécessaire pour que l'événement « dragstart » accède facilement au namespace « dndHandler »
element.addEventListener('dragstart', function(e) {
dndHandler.draggedElement = e.target; // On sauvegarde l'élément en cours de déplacement
e.dataTransfer.setData('text/plain', ''); // Nécessaire pour Firefox
});
}
};

Ainsi, nos zones de drop n'auront qu'à lire la propriétédraggedElementpour savoir quel est l'élément qui a été déposé.

Passons maintenant à la création de la méthodeapplyDropEvents()qui, comme son nom l'indique, va se charger de gérer les événements des deux zones de drop. Nous allons commencer par gérer les deux événements les plus simples :dragoveretdragleave.

var dndHandler = {
// […]
applyDropEvents: function(dropper) {
dropper.addEventListener('dragover', function(e) {
e.preventDefault(); // On autorise le drop d'éléments
this.className = 'dropper drop_hover'; // Et on applique le style adéquat à notre zone de drop quand un élément la survole
});
dropper.addEventListener('dragleave', function() {
this.className = 'dropper'; // On revient au style de base lorsque l'élément quitte la zone de drop
});
}
};

Notre but maintenant est de gérer le drop d'éléments. Notre système doit fonctionner de la manière suivante :

  • Un élément est « droppé » ;

  • Notre événementdropva alors récupérer l'élément concerné grâce à la propriétédraggedElement;

  • L'élément déplacé est cloné ;

  • Le clone est alors ajouté à la zone de drop concernée ;

  • L'élément d'origine est supprimé ;

  • Et pour terminer, le clone se voit réattribuer les événements qu'il aura perdus du fait que la méthodecloneNode()ne conserve pas les événements.

En soi, ce système n'est pas bien compliqué à réaliser, voici ce que nous vous proposons comme solution :

dropper.addEventListener('drop', function(e) {
var target = e.target,
draggedElement = dndHandler.draggedElement, // Récupération de l'élément concerné
clonedElement = draggedElement.cloneNode(true); // On créé immédiatement le clone de cet élément
target.className = 'dropper'; // Application du style par défaut
clonedElement = target.appendChild(clonedElement); // Ajout de l'élément cloné à la zone de drop actuelle
dndHandler.applyDragEvents(clonedElement); // Nouvelle application des événements qui ont été perdus lors du cloneNode()
draggedElement.parentNode.removeChild(draggedElement); // Suppression de l'élément d'origine
});

Nos deux méthodes sont maintenant terminées, il ne nous reste plus qu'à les appliquer aux éléments concernés :

(function() {
var dndHandler = {
// […]
};
var elements = document.querySelectorAll('.draggable'),
elementsLen = elements.length;
for (var i = 0 ; i < elementsLen ; i++) {
dndHandler.applyDragEvents(elements[i]); // Application des paramètres nécessaires aux éléments déplaçables
}
var droppers = document.querySelectorAll('.dropper'),
droppersLen = droppers.length;
for (var i = 0 ; i < droppersLen ; i++) {
dndHandler.applyDropEvents(droppers[i]); // Application des événements nécessaires aux zones de drop
}
})();

Essayer le code complet

Notre code est terminé, cependant il a un bug majeur que vous avez sûrement pu constater si vous avez essayé de déplacer un élément directement sur un autre élément plutôt que sur une zone de drop. Essayez par exemple de déplacer l'élément #1 sur l'élément #4, vous devriez alors voir quelque chose qui ressemble à l'image suivante.

Le code possède un bug majeur Le code possède un bug majeur

Le code possède un bug majeur

Cela s'explique par le simple fait que l'événementdropest hérité par les éléments enfants, ce qui signifie que les éléments possédant la classe.draggablese comportent alors comme des zones de drop !

Une solution serait d'appliquer un événementdropaux éléments déplaçables refusant tout élément HTML déposé, mais cela obligerait alors l'utilisateur à déposer son élément en faisant bien attention à ne pas se retrouver au-dessus d'un élément déplaçable. Essayez donc pour voir, vous allez rapidement constater que cela peut être vraiment pénible :

applyDragEvents: function(element) {
// […]
element.addEventListener('drop', function(e) {
e.stopPropagation(); // On stoppe la propagation de l'événement pour empêcher la zone de drop d'agir
});
},

Essayer une adaptation de ce code

La solution la plus pratique pour l'utilisateur serait donc de faire en sorte de « remonter » les éléments parents (avecparentNode) jusqu'à tomber sur une zone de drop. Cela est très simple et se fait en trois lignes de code (lignes 7 à 9) :

dropper.addEventListener('drop', function(e) {
var target = e.target,
draggedElement = dndHandler.draggedElement, // Récupération de l'élément concerné
clonedElement = draggedElement.cloneNode(true); // On crée immédiatement le clone de cet élément
while (target.className.indexOf('dropper') == -1) { // Cette boucle permet de remonter jusqu'à la zone de drop parente
target = target.parentNode;
}
target.className = 'dropper'; // Application du style par défaut
clonedElement = target.appendChild(clonedElement); // Ajout de l'élément cloné à la zone de drop actuelle
dndHandler.applyDragEvents(clonedElement); // Nouvelle application des événements qui ont été perdus lors du cloneNode()
draggedElement.parentNode.removeChild(draggedElement); // Suppression de l'élément d'origine
});

Essayer le code complet

Sitarget(qui représente l'élément ayant reçu un élément déplaçable) ne possède pas la classe.dropper, alors la boucle va passer à l'élément parent et va continuer comme cela jusqu'à tomber sur une zone de drop. Vous pouvez d'ailleurs constater que cela fonctionne à merveille !

Voilà qui clôt notre mise en pratique du Drag & Drop, nous espérons qu'elle vous aura satisfait. Voici le code JavaScript complet dans le cas où vous seriez un peu perdus :

(function() {
var dndHandler = {
draggedElement: null, // Propriété pointant vers l'élément en cours de déplacement
applyDragEvents: function(element) {
element.draggable = true;
var dndHandler = this; // Cette variable est nécessaire pour que l'événement « dragstart » ci-dessous accède facilement au namespace « dndHandler »
element.addEventListener('dragstart', function(e) {
dndHandler.draggedElement = e.target; // On sauvegarde l'élément en cours de déplacement
e.dataTransfer.setData('text/plain', ''); // Nécessaire pour Firefox
});
},
applyDropEvents: function(dropper) {
dropper.addEventListener('dragover', function(e) {
e.preventDefault(); // On autorise le drop d'éléments
this.className = 'dropper drop_hover'; // Et on applique le style adéquat à notre zone de drop quand un élément la survole
});
dropper.addEventListener('dragleave', function() {
this.className = 'dropper'; // On revient au style de base lorsque l'élément quitte la zone de drop
});
var dndHandler = this; // Cette variable est nécessaire pour que l'événement « drop » ci-dessous accède facilement au namespace « dndHandler »
dropper.addEventListener('drop', function(e) {
var target = e.target,
draggedElement = dndHandler.draggedElement, // Récupération de l'élément concerné
clonedElement = draggedElement.cloneNode(true); // On créé immédiatement le clone de cet élément
while (target.className.indexOf('dropper') == -1) { // Cette boucle permet de remonter jusqu'à la zone de drop parente
target = target.parentNode;
}
target.className = 'dropper'; // Application du style par défaut
clonedElement = target.appendChild(clonedElement); // Ajout de l'élément cloné à la zone de drop actuelle
dndHandler.applyDragEvents(clonedElement); // Nouvelle application des événements qui ont été perdus lors du cloneNode()
draggedElement.parentNode.removeChild(draggedElement); // Suppression de l'élément d'origine
});
}
};
var elements = document.querySelectorAll('.draggable'),
elementsLen = elements.length;
for (var i = 0; i < elementsLen; i++) {
dndHandler.applyDragEvents(elements[i]); // Application des paramètres nécessaires aux éléments déplaçables
}
var droppers = document.querySelectorAll('.dropper'),
droppersLen = droppers.length;
for (var i = 0; i < droppersLen; i++) {
dndHandler.applyDropEvents(droppers[i]); // Application des événements nécessaires aux zones de drop
}
})();
En résumé
  • Le Drag & Drop est une technologie conçue pour permettre un déplacement natif d'éléments en tous genres (texte, fichiers, etc.).

  • Une action de drag & drop nécessite généralement un transfert de données entre l'élément émetteur et l'élément récepteur, cela se fait généralement par le biais de l'objetdataTransfer.

  • Il est parfaitement possible de déplacer un élément depuis n'importe quel logiciel de votre système d'exploitation (par exemple, l'explorateur de fichiers) jusqu'à une zone d'une page Web prévue à cet effet.