Dans ce tutoriel, je vais vous expliquer comment créer un système pour insérer des balises, un peu comme la zForm qu'utilise le Site du Zéro pour insérer des balises ZCode dans la zone de texte. La technique que je vais vous présenter peut bien évidemment être utilisée avec du BBCode, du WikiCode ou encore du XHTML. :)
Le tutoriel n'est pas très compliqué, mais je vous recommande chaudement d'avoir quelques bases en Javascript et en HTML.
Donc en gros, comme vous pouvez le voir, le code se compose de différents éléments INPUT qui feront office de boutons pour insérer les balises. Un élément SELECT est aussi présent pour insérer des balises avec des valeurs, comme dans la zForm. Les deux petites images sont des smilies, et réagiront de la même manière que les boutons d'insertion, mais pour insérer des smilies.
J'ai déjà mis une case à cocher pour activer ou non la prévisualisation. previewDiv et viewDiv seront les deux éléments dans lesquels la prévisualisation et la visualisation seront affichées.
Maintenant que le HTML est en place, commençons le javascriptage.
Le script d'insertion n'est absolument pas compliqué (d'ailleurs, il n'y a rien de compliqué dans ce tuto ^^ ). Je vais créer la fonction insertTag() qui permettra d'insérer une balise autour de la sélection (ou juste le curseur, s'il n'y a pas de sélection).
function insertTag(startTag, endTag, textareaId, tagType) {
var field = document.getElementById(textareaId); // On récupère la zone de texte
var scroll = field.scrollTop; // On met en mémoire la position du scroll
field.focus(); // On remet le focus sur la zone de texte, suivant les navigateurs, on perd le focus en appelant la fonction.
// Reste du code ici...
}
Les arguments startTag et endTag représentent respectivement la balise d'ouverture et la balise de fermeture. textareaId est l'ID de la zone de texte dans laquelle sera insérée la balise. tagType servira pour après pour les insertions de balises "exotiques", comme les citations, les liens, enfin, pour tout ce qui demande un traitement autre que simplement ajouter une balise d'ouverture et une balise de fermeture. :)
Maintenant que la fonction principale est définie, on peut créer les appels au sein du code HTML, avec un événement onClick :
Comme cela, quand l'utilisateur cliquera sur le bouton G (pour mettre en gras), les balises <gras> et </gras> seront insérées de part et d'autre du curseur, ou de la sélection.
Fonctionnement de la fonction d'insertion
Dans un premier temps, il faut récupérer la portion de texte sélectionnée (la sélection). Ensuite, il faut introduire, de part et d'autre de la sélection les balises. Dans les deux cas, la manière de procéder diffère suivant les navigateurs. Internet Explorer ne gère pas du tout les sélections de la même façon que les autres navigateurs. Enfin, en réalité, IE gère bien la sélection, car il est possible de ne récupérer que la sélection directement, alors qu'avec les autres navigateurs, il faut faire un peu plus d'analyses. ^^
Récupérer la sélection
La première étape de la fonction insertTag() est de récupérer la sélection. Si l'utilisateur n'a rien sélectionné, la sélection est vide (ouais, logique ça ^^ ). Internet Explorer possède son propre système pour récupérer une sélection.
Internet Explorer
Internet Explorer utilise l'objet selection pour manipuler les sélections de texte (pas seulement dans les zones de texte). Le principe est le suivant : on récupère la sélection (on crée un TextRange) avec la méthode createRange(), et on convertit cette sélection en texte pour pouvoir l'utiliser. La méthode createRange() récupère en fait le début et la fin de la sélection, c'est pour cela qu'il faut transformer, avec la propriété text :
var textRange = document.selection.createRange(); // On récupère sous forme de TextRange
var currentSelection = textRange.text; // On convertit ce TextRange en chaine de caractères
La variable currentSelection contient donc le texte de la sélection. :)
Les autres navigateurs
La méthode n'est pas du tout la même que pour Internet Explorer. Ici, il faut récupérer la position du début de la sélection, ainsi que la fin, et récupérer "manuellement" la sélection courante. Pour trouver le début et la fin de la sélection, on doit utiliser respectivement les propriétés selectionStart et selectionEnd :
var startSelection = field.value.substring(0, field.selectionStart);
var currentSelection = field.value.substring(field.selectionStart, field.selectionEnd);
var endSelection = field.value.substring(field.selectionEnd);
Mais en fait, quelle était l'utilité de récupérer la sélection ?
Bah c'est assez simple en fait : ça permet de savoir où se trouve le curseur (le curseur est en fait une sélection dont le début est égal à la fin, elle est nulle). Et en connaissant ça, on va pouvoir injecter les balises avant et après... De plus, on pourra agir et analyser le texte sélectionné pour faire des insertions spéciales, comme des liens ou des citations.
Réintroduire les balises
Ici encore, les méthodes pour les deux navigateurs sont différentes.
Internet Explorer
On continue avec l'objetselection ! On va reprendre la variable textRange et modifier son contenu. La sélection dans le champ de texte est alors "mise à jour" :
En théorie, tout fonctionne. Mais en pratique, il y a quelque chose d'embêtant : le curseur d'écriture : il n'est plus dans le champ ! Ce qu'on veut arriver à faire, c'est resélectionner ce qui se trouve entre les balises fraîchement insérées. Ainsi par exemple, réinsérer une nouvelle paire de balises sans devoir resélectionner.
Internet Explorer offre 3 méthodes permettant d'agir sur une sélection : move()moveStart() et moveEnd().
move() sert à déplacer la sélection complète. On ne va pas l'utiliser, mais elle peut être utile ;
moveStart() sert à déplacer le début de la sélection ;
moveEnd() sert donc à déplacer la fin de la sélection.
Les méthodes move(sUnit,iCount) prennent deux arguments : sUnit et iCount. sUnit est un peu spécial : c'est la manière de déplacer la sélection. En indiquant character on indique qu'on veut déplacer lettre par lettre. D'autres valeurs sont acceptées comme word ou sentence. iCount, c'est tout simplement la grandeur du déplacement.
Dans le cas du moveStart(), iCount vaudra la longueur de la balise de fin (endTag) plus la longueur de la sélection. C'est présenté sous la forme d'une soustraction car on recule le début de la sélection. :)
Dans le cas du moveEnd(), iCount vaudra juste la longueur de endTag, toujours en négatif.
Pour terminer, on applique la méthode select() à notre TextRange pour remplacer sélectionner.
textRange.select();
Les autres navigateurs
Le principe n'est toujours pas le même que pour IE. Ici, nous allons tout simplement remplacer le contenu de la zone de texte par le nouveau contenu comprenant les balises :
field.value = startSelection + startTag + currentSelection + endTag + endSelection;
field.focus(); // On remet le focus sur la zone de texte
Comme avec Internet Explorer, on va redéfinir la sélection. On va utiliser la méthode setSelectionRange(Debut, Fin) que l'on va appliquer sur field.
Cette méthode requiert deux arguments : la position de départ de la sélection, et la position de fin. Pas besoin de deux méthodes comme avec Internet Explorer. :)
La position de départ se calcule en faisant la somme de tout ce qu'il y a avant : startSelection et startTag.
Pour la position de fin, on ajoute simplement la longueur de currentSelection.
Pour détecter si le navigateur est Internet Explorer, il suffit théoriquement de vérifier s'il gère l'objet document.selection. J'ai bien dit théoriquement, car il y a un petit fauteur de trouble qui est... Opera.
Opera, non content de supporter le JavaScript, supporte aussi le JScript, qui est l'implémentation de Javascript dans Internet Explorer. Les exceptions que nous avons vues précédemment pour IE sont en fait du JScript, qui ajoutait à l'époque des fonctions en plus au Javascript.
Donc, comme Opera gère le JScript, si on fait un document.selection, la valeur retournée sera true.
Mais si Opera gère le JScript, où est le problème ?
Le problème est juste que Opera gère mal le JScript et plus particulièrement le repositionnement du curseur si il n'y a pas de sélection. C'est plutôt gênant. Il faut donc faire en sorte que seul IE utilise les portions de code en Jscript, et pour cela, il suffit de tester si le navigateur prend en charge ActiveX, que seul IE gère !
Voici donc ce que peut donner le script :
function insertTag(startTag, endTag, textareaId, tagType) {
var field = document.getElementById(textareaId);
var scroll = field.scrollTop;
field.focus();
if (window.ActiveXObject) { // C'est IE
var textRange = document.selection.createRange();
var currentSelection = textRange.text;
textRange.text = startTag + currentSelection + endTag;
textRange.moveStart("character", -endTag.length - currentSelection.length);
textRange.moveEnd("character", -endTag.length);
textRange.select();
} else { // Ce n'est pas IE
var startSelection = field.value.substring(0, field.selectionStart);
var currentSelection = field.value.substring(field.selectionStart, field.selectionEnd);
var endSelection = field.value.substring(field.selectionEnd);
field.value = startSelection + startTag + currentSelection + endTag + endSelection;
field.focus();
field.setSelectionRange(startSelection.length + startTag.length, startSelection.length + startTag.length + currentSelection.length);
}
field.scrollTop = scroll; // et on redéfinit le scroll.
}
Et voilà. Le script principal est quasi fini. Il est temps maintenant de l'améliorer pour qu'il puisse gérer les différentes sortes d'insertions (comme les liens, les citations...).
Ce que je qualifie d'insertions spéciales, ce sont les insertions qui, par exemple, demandent quelque chose à l'utilisateur comme pour l'insertion de liens ou encore de citations...
Le type d'insertion sera renseigné dans l'argument tagType de la fonction insertTag(). Pour garder un script simple, je vais procéder en 3 étapes :
on récupère la sélection ;
on regarde si le tagType est renseigné. S'il ne l'est pas, on ne fait rien ;
on insère le tout.
J'obtiens donc ce code :
function insertTag(startTag, endTag, textareaId, tagType) {
var field = document.getElementById(textareaId);
var scroll = field.scrollTop;
field.focus();
/* === Partie 1 : on récupère la sélection === */
if (window.ActiveXObject) {
var textRange = document.selection.createRange();
var currentSelection = textRange.text;
} else {
var startSelection = field.value.substring(0, field.selectionStart);
var currentSelection = field.value.substring(field.selectionStart, field.selectionEnd);
var endSelection = field.value.substring(field.selectionEnd);
}
/* === Partie 2 : on analyse le tagType === */
if (tagType) {
switch (tagType) {
case "lien":
// Si c'est un lien
break;
case "citation":
// Si c'est une citation
break;
}
}
/* === Partie 3 : on insère le tout === */
if (window.ActiveXObject) {
textRange.text = startTag + currentSelection + endTag;
textRange.moveStart("character", -endTag.length - currentSelection.length);
textRange.moveEnd("character", -endTag.length);
textRange.select();
} else {
field.value = startSelection + startTag + currentSelection + endTag + endSelection;
field.focus();
field.setSelectionRange(startSelection.length + startTag.length, startSelection.length + startTag.length + currentSelection.length);
}
field.scrollTop = scroll;
}
J'utilise un switch(). Ce n'est pas vraiment utile dans mon cas, puisque je n'ai que deux choix et donc un simple if()...else() aurait suffi. Mais le switch sera plus simple à utiliser si vous faites plus de balises spéciales. ;) Je n'ai pas non plus mis de default. Ce n'est pas utile non plus, puisqu'il n'y a rien à faire si TagType ne correspond pas, et comme c'est vous qui le définissez, je suppose que vous saurez bien le définir. :p
Mais on aurait très bien pu analyser le TagType et puis récupérer la sélection, non ?
Non, car on va se servir du contenu de CurrentSelection pour déterminer quoi faire avec l'analyse du TagType. Regardez, voici la partie qui s'occupe des liens. En gros, on analyse le CurrentSelection et on réagit en fonction. Le code est commenté et est très simple, c'est surtout une question de logique :) :
case "lien":
endTag = "</lien>";
if (currentSelection) { // Il y a une sélection
if (currentSelection.indexOf("http://") == 0 || currentSelection.indexOf("https://") == 0 || currentSelection.indexOf("ftp://") == 0 || currentSelection.indexOf("www.") == 0) {
// La sélection semble être un lien. On demande alors le libellé
var label = prompt("Quel est le libellé du lien ?") || "";
startTag = "<lien url=\"" + currentSelection + "\">";
currentSelection = label;
} else {
// La sélection n'est pas un lien, donc c'est le libelle. On demande alors l'URL
var URL = prompt("Quelle est l'url ?");
startTag = "<lien url=\"" + URL + "\">";
}
} else { // Pas de sélection, donc on demande l'URL et le libelle
var URL = prompt("Quelle est l'url ?") || "";
var label = prompt("Quel est le libellé du lien ?") || "";
startTag = "<lien url=\"" + URL + "\">";
currentSelection = label;
}
break;
Mais pourquoi redéfinis-tu currentSelection ?
Bonne question. :) Il faut garder à l'esprit que l'insertion se fait avec trois choses : startTag + currentSelection + endTag. Dans le premier if(), je conclus que currentSelection est l'url. Or, l'url se place en attribut dans startTag, et à la place de currentSelection, c'est le libellé qui vient. :)
Voici, en total bonus, la partie pour analyser une citation :
case "citation":
endTag = "</citation>";
if (currentSelection) { // Il y a une sélection
if (currentSelection.length > 30) { // La longueur de la sélection est plus grande que 30. C'est certainement la citation, le pseudo fait rarement 20 caractères
var auteur = prompt("Quel est l'auteur de la citation ?") || "";
startTag = "<citation nom=\"" + auteur + "\">";
} else { // On a l'Auteur, on demande la citation
var citation = prompt("Quelle est la citation ?") || "";
startTag = "<citation nom=\"" + currentSelection + "\">";
currentSelection = citation;
}
} else { // Pas de selection, donc on demande l'Auteur et la Citation
var auteur = prompt("Quel est l'auteur de la citation ?") || "";
var citation = prompt("Quelle est la citation ?") || "";
startTag = "<citation nom=\"" + auteur + "\">";
currentSelection = citation;
}
break;
Les listes déroulantes sont un bon moyen pour présenter des options sans prendre trop de place, et permettent généralement de rassembler plusieurs options ayant le même thème.
Mettre en place une liste déroulante est relativement simple. Il suffit d'appeler insertTag() via l'évènement onChange. Il convient, lors de l'appel de la fonction, de récupérer la valeur de l'option sélectionnée, comme ceci :
this.options[this.selectedIndex].value;
Voilà donc ce que l'appel de la fonction insertTag() peut donner pour la liste déroulante permettant de choisir la taille :
Bah ce n'est pas bien compliqué, il suffit de réutiliser notre fonction insertTag() pour lui faire insérer un smiley et non une balise. Mais attention, vous devez bien faire attention à échapper correctement les caractères qui doivent l'être !
insertTag(':D', '', 'textarea');
Bien évidemment, utilisez l'évènement onClick des éléments IMG pour appeler la fonction :
Faire une prévisualisation en temps réel est assez facile. Il suffit de détecter quand l'utilisateur entre un caractère dans la zone de texte, puis de récupérer le contenu, convertir les balises à coup de regex et afficher le résultat dans un DIV.
Tout bon script de preview (prévisualisation) qui se respecte se doit de demander à l'utilisateur s'il souhaite activer cette prévisualisation. On le fera à l'aide d'une case à cocher, décochée par défaut. J'insiste sur le décochée. Personnellement, je déteste ce genre de truc, et je préfère que ce soit désactivé pas défaut. :)
Détecter la frappe dans le textarea
Pour cela on va tout simplement utiliser les événements onKeyUp et onSelect dont dispose l'élément textarea. On leur associera la fonction preview(), laquelle comportera un argument textareaId représentant le textarea et un argument previewDiv qui contiendra l'ID de l'élément dans lequel on inscrira le texte prévisualisé. On utilisera this pour définir textareaID, ça nous permettra de ne pas s'embêter avec un getElementById() pour récupérer le contenu du textarea :) :
function preview(textareaId, previewDiv) {
var field = textareaId.value; // Le contenu du textarea est dans la variable field
}
À coups de regex
Pour convertir les balises zCode en balises HTML affichables dans le previewDiv, on va utiliser la méthode replace() qui contiendra une regex.
Voici toute la fonction de prévisualisation. Je ne donne que les regex qui servent à convertir ce que les boutons peuvent insérer (eh oh, j'allais pas tout écrire quand même ^^ ) :
function preview(textareaId, previewDiv) {
var field = textareaId.value;
if (document.getElementById('previsualisation').checked && field) {
var smiliesName = new Array(':magicien:', ':colere:', ':diable:', ':ange:', ':ninja:', '>_<', ':pirate:', ':zorro:', ':honte:', ':soleil:', ':\'\\(', ':waw:', ':\\)', ':D', ';\\)', ':p', ':lol:', ':euh:', ':\\(', ':o', ':colere2:', 'o_O', '\\^\\^', ':\\-°');
var smiliesUrl = new Array('magicien.png', 'angry.gif', 'diable.png', 'ange.png', 'ninja.png', 'pinch.png', 'pirate.png', 'zorro.png', 'rouge.png', 'soleil.png', 'pleure.png', 'waw.png', 'smile.png', 'heureux.png', 'clin.png', 'langue.png', 'rire.gif', 'unsure.gif', 'triste.png', 'huh.png', 'mechant.png', 'blink.gif', 'hihi.png', 'siffle.png');
var smiliesPath = "http://www.siteduzero.com/Templates/images/smilies/";
field = field.replace(/&/g, '&');
field = field.replace(/</g, '<').replace(/>/g, '>');
field = field.replace(/\n/g, '<br />').replace(/\t/g, ' ');
field = field.replace(/<gras>([\s\S]*?)<\/gras>/g, '<strong>$1</strong>');
field = field.replace(/<italique>([\s\S]*?)<\/italique>/g, '<em>$1</em>');
field = field.replace(/<lien>([\s\S]*?)<\/lien>/g, '<a href="$1">$1</a>');
field = field.replace(/<lien url="([\s\S]*?)">([\s\S]*?)<\/lien>/g, '<a href="$1" title="$2">$2</a>');
field = field.replace(/<image>([\s\S]*?)<\/image>/g, '<img src="$1" alt="Image" />');
field = field.replace(/<citation nom=\"(.*?)\">([\s\S]*?)<\/citation>/g, '<br /><span class="citation">Citation : $1</span><div class="citation2">$2</div>');
field = field.replace(/<citation lien=\"(.*?)\">([\s\S]*?)<\/citation>/g, '<br /><span class="citation"><a href="$1">Citation</a></span><div class="citation2">$2</div>');
field = field.replace(/<citation nom=\"(.*?)\" lien=\"(.*?)\">([\s\S]*?)<\/citation>/g, '<br /><span class="citation"><a href="$2">Citation : $1</a></span><div class="citation2">$3</div>');
field = field.replace(/<citation lien=\"(.*?)\" nom=\"(.*?)\">([\s\S]*?)<\/citation>/g, '<br /><span class="citation"><a href="$1">Citation : $2</a></span><div class="citation2">$3</div>');
field = field.replace(/<citation>([\s\S]*?)<\/citation>/g, '<br /><span class="citation">Citation</span><div class="citation2">$1</div>');
field = field.replace(/<taille valeur=\"(.*?)\">([\s\S]*?)<\/taille>/g, '<span class="$1">$2</span>');
for (var i=0, c=smiliesName.length; i<c; i++) {
field = field.replace(new RegExp(" " + smiliesName[i] + " ", "g"), " <img src=\"" + smiliesPath + smiliesUrl[i] + "\" alt=\"" + smiliesUrl[i] + "\" /> ");
}
document.getElementById(previewDiv).innerHTML = field;
}
}
Remarquez, je vérifie avec l'if() si la case à cocher est cochée, et s'il y a quelque chose dans le textarea.
La conversion des smilies est un peu particulière. Un array, smiliesName, contient les noms des smilies. Un autre, smiliesUrl, contient le nom des images (qui correspond bien sûr au nom des smilies du premier array). J'utilise ensuite une boucle for, qui va appliquer une regex pour chaque élément de l'array. Comme j'ai besoin de spécifier une variable au sein de l'expression régulière, je crée cette dernière de façon explicite, avec new Regexp . :)
Quand toutes les conversions sont faites, on écrit la valeur de field dans le div qui sert à la prévisualisation à l'aide d'innerHTML.
Les regex ici sont relativement simples. Mon but n'est pas de vous apprendre à faire des regex ni même à analyser des portions de texte pour faire un système de prévisualisation qui gère des conversions plus avancées (comme plus de 3 attributs...).
La création d'une visualisation finale se fait avec AJAX, en exploitant l'objet XMLHttpRequest.
XMLHttpRequest va nous permettre d'envoyer le contenu de la zone de texte vers une page contenant un script PHP. Ce script va convertir le contenu en HTML un peu comme le faisait javascript pour la prévisualisation. Ce contenu est renvoyé par XMLHttpRequest et est affiché dans un DIV. L'intérêt, c'est que tout se fait sans devoir recharger la page. :)
Mais, quelle est donc l'utilité de ce système ?
Franchement, pour visualiser des balises GRAS et des LIEN, javascript suffit amplement. Le traitement par PHP peut être plus pratique pour par exemple colorer le code, trouver des erreurs d'imbrication de balises...
Par exemple, sur le Site du Zér0, la visualisation finale (via XMLHttpRequest et PHP) permet de visualiser le code coloré, les tableaux, d'avoir une gestion des erreurs, et le convertisseur est plus performant que le convertisseur javascript (car c'est un parseur XML qui est utilisé).
Le script javascript
Voici donc la fonction d'instanciation que j'utilise. Elle sert à définir un objet XMLHttpRequest, suivant les différents navigateurs :
function getXMLHttpRequest() {
var xhr = null;
if (window.XMLHttpRequest || window.ActiveXObject) {
if (window.ActiveXObject) {
try {
xhr = new ActiveXObject("Msxml2.XMLHTTP");
} catch(e) {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
} else {
xhr = new XMLHttpRequest();
}
} else {
alert("Votre navigateur ne supporte pas l'objet XMLHTTPRequest...");
return null;
}
return xhr;
}
Mise en place
Commencez par mettre un événement onClick sur le bouton servant à visualiser, comme ceci :
Maintenant, il s'agit de créer la fonction pour traiter l'envoi et la réception des données du textarea :
function view(textareaId, viewDiv){
var content = encodeURIComponent(document.getElementById(textareaId).value);
var xhr = getXMLHttpRequest();
if (xhr && xhr.readyState != 0) {
xhr.abort();
delete xhr;
}
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200){
document.getElementById(viewDiv).innerHTML = xhr.responseText;
} else if (xhr.readyState == 3){
document.getElementById(viewDiv).innerHTML = "<div style=\"text-align: center;\">Chargement en cours...</div>";
}
}
xhr.open("POST", "view.php", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("string=" + content);
}
La fonction view(), tout comme la fonction preview() reçoit deux arguments : textareaId qui contient l'ID de la zone de texte, et viewDiv qui contient l'ID du DIV dans lequel le code visualisé sera inséré.
La fonction view() se charge donc de récupérer le contenu de la zone de texte et de l'envoyer (en l'ayant préalablement encodé, avec encodeURIComponent) à la page view.php qui contient le script PHP nécessaire pour convertir le code afin qu'il soit visualisé. Le code converti est renvoyé, et réceptionné puis est affiché dans le viewDiv au moyen d'un innerHTML.
La page view.php
La page view.php contient en fait un script simple, quasi identique à celui de la fonction preview(). Pour les besoins du tuto, je n'allais pas utiliser un parseur XML bien compliqué. L'important ici c'est que vous compreniez. Cependant, si vous voulez utiliser un parseur XML, allez voir ce tuto.
Comme vous le voyez, c'est un bête script de conversion avec des regex. Pour plus de simplicité, c'est organisé en deux tableaux, et j'ai mis tout ça dans une fonction :) . Le contenu de la zone de texte est récupéré dans la variable $_POST["string"], exactement comme si on récupérait le contenu d'un formulaire via POST. Le contenu est converti, et est écrit directement dans la page. C'est ce qui est écrit qui va être récupéré par le code javascript et affiché dans la zone de visualisation. :)