Manipuler la structure du DOM

Présentation

Une fois qu'une balise est sélectionnée, la suite consiste souvent à explorer la structure du DOM ou à la modifier. Cette page regroupe deux familles de techniques. D'abord, naviguer dans la hiérarchie à partir d'un élément déjà ciblé. Ensuite, créer, insérer, déplacer, remplacer ou supprimer des balises pour faire évoluer la page.

Déplacer, ajouter ou supprimer des nœuds

Manipuler la structure HTML revient à créer des nœuds, puis à les insérer, les déplacer, les supprimer ou les remplacer dans le DOM.

Créer un élément avec document.createElement()

La méthode createElement() crée un nouvel élément HTML. À ce stade, l'élément existe en mémoire, mais n'apparaît pas encore dans la page.


                    const conteneur = document.querySelector('#conteneur');

                    // Créer une balise "section".
                    const section = document.createElement('section');

                    // Insérer la balise dans le DOM.
                    conteneur.append(texte);
                

Créer un nœud texte avec document.createTextNode()

document.createTextNode() crée un nœud texte pur, sans balise. Ce texte est inséré tel quel dans le DOM et n'est jamais interprété comme du HTML. Ensuite, ce nœud texte peut être réutilisé et modifié, par exemple via nodeValue.


                    const conteneur = document.querySelector('#conteneur');

                    // Créer un nœud texte réutilisable.
                    const texte = document.createTextNode('Chargement...');

                    // Insérer le texte dans le DOM.
                    conteneur.append(texte);

                    // Plus tard, mettre à jour exactement ce texte, sans rechercher d'élément.
                    texte.nodeValue = 'Terminé';
                

Ajouter du contenu dans le DOM avec append() et prepend()

Les méthodes append() et prepend() servent à ajouter du contenu à l'intérieur de l'élément sur lequel elles sont appliquées.

  • append() : ajoute du contenu à la fin du conteneur,
  • prepend() : ajoute du contenu au début du conteneur.

Ces méthodes peuvent recevoir plusieurs arguments. Chaque argument peut être soit un nœud (ex.: une balise créée avec createElement()), soit une chaîne de caractères. Lorsqu'une chaîne est fournie, le navigateur la transforme automatiquement en nœud texte. Le contenu n'est donc jamais interprété comme du HTML. Autrement dit, si la chaîne contient des balises, celles-ci sont échappées et affichées telles quelles dans la page (ex.: la chaîne "<b>Salut</b>" s'affichera telle quelle à l'écran, avec les balises visibles, et le mot "Salut" ne sera pas en gras).

Dans l'exemple suivant, nous illustrons le fonctionnement de prepend(). Cette méthode permet d'insérer un ou plusieurs contenus au début d'un élément parent. Chaque argument fourni est ajouté dans l'ordre et devient un enfant direct du parent.

Code HTML :


                    <section id="sectionTest">
                        <p>Un paragraphe...</p>
                    </section>
                

Code Javascript :


                    const sectionTest = document.querySelector('#sectionTest');

                    // Création d'un titre <h2>.
                    const h2 = document.createElement('h2');
                    h2.textContent = "Titre de test";

                    // Création d'un paragraphe.
                    const paragraphe = document.createElement('p');
                    paragraphe.textContent = "Paragraphe de test";

                    // prepend insère les éléments au début du conteneur.
                    // Chaque argument devient un enfant direct de <section>.
                    sectionTest.prepend(
                        h2, // <h2>Titre de test</h2>
                        paragraphe, // <p>Paragraphe de test</p>
                        "Texte ajouté en tant que nœud texte"
                    );
                

Code HTML après l'exécution du script :


                    <section id="sectionTest">
                        <h2>Titre de test</h2>
                        <p>Paragraphe de test</p>
                        Texte ajouté en tant que nœud texte
                        <p>Un paragraphe...</p>
                    </section>
                

Notez qu'un même nœud ne peut exister qu'à un seul endroit à la fois. Dès qu'une méthode reçoit un nœud existant du DOM, ce nœud est déplacé, jamais copié.

Approche historique avec appendChild()

Avant l'apparition de append() et prepend(), l'ajout de contenu se faisait principalement avec appendChild(). Cette méthode est toujours valide, mais elle est plus bas niveau et plus restrictive.

appendChild() permet d'ajouter un seul nœud à la fois à la fin d'un élément parent. Contrairement à append(), elle n'accepte pas les chaînes de caractères. Si vous souhaitez ajouter du texte, vous devez créer explicitement un nœud texte avec document.createTextNode().

Code Javascript :


                        const parent = document.querySelector('main');

                        // Création d'une nouvelle balise <section>.
                        const section = document.createElement('section');

                        // Ajout de la section à la fin de <main>.
                        // Un seul nœud est ajouté par appel.
                        parent.appendChild(section);
                    

Si vous deviez ajouter plusieurs éléments avec cette méthode, il faudrait soit appeler appendChild() plusieurs fois, soit passer par un DocumentFragment pour regrouper les ajouts. C'est précisément pour simplifier ce type de cas que append() et prepend() ont été introduits.

Insérer un élément du DOM à côté d'un autre avec before() et after()

Les méthodes before() et after() fonctionnent sur le même principe que append() et prepend(). Elles acceptent les mêmes types d'arguments et se comportent de la même manière.

La différence ne concerne donc pas le contenu, mais l'endroit où il est inséré. Ici, le contenu n'est pas ajouté à l'intérieur de l'élément, mais juste avant ou juste après celui-ci, au même niveau dans le DOM.

  • before() : insère le contenu avant l'élément de référence.
  • after() : insère le contenu après l'élément de référence.

Comme pour append() et prepend(), ces méthodes peuvent recevoir plusieurs arguments. Chaque argument peut être un nœud ou une chaîne de caractères. Les chaînes sont automatiquement transformées en nœuds texte et ne sont jamais interprétées comme du HTML.

Dans l'exemple suivant, nous insérons plusieurs contenus avant une section existante. Tous les éléments ajoutés deviennent des frères de la section de référence, et non ses enfants.

Code HTML :


                    <main>
                        <section id="sectionRef">
                            Contenu de la section de référence
                        </section>
                    </main>
                

Code Javascript :


                    const sectionDeRef = document.querySelector('#sectionRef');

                    // Création d'une nouvelle section.
                    const nouvelleSection = document.createElement('section');
                    nouvelleSection.textContent = "Nouvelle section insérée avant";

                    // Création d'un paragraphe.
                    const paragraphe = document.createElement('p');
                    paragraphe.textContent = "Paragraphe ajouté avant la section";

                    // Insertion avant la section de référence.
                    // Chaque argument est ajouté dans l'ordre.
                    sectionDeRef.before(
                        nouvelleSection,
                        paragraphe,
                        "Texte ajouté sous forme de nœud texte"
                    );
                

Structure du DOM après exécution :


                    <main>
                        <section>Nouvelle section insérée avant</section>
                        <p>Paragraphe ajouté avant la section</p>
                        Texte ajouté sous forme de nœud texte
                        <section id="sectionRef">
                            Contenu de la section de référence
                        </section>
                    </main>
                

Approche historique avec insertBefore()

Avant l'introduction des méthodes modernes before() et after(), l'insertion d'un élément à un endroit précis dans le DOM se faisait principalement avec insertBefore(). Cette méthode reste fonctionnelle et peut encore être rencontrée dans du code plus ancien.

Le principe est le même que pour before(), mais l'appel est plus verbeux. La méthode s'exécute sur le parent et nécessite de fournir explicitement l'élément de référence devant lequel on souhaite insérer le nouveau nœud.

Comme appendChild(), insertBefore() n'accepte qu'un seul nœud à la fois et ne permet pas de passer directement une chaîne de caractères. Toute insertion de texte nécessite donc la création préalable d'un nœud texte.

Code Javascript :


                        const parent = document.querySelector('main');

                        // :scope est un pseudo-sélecteur CSS.
                        // Il représente "l'élément sur lequel on fait la recherche".
                        // Ici, comme on appelle querySelector() sur parent,
                        // :scope correspond donc à la balise <main> stockée dans parent.
                        //
                        // Pourquoi l'utiliser ?
                        // Sans :scope, le sélecteur '> section' n'est pas valable tout seul en querySelector().
                        // Et si on écrivait simplement 'section', on risquerait de sélectionner
                        // une section plus profonde (ex. une section dans une autre section),
                        // alors qu'ici on veut la section enfant DIRECTE de <main>.
                        const sectionDeRef = parent.querySelector(':scope > section');

                        // Créer la nouvelle section à insérer.
                        const section = document.createElement('section');

                        // insertBefore() s'appelle sur le parent.
                        // On insère "section" juste avant "sectionDeRef" dans <main>.
                        parent.insertBefore(section, sectionDeRef);
                    

Notez qu'à l'époque, il n'existait pas de méthode insertAfter(). Il n'y avait donc pas d'équivalent direct à after().

Pour insérer un élément après un autre, on devait passer par une astuce basée sur la structure du DOM. L'idée était d'insérer le nouvel élément avant le frère suivant de l'élément de référence.


                        // Insertion après l'élément de référence.
                        parent.insertBefore(
                            nouvelElement,
                            elementDeRef.nextSibling
                        );
                    

Cette écriture produit le même résultat que sectionDeRef.after(nouvelleSection). Elle illustre bien pourquoi les méthodes modernes before() et after() sont aujourd'hui privilégiées pour leur clarté.

Remplacer un élément du DOM avec replaceWith()

La méthode replaceWith() s'appelle directement sur l'élément à remplacer. Elle permet de retirer cet élément du DOM et de le remplacer par un ou plusieurs nouveaux contenus, exactement au même endroit dans la structure.

Comme append(), prepend(), before() et after(), cette méthode accepte aussi bien des nœuds que des chaînes de caractères. Les chaînes sont automatiquement transformées en nœuds texte.

L'élément remplacé est définitivement retiré du DOM. Il n'est ni copié ni conservé ailleurs.

Code Javascript :


                    const ancienElement = document.querySelector('#section-introduction');

                    const nouvelElement = document.createElement('div');
                    nouvelElement.textContent = "Nouvelle section";

                    // Remplace complètement l'ancien élément par le nouveau.
                    ancienElement.replaceWith(nouvelElement);
                

Approche historique avec replaceChild()

Dans du code plus ancien, vous rencontrerez souvent replaceChild(). Cette méthode s'appelle sur le parent et demande le nouvel élément puis l'ancien.

Comme appendChild()et insertBefore(), replaceChild() n'accepte qu'un seul nœud à la fois et ne permet pas de passer directement une chaîne de caractères. Toute insertion de texte nécessite donc la création préalable d'un nœud texte.


                        const ancienElement = document.querySelector('#section-introduction');
                        const parent = ancienElement.parentNode;

                        const nouvelElement = document.createElement('div');
                        parent.replaceChild(nouvelElement, ancienElement);
                    

Supprimer un élément du DOM avec remove()

La méthode remove() s'appelle directement sur l'élément à supprimer. Elle permet de retirer cet élément du DOM sans avoir besoin de passer par son parent.

L'élément est alors complètement supprimé de la page. Il n'est ni masqué ni désactivé, il disparaît réellement de la structure du DOM.

Cette méthode est l'équivalent "suppression" de replaceWith(), mais sans fournir de nouvel élément pour prendre la place.

