Mis à jour le mardi 30 décembre 2014
  • 30 minutes
  • Moyenne
Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Introduction du cours

Concept de base

En créant une partie administration d'un site, il arrive que l'on veuille directement modifier le code de la page directement depuis le site Internet. Ce genre de système est assez facile à réaliser à l'aide de PHP et mySQL (ou autre) et un formulaire donnant accès au script à l'aide d'une balise "textarea".

Cependant, pour des codes relativement longs, la lisibilité est loin d'être optimale (personnellement à partir de 20 lignes, ça se complexifie). Pour remédier à ce problème, il est tentant de directement mettre de la mise en page dans le champ du formulaire, mais cela modifierait le contenu.

La solution que je propose ici part d'une idée simple : rendre le texte de la balise "textarea"  transparent, créer une balise miroir derrière la balise de code et y dupliquer le texte de la première balise. Ce sera lors de cette troisième étape que le code pourra être coloré.

Prérequis

Avant de s'attaquer au cours en tant que tel, il est important de s'être déjà familiarisé avec JavaScript. Je recommande donc la lecture des trois premiers chapitres du tutoriel de Thunderseb et Nesk : Dynamisez vos sites web avec Javascript !

De la même façon, il est recommandé d'avoir des bases en HTML et CSS. Le tutoriel de M@teo21 est parfait pour cela : Apprenez à créer votre site web avec HTML5 et CSS3

Certaines idées ont été aussi reprises de l’article (en anglais) de A list Apart : Expanding Text Areas Made Elegant par Neil Jenkins.

Mise en place des deux cadres superposés : HTML et CSS

Un tout petit peu de HTML

La première étape est de bien définir les balises :   

  • La balise textarea au premier plan qui sera appelée balise code, ou simplement code

  • La balise miroir en arrière-plan   

  • Une balise globale

Cette dernière servira comme point d'accroche de base des deux balises précédentes.

Ce qui donne, par exemple, le script suivant :

<div class="global">
  <textarea class="col_code" id="code"></textarea>
  <div class="col_code" id="mirror"></div>
</div>

Pour le moment, le script n'est pas encore trop complexe.

Mise en place du CSS

Cette étape est un tout petit peu plus complexe : Il faut veiller à garder exactement la même présentation entre le code et le cadre miroir.

Voici donc un moyen pour y arriver (la description viendra après) :

.global {
  position:relative;
}
.col_code {
  position    : absolute;
  font-family : Consolas,Monaco,"Lucida Console","Liberation Mono","DejaVu Sans Mono","Bitstream Vera Sans Mono","Courier New", monospace;
  font-size   : 12pt;
  word-break  : break-all;
  word-wrap   : break-word;
  white-space : pre-wrap;
  border-width: 2px;
  border-style: groove;
  margin      : 0px;
  padding     : 0px;
  width       : 400px;
  height      : 400px;
}
textarea.col_code {
  z-index         : 1;
  color           : rgba(0,0,0,0.3);
  background-color: transparent;
  overflow        : scroll;
}
div.col_code {
  z-index         : 0;
  color           : black;
  background-color: white;
  overflow        : hidden;
}

Premièrement, le cadre global est en position relative, afin de donner un point de référence aux deux autres cadres (en position absolue). Cette position absolue permet de les placer l'un au-dessus de l'autre à l'aide de la propriété z-index .

Il faut faire attention à ce que le code et le cadre miroir aient la même police, soient de la même taille, que l'on conserve les espaces multiples (white-space: pre-warp; ) et que le passage à la ligne des mots de fasse de façon similaire (ligne 11 et 12).

On peut aussi remarquer que le texte du code n'est pas entièrement transparent, afin de s'assurer du bon alignement du texte entre les deux cadres. De plus, il n'est pas aisé d'introduire un curseur indiquant la zone de sélection du code.

Synchronisation dynamique des cadres : JavaScript

Descendre le cadre miroir en synchronisation avec le cadre code

Pour descendre le cadre miroir, il faudra modifier les propriétés scrollTop   et scrollLeft   de celui-ci à chaque fois que l'on descend dans le code.

Heureusement, un événement JavaScript prend cette propriété en compte : l'événement scroll . Il reste donc à ajouter une fonction JavaScript qui s'occupe de tout ça :

function setMirrorScroll() {
  document.getElementById('mirror').scrollTop  = document.getElementById('code').scrollTop;
  document.getElementById('mirror').scrollLeft = document.getElementById('code').scrollLeft;
}

