frontend / html
Drag & Drop natif (HTML5 Drag and Drop API)
Explication
Ce que vous allez apprendre
- Enchaîner les événements
dragstart,dragoveretdropdans le bon ordre - Comprendre pourquoi
e.preventDefault()est obligatoire dansdragoverpour autoriser un dépôt - Transporter des données entre éléments avec l'objet
dataTransfer - Réordonner une liste par glisser-déposer sans aucune librairie externe
- Créer une zone de dépôt de fichiers compatible avec l'explorateur du système
Dans quel contexte ?
Le product owner demande de permettre à un utilisateur de réordonner ses tâches par glisser-déposer sur tableau-de-bord.html, sans ajouter une nouvelle dépendance npm juste pour cette fonctionnalité. En s'appuyant sur la Drag and Drop API native du navigateur (draggable="true", dragstart, dragover, drop), la fonctionnalité est réalisable en quelques dizaines de lignes de JavaScript pur, sans aucune librairie tierce à maintenir.
Étape 1 : une interaction qu'on croit réservée aux frameworks
Réordonner une liste de tâches en glissant un élément, ou importer un fichier en le déposant sur une zone de la page : on associe souvent ça à des applications "riches", nécessitant forcément une grosse librairie JavaScript.
Étape 2 : la réalité, une API déjà native
En réalité, le navigateur propose une API native complète pour ça, la Drag and Drop API, disponible depuis HTML5 sans aucune dépendance externe.
Étape 3 : ce n'est pas un événement, mais une séquence
D'abord, il faut comprendre que le glisser-déposer n'est pas un événement unique mais une suite d'étapes, chacune avec son propre événement : dragstart quand on commence à déplacer un élément, dragover pendant qu'on survole une zone cible, et drop quand on relâche.
Étape 4 : à quoi sert chaque étape
Cette chronologie compte, car chaque étape a un rôle précis. Styliser visuellement l'élément qu'on déplace se fait à dragstart, tandis que calculer où l'insérer se fait pendant dragover.
| Événement | Se déclenche quand | Rôle typique |
|---|---|---|
dragstart | On commence à déplacer un élément | Styliser l'élément déplacé, initialiser dataTransfer |
dragover | On survole une zone cible | preventDefault() obligatoire + calcul de position |
drop | On relâche sur une zone valide | Lire dataTransfer, appliquer le changement |
dragend | Le glissé se termine (succès ou non) | Nettoyer les classes CSS temporaires |
Étape 5 : le piège qui bloque absolument tout
Il reste LE piège classique de cette API : par défaut, le navigateur refuse purement et simplement tout dépôt, avec un curseur "interdit". Sans appeler e.preventDefault() dans le gestionnaire de dragover, l'événement drop ne se déclenche JAMAIS, quoi qu'on fasse d'autre.
C'est un comportement contre-intuitif à connaître par cœur avant d'écrire la moindre ligne de code.
Piège fréquent
Sans e.preventDefault() dans le gestionnaire de dragover, l'événement drop ne se déclenchera JAMAIS, même si tout le reste du code est correct. C'est l'erreur numéro un des débutants sur cette API : le curseur affiche systématiquement l'icône "interdit" et rien ne se passe au relâchement.
Étape 6 : dataTransfer, le vrai transporteur
Une fois ce piège évité, reste un dernier point : l'objet dataTransfer, disponible sur chaque événement de drag, ne sert pas qu'à réordonner des éléments internes. Il peut aussi transporter de vrais fichiers, exactement comme un <input type="file">.
C'est ce qui permet à une zone de dépôt de recevoir des fichiers glissés depuis l'explorateur de l'utilisateur, un cas d'usage très fréquent pour l'upload d'images.
Bonne pratique
Gérez toujours dragenter/dragleave en plus de dragover/drop pour donner un retour visuel clair (une bordure en surbrillance, par exemple) pendant que l'utilisateur survole la zone de dépôt — sans ce feedback, l'utilisateur ne sait pas si son geste a été détecté.
Et ensuite ?
Une fois le drag & drop maîtrisé, la prochaine étape consiste à apprendre à stocker des données côté navigateur avec Web Storage et IndexedDB.
Commandes & code
Drag & Drop natif (HTML5 Drag and Drop API)
Réordonner, déplacer ou importer des fichiers sans librairie, avec l'API navigateur native.
<!-- Liste réordonnable en drag & drop pur HTML/JS -->
<ul id="liste-taches">
<li draggable="true" data-id="1">Écrire la doc</li>
<li draggable="true" data-id="2">Corriger le bug #42</li>
<li draggable="true" data-id="3">Relire la PR</li>
</ul>
<!-- Zone de dépôt pour fichiers -->
<div id="zone-depot" class="zone-depot">
Glissez des fichiers ici
</div>// --- Réordonnancement de liste ---
const liste = document.getElementById("liste-taches");
let elementTraine = null;
liste.addEventListener("dragstart", (e) => {
elementTraine = e.target;
e.dataTransfer.effectAllowed = "move";
// dataTransfer transporte des données entre source et cible, même
// entre deux documents/fenêtres différents
e.dataTransfer.setData("text/plain", e.target.dataset.id);
// Léger délai avant de styliser, sinon l'image fantôme de drag
// reprend déjà le style "en cours de drag"
requestAnimationFrame(() => e.target.classList.add("en-cours-drag"));
});
liste.addEventListener("dragend", (e) => {
e.target.classList.remove("en-cours-drag");
elementTraine = null;
});
liste.addEventListener("dragover", (e) => {
// OBLIGATOIRE : sans preventDefault ici, "drop" ne se déclenche jamais
e.preventDefault();
e.dataTransfer.dropEffect = "move";
const apres = elementApres(liste, e.clientY);
if (apres == null) {
liste.appendChild(elementTraine);
} else {
liste.insertBefore(elementTraine, apres);
}
});
function elementApres(container, y) {
const elements = [...container.querySelectorAll("li:not(.en-cours-drag)")];
return elements.reduce((plusProche, enfant) => {
const rect = enfant.getBoundingClientRect();
const decalage = y - rect.top - rect.height / 2;
if (decalage < 0 && decalage > plusProche.decalage) {
return { decalage, element: enfant };
}
return plusProche;
}, { decalage: Number.NEGATIVE_INFINITY }).element;
}// --- Import de fichiers par glisser-déposer ---
const zoneDepot = document.getElementById("zone-depot");
["dragenter", "dragover"].forEach((evt) => {
zoneDepot.addEventListener(evt, (e) => {
e.preventDefault(); // nécessaire aussi pour autoriser le drop
zoneDepot.classList.add("survol-actif");
});
});
["dragleave", "drop"].forEach((evt) => {
zoneDepot.addEventListener(evt, () => {
zoneDepot.classList.remove("survol-actif");
});
});
zoneDepot.addEventListener("drop", (e) => {
e.preventDefault();
const fichiers = [...e.dataTransfer.files];
// dataTransfer.files : FileList, identique à <input type="file">
const images = fichiers.filter((f) => f.type.startsWith("image/"));
images.forEach((fichier) => {
const lecteur = new FileReader();
lecteur.onload = () => {
const img = document.createElement("img");
img.src = lecteur.result; // data: URL en base64
img.alt = fichier.name;
zoneDepot.appendChild(img);
};
lecteur.readAsDataURL(fichier);
});
});.en-cours-drag { opacity: 0.4; }
.zone-depot { border: 2px dashed #999; padding: 32px; text-align: center; }
.zone-depot.survol-actif { border-color: #2563eb; background: #eff6ff; }Résumé
preventDefault()surdragover(etdrop) est obligatoire, sinon aucun drop n'est jamais accepté.dataTransfertransporte aussi bien du texte que des fichiers réels (dataTransfer.files).- Le calcul de position d'insertion se fait à la main via
getBoundingClientRect(), l'API ne l'offre pas. FileReader.readAsDataURLconvertit un fichier déposé en URL affichable directement dans une<img>.
Exercices pratiques
Mission : débugger le glisser-déposer muet de tableau-de-bord.html
Objectif : Diagnostiquer pourquoi un glisser-déposer de tâches ne déclenche jamais l'événement drop, puis le corriger.
Contexte
Sur tableau-de-bord.html, un développeur a implémenté le réordonnancement de tâches par glisser-déposer. Chaque <li draggable="true"> a bien un écouteur dragstart et un écouteur drop, mais rien ne se passe jamais au relâchement : le curseur affiche systématiquement l'icône "interdit".
Ta mission : identifier la cause exacte et corriger le code.