Manipuler le CSS

Le JavaScript est un langage permettant de rendre une page Web dynamique du côté du client. Seulement, quand on pense à « dynamique », on pense aussi à « animations ». Or, pour faire des animations, il faut savoir accéder au CSS et le modifier. C'est ce que nous allons étudier dans ce chapitre.

Au programme, l'édition du CSS et son analyse. Pour terminer le chapitre, nous étudierons comment réaliser un petit système de drag & drop : un sujet intéressant !

Éditer les propriétés CSS

Avant de s'attaquer à la manipulation du CSS, rafraîchissons-nous un peu la mémoire :

Quelques rappels sur le CSS

CSS est l'abréviation de Cascading Style Sheets, c'est un langage qui permet d'éditer l'aspect graphique des éléments HTML et XML. Il est possible d'éditer le CSS d'un seul élément comme nous le ferions en HTML de la manière suivante :

<div style="color:red;">Le CSS de cet élément a été modifié avec l'attribut STYLE. Il n'y a donc que lui qui possède un texte de couleur rouge.</div>

Mais on peut tout aussi bien éditer les feuilles de style qui se présentent de la manière suivante :

div {
color: red; /* Ici on modifie la couleur du texte de tous les éléments <div> */
}

Il est de bon ton de vous le rappeler : les propriétés CSS de l'attributstylesont prioritaires sur les propriétés d'une feuille de style ! Ainsi, dans le code d'exemple suivant, le texte n'est pas rouge mais bleu :

<style>
div {
color: red;
}
</style>
<div style="color:blue;">I'm blue ! DABADIDABADA !</div>

Voilà tout pour les rappels sur le CSS. Oui, c'était très rapide, mais il suffisait simplement d'insister sur cette histoire de priorité des styles CSS, parce que ça va vous servir !

Éditer les styles CSS d'un élément

Comme nous venons de le voir, il y a deux manières de modifier le CSS d'un élément HTML, nous allons ici aborder la méthode la plus simple et la plus utilisée : l'utilisation de la propriétéstyle. L'édition des feuilles de style ne sera pas abordée, car elle est profondément inutile en plus d'être mal gérée par de nombreux navigateurs.

Alors comment accéder à la propriétéstylede notre élément ? Eh bien de la même manière que pour accéder à n'importe quelle propriété de notre élément :

element.style; // On accède à la propriété « style » de l'élément « element »

Une fois que l'on a accédé à notre propriété, comment modifier les styles CSS ? Eh bien tout simplement en écrivant leur nom et en leur attribuant une valeur,width(pour la largeur) par exemple :

element.style.width = '150px'; // On modifie la largeur de notre élément à 150px

Maintenant, une petite question pour vous : comment accède-t-on à une propriété CSS qui possède un nom composé ? En JavaScript, les tirets sont interdits dans les noms des propriétés, ce qui fait que ce code ne fonctionne pas :

element.style.background-color = 'blue'; // Ce code ne fonctionne pas, les tirets sont interdits

La solution est simple : supprimer les tirets et chaque mot suivant normalement un tiret voit sa première lettre devenir une majuscule. Ainsi, notre code précédent doit s'écrire de la manière suivante pour fonctionner correctement :

element.style.backgroundColor = 'blue'; // Après avoir supprimé le tiret et ajouté une majuscule au deuxième mot, le code fonctionne !

Comme vous pouvez le constater, l'édition du CSS d'un élément n'est pas bien compliquée. Cependant, il y a une limitation de taille : la lecture des propriétés CSS !

Prenons un exemple :

<style type="text/css">
#myDiv {
background-color: orange;
}
</style>
<div id="myDiv">Je possède un fond orange.</div>
<script>
var myDiv = document.getElementById('myDiv');
alert('Selon le JavaScript, la couleur de fond de ce <div> est : ' + myDiv.style.backgroundColor); // On affiche la couleur de fond
</script>

Essayer le code

