Récupérer les enfants d'un noeud DOM

Récupérer les enfants d'un noeud DOM est moins simple qu'il n'y paraît. Des solutions existent, et les récentes évolutions des spécifications W3C offrent de nouvelles perspectives mais un développeur web a encore besoin de créer ses propres outils. Petit tour de la question.

Du code HTML à l'arborescence DOM

C'est un problème connu de tous (les développeurs web s'entend) : il est souvent difficile de parcourir une arborescence DOM à la recherche des enfants d'un élément à cause de la manière dont le code HTML a été écrit. L'utilisation des caractères non imprimés (espaces, tabulations ou retours à la ligne) produit certaines ambiguïtés, surtout parce que les navigateurs ne se comportent pas tous de la même manière. Imaginons par exemple que nous ayons le code HTML suivant :

<div>
<strong>texte très important</strong> 
texte normal 
<em>Texte important.</em>
</div>
Code HTML que l'on souhaite analyser.

Si nous devions construire un arbre décrivant la hiérarchie de l'élément div, il serait certainement de la forme :

  • [ELEMENT] div
    • [ELEMENT] strong
      • [TEXTE] « texte très important »
    • [TEXTE] « texte normal »
    • [ELEMENT] em
      • [TEXTE] « texte important »

Nous nous dirions que l'élément div possède 3 enfants et que le premier de ces enfants est l'élément strong. Donc si nous voulions créer une référence de ce noeud, il nous suffirait de récupérer le premier enfant de div grâce à l'API DOM dédiée :

  • Soit par la propriété childNodes, la collection des noeuds enfants, en récupérant le premier item de la liste (monElementDiv.childNodes[0]).
  • Soit par la propriété firstChild, qui cible spécifiquement le premier enfant d'un élément (monElementDiv.firstChild).

Cependant, l'arbre du document n'est pas forcément construit de la sorte : pour certains navigateurs comme Firefox, Safari et Opera, tout « espace blanc »(1) doit être représenté dans l'arborescence, ce qui complique un peu les choses. Le fait d'aller à la ligne après la balise ouvrante div se transformera donc en un noeud texte, et l'abre ressemblera finalement à ceci :

  • [ELEMENT] div
    • [TEXTE] « \n »
    • [ELEMENT] strong
      • [TEXTE] « texte très important »
    • [TEXTE] « \ntexte normal\n »
    • [ELEMENT] em
      • [TEXTE] « texte important »
    • [TEXTE] « \n »

Quand vous avez à manipuler ce type d'arbre, vous ne pouvez pas forcément supprimer les retours à la ligne dans le code HTML, parce que vous n'êtes pas l'auteur du document ou, si vous l'êtes, parce qu'il est beaucoup plus facile de lire - et d'écrire - du code HTML ainsi que sur une seule ligne. Et vu que dans 90% des cas, vous vous moquez royalement des ces « noeuds saut de ligne », il devient rapidement utile d'avoir à votre disposition une « surcouche » de l'API DOM afin :

  • De vous débarrassez des noeuds texte vides pour qu'une requête « premier enfant de l'élément » vous renvoie toujours la même chose, qu'il y ait un retour à la ligne dans le code source ou pas.
  • D'harmoniser les comportements entre navigateurs puisque IE ne créer pas ces noeuds, peut importe la manière dont le code HTML a été écrit.

La traversée des « noeuds valides »

Comme je le disais, c'est un problème ancien et connu et nombre de solutions existent pour le contourner. J'en utilise une dans ma bibliothèque javascript depuis des années. Elle vaut ce quelle vaut mais elle m'a donné entière satisfaction jusqu'à aujourd'hui. Je la publie ici à titre d'exemple ; vous pourrez trouver bien d'autres techniques sur le web (si vous utilisez une bibliothèque javascript déjà écrite, YUI, JQuery ou autres, ce type de méthode existe déjà).

Le principe est simple : quand on cherche un noeud (le premier, le dernier, le suivant, le précédent) et que l'on tombe sur un « noeud texte vide », il faut l'ignorer et passer au noeud adjacent. Pour cela, on doit pouvoir différencier un noeud élément, un noeud texte et un « noeud texte vide ». Nous créons ensuite une méthode qui nous dira si un noeud donné est un noeud valide, à savoir un noeud élément ou un noeud texte non « vide ».

var dom = {
    /**
     * Tester si le noeud est un noeud élément.
     * @param {Element} node Le noeud à tester
     * @return {boolean}
     */
    isElement : function (node) {
        return !!node && node.nodeType && 
                node.nodeType===document.ELEMENT_NODE; 
    },
    /**
     * Vérifier s'il s'agit d'un noeud texte "vide".
     * @param {Text} Noeud texte à étudier
     * @return {boolean} Noeud vide (true) ou non (false)
     */
    isEmptyTextNode : function(node) { 
        return node.nodeValue && 
                !(/[^\t\n\r ]/.test(node.nodeValue));
    },
    /**
     * Vérifier qu'un noeud est un noeud texte.
     * @param {Node} node Noeud à analyser
     * @return {boolean}
     */
    isTextNode : function(node) {
        return (node.nodeType && 
                node.nodeType===document.TEXT_NODE);
    },
    /**
     * Est-ce un noeud valide, à savoir est-ce qu'il s'agit 
     * d'un élément ou d'un noeud texte non vide ?
     * @param {Node} node Noeud à analyser
     * @return {boolean}
     */
    isValidNode : function(node) {
        return (dom.isElement(node) || (dom.isTextNode(node) &&
                !dom.isEmptyTextNode(node)) );
    }
};
Méthodes permettant d'identifier les noeuds(2).

