Une solution cross navigateur pour tronquer du texte en ellipsis

Publié dans: 

 Ellipsis

Je présente dans ce billet une solution ExtJS pour gérer les débordements de texte dans un élément HTML à la "ellipsis". Ce qui veux dire de gérer le débordement en rompant le texte et en y ajoutant trois points de suspension vers la fin; et de pouvoir aussi définir une largeur en pixels que le texte ne dépassera pas.

Valant mieux qu'un long discours, illustrons notre objectif avec un texte gras et de taille 12pt : "Un assez long texte gros et gras".

 

Avec une largeur définie de 500px (tout le texte tient) :

Avec une largeur définie de 200px :

Avec une largeur définie de 100px :

Déjà, c'est quoi le problème ?

Tout d'abord, j'introduis la problématique. Nous avons un élément HTML (un TD, un SPAN, un DIV...) qui contient du texte. Nous voulons que cet élément ne dépasse pas une certaine largeur (en pixels) et nous voulons aussi que le texte qu'il contient ne soit pas sauvagement tronqué, mais qu'on y ajoute un ellipsis vers la fin. L'avantage de la solution que je vais présenter dans cet article est qu'elle est -au moins- compatible avec IE (depuis la version 6) et Firefox (depuis la version 2). De plus elle fonctionne très bien avec des éléments inline (SPAN, TD...) où la largeur ne devrait/pourrait pas être fixée lorsque leur contenu dépasse en largeur.
L'inconvénient est qu'elle repose sur ExtJS (si cela intéresse quelqu'un, j'écrirai une solution jQuery). Notons que la version de ExtJS utilisée ici est la 2.0.2. La version 2.0.2 de ExtJS, ainsi que la documentation de l'API ne sont plus directement accessible depuis le site officiel de Sencha, mais peuvent être téléchargé ici. Ceci n'exclut pas le fait que la solution soit valable pour des versions plus récentes (elle devrait même marcher très bien, et couvrir plus de versions de navigateurs). Nous ne l'avons juste pas testée.

Que va-t-on utiliser ?

Pour aboutir à une solution, il faudrait bien commencer par présenter deux fonctions utilitaires de ExtJS : la fonction 'ellipsis' de Ext.util.Format et la fonction 'measure' de Ext.util.TextMetrics.
Contrairement à ce que peut présager son nom, la première fonction n'est pas très utile. Elle est là juste pour nous faire gagner quelques lignes de code. En gros, elle prend en paramètres une chaîne de caractères et une longueur (entier). Si la chaîne de caractères excède (en terme de nombre de caractères) cette longueur, elle va être tronquée et un ellipsis "..." va y être ajouté. L'objectif étant de garantir que la chaîne résultante ne dépasse pas la taille (en nombre de caractères) qu'on avait passé comme deuxième argument.
La figure ci-dessous montre des exemples exécutés sous la console de Firebug :


La deuxième fonction 'measure' va nous permettre de connaitre les dimensions (en pixels) d'un texte, au cas où il serait mis dans un élément (instance de Ext.Element) donné; donc en tenant compte des styles, police, taille, heuteur de ligne associés à cet élément.
Pour la tester, j'ai créé ce petit HTML :

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<script type="text/javascript" src="js/ext-base.js"></script>
	<script type="text/javascript" src="js/ext-all.js"></script>
	<style>
		#container{
			width : 500px;
			border : 2px solid #515151;
			background-color : #84B317;
			margin : 10px 0px;
		}
		#text-container{
			font-size : 12pt;
			font-weight : bold;
		}
		#text-container-small{
			font-size : 8pt;
			font-weight : normal;
		}
	</style>
</head>
<body>
<div id="container">
	<span id="text-container">Un assez long texte gros et gras</span>
	<span id="text-container-small">Un assez long texte gros et gras</span>
</div>

<script>
	console.log("gras, 12 pt :");
	console.log(Ext.util.TextMetrics.measure(Ext.get("text-container"), "Un assez long texte gros et gras").width);
	console.log("normal, 8 pt :");
	console.log(Ext.util.TextMetrics.measure(Ext.get("text-container-small"), "Un assez long texte gros et gras").width);

</script>
</body>

 