Code Javascript :


                    const sectionIntro = document.querySelector('#section-introduction');

                    // Suppression définitive de l'élément du DOM.
                    sectionIntro.remove();
                

Approche historique avec removeChild()

Avant l'introduction de remove(), la suppression d'un élément se faisait avec removeChild(). Cette méthode s'appelle sur le parent et nécessite de fournir explicitement l'élément à supprimer.


                        const sectionIntro = document.querySelector('#section-introduction');
                        const parent = sectionIntro.parentNode;

                        // Suppression de l'élément via son parent.
                        parent.removeChild(sectionIntro);
                    

Insérer une chaîne HTML interprétée à un endroit précis du DOM avec insertAdjacentHTML()

La famille de méthodes insertAdjacent... est apparue à l'origine dans Internet Explorer. À une époque où le DOM proposait surtout des méthodes bas niveau comme appendChild() ou insertBefore(), ces méthodes ont rencontré un certain succès.

Elles offraient une écriture plus directe pour insérer du contenu autour d'un élément existant, sans devoir manipuler explicitement le parent et les frères dans l'arbre DOM. Devant leur adoption massive par les développeurs, les autres navigateurs ont fini par les intégrer à leur tour.

Aujourd'hui, le DOM propose aussi des méthodes plus lisibles et plus explicites. Pour insérer des nœuds ou du texte, on utilise souvent append(), prepend(), before() et after().

Les méthodes insertAdjacent... existent toujours et restent valables. Trois variantes sont disponibles.

  • insertAdjacentHTML(position, htmlString) Insérer une chaîne interprétée comme du HTML. Le navigateur crée les nœuds correspondants.
  • insertAdjacentElement(position, element) Insérer un nœud Element déjà créé avec createElement().
  • insertAdjacentText(position, text) Insérer du texte brut. Le navigateur crée automatiquement un nœud texte.

Dans la pratique, ces variantes recoupent souvent des méthodes modernes déjà vues. Une exception reste toutefois très présente dans du code actuel insertAdjacentHTML().

Les positions d'insertion

Toutes les méthodes insertAdjacent... reposent sur une position fournie sous forme de texte. Quatre positions sont possibles.

  • beforebegin Insérer le contenu juste avant la balise (équivalent moderne element.before(...))
  • afterbegin Insérer le contenu juste après l'ouverture de la balise, donc au début de son contenu (équivalent moderne element.prepend(...))
  • beforeend Insérer le contenu juste avant la fermeture de la balise, donc à la fin de son contenu (équivalent moderne element.append(...))
  • afterend Insérer le contenu juste après la balise (équivalent moderne element.after(...))

Deux positions insèrent à l'intérieur de l'élément afterbegin et beforeend. Deux autres insèrent à l'extérieur beforebegin et afterend.

Pourquoi insertAdjacentHTML() reste utile

insertAdjacentHTML() permet d'insérer une chaîne contenant du HTML. Le navigateur interprète cette chaîne et crée directement les nœuds correspondants dans le DOM.

Son intérêt apparaît lorsqu'on le compare à une écriture comme element.innerHTML += "...". Cette écriture implique une réassignation complète de innerHTML.

Lors d'une telle réassignation, le navigateur supprime tous les nœuds enfants existants, puis les recrée à partir de la nouvelle chaîne HTML. Les écouteurs d'événements ajoutés avec addEventListener() sont perdus. Les références JavaScript vers les anciens nœuds deviennent invalides. Certains états internes peuvent également être réinitialisés, comme la valeur d'un champ de formulaire ou la position du curseur.

À l'inverse, insertAdjacentHTML() insère uniquement le fragment demandé, sans supprimer ni reconstruire le reste du contenu. Les nœuds existants, leurs événements et leurs états sont conservés.

Dans l'exemple suivant, un nouvel élément est inséré au début d'une liste sans devoir créer chaque balise manuellement et sans reconstruire le contenu existant.

Code HTML :


                    <ul id="notifications">
                        <li>Message reçu</li>
                    </ul>
                

Code Javascript :


                    const liste = document.querySelector('#notifications');

                    // Insertion d'un nouvel item en tête de liste.
                    // La chaîne est interprétée comme du HTML.
                    liste.insertAdjacentHTML(
                        'afterbegin',
                        '<li>Nouveau commentaire</li>'
                    );
                

Code HTML après exécution du script :


                    <ul id="notifications">
                        <li>Nouveau commentaire</li>
                        <li>Message reçu</li>
                    </ul>
                

Attention, comme toute insertion de HTML interprété, insertAdjacentHTML() doit être utilisé avec précaution. Si la chaîne provient d'une source externe, elle peut contenir du code malveillant. Dans ce cas, préférer textContent ou la création explicite de nœuds avec createElement().

Optimiser l'ajout de nombreux éléments dans le DOM

Ajouter un élément dans le DOM n'est pas "gratuit". Le navigateur peut devoir mettre à jour l'affichage. Si on répète cette opération des dizaines ou des centaines de fois, on peut ralentir inutilement la page.

Une bonne habitude consiste à préparer un maximum de choses en JavaScript, puis à faire le moins d'insertions possibles dans le DOM.

Construire d'abord la structure en JavaScript, puis l'insérer en une fois

Dans l'exemple suivant, nous créons une liste ul avec trois éléments li. L'idée est de construire la liste complète en mémoire, puis d'ajouter la liste au DOM seulement à la fin.

Code Javascript :


                    // Sélectionner la balise dont l'attribut id vaut "conteneur".
                    // C'est dans cet élément que nous allons insérer la nouvelle liste <ul>.
                    const conteneur = document.querySelector('#conteneur');

                    // Construire la structure en mémoire.
                    const ul = document.createElement('ul');

                    const li1 = document.createElement('li');
                    li1.textContent = 'Élément 1';

                    const li2 = document.createElement('li');
                    li2.textContent = 'Élément 2';

                    const li3 = document.createElement('li');
                    li3.textContent = 'Élément 3';

                    // Insérer les balises "li" dans la balise "ul";
                    ul.append(li1, li2, li3);

                    // Insérer la structure complète en une seule fois.
                    conteneur.append(ul);
                

Ici, on ne fait qu'une insertion "importante" dans le DOM visible, au moment où on ajoute le ul. Tout le reste a été préparé avant.

Construire en mémoire avec DocumentFragment

Parfois, on ne veut pas créer un nouveau conteneur complet, mais ajouter une série d'éléments au même endroit. C'est le cas classique d'une liste ul déjà présente dans la page, à laquelle on veut ajouter beaucoup de li. On ne va pas placer une div dans une ul juste pour "porter" des li.

Dans ce contexte, DocumentFragment sert de zone de préparation. On y ajoute des éléments comme si c'était un parent normal, puis on insère le fragment dans le DOM en une seule opération. Le fragment ne correspond à aucune balise HTML et n'apparaît jamais à l'écran.

Dans l'exemple suivant, nous préparons une série de li dans un fragment, puis nous l'ajoutons en une fois dans la liste.

Code Javascript :


                    // Sélectionner la balise <ul> située dans la balise dont l'attribut id vaut "conteneur".
                    // C'est dans cet élément que nous allons insérer tous nos <li> en une seule opération. 
                    const ul = document.querySelector('#conteneur ul');

                    // Créer un conteneur temporaire en mémoire.
                    const fragment = document.createDocumentFragment();

                    // Créer et insérer 100 balises "li" au fragment.
                    for (let i = 1; i <= 100; i++)
                    {
                        // Créer une balise "li".
                        const li = document.createElement('li');

                        // Ajouter du contenu textuel à balise "li".
                        li.textContent = `Élément ${i}`;

                        // Ajouter la balise "li" au framgent.
                        // Tant qu'on ajoute au fragment, on ne touche pas au DOM visible.
                        fragment.appendChild(li);
                    }

                    // Insertion finale en une seule opération.
                    ul.appendChild(fragment);
                

Tant que les éléments sont ajoutés au fragment, ils ne sont pas "dans la page". C'est l'ajout final du fragment dans la ul qui insère tous les li d'un coup.

Exercices

JS: Manipuler la structure du DOM - Exo 01

Cet exercice porte sur la création dynamique d'éléments HTML et leur insertion dans le DOM. Le but est de construire un système de notifications toast qui apparaissent à l'écran lorsque l'utilisateur clique sur un bouton, et qui peuvent être fermées manuellement.

Attendu

Trois boutons permettent de créer différents types de notifications : Succès, Erreur, et Info. Lorsque l'utilisateur clique sur un bouton, une notification du type correspondant apparaît dans un conteneur dédié en haut à droite de la page. Chaque notification affiche un message et un bouton × pour la fermer. Les notifications s'empilent les unes au-dessus des autres. L'utilisateur peut fermer une notification en cliquant sur son bouton ×.

Structure

Créer un dossier nommé Exo-01-manipuler-structure-dom-toast-notifications, puis organiser les fichiers et dossiers en respectant l'arborescence suivante :


                    📁 Exo-01-manipuler-structure-dom-toast-notifications/
                    ├── 📄 index.html
                    ├── 📁 css/
                    │   └── 📄 style.css
                    └── 📁 js/
                        ├── 📄 app.js
                        └── 📁 modules/
                            └── 📄 gestionnaireToasts.js
                

Fichiers fournis

Copier les fichiers suivants à l'identique.

index.html


                    <!DOCTYPE html>
                    <html lang="fr">
                    <head>
                        <meta charset="UTF-8">
                        <meta name="viewport" content="width=device-width, initial-scale=1.0">
                        <link rel="stylesheet" href="./css/style.css">
                        <title>JS: Manipuler la structure du DOM - Exo 01</title>
                    </head>
                    <body>
                        <main>
                            <h1>Système de notifications</h1>

                            
                            <div id="boutons-conteneur" class="boutons-conteneur">
                                <button id="btn-succes" class="btn btn-succes" data-type="succes"">
                                    <span aria-hidden="true">✓</span> Notification Succès
                                </button>
                                <button id="btn-erreur" class="btn btn-erreur" data-type="erreur"">
                                    <span aria-hidden="true">✗</span> Notification Erreur
                                </button>
                                <button id="btn-info" class="btn btn-info" data-type="info"">
                                    <span aria-hidden="true">ℹ</span> Notification Info
                                </button>
                            </div>
                        </main>

                        <div id="toasts-conteneur" class="toasts-conteneur">
                            <!-- Les notifications seront insérées ici dynamiquement -->
                        </div>

                        <script src="./js/app.js" type="module"></script>
                    </body>
                    </html>
                