Et on n'obtient rien ! Pourquoi ? Parce que notre code va lire uniquement les valeurs contenues dans la propriétéstyle. C'est-à-dire, rien du tout dans notre exemple, car nous avons modifié les styles CSS depuis une feuille de style, et non pas depuis l'attributstyle.

En revanche, en modifiant le CSS avec l'attributstyle, on retrouve sans problème la couleur de notre fond :

<div id="myDiv" style="background-color: orange">Je possède un fond orange.</div>
<script>
var myDiv = document.getElementById('myDiv');
alert('Selon le JavaScript, la couleur de fond de ce DIV est : ' + myDiv.style.backgroundColor); // On affiche la couleur de fond
</script>

Essayer le code

C'est gênant n'est-ce pas ? Malheureusement, on ne peut pas y faire grand-chose à partir de la propriétéstyle, pour cela nous allons devoir utiliser la méthodegetComputedStyle()!

Récupérer les propriétés CSS

La fonctiongetComputedStyle()

Comme vous avez pu le constater, il n'est pas possible de récupérer les valeurs des propriétés CSS d'un élément par le biais de la propriétéstylevu que celle-ci n'intègre pas les propriétés CSS des feuilles de style, ce qui nous limite énormément dans nos possibilités d'analyse… Heureusement, il existe une fonction permettant de remédier à ce problème :getComputedStyle()!

Cette fonction va se charger de récupérer, à notre place, la valeur de n'importe quel style CSS ! Qu'il soit déclaré dans la propriétéstyle, une feuille de style ou bien même encore calculé automatiquement, cela importe peu :getComputedStyle()la récupérera sans problème.

Son fonctionnement est très simple et se fait de cette manière :

<style>
#text {
color: red;
}
</style>
<span id="text"></span>
<script>
var text = document.getElementById('text'),
color = getComputedStyle(text).color;
alert(color);
</script>

Essayer le code

 

Les propriétés de typeoffset

Certaines valeurs de positionnement ou de taille des éléments ne pourront pas être obtenues de façon simple avecgetComputedStyle(), pour pallier ce problème il existe les propriétésoffsetqui sont, dans notre cas, au nombre de cinq :

Nom de l'attribut

Contient…

offsetWidth

Contient la largeur complète (width+padding+border) de l'élément.

offsetHeight

Contient la hauteur complète (height+padding+border) de l'élément.

offsetLeft

Surtout utile pour les éléments en position absolue.
Contient la position de l'élément par rapport au bord gauche de son élément parent.

offsetTop

Surtout utile pour les éléments en position absolue.
Contient la position de l'élément par rapport au bord supérieur de son élément parent.

offsetParent

Utile que pour un élément en position absolue ou relative !
Contient l'objet de l'élément parent par rapport auquel est positionné l'élément actuel.

Leur utilisation ne se fait pas de la même manière que n'importe quel style CSS, tout d'abord parce que ce ne sont pas des styles CSS ! Ce sont juste des propriétés (en lecture seule) mises à jour dynamiquement qui concernent certains états physiques d'un élément.
Pour les utiliser, on oublie la propriétéstylevu qu'il ne s'agit pas de styles CSS et on les lit directement sur l'objet de notre élément HTML :

alert(el.offsetHeight); // On affiche la hauteur complète de notre élément HTML
La propriétéoffsetParent

Concernant la propriétéoffsetParent, elle contient l'objet de l'élément parent par rapport auquel est positionné votre élément actuel. C'est bien, mais qu'est-ce que ça veut dire ?

Ce que nous allons vous expliquer concerne des connaissances en HTML et en CSS et non pas en JavaScript ! Seulement, il est fort possible que certains d'entre vous ne connaissent pas ce fonctionnement particulier du positionnement absolu, nous préférons donc vous le rappeler.

Lorsque vous décidez de mettre un de vos éléments HTML en positionnement absolu, celui-ci est sorti du positionnement par défaut des éléments HTML et va aller se placer tout en haut à gauche de votre page Web, par-dessus tous les autres éléments. Seulement, ce principe n'est applicable que lorsque votre élément n'est pas déjà lui-même placé dans un élément en positionnement absolu. Si cela arrive, alors votre élément se positionnera non plus par rapport au coin supérieur gauche de la page Web, mais par rapport au coin supérieur gauche du précédent élément placé en positionnement absolu, relatif ou fixe.