Ce qu'il faut retenir c'est qu'il y a deux balises span : une avec du texte en gras et d'une taille de 12 pt, le deuxième est normal (n'est pas gras) avec une taille de police de 8pt.
Le script exécuté vers la fin nous montre qu'un même texte (notamment notre assez long texte gros et gras) mis dans les deux span aura une largeur de 212px dans le premier cas, et de 135 px dans le second.

 

Notre solution

Vous l'avez peut être compris, notre solution va combiner ces deux fonctions pour chercher la taille du texte (en nombre de caractères) pour pouvoir tenir dans la largeur souhaité (en pixels). L'idée est de partir de la taille du texte et de décrémenter (le nombre de caractères) à chaque fois jusqu'à aboutir à la largeur en pixels souhaitée. Concrètement, notre fonction qui prendra en paramètre l'élément HTML, et la largeur souhaité serait écrite ainsi :

makeEllipsis = function(element,width){
var text = element.dom.innerHTML;
var actualWidth = Ext.util.TextMetrics.measure(element,text).width;
var ellipsisValue = text.length;
var result = text;
while (ellipsisValue > 2 && actualWidth > width){
  ellipsisValue--;
  result = Ext.util.Format.ellipsis(text,ellipsisValue);
  actualWidth = Ext.util.TextMetrics.measure(element, result).width;
}
element.dom.innerHTML = Ext.util.Format.ellipsis(text,ellipsisValue); 
};

L'algorithme utilisé ici -qui consiste à décrémenter ellipsisValue à chaque itération-, n'est certainement pas le plus efficace en terme de complexité. Nous allons considérer cependant que le contexte d'utilisation de cette fonction fera intervenir des chaînes de caractères assez courtes (moins de 80 caractères) et que le temps de traitement n'est pas pénalisant. A titre d'indication, nous aurions pu penser à un algorithme dichotomique (plus généralement à l'optimisation combinatoire sous contrainte), mais je pense personnellement, que cela ne vaut pas la peine vu le contexte.

Exemples d'utilisation

Un premier exemple d'utilisation où notre page contient un span et bouton. Chaque clic sur le bouton fait rétrécir le texte dans le span de 25px.
 

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
	<script type="text/javascript" src="js/ext-base.js"></script>
	<script type="text/javascript" src="js/ext-all.js"></script>
	<style>
		#text-container{
			border : 2px solid #515151;
			background-color : #84B317;
			margin : 10px 0px;
			font-size : 12pt;
			font-weight : bold;			
		}
	</style>
</head>
<body>
	<span id="text-container">Un assez long texte gros et gras</span>
	<button onclick="shrinkText()">Retrecis !</button>
	
	<script>
		makeEllipsis = function(element,width){
			var text = element.dom.innerHTML;
			var actualWidth = Ext.util.TextMetrics.measure(element,text).width;
			var ellipsisValue = text.length;
			var result = text;
			while (ellipsisValue > 2 && actualWidth > width){
				ellipsisValue--;
				result = Ext.util.Format.ellipsis(text,ellipsisValue);
				actualWidth = Ext.util.TextMetrics.measure(element, result).width;
			}
			element.dom.innerHTML = Ext.util.Format.ellipsis(text,ellipsisValue);		
		};

		shrinkText = function(){
			currWidth = currWidth-25;
			makeEllipsis(Ext.get('text-container'),currWidth);
		};
		var currWidth = 200;
	</script>
</body>

L'exécution de cet exemple donne :

Premier aperçu :

Après un premier clic :

Après un second clic :

Après un troisième clic :

Après un quatrième clic :

Pour notre deuxième exemple, nous allons juste effectuer une petite modification. Au lieu que le texte rétrécis à chaque clic, nous allons faire en sorte que le texte bascule alternativement entre les tailles 100px et 200px. Pour cela, nous allons juste modifier la fonction shrinkText comme suit :

shrinkText = function(){
	currWidth = (currWidth==200)?100:200;
	makeEllipsis(Ext.get('text-container'),currWidth);
};

En cliquant plusieurs fois sur le bouton, nous remarquons que le texte rétrécit à 100px la première fois, et reste à 100px par la suite.

Premier aperçu :

Après un premier clic :

Après un second clic :

Après un troisième clic :

Après un quatrième clic :

Ce deuxième exemple illustre un problème de notre implémentation. En fait, notre fonction écrase complétement la valeur d'origine et ne garde que le texte tronqué. Cela fait qu'après le premier clic, tous les clics qui suivent essaient de tronquer le texte "Un assez lon..." pour qu'il ne dépasse pas les 200px. Evidemment, ce texte reste inchangé puisqu'il est déjà de largeur 100px, et ne dépasse donc pas les 200px souhaités.

Solution proposée

Une solution serait de stocker le texte d'origine dans un attribut de l'élément. Le traitement ne se fera donc plus sur le contenu de l'élément (innerHTML), mais sur la valeur de cet attribut. Cette solution n'est pas la plus élégante puisqu'elle définit un attribut non standard dans nos balises HTML. La nouvelle implémentation (utilisant un attribut 'originaltext') est la suivante :

makeEllipsis = function(element,width){
var text = element.dom.getAttribute('originaltext');
if(!text){
  text = element.dom.innerHTML;
  element.dom.setAttribute('originaltext', text);
}
var actualWidth = Ext.util.TextMetrics.measure(element,text).width;
var ellipsisValue = text.length;
while (ellipsisValue > 2 && actualWidth > width){
  ellipsisValue--;
  actualWidth = Ext.util.TextMetrics.measure(element,Ext.util.Format.ellipsis(text,ellipsisValue)).width;
}
element.dom.innerHTML = Ext.util.Format.ellipsis(text,ellipsisValue); 
};

Si on ré exécute l'exemple 2 on obtient :

Premier aperçu :

Après un premier clic :

Après un second clic :

Après un troisième clic :

Après un quatrième clic :