Retour au cours

frontend / html

Drag & Drop natif (HTML5 Drag and Drop API)

Leçon 151 exercice

Explication

Ce que vous allez apprendre

  • Enchaîner les événements dragstart, dragover et drop dans le bon ordre
  • Comprendre pourquoi e.preventDefault() est obligatoire dans dragover pour 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énementSe déclenche quandRôle typique
dragstartOn commence à déplacer un élémentStyliser l'élément déplacé, initialiser dataTransfer
dragoverOn survole une zone ciblepreventDefault() obligatoire + calcul de position
dropOn relâche sur une zone valideLire dataTransfer, appliquer le changement
dragendLe 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.

html
<!-- 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>
js
// --- 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;
}
js
// --- 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);
    });
});
css
.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() sur dragover (et drop) est obligatoire, sinon aucun drop n'est jamais accepté.
  • dataTransfer transporte 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.readAsDataURL convertit un fichier déposé en URL affichable directement dans une <img>.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →