Chapitre 12: Le modèle objet de documents
Dans le chapitre 11 nous avons vu les objets JavaScript qui font référence aux
balises form
et input
du document HTML. De tels objets font partie d’une
structure appelée le modèle objet de documents (DOM). Chaque élément du
document est représenté dans ce modèle, on peut l’examiner et interagir avec
lui.
Les documents HTML ont ce qu’on peut appeler une structure hiérarchique. Chaque
élément (ou balise) à l’exception de la balise de premier niveau <html>
est
contenu dans un autre élément, son parent. Cet élément peut en retour contenir
d’autres éléments. Vous pouvez vous le représenter comme une sorte d’arbre
généalogique.

Le modèle objet de documents est basé sur une telle vue du document. Veuillez noter que l’arbre contient deux types d’éléments : les nœuds, représentés comme des boîtes bleues et de simples morceaux de texte. Les morceaux de texte sont comme nous le verrons assez différents des autres éléments. L’une de ces différences est qu’ils n’ont jamais d’enfants.
Ouvrez le fichier example_alchemy.html
qui contient le document présenté dans
l’image et attachez-y la console.
attach(window.open("example_alchemy.html"));
L’objet de la racine de l’arbre document, le nœud
html
, peut-être atteint via la propriété documentElement
de l’objet
document
. La plupart du temps, nous avons plutôt besoin de la partie body
du document qui se trouve à document.body
.
Les liens entre ces nœuds sont disponibles sous forme de propriétés de l’objet
nœud. Chaque objet DOM possède une propriété parentNode
qui fait référence
à l’objet dans lequel il est contenu, si objet il y a. Ces parents ont aussi
des liens pointant vers leurs enfants. Mais parce qu’il peut y avoir plusieurs
enfants, ils sont stockés dans un pseudo-tableau appelé childNodes
.
show(document.body); show(document.body.parentNode); show(document.body.childNodes.length);
Par commodité, il y a aussi des liens appelés firstChild
et lastChild
,
pointant respectivement au premier et au dernier enfant à l’intérieur d’un nœud
ou null
quand il n’y a pas d’enfant.
show(document.documentElement.firstChild); show(document.documentElement.lastChild);
Enfin, il y a des propriétés nommées nextSibling
et previousSibling
,
qui pointent vers les nœuds présents aux côtés d’un autre nœud ― les nœuds qui
sont des enfants du même parent, venant avant ou après le nœud courant. Encore
une fois, lorsque ces nœuds ne sont pas présents, la valeur de ces propriétés
est null
.
show(document.body.previousSibling); show(document.body.nextSibling);
Pour savoir si un nœud représente un simple morceau de texte ou un nœud HTML,
nous pouvons jeter un œil à sa propriété nodeType
. Elle contiendra un
nombre, 1
pour un nœud classique et 3
pour un nœud texte. Il existe en fait
d’autres sortes d’objets qui possèdent un nodeType
, comme l’objet document
qui vaudra 9
, mais l’usage le plus commun de cette propriété est la
distinction entre les nœuds textes et les autres nœuds.
function isTextNode(noeud) { return noeud.nodeType == 3; } show(isTextNode(document.body)); show(isTextNode(document.body.firstChild.firstChild));
Les nœuds classiques ont une propriété appelée nodeName
, indiquant le type
de balise HTML qu’ils représentent. En revanche, les nœuds textes ont une
propriété nodeValue
, ayant pour valeur leur contenu texte.
show(document.body.firstChild.nodeName); show(document.body.firstChild.firstChild.nodeValue);
Les propriétés nodeName
sont toujours mises en majuscules, c’est quelque
chose qui doit être pris en compte si jamais vous voulez les comparer à quoi
que ce soit.
function isImage(noeud) { return !isTextNode(noeud) && noeud.nodeName == "IMG"; } show(isImage(document.body.lastChild));
Écrivez une fonction asHTML
qui, appelée avec un nœud DOM, produira une
chaîne représentant le texte HTML de ce nœud et de ses éléments. Vous pouvez
ignorer les attributs, afficher juste les nœuds comme <nodename>
. La fonction
escapeHTML
du chapitre 10 est disponible afin d’échapper correctement le
contenu des nœuds textes.
Indice : Récursion!
function asHTML(noeud) { if (isTextNode(noeud)) return escapeHTML(noeud.nodeValue); else if (noeud.childNodes.length == 0) return "<" + noeud.nodeName + "/>"; else return "<" + noeud.nodeName + ">" + map(asHTML, noeud.childNodes).join("") + "</" + noeud.nodeName + ">"; } print(asHTML(document.body));
En réalité, les nœuds ont déjà quelque chose de similaire à asHTML
. Leur
propriété innerHTML
peut être utilisée afin de récupérer le texte HTML à
l’intérieur du nœud, sans les balises du nœud en question. Quelques
navigateurs Web, mais pas tous, prennent aussi en charge la propriété
outerHTML
, qui inclut le nœud lui-même.
print(document.body.innerHTML);
Certaines de ces propriétés peuvent aussi être modifiées. Modifier le
innerHTML
d’un nœud ou la nodeValue
d’un nœud texte changera son contenu. À
noter que dans le premier cas, la chaîne donnée est interprétée comme du HTML
alors que dans le second cas elle est interprétée comme du simple texte.
document.body.firstChild.firstChild.nodeValue = "Chapitre 1 : La grande importance de la bouteille";
Ou…
document.body.firstChild.innerHTML = "Connaissiez-vous déjà la balise « blink » ? <blink>Oh joie !</blink>";
Nous avons accédé à des nœuds au travers d’une série de propriétés firstChild
et lastChild
. Cela peut fonctionner, mais c’est assez verbeux et facilement
cassable ― si nous ajoutons un autre nœud au début de notre document,
document.body.firstChild
ne pointera plus sur l’élément h1
et si le code
prétend le contraire il se trompe. En plus de cela, certains navigateurs Web
ajouteront des nœuds textes pour des choses comme les espaces et les retours à
la ligne entre les balises alors que d’autres non. La représentation exacte de
l’arbre DOM peut varier.
Comme alternative, il vous est possible de donner un attribut id
aux éléments
auxquels vous avez besoin d’accéder. Dans la page d’exemple, l’image à un id
"image"
, nous pouvons l’utiliser pour retrouver l’image.
var image = document.getElementById("image"); show(image.src); image.src = "img/ostrich.png";
Quand vous tapez getElementById
, notez que la
dernière lettre est en minuscule. Faites donc attention, lorsque vous le
taperez souvent, au syndrome du canal carpien. Parce que
document.getElementById
est un nom ridiculement long pour une opération aussi
commune, l’abréger par $
est devenu une convention au sein des développeurs
JavaScript. $
, comme vous vous en souvenez peut-être, est considéré comme une
lettre par JavaScript et c’est donc un nom de variable valide.
function $(id) { return document.getElementById(id); } show($("image"));
Les nœuds DOM ont aussi une méthode getElementsByTagName
(un autre nom
sympa et court), qui appelée avec un nom de balise, retourne un tableau de tous
les nœuds de ce type contenu dans le nœud sur lequel la méthode a été appelée.
show(document.body.getElementsByTagName("BLINK")[0]);
Une autre chose que nous pouvons faire avec ces nœuds est d’en créer de nouveaux nous-même. Cela rend possible l’ajout d’éléments dans un document, qui pourront être utilisés pour créer des effets intéressants. Malheureusement l’interface permettant cela est extrêmement mal fichue. Mais il est possible de remédier à cela avec des fonctions d’aide.
L’objet document
possède les méthodes createElement
et createTextNode
. La première est
utilisée pour créer un nœud classique, la seconde, comme son nom le suggère,
pour créer un nœud texte.
var deuxiemeEntete = document.createElement("H1"); var deuxiemeChapitre = document.createTextNode("Chapitre 2 : Magie intense");
Nous voulons ensuite mettre le titre dans l’élément h1
, puis ajouter cet
élément au document. Le moyen le plus simple de le faire est la méthode
appendChild
qui peut-être appelée sur chaque nœud qui n’est pas un nœud
texte.
deuxiemeEntete.appendChild(deuxiemeChapitre); document.body.appendChild(deuxiemeEntete);
Souvent, vous voudrez aussi ajouter des attributs à ces nouveaux nœuds . Par
exemple, une balise img
(image) est plutôt inutile sans la propriété src
qui dit au navigateur quelle image il doit afficher. La plupart des attributs
peuvent être retrouvés directement comme des propriétés du nœud DOM mais il y a
aussi les méthodes setAttribute
et getAttribute
, qui sont utilisées
pour accéder aux attributs de façon plus générale :
var nouvelleImage = document.createElement("IMG"); nouvelleImage.setAttribute("src", "img/Hiva Oa.png"); document.body.appendChild(nouvelleImage); show(nouvelleImage.getAttribute("src"));
Mais, lorsque l’on veut créer davantage que quelques nœuds simples , il devient
assez assommant de créer chaque nœud avec un appel vers
document.createElement
ou document.createTextNode
, et ensuite d’y ajouter
les attributs et les éléments un par un. Heureusement, il n’est pas trop
difficile d’écrire une fonction qui ferait le plus gros du travail pour nous.
Avant de s’y mettre, il y a un petit détail qu’il faut prendre en compte : la
méthode setAttribute
, qui fonctionne correctement sur la plupart des
navigateurs, ne fonctionne pas toujours sur Internet Explorer. Les noms de
certains attributs HTML ont déjà un sens particulier en JavaScript, de ce fait,
la propriété de l’objet correspondant obtient un nom ajusté. Plus
spécifiquement, l’attribut class
devient className
, for
devient
htmlFor
et checked
est renommé en defaultChecked
. Sur Internet Explorer,
setAttribute
et getAttribute
fonctionnent aussi avec ces noms ajustés, à la
place des noms HTML originaux ce qui peut être source de confusion. En plus de
cela l’attribut style
, qui sera abordé plus tard dans ce chapitre avec
class
, ne peut être défini avec setAttribute
sur ce navigateur.
Une solution de contournement pourrait ressembler à quelque chose comme ça :
function setNodeAttribute(noeud, attribut, valeur) { if (attribut == "class") noeud.className = valeur; else if (attribut == "checked") noeud.defaultChecked = valeur; else if (attribut == "for") noeud.htmlFor = valeur; else if (attribut == "style") noeud.style.cssText = valeur; else noeud.setAttribute(attribut, valeur); }
À chaque fois qu’Internet Explorer dévie du comportement des autres navigateurs, il fait quelque chose qui fonctionne dans tous les cas. Ne vous inquiétez pas des détails, c’est ce genre d’astuces sales dont nous aimerions ne pas avoir besoin mais que les navigateurs non conformes nous obligent à écrire. Considérant ceci, il est possible d’écrire une simple fonction pour créer des éléments DOM.
function dom(nom, attributs) { var noeud = document.createElement(nom); if (attributs) { forEachIn(attributs, function(nom, valeur) { setNodeAttribute(noeud, nom, valeur); }); } for (var i = 2; i < arguments.length; i++) { var noeudEnfant = arguments[i]; if (typeof noeudEnfant == "string") noeudEnfant = document.createTextNode(noeudEnfant); noeud.appendChild(noeudEnfant); } return noeud; } var nouveauParagraphe = dom("P", null, "Un paragraphe avec un ", dom("A", {href: "http://fr.wikipedia.org/wiki/Alchimie"}, "lien"), " à l’intérieur."); document.body.appendChild(nouveauParagraphe);
La fonction dom
crée un arbre DOM. Son premier argument donne le nom de la
balise du nœud, son second argument est un objet contenant l’attribut du nœud,
ou null
quand aucun attribut n’est requis. Après quoi, n’importe quel nombre
d’arguments peut suivre et ils seront ajoutés au nœud comme enfants nœuds.
Quand des chaînes apparaissent ici, elles sont ajoutées dans un nœud texte.
appendChild
n’est pas le seul moyen d’insérer des nœuds dans un autre nœud.
Quand le nouveau nœud ne doit pas apparaître à la fin de son parent, la méthode
insertBefore
peut-être utilisée pour le placer avant un autre nœud enfant.
Elle prend le nouveau nœud comme premier argument et l’élément existant comme
second argument.
var lien = nouveauParagraphe.childNodes[1]; nouveauParagraphe.insertBefore(dom("STRONG", null, "super "), lien);
Si un nœud possèdant déjà un parentNode
est placé ailleurs, il est
automatiquement supprimé de sa position actuelle ― les nœuds ne peuvent pas
exister dans le document à plus d’un endroit à la fois.
Quand un nœud doit être remplacé par un autre, utilisez la méthode
replaceChild
, qui prend encore le nouveau nœud comme premier argument et le
nœud existant comme second argument.
nouveauParagraphe.replaceChild(document.createTextNode("mauvais "), nouveauParagraphe.childNodes[1]);
Et finalement, il y a removeChild
pour supprimer un nœud enfant. À noter
que cette méthode doit être appelée sur le parent du nœud à supprimer, en lui
donnant l’enfant comme argument.
nouveauParagraphe.removeChild(nouveauParagraphe.childNodes[1]);
Écrivez la fonction utile removeElement
qui supprime de son nœud parent le
nœud DOM donné en paramètre
function removeElement(noeud) { if (noeud.parentNode) noeud.parentNode.removeChild(noeud); } removeElement(nouveauParagraphe);
Pendant la création de nœud et le déplacement de nœuds, il est nécessaire de
prendre en compte la règle suivante : les nœuds ne sont pas autorisés à être
insérés dans un autre document que celui dans lequel ils ont été créés. Ce qui
veut dire que si vous avez des pages ou des fenêtres supplémentaires ouvertes,
vous ne pouvez pas prendre un fragment de document de l’un pour le placer dans
un autre et que les nœuds créés avec les méthodes d’un objet document
doivent
rester dans ce document. Certains navigateurs, notamment Firefox, n’appliquent
pas cette restriction, de ce fait un programme qui la viole pourra fonctionner
correctement dans ce navigateur mais pas dans d’autres.
Un exemple de quelque chose d’utile qui peut être fait avec cette fonction
dom
est un programme qui prend des objets JavaScript et en fait un résumé
dans un tableau. Les tableaux, en HTML, sont créés avec un ensemble de
balises commençant par t
, quelque chose comme :
<table> <tbody> <tr> <th>Arbre </th> <th>Fleurs </th> </tr> <tr> <td>Pommier</td> <td>Blanches</td> </tr> <tr> <td>Corail </td> <td>Rouges </td> </tr> <tr> <td>Pin </td> <td>Aucune </td> </tr> </tbody> </table>
Chaque élément tr
est une ligne du tableau. Les éléments th
et td
sont
les cellules du tableau, td
pour des cellules normales, th
pour les
cellules d’en-tête qui seront affichées dans un style plus visible. La balise
tbody
(table body) n’a pas à être incluse quand un tableau est écrit en HTML,
mais s’il est construit avec des nœuds DOM, elle doit être ajoutée car Internet
Explorer refuse d’afficher les tableaux créés sans tbody
.
La fonction makeTable
prend deux tableaux en arguments. Le premier contient
les objets JavaScript qu’il doit récapituler et le second contient des chaînes,
qui nomment les colonnes du tableau et les propriétés des objets qui doivent
être affichées dans ces colonnes. Ce qui suit devrait produire le tableau
ci-dessus :
makeTable([{Arbre: "Pommier", Fleurs: "Blanches"}, {Arbre: "Corail", Fleurs: "Rouges"}, {Arbre: "Pin", Fleurs: "Aucune"}], ["Arbre", "Fleurs"]);
Écrivez cettte fonction.
function makeTable(donnees, colonnes) { var ligneEntete = dom("TR"); forEach(colonnes, function(nom) { ligneEntete.appendChild(dom("TH", null, nom)); }); var corps = dom("TBODY", null, ligneEntete); forEach(donnees, function(objet) { var ligne = dom("TR"); forEach(colonnes, function(nom) { ligne.appendChild(dom("TD", null, String(objet[nom]))); }); corps.appendChild(ligne); }); return dom("TABLE", null, corps); } var table = makeTable(document.body.childNodes, ["nodeType", "tagName"]); document.body.appendChild(table);
N’oubliez par de convertir les valeurs des objets en chaînes avant de les
ajouter au tableau ― notre fonction dom
comprend seulement les chaînes et
les nœuds DOM.
Le sujet des feuilles de style est étroitement lié à HTML et au modèle objet de documents. C’est un vaste sujet et je ne l’aborderai pas entièrement. Mais quelques notions sur les feuilles de styles sont nécessaires pour beaucoup de techniques intéressantes en JavaScript, nous allons donc en voir les rudiments.
Aux débuts de l’HTML, le seul moyen de changer l’apparence des éléments dans un
document était de leur donner des attributs supplémentaires ou de les contenir
dans des balises supplémentaires. Comme center
qui permet de centrer les
éléments horizontalement ou font
pour changer le style ou la couleur de la
police. La plupart du temps, cela veut dire que si vous vouliez que les
paragraphes ou les tableaux de votre document apparaissent d’une certaine
façon, vous deviez ajouter un ensemble d’attributs et de balises à chacun des
éléments. Cela a rapidement ajouté beaucoup de « bruits parasites » aux
documents et les a rendus particulièrement compliqués à écrire ou à modifier à
la main.
Évidemment, les gens étant des singes ingénieux, quelqu’un a proposé une solution. Les feuilles de style sont un moyen de déclarer quelque chose comme « Dans ce document, tous les paragraphes utiliseront la police Comic Sans et seront mauves et tous les tableaux auront une fine bordure verte ». Vous spécifiez ces règles une fois pour toutes, en haut du document ou dans un fichier séparé et elles affecteront le document entier. Voici, pour exemple, une feuille de style permettant d’afficher les en-têtes centrés avec une taille de 22 points et de faire en sorte que les paragraphes utilisent la police et la couleur mentionnées plus haut quand ils appartiennent à la classe « moche ».
<style type="text/css"> h1 { font-size: 22pt; text-align: center; } p.moche{ font-family: Comic Sans MS; color: purple; } </style>
Les classes sont un concept lié aux styles. Si vous avez différentes sortes de
paragraphes, des moches et des jolis par exemple, et que vous ne voulez pas
attribuer le style à tous les éléments p
, alors les classes peuvent être
utilisées pour les distinguer. Le style précédent ne sera appliqué qu’aux
paragraphes comme :
<p class="moche">Miroir, miroir…</p>
Et c’est aussi le sens de la propriété className
qui était brièvement
mentionnée dans la fonction setNodeAttribute
. L’attribut style
peut être
utilisé afin d’ajouter des éléments de styles directement à un élément. Par
exemple, ceci donne pour notre image une bordure en trait continu de 4 pixels
("px").
setNodeAttribute($("image"), "style", "border-width: 4px; border-style: solid;");
On peut aller bien plus loin avec les styles : certains sont hérités par les
nœuds enfants de leurs nœuds parents et interfèrent les uns avec les autres de
façon complexe et intéressante, mais en ce qui concerne la programmation DOM,
la chose la plus importante à savoir est que chaque nœud DOM possède une
propriété style
, qui peut être utilisée pour manipuler le style de ce nœud et
qu’il y a quelques styles qui peuvent être utilisés pour que les nœuds fassent
des choses extraordinaires.
Cette propriété style
fait référence à un objet, qui possède des propriétés
pour chaque élément de ce style. Nous pouvons par exemple décider que la
bordure de l’image sera verte.
$("image").style.borderColor = "green"; show($("image").style.borderColor);
Veuillez noter que dans les feuilles de style, les mots sont séparés par des
traits d’unions comme dans border-color
, alors qu’en JavaScript, les lettres
majuscules sont utilisées pour séparer les différents mots, comme dans
borderColor
.
Un exemple de style très pratique est display: none
. Il peut être utilisé
pour temporairement cacher un nœud : quand style.display
est "none"
,
l’élément n’apparaît plus du tout à la personne qui visualise le document, même
s’il existe. Plus tard, display
peut être affecté d’une chaîne vide et
l’élément réapparaîtra.
$("image").style.display = "none";
Et pour faire revenir notre image :
$("image").style.display = "";
Il existe d’autres types de style dont on peut user et abuser de façon intéressante, ceux liés au positionnement. Dans un document HTML simple, le navigateur s’occupe de déterminer la position de l’écran et de tous les éléments ― chaque élément est mis à la suite ou au-dessous de l’élément qui le précède et les nœuds ne se chevauchent pas (en général).
Quand la propriété position
de sa propriété style vaut
"absolute"
, un nœud est retiré du flux normal du document. Il n’a plus sa
place dans le document mais en quelque sorte au-dessus de lui. Les styles
left
et top
peuvent ensuite être utilisés pour influencer sa position.
Ceux-ci peuvent être utilisés pour de nombreux usages, que ce soit pour faire
un nœud qui suivrait obstinément le curseur de la souris ou pour faire des «
fenêtres » qui s’ouvriraient au-dessus du reste du document.
$("image").style.position = "absolute"; var angle = 0; var spin = setInterval(function() { angle += 0.1; $("image").style.left = (100 + 100 * Math.cos(angle)) + "px"; $("image").style.top = (100 + 100 * Math.sin(angle)) + "px"; }, 100);
Si vous n’êtes pas familier de la trigonométrie, faites-moi confiance quand je
vous dis que le cosinus et le sinus sont utilisés pour construire des
coordonnées reposant sur le contour d’un cercle. Dix fois par seconde, l’angle
sur lequel on a placé l’image est modifié et les nouvelles coordonnées sont
calculées. C’est une erreur commune lorsqu’on attribue des styles comme cela
d’oublier d’ajouter "px"
à la valeur. Dans la plupart des cas, attribuer un
nombre sans unité à un style ne fonctionne pas donc vous devez ajouter "px"
pour pixels, "%"
pour pourcentage, "em"
pour cadratin (la largeur d’un
caractère M
) ou encore "pt"
pour points.
(Maintenant, mettons une image pour nous reposer encore…)
clearInterval(spin);
L’emplacement qui est traité comme 0,0 pour ces positions dépend de la place du
nœud dans le document. Lorsqu’il est placé à l’intérieur d’un autre nœud qui a
position: absolute
ou position: relative
, le coin supérieur gauche de ce
nœud est utilisé. Sinon, c’est le coin supérieur gauche du document.
Un dernier aspect des nœuds DOM avec lequel il
est sympa de jouer est leur taille. Il existe des types de style nommés width
(largeur) et height
(hauteur), qui sont utilisés pour définir la taille
absolue d’un élément.
$("image").style.width = "400px"; $("image").style.height = "200px";
Mais lorsque vous avez besoin de définir précisément la taille d’un élément, il y a un problème épineux à prendre en compte. Certains navigateurs, dans certaines circonstances, considèrent que ces tailles correspondent à la taille extérieure de l’objet, incluant les bordures et les marges intérieures. D’autres navigateurs, dans d’autres circonstances, ne tiennent pas compte de la largeur des bordures et des marges. Ainsi, si vous définissez la taille d’un objet qui a une bordure ou une marge, il n’apparaîtra pas toujours à la même taille.
Heureusement, vous pouvez examiner la taille intérieure et extérieure du nœud,
ce qui, si vous avez vraiment besoin de définir précisément la taille de
quelque chose, peut être utilisé pour compenser le comportement du navigateur.
Les propriétés offsetWidth
et offsetHeight
vous donnent la taille
extérieure de votre élément (la place qu’il occupe dans le document), tandis
que les propriétés clientWidth
et clientHeight
vous donnent la place à
l’intérieur de l’objet, s’il y en a.
print("Dimension extérieure: ", $("image").offsetWidth, " sur ", $("image").offsetHeight, " pixels."); print("Dimension intérieure: ", $("image").clientWidth, " sur ", $("image").clientHeight, " pixels.");
Si vous avez consciencieusement suivi et utilisé tous les exemples de ce chapitre et peut-être effectué vous-même quelques expériences supplémentaires, vous aurez complètement saccagé le malheureux document initial qui nous a servi de point de départ. Laissez-moi vous faire les gros yeux une minute et vous dire de ne surtout pas faire subir le même traitement à de vraies pages. Parfois la tentation sera grande d’ajouter des tas de choses bling-bling et qui bougent. Retenez-vous, sinon vos pages deviendront certainement illisibles ou même, si vous allez assez loin, pourraient provoquer de temps en temps une crise d’épilepsie.