css/style.css


                    /* ANIMATIONS */
                    @keyframes slideIn
                    {
                        from
                        {
                            transform: translateX(400px);
                            opacity: 0;
                        }
                        to
                        {
                            transform: translateX(0);
                            opacity: 1;
                        }
                    }

                    /* VARIABLES */
                    :root
                    {
                        --fond-principal: #0e1319;
                        --fond-secondaire: #151c24;
                        --texte-principal: #e7e1ca;
                        --texte-secondaire: #b9b39f;
                        --couleur-succes: #4caf50;
                        --couleur-erreur: #f44336;
                        --couleur-info: #2196f3;

                        --espace-s: 0.5rem;
                        --espace-m: 1rem;
                        --espace-l: 2rem;

                        --police-corps: Arial, sans-serif;

                        --rayon-s: 0.5rem;
                        --bordure: 1px solid rgba(231, 225, 202, 0.15);
                    }

                    /* RESET */
                    html, body, main, div, h1, button
                    {
                        margin: 0;
                        padding: 0;
                        box-sizing: border-box;
                    }

                    /* BASE */
                    html
                    {
                        font-family: var(--police-corps);
                        color: var(--texte-principal);
                        background-color: var(--fond-principal);
                    }

                    body
                    {
                        min-height: 100vh;
                    }

                    main
                    {
                        margin: 0 auto;
                        padding: var(--espace-l);
                        width: min(700px, 100%);
                        display: flex;
                        flex-direction: column;
                        gap: var(--espace-l);
                    }

                    h1
                    {
                        text-align: center;
                    }

                    /* BOUTONS */
                    .boutons-conteneur
                    {
                        display: flex;
                        flex-wrap: wrap;
                        gap: var(--espace-m);
                        justify-content: center;
                    }

                    .btn
                    {
                        border: none;
                        border-radius: var(--rayon-s);
                        padding: 0.75rem 1.5rem;
                        font-weight: bold;
                        cursor: pointer;
                        font-size: 1rem;
                        transition: opacity 0.2s;
                        color: white;
                    }

                    .btn:hover
                    {
                        opacity: 0.9;
                    }

                    .btn:active
                    {
                        opacity: 0.8;
                    }

                    .btn-succes
                    {
                        background: var(--couleur-succes);
                    }

                    .btn-erreur
                    {
                        background: var(--couleur-erreur);
                    }

                    .btn-info
                    {
                        background: var(--couleur-info);
                    }

                    /* CONTENEUR TOASTS */
                    .toasts-conteneur
                    {
                        position: fixed;
                        top: var(--espace-m);
                        right: var(--espace-m);
                        display: flex;
                        flex-direction: column;
                        gap: var(--espace-m);
                        z-index: 1000;
                        max-width: 350px;
                    }

                    /* TOAST */
                    .toast
                    {
                        border-radius: var(--rayon-s);
                        padding: var(--espace-m);
                        background: var(--fond-secondaire);
                        border-left: 4px solid;
                        display: flex;
                        align-items: center;
                        gap: var(--espace-m);
                        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
                        animation: slideIn 0.3s ease-out;
                    }

                    .toast-succes
                    {
                        border-left-color: var(--couleur-succes);
                    }

                    .toast-erreur
                    {
                        border-left-color: var(--couleur-erreur);
                    }

                    .toast-info
                    {
                        border-left-color: var(--couleur-info);
                    }

                    .toast-icone
                    {
                        font-size: 1.5rem;
                        flex-shrink: 0;
                    }

                    .toast-succes .toast-icone
                    {
                        color: var(--couleur-succes);
                    }

                    .toast-erreur .toast-icone
                    {
                        color: var(--couleur-erreur);
                    }

                    .toast-info .toast-icone
                    {
                        color: var(--couleur-info);
                    }

                    .toast-message
                    {
                        flex: 1;
                        line-height: 1.4;
                    }

                    .toast-fermer
                    {
                        border: none;
                        background: transparent;
                        color: var(--texte-secondaire);
                        font-size: 1.5rem;
                        cursor: pointer;
                        padding: 0;
                        line-height: 1;
                        transition: color 0.2s;
                        flex-shrink: 0;
                    }

                    .toast-fermer:hover
                    {
                        color: var(--texte-principal);
                    }

                    /* RESPONSIVE */
                    @media (max-width: 520px)
                    {
                        main
                        {
                            padding: var(--espace-m);
                        }

                        .toasts-conteneur
                        {
                            left: var(--espace-m);
                            right: var(--espace-m);
                            max-width: none;
                        }
                    }
                

Instructions

Le travail se fait dans js/app.js et js/modules/gestionnaireToasts.js. Le système de notifications doit être construit progressivement avec des console.log(), afin de valider chaque étape avant de passer à la suivante.

Dans cet exercice, vous allez créer des notifications en JavaScript, les insérer dans le DOM, et permettre à l'utilisateur de les fermer. Pour gérer les clics, vous allez profiter du bubbling (l'événement remonte de l'élément cliqué vers ses parents) et placer un seul écouteur d'événement sur le conteneur qui contient les boutons, au lieu d'en ajouter un sur chaque bouton. Les boutons qui déclenchent la création auront chacun un attribut data-type (succès, erreur, info) afin d'indiquer quel type de notification il faut générer au clic de l'utilisateur.

Étape 01 : Initialisation du module

  1. Créer le fichier js/modules/gestionnaireToasts.js.
  2. En haut du fichier, créer une variable initialisée à null :
    • conteneurToastsElem : contiendra la référence vers le conteneur où les notifications seront insérées
  3. À la suite de cette variable, créer une fonction nommée initialiserGestionnaireToasts. Cette fonction reçoit un paramètre qui contient le sélecteur CSS sous forme de texte. Ce sélecteur sera fourni au moment où app.js appellera la fonction afin que le module puisse retrouver, dans la page, le conteneur HTML des toasts :
    • selecteurCssConteneurToasts
  4. Dans initialiserGestionnaireToasts, sélectionner le conteneur HTML des toasts à l'aide de la méthode querySelector et du sélecteur CSS passé en arguments selecteurCssConteneurToasts, puis stocker la référence dans la variable conteneurToastsElem.
  5. Ajouter un test de sécurité. Si la variable vaut null, afficher le message Gestionnaire de toasts non initialisé : conteneur HTML des toast introuvable. avec console.warn(), puis arrêter la fonction immédiatement avec return. Ce contrôle évite d'exécuter la suite du script su un élément inexistant, ce qui provoquerait une erreur et bloquerait tous les autres scripts de la page.
  6. Juste après ce test de sécurité, ajouter un console.log() temporaire affichant Gestionnaire de toasts initialisé avec succès. Ce message servira de repère lors des tests pour confirmer que tous les éléments ont été correctement sélectionnés.
  7. Exporter la fonction initialiserGestionnaireToasts en utilisant un export nommé, afin qu'elle puisse être importée explicitement dans app.js.

Étape 02 : Configuration dans app.js

  1. Créer le fichier js/app.js.
  2. Dans js/app.js, importer la fonction initialiserGestionnaireToasts depuis ./modules/gestionnaireToasts.js.
  3. Appeler initialiserGestionnaireToasts en lui passant le sélecteurs CSS du conteneur de toasts dont l'attribut id vaut toasts-conteneur
  4. Tester :
    • recharger la page
    • ouvrir la console
    • vérifier que le message Gestionnaire de toasts initialisé avec succès s'affiche

Étape 03 : Création d'une notification (structure HTML)

  1. Dans js/modules/gestionnaireToasts.js, créer une fonction nommée creerToast. Cette fonction reçoit deux paramètres :
    • type : le type de notification (succes, erreur ou info)
    • message : le texte de la notification à afficher
    Le rôle de cette fonction est de créer et de retourner un élément HTML complet représentant une notification, prêt à être inséré dans le DOM.
  2. Dans creerToast, créer un élément div avec la méthode createElement(). Stocker cet élément dans une constante nommée divToast. Cette div servira de conteneur principal pour la notification.
  3. Ajouter des classes CSS avec classList à divToast pour séparer les responsabilités.
    • Ajouter la classe toast. Elle contient le style commun à tous les toasts mise en page, espacements, bordures, animation, etc.
    • Ajouter une deuxième classe construite à partir du paramètre type. Construire son nom en concaténant le préfixe toast- avec la valeur de type. Cette classe sert à appliquer une variante de style selon le type. En fonction du type, on peut donc obtenir 3 classes CSS différentes : toast-succes, toast-erreur et toast-info.
    • Ajouter la classe js-toast. Elle sert uniquement au JavaScript. Comme l'écouteur click est placé sur le conteneur des toasts (délégation), cette classe reste un repère stable pour remonter au bon toast avec closest, même si les classes de style évoluent plus tard.
  4. Créer un élément span pour l'icône avec la méthode createElement(). Stocker cet élément dans une constante nommée spanIcone. Ce span contiendra l'icône visuelle de la notification.
  5. Ajouter la classe CSS toast-icone à spanIcone.
  6. Définir le contenu textuel de spanIcone en fonction du type de notification. Pour cela, utiliser une structure conditionnelle qui teste la valeur du paramètre type :
    • Si type vaut "succes", définir textContent à "✓"
    • Sinon, si type vaut "erreur", définir textContent à "✗"
    • Sinon (donc pour "info"), définir textContent à "ℹ"
  7. Créer un élément span pour le message. Stocker cet élément dans une constante nommée spanMessage. Ce span contiendra le texte de la notification.
  8. Ajouter la classe CSS toast-message à spanMessage.
  9. Définir le contenu textuel de spanMessage en utilisant la propriété textContent. La valeur doit être celle du paramètre message.
  10. Créer un élément button pour le bouton de fermeture. Stocker cet élément dans une constante nommée btnFermer. Ce bouton permettra à l'utilisateur de fermer la notification.
  11. Ajouter deux classes CSS à btnFermer pour séparer les responsabilités :
    • toast-fermer qui sert uniquement au CSS, et peut évoluer selon la mise en forme.
    • js-toast-fermer sert au JavaScript. Comme l'écouteur click est placé sur le conteneur des toasts (délégation), il faut pouvoir vérifier que le clic provient bien de la croix et pas d'un autre endroit du toast. La classe js-toast-fermer reste un sélecteur stable pour cibler uniquement le bouton de fermeture avec closest, même si d'autres boutons sont ajoutés plus tard dans la même notification.
  12. Définir le contenu textuel de btnFermer à × (le symbole de multiplication, qui ressemble à une croix). Ce symbole est une convention visuelle courante pour un bouton de fermeture.
  13. Maintenant que tous les éléments enfants sont créés (icône, message, bouton), il faut les assembler dans divToast. Utiliser la méthode append() sur divToast pour ajouter, dans l'ordre, spanIcone, spanMessage, et btnFermer. L'ordre est important car il détermine l'affichage de gauche à droite : icône, message, bouton de fermeture.
  14. Retourner divToast à la fin de la fonction. Cette div contient maintenant tous les éléments d'une notification complète et peut être insérée dans le DOM.
  15. Pour tester cette fonction, ajouter temporairement à la fin de initialiserGestionnaireToasts les lignes suivantes :
    
                                const toastTest = creerToast('succes', 'Ceci est un test');
    
                                // La méthode append permet d'ajouter un noeud (ex.: une balise et son contenu)
                                // dans la balise sur laquelle elle est appliquée (ici, la balise référencée dans conteneurToastsElem).
                                conteneurToastsElem.append(toastTest);
                            
    Recharger la page et vérifier qu'une notification de test verte apparaît en haut à droite. Si c'est le cas, supprimer ces deux lignes de test.