Ce système de positionnement est clair ? Bon, nous pouvons alors revenir à notre propriétéoffsetParent! Si elle existe, c'est parce que les propriétésoffsetTopetoffsetLeftcontiennent le positionnement de votre élément par rapport à son précédent élément parent et non pas par rapport à la page ! Si nous voulons obtenir son positionnement par rapport à la page, il faudra alors aussi ajouter les valeurs de positionnement de son (ses) élément(s) parent(s).

Voici le problème mis en pratique ainsi que sa solution :

<style>
#parent,
#child {
position: absolute;
top: 50px;
left: 100px;
}
#parent {
width: 200px;
height: 200px;
background-color: blue;
}
#child {
width: 50px;
height: 50px;
background-color: red;
}
</style>
<div id="parent">
<div id="child"></div>
</div>
<script>
var parent = document.getElementById('parent');
var child = document.getElementById('child');
alert("Sans la fonction de calcul, la position de l'élément enfant est : \n\n" +
'offsetTop : ' + child.offsetTop + 'px\n' +
'offsetLeft : ' + child.offsetLeft + 'px');
function getOffset(element) { // Notre fonction qui calcule le positionnement complet
var top = 0,
left = 0;
do {
top += element.offsetTop;
left += element.offsetLeft;
} while (element = element.offsetParent); // Tant que « element » reçoit un « offsetParent » valide alors on additionne les valeurs des offsets
return { // On retourne un objet, cela nous permet de retourner les deux valeurs calculées
top: top,
left: left
};
}
alert("Avec la fonction de calcul, la position de l'élément enfant est : \n\n" +
'offsetTop : ' + getOffset(child).top + 'px\n' +
'offsetLeft : ' + getOffset(child).left + 'px');
</script>

Essayer le code

Comme vous pouvez le constater, les valeurs seules de positionnement de notre élément enfant ne sont pas correctes si nous souhaitons connaître son positionnement par rapport à la page et non pas par rapport à l'élément parent. Nous sommes finalement obligés de créer une fonction pour calculer le positionnement par rapport à la page.

Concernant cette fonction, nous allons insister sur la boucle qu'elle contient car il est probable que le principe ne soit pas clair pour vous :

do {
top += element.offsetTop;
left += element.offsetLeft;
} while (element = element.offsetParent);

Si on utilise ce code HTML :

<body>
<div id="parent" style="position:absolute; top:200px; left:200px;">
<div id="child" style="position:absolute; top:100px; left:100px;"></div>
</div>
</body>

son schéma de fonctionnement sera le suivant pour le calcul des valeurs de positionnement de l'élément#child:

  • La boucle s'exécute une première fois en ajoutant les valeurs de positionnement de l'élément#childà nos deux variablestopetleft. Le calcul effectué est donc :

    top = 0 + 100; // 100
    left = 0 + 100; // 100
  • Ligne 4, on attribue àelementl'objet de l'élément parent de#child. En gros, on monte d'un cran dans l'arbre DOM. L'opération est donc la suivante :

    element = child.offsetParent; // Le nouvel élément est « parent »
  • Toujours ligne 4,elementpossède une référence vers un objet valide (qui est l'élément#parent), la condition est donc vérifiée (l'objet est évalué àtrue) et la boucle s'exécute de nouveau.

  • La boucle se répète en ajoutant cette fois les valeurs de positionnement de l'élément#parentà nos variablestopetleft. Le calcul effectué est donc :

    top = 100 + 200; // 300
    left = 100 + 200; // 300
  • Ligne 4, cette fois l'objet parent de#parentest l'élément<body>. La boucle va donc se répéter avec<body>qui est un objet valide. Comme nous n'avons pas touché à ses styles CSS il ne possède pas de valeurs de positionnement, le calcul effectué est donc :

    top = 300 + 0; // 300
    left = 300 + 0; // 300
  • Ligne 4,<body>a une propriétéoffsetParentqui est àundefined, la boucle s'arrête donc.