Il existe plusieurs moyens de rajouter un événement JavaScript, mais afin de garder notre script le plus léger possible, il sera inclus directement dans le code HTML de la page qui, une fois modifié, donne :

<div class="global">
  <textarea class="col_code" id="code"
        onscroll="setMirrorScroll();"></textarea>
  <div class="col_code" id="mirror"></div>
</div>

Redimensionner le cadre miroir

Cette partie demande un peu plus de subtilité quant à la prise en charge des événements. En effet, il n'existe pas d'événement JavaScript prenant en charge le redimensionnement d'un cadre. Néanmoins, redimensionner le cadre du code nécessite l'utilisation de la souris, et prend effet lorsque celle-ci est relâchée.

L'événement utilisé sera mouseup  associé au cadre du code. La taille de celui-ci sera récupérée à l'aide de ses propriétés clientWidth  et clientHeight  et ainsi modifier le height   et width   du CSS du cadre miroir.

La fonction JavaScript sera donc :

function setMirrorSize() {
  var textarea_elem   = document.getElementById('code');
  var textarea_width  = textarea_elem.clientWidth;
  var textarea_height = textarea_elem.clientHeight;
  var background_elem = document.getElementById('mirror');
  background_elem.style.width  = textarea_width +"px";
  background_elem.style.height = textarea_height+"px";
}

Et le code HTML devient :

<div class="global">
  <textarea class="col_code" id="code"
        onscroll="setMirrorScroll();"
        onmouseup="setMirrorSize();"></textarea>
  <div class="col_code" id="mirror"></div>
</div>

Voilà maintenant deux cadres parfaitement synchronisés !

Il ne reste donc plus qu'à s'attaquer à la mise en couleur du contenu du cadre code dans le cadre miroir...

Copie et coloration du code : JavaScript

Copie du code

La première chose à prendre en compte est de se demander quand transférer le code d'un cadre à l'autre. Il serait tentant de le faire de façon récursive toutes les 500 ms par exemple, mais cette méthode fort lourde est loin d'être optimale. Il existe cependant une alternative.

Celle-ci est l'utilisation d'un événement plus spécifique : l'événement input. Celui-ci s'enclenche lorsque l'on envoie une entée vers le champ (par exemple un caractère tapé sur le clavier). Cependant, cet événement n'est pas compatible avec certains vieux navigateurs tels qu’Internet Explorer 6, 7 et 8. Une méthode pour que ceux-ci soient compatibles sera expliquée à la fin de ce tutoriel.

Le code HTML devient alors :

<div class="global">
  <textarea class="col_code" id="code"
        onscroll="setMirrorScroll();"
        onmouseup="setMirrorSize();"
        oninput="refreshMirrorCode();"></textarea>
  <div class="col_code" id="mirror"></div>
</div>

La fonction JavaScript pour transférer le code du cadre code vers le cadre miroir est quand à elle :

function refreshMirrorCode() {
  var script_in  = document.getElementById('code').value;
  var script_out = script_in;
  document.getElementById('mirror').innerHTML = script_out;
}

Cependant, il persiste un problème : le code du cadre mirroir en arrière-plan ne passe pas à la ligne !

Pour y remédier, nous allons utiliser les expressions régulières de JavaScript afin que les passages à la ligne dans le cadre code soient remplacés par la balise HTML de passage à la ligne <br />  dans le cadre miroir.

De plus lorsque l'un descend tout en bas dans la balise code, il y a un décalage entre le cadre de code et le cadre miroir. Pour y remédier, un passage à la ligne est ajouté à la fin du second cadre.

Finalement, lorsque l'on inscrit du code HTML dans le dans le cadre du code, celui-ci est interprété dans le cadre miroir, if faut donc remplacer certains caractères spéciaux par leur équivalent de caractère spécial HTML afin d'éviter ce problème.

Pour y arriver, nous allons donc rajouter les quelques expressions régulières suivant :