Étape 04 : Ajouter l'affichage d'une notification dans le DOM

  1. Dans js/modules/gestionnaireToasts.js, créer une fonction nommée afficherToast. Cette fonction reçoit deux paramètres :
    • type : le type de notification
    • message : le texte de la notification
    Cette fonction créer une nouvelle notification à l'aide de la fonction creerToast avant de l'insérer dans le conteneur.
  2. Dans afficherToast, appeler la fonction creerToast en lui passant type et message comme arguments. Stocker l'élément retourné dans une constante nommée nouveauToast.
  3. Ajouter nouveauToast au conteneur de notifications en utilisant la méthode prepend() sur conteneurToastsElem. On utilise prepend() plutôt que append() car cela insère la nouvelle notification en haut de la pile, au-dessus des notifications déjà présentes. Ainsi, la notification la plus récente apparaît toujours en premier.
  4. Exporter la fonction afficherToast en utilisant un export nommé, afin qu'elle puisse être importée explicitement dans app.js.
  5. Dans js/app.js, importer la fonction afficherToast depuis ./modules/gestionnaireToasts.js.
  6. Pour tester cette fonction, rester dans app.js et ajouter temporairement, juste après l'appel à initialiserGestionnaireToasts, la ligne suivante :
    
                                afficherToast('succes', 'Ceci est un test');
                            
    Recharger la page et vérifier qu'une notification de test verte apparaît en haut à droite. Si c'est le cas, supprimer cette ligne de test.

Étape 05 : Affichage d'une notification au clic

  1. La gestion de l'affichage des toasts au clic se fera dans app.js plutôt que dans le gestionnaire de toasts. Les boutons présents dans l'interface servent uniquement à des fins de test et de démonstration. Le gestionnaire de toasts se limite à la création et à la gestion des notifications, tandis que app.js simule des déclenchements proches de situations réelles comme un succès, une erreur ou une information système.
  2. Dans app.js, à la suite de l'appel à la fonction initialiserGestionnaireToasts, sélectionner le conteur HTML de boutons dont l'attribut id vaut boutons-conteneur et la stocker dans une constante nommée boutonsConteneurElem.
  3. Ajouter un seul écouteur d'événement click sur le conteneur des boutons boutonsConteneurElem. Cet écouteur doit appeler la fonction gererClicBtn.
    • Quand un clic arrive sur un élément, l'événement est déclenché sur cet élément, puis il remonte vers ses parents (bubbling). Le conteneur reçoit donc aussi les clics effectués sur ses enfants.
    • Un seul écouteur suffit pour gérer tous les boutons. Le code est plus simple et reste valable si des boutons sont ajoutés ou retirés plus tard.
  4. Toujours dans app.js, créer la fonction gererClicBtn. Dans cette fonction, récupérer l'élément réellement cliqué avec e.target, puis utiliser closest sur le sélecteur CSS button pour retrouver le bouton parent et éviter de cibler un éventuel élément enfant du bouton. Stocker le résutlat dans la constante btnElem.
    • e.target pointe l'élément exact qui a reçu le clic. Si le bouton contient un <span> ou une icône, le clic peut viser cet enfant.
    • closest remonte depuis l'élément sur lequel il est appliqué (ici le bouton cliqué ou un élément enfant) vers ses parents et retourne le premier paret qui correspond au sélecteur donné. L'objectif retrouver le bouton même si le clic a touché un élément interne.
  5. Si aucun bouton n'est trouvé, ne rien faire et quitter la fonction. Cela évite les erreurs si l'utilisateur clique dans le conteneur mais pas sur un bouton.
  6. Utiliser dataset sur le bouton cliqué (btnElem) pour récupérer le type configuré dans son attribut HTML data-type et stocker le dans la constante type.
  7. Construire le message en fonction du type (type) et stocker le dans la constante message. type.
    • Si type vaut succes, utiliser le message : "Action réussie.".
    • Si type vaut erreur, utiliser le message : "Une erreur est survenue.".
    • Si type vaut info, utiliser le message : "Information.".
  8. Appeler afficherToast en lui passant le type (type) et le message (message). La fonction afficherToast se chargera d'appeler creerToast, puis d'ajouter le toast dans le DOM, en haut du conteneur prévu à cet effet.
  9. Tester

    • Recharger la page
    • Cliquer sur les différents boutons et vérifier que le type est bien détecté
    • Vérifier que la notification affichée correspond au bouton cliqué, couleur et message
    • Cliquer plusieurs fois et vérifier que les notifications s'empilent correctement en haut du conteneur