Voilà tout pour cette boucle ! Son fonctionnement n'est pas bien compliqué mais peut en dérouter certains, c'est pourquoi il valait mieux vous l'expliquer en détail.

Avant de terminer : pourquoi avoir écrit « hauteur complète (width+padding+border) » dans le tableau ? Qu'est-ce que ça veut dire ?

Il faut savoir qu'en HTML, la largeur (ou hauteur) complète d'un élément correspond à la valeur de width + celle du padding + celle des bordures.

Par exemple, sur ce code :

<style>
#offsetTest {
width: 100px;
height: 100px;
padding: 10px;
border: 2px solid black;
}
</style>
<div id="offsetTest"></div>

la largeur complète de notre élément<div>vaut :100(width) +10(padding-left) +10(padding-right) +2(border-left) +2(border-right) = 124px.

Et il s'agit bien de la valeur retournée paroffsetWidth:

var offsetTest = document.getElementById('offsetTest');
alert(offsetTest.offsetWidth);

Essayer le code complet

Votre premier script interactif !

Soyons francs : les exercices que nous avons fait précédemment n'étaient quand même pas très utiles sans une réelle interaction avec l'utilisateur. Lesalert(),confirm()etprompt()c'est sympa un moment, mais on en a vite fait le tour ! Il est donc temps de passer à quelque chose de plus intéressant : un système de drag & drop ! Enfin… une version très simple !

Il s'agit ici d'un mini-TP, ce qui veut dire qu'il n'est pas très long à réaliser, mais il demande quand même un peu de réflexion. Ce TP vous fera utiliser les événements et les manipulations CSS.

Présentation de l'exercice

Tout d'abord, qu'est-ce que le drag & drop ? Il s'agit d'un système permettant le déplacement d'éléments par un simple déplacement de souris. Pour faire simple, c'est comme lorsque vous avez un fichier dans un dossier et que vous le déplacez dans un autre dossier en le faisant glisser avec votre souris.

Et je suis vraiment capable de faire ça ?

Bien évidemment ! Bon, il faut avoir suivi attentivement le cours et se démener un peu, mais c'est parfaitement possible, vous en êtes capables !

Avant de se lancer dans le code, listons les étapes de fonctionnement d'un système de drag & drop :

  • L'utilisateur enfonce (et ne relâche pas) le bouton gauche de sa souris sur un élément. Le drag & drop s'initialise alors en sachant qu'il va devoir gérer le déplacement de cet élément. Pour information, l'événement à utiliser ici estmousedown.

  • L'utilisateur, tout en laissant le bouton de sa souris enfoncé, commence à déplacer son curseur, l'élément ciblé suit alors ses mouvements à la trace. L'événement à utiliser estmousemoveet nous vous conseillons de l'appliquer à l'élémentdocument, nous vous expliquerons pourquoi dans la correction.

  • L'utilisateur relâche le bouton de sa souris. Le drag & drop prend alors fin et l'élément ne suit plus le curseur de la souris. L'événement utilisé estmouseup.

Alors ? Ça n'a pas l'air si tordu que ça, n'est-ce pas ?

Maintenant que vous savez à peu près ce qu'il faut faire, nous allons vous fournir le code HTML de base ainsi que le CSS, vous éviterez ainsi de vous embêter à faire cela vous-mêmes :

<div class="draggableBox">1</div>
<div class="draggableBox">2</div>
<div class="draggableBox">3</div>
.draggableBox {
position: absolute;
width: 80px; height: 60px;
padding-top: 10px;
text-align: center;
font-size: 40px;
background-color: #222;
color: #CCC;
cursor: move;
}

Juste deux dernières petites choses. Il serait bien :

  1. Que vous utilisiez une IIFE dans laquelle vous allez placer toutes les fonctions et variables nécessaires au bon fonctionnement de votre code, ce sera bien plus propre. Ainsi, votre script n'ira pas polluer l'espace global avec ses propres variables et fonctions ;

  2. Que votre code ne s'applique pas à tous les<div>existants mais uniquement à ceux qui possèdent la classe.draggableBox.