function refreshMirrorCode() {
  var script_in  = document.getElementById('code').value;
      script_in  = script_in.replace(/&/g, '&amp;'); 
      script_in  = script_in.replace(/\//g, '&#47;');
      script_in  = script_in.replace(/</g, '&lt;');
      script_in  = script_in.replace(/>/g, '&gt;');
      script_in  = script_in.replace(/"/g, '&quot;');
      script_in  = script_in.replace(/'/g, '&apos;');
  var script_out = script_in.replace(/(?:\r\n|\r|\n)/g, '<br />');
  document.getElementById('mirror').innerHTML = script_out + '<br />';
}

Et voilà ! La balise mirroir recrée à l'identique la balise code !

Même si ce n'est pas encore fort impressionnant, nous verrons dans la section suivante comment ajouter des couleurs à votre joli code.

Coloration du code

Voilà le moment tant attendu: mettre le code en couleur.

Le principe est simple : rajouter à la manière d'un BBcode un ensemble d'expressions régulières permettant la mise en couleur de certains éléments. L'exemple qui suit est un exemple simple mettant en couleur du code HTML : les balises en vert, les guillemets en rouge, les chiffres en jaune et les commentaires en gris. Mais cela peut facilement être modifié à l’envie. Que ce soit pour le choix des couleurs ou pour mettre en couleur un autre langage (comme le JavaScript par exemple...).

function refreshMirrorCode() {
  var script_in  = document.getElementById('code').value;
      script_in  = script_in.replace(/&/g, '&amp;'); 
      script_in  = script_in.replace(/\//g, '&#47;');
      script_in  = script_in.replace(/</g, '&lt;');
      script_in  = script_in.replace(/>/g, '&gt;');
      script_in  = script_in.replace(/"/g, '&quot;');
      script_in  = script_in.replace(/'/g, '&apos;');
      script_in  = script_in.replace(/(&lt;[a-zA-Z](.*?)&gt;)/g, '<span style="color:green;">$1</span>'); // balises en vert
      script_in  = script_in.replace(/(&lt;&#47;[a-zA-Z](.*?)&gt;)/g, '<span style="color:green;">$1</span>'); // balises en vert
      script_in  = script_in.replace(/(&lt;!--(.+?)--&gt;)/g, '<span style="color:lightgrey;">$1</span>'); // commentaires en gris
      script_in  = script_in.replace(/(&quot;(.+?)&quot;)/g, '<span style="color:red;">$1</span>'); // guillements en rouge
        var script_out = script_in.replace(/(?:\r\n|\r|\n)/g, '<br />');
  document.getElementById('mirror').innerHTML = script_out + '<br />';
}

Voilà qui est fait !

Récapitulatif

Avant de terminer, voici un petit récapitulatif du code HTML, CSS et JavaScript complet :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Tutoriel : Mise en couleur du code d'une balise "texarea" grâce à Javascript</title>
    <style>
      .global {
        position:relative;
      }
      .col_code {
        position    : absolute;
        font-family : Consolas,Monaco,"Lucida Console","Liberation Mono","DejaVu Sans Mono","Bitstream Vera Sans Mono","Courier New", monospace;
        font-size   : 12pt;
        word-break  : break-all;
        word-wrap   : break-word;
        white-space : pre-wrap;
        border-width: 2px;
        border-style: groove;
        margin      : 0px;
        padding     : 0px;
        width       : 400px;
        height      : 400px;
      }
      textarea.col_code {
        z-index         : 1;
        color           : rgba(0,0,0,0.3);
        background-color: transparent;
        overflow        : scroll;
      }
      div.col_code {
        z-index         : 0;
        color           : black;
        background-color: white;
        overflow        : hidden;
      }
    </style>
  </head>

  <body>

    <div class="global">
      <textarea class="col_code" id="code"
            onscroll="setMirrorScroll();"
            onmouseup="setMirrorSize();"
            oninput="refreshMirrorCode();"></textarea>
      <div class="col_code" id="mirror"></div>
    </div>

    <script type="text/javascript">
      function setMirrorScroll() {
        document.getElementById('mirror').scrollTop  = document.getElementById('code').scrollTop;
        document.getElementById('mirror').scrollLeft = document.getElementById('code').scrollLeft;
      }
      function setMirrorSize() {
        var textarea_elem   = document.getElementById('code');
        var textarea_width  = textarea_elem.clientWidth;
        var textarea_height = textarea_elem.clientHeight;
        var background_elem = document.getElementById('mirror');
        background_elem.style.width  = textarea_width +"px";
        background_elem.style.height = textarea_height+"px";
      }
      function refreshMirrorCode() {
        var script_in  = document.getElementById('code').value;
            script_in  = script_in.replace(/&/g, '&amp;'); 
            script_in  = script_in.replace(/\//g, '&#47;');
            script_in  = script_in.replace(/</g, '&lt;');
            script_in  = script_in.replace(/>/g, '&gt;');
            script_in  = script_in.replace(/"/g, '&quot;');
            script_in  = script_in.replace(/'/g, '&apos;');
            script_in  = script_in.replace(/(&lt;[a-zA-Z](.*?)&gt;)/g, '<span style="color:green;">$1</span>'); // balises en vert
            script_in  = script_in.replace(/(&lt;&#47;[a-zA-Z](.*?)&gt;)/g, '<span style="color:green;">$1</span>'); // balises en vert
            script_in  = script_in.replace(/(&lt;!--(.+?)--&gt;)/g, '<span style="color:lightgrey;">$1</span>'); // commentaires en gris
            script_in  = script_in.replace(/(&quot;(.+?)&quot;)/g, '<span style="color:red;">$1</span>'); // guillements en rouge
              var script_out = script_in.replace(/(?:\r\n|\r|\n)/g, '<br />');
        document.getElementById('mirror').innerHTML = script_out + '<br />';
      }
    </script>
  </body>
</html>

Pour aller plus loin...

Il est possible de faire de nombreuses améliorations de cette mise en couleur, par exemple :   

  • Rajouter un cadre sur la gauche qui indique le numéro des lignes.

  • Permettre de changer la coloration suivant le language du code.

  • Proposer une autocomplétion des balises.

  • Utiliser des raccourcis clavier pour simplifier l'édition (afin de se simplifier la vie, un script pour faciliter l'utilisation des raccourcis est disponible ici).

  • ...

Bon amusement !

Bonus : compatibilité avec de plus anciens navigateurs

Comme exprimé plus tôt, l'événement input  n'est pas compatible avec des navigateurs plus anciens. Il est cependant possible de contourner le problème.

Pour y arriver, nous allons reprendre le petit script de la gestion d'événement repris du tutoriel JavaScript cité plus haut :

function addEvent(element, event, func) {
  if (element.addEventListener) {
    element.addEventListener(event, func, false);
  }
  else {
  element.attachEvent('on' + event, func);
  }
}


function removeEvent(element, event, func) {
  if (element.removeEventListener) {
    element.removeEventListener(event, func, false);
  }
  else {
  element.detachEvent('on' + event, func);
  }
}

Pour commencer, Internet Explorer 8 ne dispose pas de l'événement input, mais dispose d'un événement similaire appelé propertychange. Il faudra donc détecter le navigateur web, et si celui-ci est Internet Explorer 8, modifier l'événement input en propertychange.

function addInputEvent(elem, func) {  
  if (elem.addEventListener) {
    elem.addEventListener('input', func, false);
  }
  else if (elem.attachEvent) { // IE8 compatibility
    elem.attachEvent('onpropertychange', func);
  }
}

function removeInputEvent(elem, func) {  
  if (elem.removeEventListener) {
    elem.removeEventListener('input', func, false);
  }
  else if (elem.detachEvent) { // IE8 compatibility
    elem.detachEvent('onpropertychange', func);
  }
}

Cependant, des navigateurs plus anciens qui ne gèrent pas l'événement input existent toujours (comme Internet Explorer 6 et 7), il est aussi possible de garantir une certaine compatibilité, même si celle-ci n'est pas complète en remplaçant l'événement input par keyup (lorsque l'on relève une touche de clavier).  Cet événement ne gère pas le copier-coller à la souris, et le fait d'appuyer longtemps sur une touche. Il permet cependant d'effectuer un service minimum pour la compatibilité.

Le script devient alors :

function addInputEvent(elem, func) {  
  if (elem.addEventListener) {
    elem.addEventListener('input', func, false);
  }
  else if (elem.attachEvent) { // IE8 compatibility
    if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)){ //test for MSIE x.x;
      var ieversion=new Number(RegExp.$1) // capture x.x portion and store as a number
      if (ieversion<=8) {
        elem.attachEvent('onkeyup', func );
      }
      else {
        elem.attachEvent('onpropertychange', func);
      }
    }
  }
}

function removeInputEvent(elem, func) {  
  if (elem.removeEventListener) {
    elem.removeEventListener('input', func, false);
  }
  else if (elem.detachEvent) { // IE8 compatibility
    if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)){ //test for MSIE x.x;
      var ieversion=new Number(RegExp.$1) // capture x.x portion and store as a number
      if (ieversion<=8){
        elem.detachEvent('onkeyup', func );
      }
      else {
        elem.detachEvent('onpropertychange', func);
      }
    }
  }
}

Et voilà, il ne reste plus qu'à insérer ces fonctions dans le code, et c'est parti pour une meilleure compatibilité pour votre code en couleur !

Exemple de certificat de réussite
Exemple de certificat de réussite