Étape 06 : Fermeture d'une notification

  1. Dans js/modules/gestionnaireToasts.js, créer une fonction nommée supprimerToast. Cette fonction reçoit un paramètre :
    • event : l'événement de clic reçu par le conteneur des toasts
    Cette fonction sera appelée lorsqu'un clic remonte jusqu'au conteneur des toasts. Son rôle est de supprimer du DOM la notification dont l'utilisateur a cliqué sur la croix.
  2. Dans initialiserGestionnaireToasts, ajouter un seul écouteur d'événement click sur le conteneur des toasts conteneurToastsElem. Cet écouteur doit appeler la fonction supprimerToast. Ici aussi, on s'appuie sur le bubbling pour gérer tous les boutons de fermeture avec un seul écouteur.
  3. Dans supprimerToast, partir de event.target (l'élément réellement cliqué), puis utiliser closest pour retrouver le bouton de fermeture grâce au sélecteur de la classe CSS toast-fermer. Stocker le résultat dans une constante nommée btnDeFermeture.
  4. Si btnDeFermeture vaut null, ne rien faire et quitter la fonction (return). Cela évite de supprimer un toast lorsque l'utilisateur clique ailleurs que sur une croix.
  5. Ici on cible la classe CSS jtoast-fermer et pas directement la balise button car avec la délégation, le conteneur reçoit tous les clics. Si un jour une notification contient un autre bouton (ex.: Détails), un closest('button') pourrait supprimer le toast par erreur. Le sélecteur de la classe CSS toast-fermer cible uniquement la croix.
  6. À partir de btnDeFermeture, utiliser closest pour retrouver le conteneur HTMML de la notification à partir de la classe CSS js-toast. Stocker le résultat dans une constante nommée toastAFermer.
  7. Si toastAFermer vaut null, ne rien faire et quitter la fonction.
  8. Supprimer toastAFermer du DOM en appelant la méthode remove() sur cet élément. La notification disparaît immédiatement de la page.
  9. Tester :
    • recharger la page
    • créer plusieurs notifications en cliquant sur les boutons
    • cliquer sur la croix d'une notification et vérifier qu'elle disparaît
    • cliquer ailleurs dans une notification et vérifier qu'elle ne disparaît pas
    • vérifier que les autres notifications restent visibles
    • vérifier que chaque notification peut être fermée indépendamment

Étape 07 : Amélioration de l'accessibilité

Maintenant que le système de notifications est fonctionnel, nous allons améliorer son accessibilité pour les utilisateurs de technologies d'assistance et pour ceux qui naviguent au clavier.

Ajouter un label accessible pour le bouton de fermeture

Le symbole x utilisé pour fermer les toasts n'est pas suffisamment explicite pour les lecteurs d'écran, il faut utiliser un attribut aria-label pour fournir une description claire de l'action.

  1. Modifier la fonction creerToast. Juste après la création de btnFermer, ajouter un attribut aria-label à btnFermer avec la valeur Fermer la notification.

Créer une région ARIA live pour les annonces

Créer une région ARIA live permet d'annoncer automatiquement certains changements de l'interface aux utilisateurs de lecteurs d'écran, sans qu'ils aient besoin de déplacer le focus ou de parcourir la page. C'est utile pour des éléments qui apparaissent ou disparaissent dynamiquement, comme un toast ajouté après une action, un message de confirmation, ou une erreur. Cette région sert de "canal d'annonces" dédié, ce qui évite de rendre chaque toast lui-même live et limite les lectures inattendues ou répétées.

  1. En haut du fichier gestionnaireToasts.js, créer une variable de module nommée annonceurElem initialisée à null. Cette variable servira à stocker la référence vers une balise invisible dont l'objectif sera d'annoncer les changements aux utilisateurs de lecteurs d'écran.
  2. Dans initialiserGestionnaireToasts, juste après les sélections des éléments et le test de sécurité, créer un élément div avec document.createElement(). Stocker cet élément dans annonceurElem. Cet élément invisible servira à annoncer les changements aux utilisateurs de lecteurs d'écran.
  3. Ajouter l'attribut aria-live à annonceurElem avec la valeur polite. Cela indique aux lecteurs d'écran d'annoncer les changements de contenu dès que l'utilisateur termine son action en cours, sans l'interrompre.
  4. Ajouter l'attribut aria-atomic à annonceurElem avec la valeur true. Cela garantit que le lecteur d'écran annonce tout le contenu de la zone, même si seule une partie a changé.
  5. Masquer visuellement annonceurElem en définissant ses styles CSS avec la propriété JS style :
    • position à absolute,
    • style.left à -10000px,
    • style.width à 1px.
    Cette configuration rend l'élément invisible à l'écran tout en restant accessible aux lecteurs d'écran.
  6. Ajouter annonceurElem au body en utilisant la méthode append.
  7. Annoncer l'affichage d'une notification
    Modifier la fonction afficherToast. Juste après avoir ajouté nouveauToast au conteneur, définir le contenu textuel de annonceurElem avec un message qui inclut le type et le message de la notification. Par exemple, si le type est "succes" et le message est "Opération réussie !", le texte pourrait être "Notification de succès : Opération réussie !". Vous pouvez utiliser la concaténation ou un template literal pour construire ce message.
  8. Tester avec un lecteur d'écran :
    • créer une notification et vérifier que le lecteur d'écran annonce son apparition
    Tester également la navigation au clavier :
    • utiliser Tab pour naviguer entre les boutons
    • utiliser Entrée ou Espace pour activer un bouton
    • vérifier qu'on peut fermer une notification avec Entrée ou Espace sur le bouton x

Pour aller plus loin (optionnel)

Si vous souhaitez améliorer encore davantage ce système de notifications, voici quelques pistes d'amélioration que vous pourrez explorer une fois que vous aurez découvert les notions d'asynchronisme dans un prochain chapitre :

  • Fermeture automatique : Faire disparaître automatiquement les notifications après 3 secondes en utilisant setTimeout().
  • Animation de fermeture : Ajouter une animation de sortie avant la suppression pour une transition plus douce.
  • Limite de notifications : Limiter le nombre de notifications affichées simultanément (par exemple, maximum 5) et supprimer automatiquement les plus anciennes.

JS: Manipuler la structure du DOM - Exo 02

Cet exercice porte sur la génération dynamique de contenu à partir de données, la création et l'insertion d'éléments HTML, la suppression, et la réorganisation d'éléments dans le DOM. Le but est de construire une playlist musicale où l'utilisateur peut ajouter des chansons depuis un catalogue, les réorganiser, et les supprimer.

Attendu

Un catalogue de chansons est affiché à partir de données JavaScript. L'utilisateur peut ajouter une chanson à sa playlist en cliquant sur le bouton "Ajouter". Les chansons ajoutées apparaissent dans une zone dédiée avec leur pochette, titre, artiste et durée. L'utilisateur peut réorganiser les chansons dans sa playlist avec des boutons ↑ et ↓. L'utilisateur peut supprimer une chanson de sa playlist avec le bouton x. Le nombre total de chansons et la durée totale de la playlist sont affichés et mis à jour automatiquement. Une chanson ne peut pas être ajoutée deux fois à la playlist.

Structure

Créer un dossier nommé Exo-02-manipuler-structure-dom-playlist-musicale, puis organiser les fichiers et dossiers en respectant l'arborescence suivante :


                    📁 Exo-02-manipuler-structure-dom-playlist-musicale/
                    ├── 📄 index.html
                    ├── 📁 css/
                    │   └── 📄 style.css
                    └── 📁 js/
                        ├── 📄 app.js
                        └── 📁 modules/
                            ├── 📄 donneesCatalogue.js
                            └── 📄 gestionnairePlaylist.js
                

Fichiers fournis

Copier les fichiers suivants à l'identique.

index.html


                    <!DOCTYPE html>
                    <html lang="fr">
                    <head>
                        <meta charset="UTF-8">
                        <meta name="viewport" content="width=device-width, initial-scale=1.0">
                        <link rel="stylesheet" href="./css/style.css">
                        <title>JS: Manipuler la structure du DOM - Exo 02</title>
                    </head>
                    <body>
                        <main>
                            <h1>Ma Playlist Musicale</h1>

                            <div class="conteneur-principal">
                                <section class="section-catalogue">
                                    <h2>Catalogue</h2>
                                    <div id="catalogue" class="catalogue">
                                        <!-- Les chansons du catalogue seront générées en JavaScript -->
                                    </div>
                                </section>

                                <section class="section-playlist">
                                    <h2>Ma Playlist</h2>
                                    <div class="stats">
                                        <span id="nb-chansons">0 chanson</span>
                                        <span class="separateur">•</span>
                                        <span id="duree-totale">0:00</span>
                                    </div>
                                    <div id="liste-playlist" class="liste-playlist">
                                        <!-- Les chansons ajoutées apparaîtront ici -->
                                    </div>
                                </section>
                            </div>
                        </main>

                        <script src="./js/app.js" type="module"></script>
                    </body>
                    </html>
                

css/style.css


                    /* VARIABLES */
                    :root
                    {
                        --fond-principal: #0e1319;
                        --fond-secondaire: #151c24;
                        --fond-tertiaire: #1a2332;
                        --texte-principal: #e7e1ca;
                        --texte-secondaire: #b9b39f;
                        --accent-primaire: #ff6b6b;
                        --accent-secondaire: #4ecdc4;

                        --espace-xs: 0.25rem;
                        --espace-s: 0.5rem;
                        --espace-m: 1rem;
                        --espace-l: 2rem;

                        --police-corps: Arial, sans-serif;

                        --rayon-s: 0.5rem;
                        --bordure: 1px solid rgba(231, 225, 202, 0.15);
                    }

                    /* RESET */
                    html, body, main, section, div, h1, h2, span, button, img
                    {
                        margin: 0;
                        padding: 0;
                        box-sizing: border-box;
                    }

                    /* BASE */
                    html
                    {
                        font-family: var(--police-corps);
                        color: var(--texte-principal);
                        background-color: var(--fond-principal);
                    }

                    body
                    {
                        min-height: 100vh;
                    }

                    main
                    {
                        margin: 0 auto;
                        padding: var(--espace-l);
                        width: min(1200px, 100%);
                    }

                    h1
                    {
                        text-align: center;
                        margin-bottom: var(--espace-l);
                    }

                    h2
                    {
                        margin-bottom: var(--espace-m);
                        color: var(--accent-primaire);
                    }

                    /* CONTENEUR PRINCIPAL */
                    .conteneur-principal
                    {
                        display: grid;
                        grid-template-columns: 1fr 1fr;
                        gap: var(--espace-l);
                    }

                    /* SECTIONS */
                    .section-catalogue,
                    .section-playlist
                    {
                        border: var(--bordure);
                        border-radius: var(--rayon-s);
                        padding: var(--espace-l);
                        background: var(--fond-secondaire);
                    }

                    /* CATALOGUE */
                    .catalogue
                    {
                        display: flex;
                        flex-direction: column;
                        gap: var(--espace-m);
                    }

                    .chanson-catalogue
                    {
                        border: var(--bordure);
                        border-radius: var(--rayon-s);
                        padding: var(--espace-m);
                        background: var(--fond-tertiaire);
                        display: grid;
                        grid-template-columns: 60px 1fr auto;
                        gap: var(--espace-m);
                        align-items: center;
                    }

                    .chanson-catalogue img
                    {
                        width: 60px;
                        height: 60px;
                        border-radius: var(--rayon-s);
                        object-fit: cover;
                    }

                    .chanson-catalogue .infos
                    {
                        display: flex;
                        flex-direction: column;
                        gap: var(--espace-xs);
                    }

                    .chanson-catalogue .titre
                    {
                        font-weight: bold;
                    }

                    .chanson-catalogue .artiste,
                    .chanson-catalogue .duree
                    {
                        color: var(--texte-secondaire);
                        font-size: 0.9rem;
                    }

                    .btn-ajouter
                    {
                        border: none;
                        border-radius: var(--rayon-s);
                        padding: 0.5rem 1rem;
                        background: var(--accent-secondaire);
                        color: var(--fond-principal);
                        font-weight: bold;
                        cursor: pointer;
                        transition: opacity 0.2s;
                        white-space: nowrap;
                    }

                    .btn-ajouter:hover
                    {
                        opacity: 0.9;
                    }

                    .btn-ajouter:disabled
                    {
                        opacity: 0.5;
                        cursor: not-allowed;
                    }

                    /* PLAYLIST */
                    .stats
                    {
                        margin-bottom: var(--espace-m);
                        padding: var(--espace-m);
                        background: var(--fond-tertiaire);
                        border-radius: var(--rayon-s);
                        text-align: center;
                        font-weight: bold;
                        color: var(--accent-primaire);
                    }

                    .stats .separateur
                    {
                        margin: 0 var(--espace-s);
                        color: var(--texte-secondaire);
                    }

                    .liste-playlist
                    {
                        display: flex;
                        flex-direction: column;
                        gap: var(--espace-m);
                        min-height: 100px;
                    }

                    .chanson-playlist
                    {
                        border: var(--bordure);
                        border-radius: var(--rayon-s);
                        padding: var(--espace-m);
                        background: var(--fond-tertiaire);
                        display: grid;
                        grid-template-columns: 50px 1fr auto auto;
                        gap: var(--espace-m);
                        align-items: center;
                    }

                    .chanson-playlist img
                    {
                        width: 50px;
                        height: 50px;
                        border-radius: var(--rayon-s);
                        object-fit: cover;
                    }

                    .chanson-playlist .infos
                    {
                        display: flex;
                        flex-direction: column;
                        gap: var(--espace-xs);
                    }

                    .chanson-playlist .titre
                    {
                        font-weight: bold;
                    }

                    .chanson-playlist .artiste
                    {
                        color: var(--texte-secondaire);
                        font-size: 0.9rem;
                    }

                    .chanson-playlist .duree
                    {
                        color: var(--texte-secondaire);
                        font-size: 0.9rem;
                        margin-right: var(--espace-m);
                    }

                    .actions
                    {
                        display: flex;
                        gap: var(--espace-s);
                    }

                    .btn-monter,
                    .btn-descendre,
                    .btn-supprimer
                    {
                        border: none;
                        background: transparent;
                        color: var(--texte-secondaire);
                        font-size: 1.2rem;
                        cursor: pointer;
                        padding: var(--espace-xs);
                        line-height: 1;
                        transition: color 0.2s;
                        width: 28px;
                        height: 28px;
                        display: flex;
                        align-items: center;
                        justify-content: center;
                    }

                    .btn-monter:hover,
                    .btn-descendre:hover
                    {
                        color: var(--accent-secondaire);
                    }

                    .btn-supprimer:hover
                    {
                        color: var(--accent-primaire);
                    }

                    .btn-monter:disabled,
                    .btn-descendre:disabled
                    {
                        opacity: 0.3;
                        cursor: not-allowed;
                    }

                    .btn-monter:disabled:hover,
                    .btn-descendre:disabled:hover
                    {
                        color: var(--texte-secondaire);
                    }

                    /* RESPONSIVE */
                    @media (max-width: 900px)
                    {
                        .conteneur-principal
                        {
                            grid-template-columns: 1fr;
                        }
                    }

                    @media (max-width: 520px)
                    {
                        main
                        {
                            padding: var(--espace-m);
                        }

                        .section-catalogue,
                        .section-playlist
                        {
                            padding: var(--espace-m);
                        }

                        .chanson-catalogue,
                        .chanson-playlist
                        {
                            grid-template-columns: 50px 1fr;
                            gap: var(--espace-s);
                        }

                        .chanson-catalogue img,
                        .chanson-playlist img
                        {
                            width: 50px;
                            height: 50px;
                        }

                        .btn-ajouter
                        {
                            grid-column: 2;
                            justify-self: end;
                        }

                        .chanson-playlist .duree
                        {
                            grid-column: 2;
                            justify-self: start;
                            margin-right: 0;
                            margin-top: var(--espace-xs);
                        }

                        .actions
                        {
                            grid-column: 2;
                            justify-self: end;
                        }
                    }
                

js/modules/donneesCatalogue.js


                    const catalogue = [
                        {
                            id: 1,
                            titre: "Bohemian Rhapsody",
                            artiste: "Queen",
                            duree: 354, // 5:54
                            pochette: "https://placehold.co/150/FF6B6B/FFFFFF?text=Queen"
                        },
                        {
                            id: 2,
                            titre: "Stairway to Heaven",
                            artiste: "Led Zeppelin",
                            duree: 482, // 8:02
                            pochette: "https://placehold.co/150/4ECDC4/FFFFFF?text=Led+Zeppelin"
                        },
                        {
                            id: 3,
                            titre: "Imagine",
                            artiste: "John Lennon",
                            duree: 183, // 3:03
                            pochette: "https://placehold.co/150/95E1D3/FFFFFF?text=John+Lennon"
                        },
                        {
                            id: 4,
                            titre: "Smells Like Teen Spirit",
                            artiste: "Nirvana",
                            duree: 301, // 5:01
                            pochette: "https://placehold.co/150/F38181/FFFFFF?text=Nirvana"
                        },
                        {
                            id: 5,
                            titre: "Hotel California",
                            artiste: "Eagles",
                            duree: 391, // 6:31
                            pochette: "https://placehold.co/150/AA96DA/FFFFFF?text=Eagles"
                        },
                        {
                            id: 6,
                            titre: "Billie Jean",
                            artiste: "Michael Jackson",
                            duree: 294, // 4:54
                            pochette: "https://placehold.co/150/FCBAD3/FFFFFF?text=MJ"
                        },
                        {
                            id: 7,
                            titre: "Sweet Child O' Mine",
                            artiste: "Guns N' Roses",
                            duree: 356, // 5:56
                            pochette: "https://placehold.co/150/FFFFD2/333333?text=GNR"
                        },
                        {
                            id: 8,
                            titre: "Come Together",
                            artiste: "The Beatles",
                            duree: 259, // 4:19
                            pochette: "https://placehold.co/150/A8D8EA/FFFFFF?text=Beatles"
                        }
                    ];

                    export { catalogue };
                

Instructions

Le travail se fait dans js/app.js et js/modules/gestionnairePlaylist.js. La playlist doit être construite progressivement avec des console.log(), afin de valider chaque étape avant de passer à la suivante.

Dans cet exercice, vous allez générer un catalogue de chansons à partir de données JavaScript, créer dynamiquement des éléments pour chaque chanson ajoutée à la playlist, réorganiser ces éléments avec des boutons, et mettre à jour des compteurs en temps réel. Chaque chanson possède un identifiant unique stocké dans un attribut data-id qui permettra de gérer les doublons et la réorganisation.

Étape 01 : Initialisation du module

  1. Créer le fichier js/modules/gestionnairePlaylist.js.
  2. En haut du fichier, importer catalogue depuis ./donneesCatalogue.js. L'import est un import nommé, il utilise donc le même nom. La constante catalogue deviendra ainsi une constante du module actuel et pourra être utilisée au sein des fonctions de celui-ci.
  3. À la suite de l'import, créer quatre variables initialisées à null :
    • catalogueElem : contiendra la référence vers le conteneur du catalogue
    • listePlaylistElem : contiendra la référence vers le conteneur de la playlist
    • nbChansonsElem : contiendra la référence vers l'élément affichant le nombre de chansons
    • dureeTotaleElem : contiendra la référence vers l'élément affichant la durée totale
  4. À la suite de ces variables, créer une fonction nommée initialiserGestionnairePlaylist. Cette fonction reçoit quatre paramètres qui contiennent des sélecteurs CSS sous forme de texte :
    • selecteurCssCatalogue
    • selecteurCssListePlaylist
    • selecteurCssNbChansons
    • selecteurCssDureeTotale
  5. Dans initialiserGestionnairePlaylist, sélectionner chacun des quatre éléments à l'aide de la méthode querySelector et des sélecteurs CSS passés en arguments, puis stocker les références dans les variables correspondantes créées précédemment.
  6. Ajouter un test de sécurité. Si l'une des quatre variables vaut null, afficher le message Gestionnaire de playlist non initialisé : un ou plusieurs éléments introuvables. avec console.warn(), puis arrêter la fonction immédiatement avec return.
  7. Juste après ce test de sécurité, ajouter un console.log() temporaire affichant Gestionnaire de playlist initialisé avec succès.
  8. Exporter la fonction initialiserGestionnairePlaylist en utilisant un export nommé.

Étape 02 : Configuration dans app.js

  1. Créer le fichier js/app.js.
  2. Dans js/app.js, importer la fonction initialiserGestionnairePlaylist depuis ./modules/gestionnairePlaylist.js.
  3. Appeler initialiserGestionnairePlaylist en lui passant quatre sélecteurs CSS :
    • le conteneur du catalogue dont l'attribut id vaut catalogue
    • le conteneur de la playlist dont l'attribut id vaut liste-playlist
    • l'élément affichant le nombre de chansons dont l'attribut id vaut nb-chansons
    • l'élément affichant la durée totale dont l'attribut id vaut duree-totale
  4. Tester :
    • recharger la page
    • ouvrir la console
    • vérifier que le message Gestionnaire de playlist initialisé avec succès s'affiche

Étape 03 : Conversion des secondes en minutes

  1. Dans js/modules/gestionnairePlaylist.js, créer une fonction nommée convertirSecondesEnMinutes. Cette fonction reçoit un paramètre :
    • secondes : un nombre entier représentant une durée en secondes
    Le rôle de cette fonction est de convertir une durée en secondes (par exemple 354) en une chaîne de caractères au format minutes:secondes (par exemple "5:54").
  2. Dans convertirSecondesEnMinutes, calculer le nombre de minutes en divisant secondes par 60, puis utiliser Math.floor() pour garder uniquement les minutes complètes (ex.: 125 / 60 vaut environ 2.08, mais on veut 2 minutes, pas une valeur décimale). Stocker le résultat dans une constante nommée minutes.
  3. Calculer le nombre de secondes restantes avec le modulo (%), car on veut ce qui dépasse les minutes complètes. secondes % 60 donne le reste de la division par 60, donc une valeur toujours comprise entre 0 et 59. (ex.: 125 % 60, 120 est la plus grande valeur divisible par 60, il reste donc 5). Stocker le résultat dans une constante nommée sec.
  4. Créer une constante secondesFormatees qui contient les secondes avec un zéro devant si nécessaire. Pour cela, convertir sec en chaîne de caractères avec String(sec), puis utiliser la méthode padStart(2, '0') sur cette chaîne. Cette méthode garantit que la chaîne fait au moins 2 caractères en ajoutant un zéro devant si besoin (ex.: 5 devient "05").
  5. Retourner une chaîne de caractères construite en concaténant minutes, le caractère :, et secondesFormatees. Vous pouvez utiliser un template literal (`${...}`) pour faciliter la construction.
  6. Pour tester cette fonction, ajouter temporairement à la fin de initialiserGestionnairePlaylist les lignes suivantes :
    
                                const secEnMinTest = convertirSecondesEnMinutes(125);
                                console.log(secEnMinTest); // Affiche: 2 : 05
                            
    Recharger la page et vérifier dans la console que la conversion des secondes se comporte comme attendu. Si c'est le cas, supprimer ces deux lignes de test.

Étape 04 : Génération du catalogue

  1. Dans js/modules/gestionnairePlaylist.js, créer une fonction nommée genererCatalogue. Cette fonction ne reçoit aucun paramètre. Son rôle est de parcourir le tableau catalogue et de créer une carte de chanson pour chaque élément du tableau, puis d'insérer toutes ces cartes dans le conteneur du catalogue.
  2. Dans genererCatalogue, utiliser une structure itérative pour parcourir le tableau catalogue. Pour chaque chanson du catalogue, créer les éléments suivants :
  3. Créer le conteneur de la carte de chanson.
    • Créer un élément div avec createElement et le stocker dans une constante nommée divChansonElem.
    • Ajouter deux classes CSS pour séparer les responsabilités.
      • Ajouter la classe chanson-catalogue. Elle sert au style visuel de la carte côté CSS.
      • Ajouter la classe js-chanson-catalogue. Elle sert de sélecteur stable côté JavaScript pour retrouver facilement une carte (par exemple avec closest lors d'un clic).
    • Ajouter un attribut data-id avec dataset. Lui donner la valeur de id de la chanson courante (celle que vous êtes en train de traiter dans la boucle). Cet identifiant permettra ensuite de savoir précisément quelle chanson a été cliquée.
  4. Créer une nouvelle fonction genererImgElem qui contient deux paramètres :
    • titre : le titre de la chanson
    • pochette : le chemin vers la pochette
  5. Dans la fonction genererImgElem,
    • Créer un élément img pour la pochette et stocker cet élément dans une constante nommée imgElem.
    • Définir son attribut src (setAttribute) avec la valeur de pochette de la chanson courante.
    • Définir son attribut alt avec une description comme "Pochette de [titre]" en remplaçant [titre] par le titre de la chanson courante.
    • Retourner imgElem.
  6. Revenir dans la fonction genererCatalogue, appeler la fonction genererImgElem en lui passant le titre et le chemin vers la pochette et stocker le résultat dans une constante imgElem.
  7. Créer une nouvelle fonction genererDivInfoElem qui contient quatre paramètres :
    • titre : le titre de la chanson
    • artiste : le nom de l'artiste
    • duree : la durée de la chanson en secondes
  8. Dans la fonction genererDivInfoElem,
    • Créer un élément div pour les informations textuelles. Stocker cet élément dans une constante nommée divInfosElem. Ajouter la classe infos à cet élément.
    • Créer un élément span pour le titre. Stocker cet élément dans une constante nommée spanTitreElem. Ajouter la classe titre. Définir son textContent avec le titre de la chanson courante.
    • Créer un élément span pour l'artiste. Stocker cet élément dans une constante nommée spanArtisteElem. Ajouter la classe artiste. Définir son textContent avec l'artiste de la chanson courante.
    • Créer un élément span pour la durée. Stocker cet élément dans une constante nommée spanDureeElem. Ajouter la classe duree. Définir son textContent en appelant la fonction convertirSecondesEnMinutes avec la durée de la chanson courante comme argument.
    • Assembler les éléments d'information. Utiliser la méthode append() sur divInfosElem pour ajouter, dans l'ordre, spanTitreElem, spanArtisteElem, et spanDureeElem.
    • Retourner divInfosElem
  9. Revenir dans la fonction genererCatalogue, appeler la fonction genererDivInfoElem en lui passant le titre, le nom de l'artiste et la durée de la chanson et stocker le résultat dans une constante divInfosElem.
  10. Créer le bouton d'ajout.
    • Créer un élément button et le stocker dans une constante nommée btnAjouterElem.
    • Ajouter deux classes CSS pour séparer les responsabilités.
      • Ajouter la classe btn-ajouter. Elle sert au style du bouton côté CSS.
      • Ajouter la classe js-btn-ajouter. Elle sert de sélecteur stable côté JavaScript pour retrouver facilement ce bouton (par exemple avec querySelector ou closest), même si les classes de style changent plus tard.
    • Définir textContent à + Ajouter.
  11. Assembler la carte complète. Utiliser la méthode append() sur divChansonElem pour ajouter, dans l'ordre, imgElem, divInfosElem, et btnAjouterElem.
  12. Insérer la carte dans le catalogue. Utiliser la méthode append() sur catalogueElem pour ajouter divChansonElem.
  13. Dans initialiserGestionnairePlaylist, juste après le console.log() de test, appeler la fonction genererCatalogue. Cela générera le catalogue dès le chargement de la page.
  14. Tester :
    • recharger la page
    • vérifier que 8 cartes de chansons apparaissent dans la section Catalogue
    • vérifier que chaque carte affiche la pochette, le titre, l'artiste, la durée et un bouton "Ajouter"
    • vérifier que les durées sont bien au format "m:ss" (par exemple "5:54")

Étape 05 : Ajout d'une chanson à la playlist

  1. Dans js/modules/gestionnairePlaylist.js, créer une fonction nommée creerChansonPlaylist. Cette fonction reçoit un paramètre :
    • chanson : un objet contenant les propriétés id, titre, artiste, duree et pochette
    Le rôle de cette fonction est de créer et de retourner un élément HTML complet représentant une chanson dans la playlist, avec tous ses boutons d'action (monter, descendre, supprimer).
  2. Dans creerChansonPlaylist, créer le conteneur HTML d'une chanson.
    • Créer un élément div et le stocker dans une constante nommée divChansonElem.
    • Ajouter deux classes CSS pour séparer les responsabilités.
      • Ajouter la classe chanson-playlist. Elle sert au style de la carte côté CSS.
      • Ajouter la classe js-chanson-playlist. Elle sert de sélecteur stable côté JavaScript pour retrouver facilement une carte de chanson (par exemple avec closest) même si les classes de style changent plus tard.
    • Ajouter un attribut data-id avec la valeur de chanson.id. Cet identifiant permettra de savoir précisément quelle chanson est concernée.
    • Ajouter un attribut data-duree avec la valeur de chanson.duree. Cet attribut servira plus tard pour calculer la durée totale de la playlist.
  3. Créer les éléments suivants en suivant la même logique que pour le catalogue :
    • imgElem en lui affectant l'élément retourné par la fonction genererImgElem, à laquelle on passe le titre et le chemin de la pochette.
    • divInfosElem en lui affectant l'élément retourné par la fonction genererDivInfoElem, à laquelle on passe le titre, le nom de l'artiste et la durée du morceau.
  4. Créer un élément div qui contiendra les boutons d'action. Stocker cet élément dans une constante nommée divActionsElem. Ajouter la classe actions.
  5. Créer trois boutons :
    • un bouton btnMonterElem avec le texte ↑
    • un bouton btnDescendreElem avec le texte ↓
    • et un bouton btnSupprimerElem avec le texte x
  6. Pour chacun de ces trois boutons, ajouter deux classes afin de séparer les responsabilités.
    • Une classe pour le style côté CSS préfixée par btn- suivi du nom de l'action (ex.: pour le bouton supprimer, btn-supprimer).
    • Une classe stable côté JavaScript, nommée sur le même modèle que la classe CSS de style mais avec le préfixe js-. Cette classe sert de repère fiable pour sélectionner le bouton en JavaScript même si les classes de style évoluent (ex.: pour le bouton supprimer, ajouter js-btn-supprimer).
  7. Assembler les boutons dans divActionsElem en utilisant append().
  8. Assembler tous les éléments dans divChansonElem : l'image imgElem, le div infos divInfosElem, et le div actions divActionsElem.
  9. Retourner divChansonElem.
  10. Pour tester la fonction creerChansonPlaylist, ajouter temporairement, à la fin de initialiserGestionnairePlaylist, deux lignes qui créent un élément de chanson à partir de la première entrée du catalogue, puis l'ajoutent dans la playlist.
    
                                const divChansonElem = creerChansonPlaylist(catalogue[0]);
                                listePlaylistElem.append(divChansonElem);
                            
    Recharger la page et vérifier qu'une chanson apparaît bien dans la playlist, avec ses informations affichées correctement. Si le test est concluant, supprimer ces deux lignes de test.
  11. Créer une fonction nommée ajouterChansonALaPlaylist. Cette fonction reçoit un paramètre :
    • e : l'événement de clic sur le bouton "Ajouter"
  12. Dans ajouterChansonALaPlaylist, récupérer la carte chanson (HTML) correpondant au bouton cliqué. Pour cela, utiliser e.target pour obtenir le bouton, puis utiliser la propriété closest() avec la classe CSS js-chanson-catalogue pour remonter jusqu'à la carte de chanson parente cliquée et stocker la référence dans la constante chansonElem.
  13. Récupérer l'ID de la chanson en récuperant la valeur de data-id avec dataset sur la référence de la carte chanson (chansonElem). Stocker cette valeur dans une constante nommée idChanson. Convertir cette valeur en nombre avec Number car les attributs data sont toujours des chaînes de caractères.
  14. Vérifier si la chanson est déjà présente dans la playlist. Pour cela, utiliser querySelector sur le conteneur de la playlist stockée dans listePlaylistElem et rechercher un élément qui possède un attribut data-id correspondant à l'identifiant de la chanson à ajouter (idChanson). Si un tel élément est trouvé, cela signifie que la chanson est déjà dans la playlist. Dans ce cas, arrêter immédiatement la fonction avec return afin d'éviter d'ajouter un doublon.
  15. Trouver l'objet chanson correspondant dans le tableau catalogue. Utiliser la méthode find() avec une fonction de rappel. Cette fonction de rappel sera exécutée pour chaque élément du tableau. À chaque passage, elle reçoit un objet chanson en paramètre et doit retourner true quand l'id de cet objet correspond à idChanson. Dès que la fonction renvoie true, find() s'arrête et retourne l'objet trouvé. Stocker le résultat dans une constante nommée chanson. Si aucune chanson ne correspond, find() retourne undefined.
  16. Appeler la fonction creerChansonPlaylist en lui passant chanson comme argument. Stocker l'élément retourné dans une constante nommée chansonPourPlaylistElem.
  17. Ajouter chansonPourPlaylistElem à la playlist en utilisant append() sur listePlaylistElem.
  18. Mettre en place la délégation d'événements pour l'ajout à la playlist.
    • Dans initialiserGestionnairePlaylist, ajouter un seul écouteur click sur le conteneur du catalogue (catalogueElem).
    • Cet écouteur devra appeler la fonction ajouterChansonALaPlaylist. L'idée est de centraliser la gestion des clics au même endroit, plutôt que d'ajouter un addEventListener sur chaque bouton "+ Ajouter".
    • Rappel, principe du bubbling. Quand on clique sur un bouton, l'événement est déclenché sur l'élément cliqué, puis il remonte automatiquement vers ses parents. Le conteneur du catalogue reçoit donc aussi le clic.
  19. Tester :
    • recharger la page
    • cliquer sur "Ajouter" pour plusieurs chansons
    • vérifier que les chansons apparaissent dans la section "Ma Playlist"
    • vérifier qu'on ne peut pas ajouter deux fois la même chanson (cliquer plusieurs fois sur le même bouton "Ajouter")

Étape 06 : Suppression d'une chanson de la playlist

  1. Créer une fonction nommée gererClickPlaylist. Cette fonction reçoit un paramètre :
    • e : l'événement de clic (délégation depuis la playlist)
    Son rôle est de centraliser la gestion des clics sur la playlist. Pour l'instant, elle ne gère que la suppression, mais elle servira ensuite aussi pour ↑ et ↓.
  2. Dans gererClickPlaylist, vérifier si le clic provient d'un bouton de suppression.
    • À partir de e.target, utiliser closest() avec le sélecteur CSS .js-btn-supprimer.
    • Stocker le résultat dans une constante nommée btnSupprimerElem.
    • Si btnSupprimerElem vaut null, arrêter la fonction avec return. Ainsi, un clic ailleurs dans la playlist ne déclenche rien.
  3. Récupérer la carte de chanson concernée en remontant depuis le bouton.
    • À partir de btnSupprimerElem, utiliser closest() pour remonter jusqu'à la carte parente.
    • Utiliser la classe CSS stable .js-chanson-playlist.
    • Stocker le résultat dans une constante nommée chansonElem.
    • Si chansonElem vaut null, arrêter la fonction avec return (cas anormal, mais ça évite une erreur).
  4. Mettre en place la délégation d'événements sur la playlist.
    • Dans initialiserGestionnairePlaylist, récupérer le conteneur HTML de la playlist et le stocker dans une constante nommée listePlaylistElem (si ce n'est pas déjà fait).
    • Ajouter ensuite un seul écouteur click sur listePlaylistElem.
    • Cet écouteur doit appeler gererClickPlaylist.
    • Rappel, principe du bubbling. Un clic sur le bouton "x" déclenche l'événement sur le bouton, puis l'événement remonte jusqu'au conteneur. C'est ce qui permet d'avoir un seul écouteur pour toutes les chansons.
  5. Tester la détection avant de supprimer quoi que ce soit.
    • Dans gererClickPlaylist, ajouter temporairement un console.log(chansonElem) pour vérifier que la bonne carte est récupérée.
    • Recharger la page, ajouter plusieurs chansons, puis cliquer sur "x". Vérifier dans la console que l'élément affiché correspond bien à la chanson cliquée.
    • Vérifier aussi qu'un clic ailleurs (sur le texte, sur la carte, etc.) ne déclenche rien, car btnSupprimerElem vaut null et la fonction s'arrête avec return.
    • Si tout se déroule comme prévu, supprimer le log de test.
  6. Créer une fonction nommée supprimerChansonDeLaPlaylist. Cette fonction reçoit un paramètre :
    • chansonElem : la carte HTML de la chanson à supprimer (celle récupérée avec closest()).
  7. Dans supprimerChansonDeLaPlaylist, supprimer la chanson du DOM.
    • Appeler remove() sur chansonElem.
  8. Tester la suppression.
    • Recharger la page, ajouter plusieurs chansons, cliquer sur "x" et vérifier que la chanson disparaît.
    • Vérifier qu'on peut ensuite réajouter cette chanson depuis le catalogue.

Étape 07 : Réorganisation de la playlist

  1. Adapter gererClickPlaylist pour gérer aussi ↑ et ↓.
    • Objectif
      • Continuer à faire la détection du clic (quel bouton) dans gererClickPlaylist.
      • Continuer à récupérer la chanson concernée (la carte HTML) dans gererClickPlaylist.
      • Puis appeler la fonction métier correspondante supprimerChansonDeLaPlaylist, monterChanson, descendreChanson.
    • Intérêt
      • Un seul écouteur click sur la playlist.
      • Une seule fonction qui décide quoi faire selon le bouton cliqué.
      • Les fonctions monterChanson et descendreChanson deviennent plus simples car elles ne gèrent plus e.target.
  2. Dans gererClickPlaylist, détecter le bouton cliqué.
    • Partir de e.target et utiliser closest().
    • Stocker les références trouvées (ou null) dans ces constantes :
      • btnSupprimerElem pour la classe CSS js-btn-supprimer
      • btnMonterElem pour la classe CSS js-btn-monter
      • btnDescendreElem pour la classe CSS js-btn-descendre
    • Si aucun de ces trois boutons n'a été trouvé, arrêter la fonction avec return. Ainsi, un clic sur le texte d'une chanson ne fait rien.
  3. Récupérer la carte de chanson concernée une seule fois.
    • Créer une constante nommée boutonActionElem.
    • Lui assigner le premier bouton trouvé parmi btnSupprimerElem, btnMonterElem, btnDescendreElem.
    • À partir de boutonActionElem, utiliser closest() avec la classe CSS stable js-chanson-playlist.
    • Stocker le résultat dans une constante nommée chansonElem.
    • Si chansonElem vaut null, arrêter avec return.
  4. Appeler la bonne action depuis gererClickPlaylist.
    • Si btnSupprimerElem existe, appeler supprimerChansonDeLaPlaylist en lui passant chansonElem.
    • Sinon, si btnMonterElem existe, appeler monterChanson en lui passant chansonElem.
    • Sinon, si btnDescendreElem existe, appeler descendreChanson en lui passant chansonElem.
  5. Créer une fonction nommée monterChanson.
    • Cette fonction reçoit un paramètre nommé chansonElem. Ce paramètre représente la carte HTML de la chanson à déplacer.
  6. Dans monterChanson, déplacer la chanson d'une position vers le haut.
    • Récupérer la carte située juste au-dessus avec previousElementSibling sur chansonElem.
    • Stocker le résultat dans une constante nommée chansonPrecedenteElem.
    • Si chansonPrecedenteElem vaut null, la chanson est déjà en haut de la playlist, donc ne rien faire.
    • Sinon, utiliser before() sur chansonPrecedenteElem pour insérer chansonElem juste avant. Résultat, la chanson remonte d'une position.
  7. Créer une fonction nommée descendreChanson.
    • Cette fonction reçoit un paramètre nommé chansonElem. Ce paramètre représente la carte HTML de la chanson à déplacer.
  8. Dans descendreChanson, déplacer la chanson d'une position vers le bas.
    • Récupérer la carte située juste en dessous avec nextElementSibling sur chansonElem.
    • Stocker le résultat dans une constante nommée chansonSuivanteElem.
    • Si chansonSuivanteElem vaut null, la chanson est déjà en bas de la playlist, donc ne rien faire.
    • Sinon, utiliser after() sur chansonSuivanteElem pour insérer chansonElem juste après. Résultat, la chanson descend d'une position.
  9. Tester :
    • recharger la page
    • ajouter au moins 3 chansons à la playlist
    • cliquer sur ↑ pour la chanson du milieu et vérifier qu'elle monte d'une position
    • cliquer sur ↓ pour une chanson et vérifier qu'elle descend d'une position
    • vérifier que cliquer sur ↑ pour la première chanson ne fait rien
    • vérifier que cliquer sur ↓ pour la dernière chanson ne fait rien
    • vérifier que x fonctionne toujours

Étape 08 : Mise à jour des statistiques

  1. Créer une fonction nommée mettreAJourStats. Cette fonction ne reçoit aucun paramètre. Son rôle est de calculer et d'afficher le nombre de chansons et la durée totale de la playlist.
  2. Dans mettreAJourStats, sélectionner toutes les chansons actuellement dans la playlist en utilisant querySelectorAll avec la classe CSS js-chanson-playlist. Stocker le résultat dans une constante nommée chansonElements.
  3. Calculer le nombre de chansons en récupérant la propriété length de chansonElements. Stocker ce nombre dans une constante nommée nbChansons.
  4. Créer une variable dureeTotaleSecondes initialisée à 0. Cette variable servira à accumuler la durée totale.
  5. Parcourir toutes les chansons avec une structure itérative. Pour chaque chanson, récupérer la valeur de l'attribut data-duree, la convertir en nombre avec Number(), et l'ajouter à dureeTotaleSecondes.
  6. Mettre à jour l'affichage du nombre de chansons. Définir le textContent de nbChansonsElem avec une chaîne qui dépend du nombre :
    • si nbChansons vaut 0 ou 1, afficher "0 chanson" ou "1 chanson"
    • sinon, afficher "[nombre] chansons" (au pluriel)
  7. Mettre à jour l'affichage de la durée totale. Appeler convertirSecondesEnMinutes avec dureeTotaleSecondes et définir le textContent de dureeTotaleElem avec le résultat.
  8. Appeler mettreAJourStats dans les fonctions suivantes :
    • à la fin de ajouterChansonALaPlaylist (après avoir ajouté la chanson)
    • à la fin de supprimerChansonDeLaPlaylist (après avoir supprimé la chanson)
    Cela garantit que les statistiques sont toujours à jour après chaque modification de la playlist.
  9. Tester :
    • recharger la page
    • vérifier que l'affichage indique "0 chanson • 0:00"
    • ajouter une chanson et vérifier que l'affichage se met à jour (ex.: "1 chanson • 5:54")
    • ajouter plusieurs chansons et vérifier que le nombre et la durée augmentent correctement
    • supprimer une chanson et vérifier que les statistiques diminuent
    • vérifier que le pluriel "chansons" s'affiche correctement quand il y a 2 chansons ou plus

Étape 09 : Désactivation des boutons en bordure

  1. Créer une fonction nommée mettreAJourActivationBoutons. Cette fonction ne reçoit aucun paramètre. Son rôle est de désactiver le bouton ↑ de la première chanson et le bouton ↓ de la dernière chanson.
  2. Dans mettreAJourActivationBoutons, récupérer toutes les chansons actuellement dans la playlist.
    • Utiliser document.querySelectorAll() avec la classe stable js-chanson-playlist.
    • Stocker le résultat dans une constante nommée chansonsElems.
    • Créer une constante nbChansons qui contient le nombre d'éléments stocké dans la liste de noeuds chansonsElems.
  3. Parcourir toutes les chansons avec une boucle for. À chaque tour, récupérer l'élément courant et le stocker dans une constante nommée chansonElem.
  4. Pour chaque chanson, récupérer les deux boutons de déplacement.
    • Rechercher le bouton ↑ avec querySelector et la classe CSS stable js-btn-monter. Stocker le résultat dans une constante nommée btnMonterElem.
    • Rechercher le bouton ↓ avec querySelector et la classe CSS stable js-btn-descendre. Stocker le résultat dans une constante nommée btnDescendreElem.
  5. Mettre à jour l'activation du bouton ↑.
    • Vérifier que btnMonterElem existe. Si c'est le cas, assigner sa propriété disabled.
    • Le bouton ↑ est désactivé uniquement si la chanson est le premier élément de la liste.
  6. Mettre à jour l'activation du bouton ↓.
    • Vérifier que btnDescendreElem existe. Si c'est le cas, assigner sa propriété disabled.
    • Le bouton ↓ est désactivé uniquement si la chanson est le dernier élément de la liste.
  7. Appeler mettreAJourActivationBoutons aux bons moments.
    • À la fin de ajouterChansonALaPlaylist, après l'ajout de la chanson dans le DOM.
    • À la fin de gererClickPlaylist, après avoir exécuté l'action demandée suppression, montée ou descente.
  8. Tester.
    • Recharger la page.
    • Ajouter au moins 3 chansons.
    • Vérifier que la première chanson a le bouton ↑ désactivé.
    • Vérifier que la dernière chanson a le bouton ↓ désactivé.
    • Monter ou descendre une chanson et vérifier que les désactivations se mettent à jour.

Étape 10 : Amélioration de l'accessibilité

Maintenant que la playlist est entièrement fonctionnelle, nous allons améliorer son accessibilité pour les utilisateurs de technologies d'assistance et pour ceux qui naviguent au clavier.

Rendre les boutons d'action compréhensibles

Les boutons ↑, ↓ et x affichent des symboles visuels. Pour un lecteur d'écran, ces symboles ne décrivent pas clairement l'action. L'objectif est donc de donner un nom accessible à chaque bouton grâce à aria-label.

  1. Modifier la fonction creerChansonPlaylist. Juste après la création de chaque bouton d'action, ajouter un attribut aria-label adapté :
    • pour le bouton ↑ Monter la chanson dans la playlist
    • pour le bouton ↓ Descendre la chanson dans la playlist
    • pour le bouton x Retirer la chanson de la playlist
  2. Tester ce point avant de continuer :
    • Recharger la page et ajouter quelques chansons.
    • Naviguer au clavier avec Tab jusqu'aux boutons ↑, ↓ et x.
    • Vérifier avec un lecteur d'écran que chaque bouton est annoncé avec une action claire et pas seulement "bouton" ou "x".

Annoncer l'ajout et la suppression

Lorsqu'une chanson est ajoutée ou supprimée, un lecteur d'écran ne reçoit pas forcément d'information immédiate, car le focus ne se déplace pas automatiquement. Une région ARIA live permet d'annoncer ces changements au bon moment.

Dans cet exercice, la région ARIA live sert uniquement à annoncer l'ajout et la suppression. Pour les déplacements (↑/↓), on ne fait pas d'annonce. On conserve un focus stable sur le bouton activé, ce qui suffit à guider l'utilisateur.

  1. Créer une variable de module nommée annonceurElem initialisée à null.
    • Cette variable stockera la référence vers la région ARIA live, créée une seule fois.
  2. Dans initialiserGestionnairePlaylist, juste après les sélections des éléments et le test de sécurité, créer un élément div destiné aux annonces et l'affecter à annonceurElem.
  3. Ajouter les attributs suivants à annonceurElem :
    • aria-live="polite" pour annoncer sans interrompre brutalement
    • aria-atomic="true" pour annoncer le message complet à chaque mise à jour
  4. Masquer visuellement cette région sans la retirer de l'accessibilité :
    • La placer hors écran via des styles appliqués en JavaScript
    • Ne pas utiliser display: none car l'élément ne serait plus annoncé
  5. Ajouter annonceurElem au body pour qu'il soit présent dans le DOM et utilisable par le lecteur d'écran.
  6. Annoncer l'ajout d'une chanson :
    • Dans ajouterChansonALaPlaylist, juste après avoir ajouté la chanson dans la playlist, mettre à jour annonceurElem.textContent.
    • Construire un message du type "[Titre] de [Artiste] ajouté à la playlist" en utilisant l'objet chanson récupéré depuis le catalogue.
  7. Annoncer la suppression d'une chanson :
    • Dans supprimerChansonDeLaPlaylist, utiliser l'identifiant présent sur la carte (via data-id) pour retrouver l'objet chanson correspondant.
    • Après la suppression du DOM, mettre à jour annonceurElem.textContent avec un message du type "[Titre] retiré de la playlist".
  8. Tester ce point avant de continuer :
    • Recharger la page et activer un lecteur d'écran.
    • Ajouter une chanson et vérifier qu'une annonce est faite.
    • Supprimer une chanson et vérifier qu'une annonce est faite.
    • Cliquer sur ↑ et ↓ et vérifier qu'il n'y a pas d'annonce supplémentaire. Le focus doit simplement rester sur le bouton activé.

Garder un focus cohérent lors des déplacements

Quand on active ↑ ou ↓, la chanson change de place dans le DOM. Visuellement, c'est clair. Au clavier, par contre, il devient difficile d'enchaîner les déplacements car on perd le focus sur le bouton cliqué suite à l'action. L'objectif est de constater ce souci, puis de le corriger en replaçant le focus au bon endroit après chaque déplacement.

  1. Faire un test avant toute modification pour observer le problème.
    • Recharger la page et ajouter au moins 3 chansons.
    • Aller au clavier avec Tab jusqu'au bouton ↓ de la première chanson.
    • Activer le bouton avec Entrée ou Espace.
    • Répéter plusieurs fois jusqu'à placer la première chanson tout en bas pour constater le problème de ce type de navigation au clavier si elle n'est pas gérée par JS.
  2. Comprendre ce que l'on veut corriger.
    • Après un déplacement, conserver un focus sur un bouton d'action pour pouvoir enchaîner au clavier.
    • Si le bouton utilisé devient désactivé après la mise à jour (première ou dernière position), basculer le focus sur le bouton qui permet de se déplacer dans la direction opposée.
  3. Ajouter la gestion du focus au bon endroit.
    • Ouvrir la fonction gererClickPlaylist. C'est le bon endroit car cette fonction sait déjà quel bouton a été cliqué et elle appelle ensuite monterChanson ou descendreChanson.
    • Placer la logique de focus après l'appel à mettreAJourActivationBoutons, puisque c'est à ce moment que les boutons ↑ et ↓ peuvent devenir activés ou désactivés.
  4. Ne pas aller plus loin si le bouton cliqué est le bouton de suppression btnSupprimerElem.
    • Si le bouton cliqué correspond à celui de suppression, sortir de la fonction avec return. La carte vient d'être retirée du DOM, il ne faut donc pas essayer de replacer le focus ici.
  5. Vérifier si le bouton cliqué btnActionElem est désactivé.
    • Utiliser la propriété disabled du bouton. Cette propriété reflète l'état réel après la mise à jour des bordures.
    • Si btnActionElem n'est pas désactivé, lui redonner le focus avec la méthode focus().
  6. Si le bouton cliqué est désactivé, passer le focus au bouton opposé (descendre <=> monter).
    • Créer une constante nommée btnOpposeElem.
    • Utiliser une structure ternaire qui vérifie si btnMonterElem est le bouton cliqué.
    • Si oui, récupérer le bouton descendre en utilisant querySelector avec le sélecteur CSS .js-btn-descendre sur la carte courante chansonElem.
    • Sinon, récupérer le bouton monter en utilisant querySelector avec le sélecteur CSS .js-btn-monter sur la carte courante chansonElem.
    • Une fois btnOpposeElem récupéré, lui donner le focus avec la méthode focus().
  7. Tester après modification.
    • Recharger la page et ajouter au moins 3 chansons.
    • Au clavier, se placer sur ↑ ou ↓ et enchaîner plusieurs activations.
    • Vérifier que le focus reste sur un bouton utile après chaque déplacement.
    • Quand une chanson atteint une bordure, vérifier que le focus bascule sur l'autre bouton si nécessaire.