Sur ce, bon courage !

Correction

Vous avez terminé l'exercice ? Nous espérons que vous l'avez réussi, mais si ce n'est pas le cas ce n'est pas grave ! Regardez attentivement la correction, et tout devrait être plus clair.

(function() { // On utilise une IIFE pour ne pas polluer l'espace global
var storage = {}; // Contient l'objet de la div en cours de déplacement
function init() { // La fonction d'initialisation
var elements = document.querySelectorAll('.draggableBox'),
elementsLength = elements.length;
for (var i = 0; i < elementsLength; i++) {
elements[i].addEventListener('mousedown', function(e) { // Initialise le drag & drop
var s = storage;
s.target = e.target;
s.offsetX = e.clientX - s.target.offsetLeft;
s.offsetY = e.clientY - s.target.offsetTop;
});
elements[i].addEventListener('mouseup', function() { // Termine le drag & drop
storage = {};
});
}
document.addEventListener('mousemove', function(e) { // Permet le suivi du drag & drop
var target = storage.target;
if (target) {
target.style.top = e.clientY - storage.offsetY + 'px';
target.style.left = e.clientX - storage.offsetX + 'px';
}
});
}
init(); // On initialise le code avec notre fonction toute prête.
})();

Essayer le code complet

Concernant la variablestorage, il ne s'agit que d'un espace de stockage dont nous allons vous expliquer le fonctionnement au cours de l'étude de la fonctioninit().

Commençons !

L'exploration du code HTML

Notre fonctioninit()commence par le code suivant :

var elements = document.querySelectorAll('.draggableBox'),
elementsLength = elements.length;
for (var i = 0; i < elementsLength; i++) {
// Code...
}

Dans ce code, nous avons volontairement caché les codes d'ajout d'événements, car ce qui nous intéresse c'est cette boucle. Cette boucle couplée à la méthodequerySelectorAll() — vous l'avez déjà vue dans le chapitre sur la manipulation du code HTML — elle permet de parcourir tous les éléments HTML filtrés par un sélecteur CSS. Dans notre cas, nous parcourons tous les éléments avec la classe .draggableBox .

L'ajout des événementsmousedownetmouseup

Dans notre boucle qui parcourt le code HTML, nous avons deux ajouts d'événements que voici :

elements[i].addEventListener('mousedown', function(e) { // Initialise le drag & drop
var s = storage;
s.target = e.target;
s.offsetX = e.clientX - s.target.offsetLeft;
s.offsetY = e.clientY - s.target.offsetTop;
});
elements[i].addEventListener('mouseup', function() { // Termine le drag & drop
storage = {};
});

Comme vous pouvez le voir, ces deux événements ne font qu'accéder à la variablestorage. À quoi nous sert donc cette variable ? Il s'agit tout simplement d'un objet qui nous sert d'espace de stockage, il permet de mémoriser l'élément actuellement en cours de déplacement ainsi que la position du curseur par rapport à notre élément (nous reviendrons sur ce dernier point plus tard).

Bref, dans notre événementmousedown(qui initialise le drag & drop), nous ajoutons l'événement ciblé dans la propriétéstorage.target puis les positions du curseur par rapport à notre élément dansstorage.offsetX etstorage.offsetY.

En ce qui concerne notre événementmouseup(qui termine le drag & drop), on attribue juste un objet vide à notre variablestorage, comme ça tout est vidé !

La gestion du déplacement de notre élément

Jusqu'à présent, notre code ne fait qu'enregistrer dans la variablestoragel'élément ciblé pour notre drag & drop. Cependant, notre but c'est de faire bouger cet élément. Voilà pourquoi notre événementmousemoveintervient !

document.addEventListener('mousemove', function(e) { // Permet le suivi du drag & drop
var target = storage.target;
if (target) {
target.style.top = e.clientY - storage.offsetY + 'px';
target.style.left = e.clientX - storage.offsetX + 'px';
}
});