Pour identifier les types de noeud, nous utilisons des constantes définies dans l'interface Node, comme document.ELEMENT_NODE. Ces constantes n'existent pas dans l'API DOM implémentée par le navigateur Internet Explorer. Il faudra donc penser à les initialiser en tant que propriété de l'objet Document. Ces constantes sont de simples entiers ; nous devrons donc par exemple écrire document.ELEMENT_NODE = 1.

Une fois ces méthodes de test à disposition, nous pouvons « surcharger » l'API DOM. Nous illustrerons ici la surcharge de la propriété firstChild (récupérer le premier enfant d'un élément) mais le même raisonnement s'appliquera - en l'adaptant évidemment - pour les propriétés lastChild (dernier enfant), nextSibling (noeud adjacent suivant) et previousSibling (noeud adjacent précédent).

/**
 * Récupérer le premier noeud de l'élément, 
 * que ce soit un élément HTML ou un
 * noeud texte non vide.
 * @param {Element} parent Noeud parent
 * @return {Element|Text|null}
 */
dom.getFirstChild = function (parent) {
    var el = parent.firstChild;    
    while (el) {
        if (rnb.dom.isValidNode(el)) {
            return el;
        }   
        el = el.nextSibling;
    }
    return null;
};
Fonction permettant de récupérer le premier enfant d'un élément qui n'est pas un noeud texte « vide ».

La traversée d'éléments

Sans doute conscient des problèmes de nous autres, petites mains du web, le W3C publie depuis juillet 2007 les spécifications d'une API apte à faciliter l'exploration des noeuds DOM. Cette API possède la même structure que celle existente sauf qu'elle permet de naviguer entre les noeuds éléments, en laissant de côté les noeuds texte (donc les noeuds texte « vides »). Nous avons dorénavant à notre disposition les propriétés firstElementChild, lastElementChild, nextElementSibling et previousElementSibling.

Là aussi, pour assurer une compatibilité entre les différents navigateurs, ceux qui supportent cette API et les autres, nous devons surcharger les propriétés. Comme précédemment, je donne ici l'exemple permettant de récupérer le premier élément enfant mais le raisonnement sera le même pour le reste de l'API.(3)

/**
 * Récupérer le premier noeud élément d'un élément.
 * @param {Element} parent Noeud parent
 * @return {Element|null}
 */
dom.getFirstElement = function (parent) {
    if (parent.firstElementChild) {
        dom.getFirstElement = function (node) {
            return node.firstElementChild;
        };
    } else {
        dom.getFirstElement = function (node) {
            var el = node.firstChild;    
            while (el) {
                if (dom.isElement(el)) {
                    return el;
                }
                el = el.nextSibling;
            }
            return null;
        };
    }
    return dom.getFirstElement(parent);
};
Fonction permettant de récupérer le premier élément enfant d'un élément.

Vous remarquerez que, bien que proches, les deux surcharges de l'API DOM ne sont pas équivalentes. Si on reprend le code HTML de notre exemple, la traversée d'éléments ne permettra d'accéder qu'à deux des trois enfants de notre élément div (l'élément strong et l'élément em) alors que la première traversée nous donne accès aux trois enfants. On utilisera l'une ou l'autre en fonction des besoins.

4. Se débarrasser des caractères non imprimables dans les noeuds texte

Enfin, toujours dans l'optique de se débarrasser des caractères non imprimables qui peuvent géner la manipulation des noeuds texte, nous pouvons créer une fonction « trim ». Assez commun dans de nombreux languages de programmation mais absent en javascript, ce type de fonction permet de supprimer les caractères invisibles aux extrémités d'une chaîne de caractères :

/**
 * Supprimer les espaces blancs aux extrémités d'une 
 * chaîne de caractères.
 * @param {string} str Chaîne à traiter
 * @return {String} La chaîne traitée.
 */
function trim(str) {
    return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}
Fonction pour supprimer les espaces blanc aux extrémités d'une chaîne de caractères.

5. Ressources et références

Titre
Document Object Model (DOM) Level 2 Core Specification
Auteurs
Arnaud LE HORS
Auteurs
Philippe LE HÉGARET
Auteurs
Lauren WOOD
Auteurs
Gavin NICOL
Auteurs
Jonathan ROBIE
Auteurs
Mike CHAMPION
Auteurs
Steve BYRNE
Editeur
W3C
Date
Chapitre
Interface Node
Titre
Element Traversal Specification
Auteurs
  • Doug SCHEPERS
  • Robin BERJON
Editeur
W3C
Date
Titre
W3C DOM Compatibility - Traversal
Auteurs
  • Peter-Paul KOCH
Editeur
quirksmode
Date