Pourquoi notre événement est-il appliqué à l'élémentdocument?

Réfléchissons ! Si nous appliquons cet événement à l'élément ciblé, que va-t-il se passer ? Dès que l'on bougera la souris, l'événement se déclenchera et tout se passera comme on le souhaite, mais si je me mets à bouger la souris trop rapidement, le curseur va alors sortir de notre élément avant que celui-ci n'ait eu le temps de se déplacer, ce qui fait que l'événement ne se déclenchera plus tant que l'on ne replacera pas notre curseur sur l'élément. La probabilité pour que cela se produise est plus élevée que l'on ne le pense, autant prendre toutes les précautions nécessaires.

Un autre problème peut aussi surgir : dans notre code actuel, nous ne gérons pas le style CSSz-index, ce qui fait que lorsqu'on déplace le premier élément et que l'on place notre curseur sur un des deux autres éléments, le premier élément se retrouve alors en dessous d'eux. En quoi est-ce un problème ? Eh bien si on a appliqué lemousemovesur notre élément au lieu dudocumentalors cet événement ne se déclenchera pas vu que l'on bouge notre curseur sur un des deux autres éléments et non pas sur notre élément en cours de déplacement.

La solution est donc de mettre l'événementmousemovesur notredocument. Vu que cet événement se propage aux enfants, nous sommes sûrs qu'il se déclenchera à n'importe quel déplacement du curseur sur la page.

Le reste du code n'est pas bien sorcier :

  • On utilise une condition qui vérifie qu'il existe bien un indicetargetdans notre espace de stockage. Si il n'y en a pas c'est qu'il n'y a aucun drag & drop en cours d'exécution.

  • On assigne à notre élément cible (target) ses nouvelles coordonnées par rapport au curseur.

Alors revenons sur un point important du précédent code : il nous a fallu enregistrer la position du curseur par rapport au coin supérieur gauche de notre élément dès l'initialisation du drag & drop :

La valeur X désigne le décalage (en pixels) entre les bordures gauche de l'élément et du curseur, la valeur Y fait de même entre les bordures supérieures
La valeur X désigne le décalage (en pixels) entre les bordures gauche de l'élément et du curseur, la valeur Y fait de même entre les bordures supérieures

Pourquoi ? Car si vous ne le faites pas, à chaque fois que vous déplacerez votre élément, celui-ci placera son bord supérieur gauche sous votre curseur et ce n'est clairement pas ce que l'on souhaite.

Essayez donc par vous-mêmes pour vérifier !

Empêcher la sélection du contenu des éléments déplaçables

Comme vous l'avez peut-être constaté, il est possible que l'utilisateur sélectionne le texte contenu dans vos éléments déplaçables, cela est un peu aléatoire. Heureusement, il est possible de résoudre simplement ce problème avec quelques propriétés CSS appliquées aux éléments déplaçables :

/* L'utilisateur ne pourra plus sélectionner le texte de l'élément qui possède cette propriété CSS */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;

Essayer une adaptation de ce code

Et voilà pour ce mini-TP, nous espérons qu'il vous a plu !

En résumé
  • Pour modifier les styles CSS d'un élément, il suffit d'utiliser la propriétéstyle. Il ne reste plus qu'à accéder à la bonne propriété CSS, par exemple :element.style.height = '300px'.

  • Le nom des propriétés composées doit s'écrire sans tiret et avec une majuscule pour débuter chaque mot, à l'exception du premier. Ainsi,border-radiusdevientborderRadius.

  • La fonctiongetComputedStyle()récupère la valeur de n'importe quelle propriété CSS. C'est utile, car la propriétéstylen'accède pas aux propriétés définies dans la feuille de style.

  • Les propriétés de typeoffset, au nombre de cinq, permettent de récupérer des valeurs liées à la taille et au positionnement.

  • Le positionnement absolu peut poser des problèmes. Voilà pourquoi il faut savoir utiliser la propriétéoffsetParent, combinée aux autres propriétésoffset.