Conception de contrôles Web JavaScript avancés : Créer une table HTML éditable - HtmlEditTable v2.0

Nourdine FALOLA

Dans un précédent tutoriel, nous avons conçu une table HTML éditable en JavaScript : HtmlEditTable v1.0. Pour la v2.0, nous allons réviser un peu le code et ajouter les fonctionnalités suivantes :

- création un HtmlEditTable à partir d'une table HTML existante ;

- navigation au clavier.

Vous pouvez participer à la discussion relative à cet article sur le site communautaire www.developpez.com. Commentez ♪

- Titre : Conception de contrôles Web JavaScript avancés : Créer une table HTML éditable - HtmlEditTable v2.0

- Auteur : Nourdine FALOLA

- Parution : 5 janvier 2010

- Mise à jour : 17 janvier 2010

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2009 Nourdine FALOLA. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Améliorations du script précédent

- I-A. Collection tBodies

- I-B. Sélection du texte lors de l'édition d'une cellule

- I-C. Shunter les événements au niveau de la cellule lorsqu'on est en édition

- I-D. Classe utilitaire KeyCodes

- I-E. Classe utilitaire Tools

- II. Création d'un HtmlEditTable à partir d'une table HTML existante

- II-A. Page de test

- II-B. JS : HtmlEditTable(param)

- III. Navigation au clavier

- III-A. Prémisses

- III-B. Initier l'édition d'une cellule

- III-C. Annuler l'édition d'une cellule

- III-D. Supprimer le contenu d'une cellule

- III-E. Déplacement à l'aide des flèches

- III-F. Déplacement à l'aide de la touche de tabulation

- IV. Remerciements

- V. Toutes les publications

- V-A. Espaces de noms (ou namespace) en JavaScript

- V-B. Conception de contrôles Web avancés : Créer une table HTML éditable - HtmlEditTable v1.0

- V-C. Conception de contrôles Web avancés : Créer une table HTML éditable - HtmlEditTable v2.0

- V-D. Bibliothèque d'effets JavaScript : Smile FX

I - Améliorations du script précédent

I-A - Collection tBodies

Un nœud table possède une collection tBodies (1) de tous les objets tbody qu'il contient (une table pouvant avoir plus d'un tbody…). De fait on peut remplacer les

script original

getElementsByTagName("tbody")[0]

par

script revu

tBodies[0]

I-B - Sélection du texte lors de l'édition d'une cellule

Afin que le texte de la zone de texte soit sélectionné au moment de l'édition, modifions le constructeur de la classe HtmlEdit et la fonction HtmlEdit.AppendTo() :

sélection du texte à l'édition

var HtmlEdit = function(value){
[...]
this.control.select();
};

HtmlEdit.prototype = {
AppendTo: function(parent){
[...]
this.control.select();
this.control.focus();
}
}

I-C - Shunter les événements au niveau de la cellule lorsqu'on est en édition

Lorsqu'une cellule entre en édition, i.e. quand son contenu textuel est remplacé par une zone de texte, les événements gérés au niveau de la cellule n'ont plus lieu d'être. Au contraire, ils parasiteront ceux gérés au niveau de la zone de texte. On va donc les supprimer à l'édition (sachant qu'à la fin de l'édition ils sont de toute façon repositionnés par la fonction HtmlEditTableHelper.CellInitialize()).

La suppression des gestionnaires d'événement au niveau de la cellule sera effectuée par la fonction HtmlEditTableHelper.CellNeutralizeEvents() :

HtmlEditTableHelper.CellNeutralizeEvents v1.1

CellNeutralizeEvents: function(cell){
if (cell){
cell.onclick = null;
cell.ondblclick = null;
}
}

Cette fonction est appelée au moment de l'ajout de la zone de texte dans la cellule :

HtmlEdit.AppendTo

AppendTo: function(parent){
if (document.all){
this.control.style.height = parent.clientHeight - 2*parent.clientTop + "px";
this.control.style.width = parent.clientWidth - 2*parent.clientLeft + "px";
}
else{
this.control.style.height = parent.offsetHeight - 2*parent.clientTop + "px";
this.control.style.width = parent.offsetWidth - 2*parent.clientLeft + "px";
}
Tools.Purge(parent);
HtmlEditTableHelper.CellNeutralizeEvents(parent);
parent.appendChild(this.control);

this.control.select();
this.control.focus();
}

Auparavant on supprimait le gestionnaire d'événement ondblclick après l'appel à HtmlEdit.AppendTo(). Dorénavant cela n'est plus nécessaire.

HtmlEditTableHelper.DblClickHandler v1.1

DblClickHandler: function(e){
var src = Tools.Node(Tools.Target(e), "TD");
if (!src){
Tools.Event(e).returnValue = false;
return false;
}
var htmlEdit = new HtmlEdit(src.firstChild.data);
htmlEdit.AppendTo(src);
// src.ondblclick = null;
src.grid.ActiveCellChange(src, false);
}

I-D - Classe utilitaire KeyCodes

Pour une meilleure compréhension du code dès la première lecture, il est préférable d'utiliser des constantes aux noms explicites plutôt que des valeurs dont on ne sait pas toujours ce qu'elles représentent. En ce qui concerne la gestion des événements clavier, une classe de constantes de codes de touche serait utile même si pour l'instant le seul code utilisé est celui de la touche Entrée (13). Voici l'implémentation de la classe KeyCodes (qui pourra toujours être enrichi au fur et à mesure des besoins) :

classe KeyCodes

var KeyCodes = {
BACKSPACE: 8,
TAB: 9,
RETURN: 13,
PAUSE: 19,
CAPS_LOCK: 20,
ESCAPE: 27,
PAGE_UP: 33,
PAGE_DOWN: 34,
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
INSERT: 45,
DELETE: 46
};

I-E - Classe utilitaire Tools

Pour l'instant la classe Tools nous est utile comme fourre-tout pour nos fonctions utilitaires. À l'avenir il sera sûrement bon de la splitter en classes utilitaires plus spécifiques regroupant des fonctionnalités ayant un rapport entre elles. Mais bon, passons cette problématique dans l'immédiat…

La classe KeyCodes associe un nom explicite aux codes de certaines touches du clavier. Ce code est obtenu par la lecture de l'attribut keyCode de l'événement déclenché. Cependant, cette méthode de détermination de la touche pressée n'est pas valable pour les touches spéciales suivantes : Alt, Ctrl, Shift. En effet, ces touches sont utilisées pour réaliser des combinaisons de touches, donc comme information supplémentaire au keyCode des touches classiques (la pression des touches Alt, Ctrl ou Shift ne suffit pas à déclencher un événement clavier). Voici l'implémentation de la fonction Tools.SpecialKeys() qui renvoie pour chacune des touches spéciales un booléen spécifiant si la touche est pressée ou non :

Tools.SpecialKeys()

SpecialKeys: function(e){
if (e.modifiers){
var mString =(e.modifiers+32).toString(2).substring(3,6);
return {
"ShiftKey": mString.charAt(0)=="1",
"CtrlKey": mString.charAt(1)=="1",
"AltKey": mString.charAt(2)=="1"
};
}
else{
return {
"ShiftKey": e.shiftKey,
"CtrlKey": e.ctrlKey,
"AltKey": e.altKey
};
}
}

// exemple d'utilisation dans un gestionnaire d'événement :
function myKeydownHandlerEvent(e){
alert(Tools.SpecialKeys(e).ShiftKey); // true si la touche SHIFT est pressée, false sinon
alert(Tools.SpecialKeys(e).CtrlKey); // true si la touche CTRL est pressée, false sinon
alert(Tools.SpecialKeys(e).AltKey); // true si la touche ALT est pressée, false sinon
alert(Tools.KeyCode(e)); // code de la touche pressée ayant déclenché l'événement clavier
}

II - Création d'un HtmlEditTable à partir d'une table HTML existante

Vous avez peut-être tardivement entendu parler de la révolutionnaire (mytho?) HtmlEditTable et vous aimeriez l'utiliser sans pour autant devoir réécrire vos pages. Eh oui! Vous avez déjà construit vos tables en HTML ou via un script et il serait intéressant de leur ajouter les capacités de la HtmlEditTable sans toucher à l'existant. Et bien, allons-y !

II-A - Page de test

Notre page de test est simple : une table HTML suivie d'un script de création d'un HtmlEditEditTable à partir de la table HTML. Le script consiste en l'appel du constructeur de la classe HtmlEditTable auquel on passe l'objet paramètre avec la propriété « table » qui n'est autre que l'objet table.

Page de test - HtmlEditTable.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<script type="text/javascript" src="HtmlEditTable.js"></script>
<style type="text/css">
@import url(HtmlEditTable.css);
</style>
</head>
<body>
<table id="tableHTML">
<thead>
<tr>
<td>head</td>
<td>head</td>
<td>head</td>
<td>head</td>
<td>head</td>
</tr>
</thead>
<tbody>
<tr>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
<tr>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
<td>data</td>
</tr>
</tbody>
</table>
<br/>
<script>
var tableHTML = document.getElementById("tableHTML");
var tableEdit = new HtmlEditTable({"table": tableHTML});
</script>
</body>
</html>

II-B - JS : HtmlEditTable(param)

Si la table HTML existe, alors passons-la au constructeur de la classe HtmlEditTable. Bref rappel, le constructeur accepte un unique paramètre qui est un objet disposant des propriétés (facultatives) suivantes :

Propriétés du paramètre du constructeur :

- le nombre de colonnes (X) ou la liste des noms de colonnes (Xn) ;

- le nombre de lignes (Y) ou la liste des noms de lignes (Yn) ;

- les données (data) ;

- le titre de la table (caption) ;

- la liste des en-têtes de colonnes (head).

À cette liste de propriétés possibles, on ajoute donc la suivante :

Nouvelle propriété du paramètre du constructeur

- la table HTML (table) à étendre en HtmlEditTable.

Concrètement l'impact de cette fonctionnalité se situe au niveau du constructeur :

Constructeur HtmlEditTable - v1.1

HtmlEditTable = function(){
if (arguments.length > 0 && arguments[0].table){
this.Transform(arguments);
}
else{
this.control = document.createElement("table");
if (arguments.length > 0){
this.Build(arguments[0]);
}
}
this.control.cellSpacing = 0;
this.control.cellPadding = 0;
this.control.className = "HtmlEditTable";
};

Que s'est-il passé? La nouveauté vient de l'instruction conditionnelle. Si la propriété table du paramètre existe, alors il s'agit d'étendre les fonctionnalités d'une table existante, sinon il s'agit de créer un HtmlEditTable de A à Z.

Dans le cas de l'extension d'une table, la fonction Transform() intervient. Son rôle est identique à celui de la fonction Build() à ceci près qu'au lieu d'initialiser le HtmlEditTable à partir des propriétés de la v1.0 du paramètre du constructeur, l'initialisation se base sur la table, propriété de la v2.0 :).

HtmlEditTable.prototype.Transform v1.1

Transform: function(args){
this.columns = [];
this.lines = [];
this.headers = [];
this.control = args[0].table;
this.caption = this.control.caption;
for (var i=0, imax=this.control.rows[0].cells.length; i<imax; i++){
this.columns.push("" + i);
}
for (var i=0, imax=this.control.rows.length; i<imax; i++){
this.lines.push("" + i);
}
if (this.control && this.control.tBodies){
for (var i=0, imax=this.control.tBodies.length; i<imax; i++){
var tBody = this.control.tBodies[i];
for (var j=0, jmax=tBody.rows.length; j<jmax; j++){
for (var k=0, kmax=tBody.rows[j].cells.length; k<kmax; k++){
var cell = tBody.rows[j].cells[k];
cell.tabIndex = 0;
HtmlEditTableHelper.CellInitialize(this, cell);
if (cell.childNodes.length == 0){
cell.appendChild(document.createTextNode(""));
}
}
}
}
}
}

Dans un premier temps, la fonction Transform() :

- crée les arrays columns, lines et headers ;

- assigne la table à la propriété control ;

- assigne le titre (caption) de la table au HtmlEditTable.

Puis chaque colonne et chaque ligne sont parcourues afin d'initialiser les arrays columns et lines. Pour rappel, il s'agit des listes de noms de colonnes et de lignes. Ceux-ci sont initialisés comme étant l'indice de la colonne ou de la ligne dans la table.

Pour finir, toutes les cellules des tBody sont parcourues et leurs fonctionnalités sont étendues grâce à la fonction HtmlEditTableHelper.CellInitialize(). Les cellules n'ayant pas de contenu se voient ajouter un nœud texte vide.

Pour finir ? Oui, mais non… Si vous disposez d'un outil pour visualiser le DOM de votre document sous Internet Explorer, vous constaterez que la structure de la table est « propre ». Par contre avec d'autres navigateurs (Firefox, Chrome, pour ne citer qu'eux) vous remarquerez qu'il y a pas mal de nœuds texte qui polluent la table. Ces navigateurs interprètent les retours chariot comme des nœuds texte et donc ces nœuds s'intercalent avec les nœuds « légitimes » de la table.

Comme par la suite nous voudrons naviguer au clavier entre les cellules de la table, il est préférable de nettoyer la structure de cette dernière dès maintenant. Cela facilite les développements futurs et rendra cohérente la structure du contrôle qu'il ait été généré à partir d'une table HTML préexistante ou non (car lorsqu'on construit le contrôle à partir de rien, comme dans la v1.0, il n'y a pas de nœuds parasites).

Ajoutons à la classe utilitaire Tools une fonction de suppression des nœuds texte :

Tools.RemoveTextNode

RemoveTextNode: function(o){
if (o == null){
return;
}

for (var i=o.childNodes.length-1; i>-1; i--){
var child = o.childNodes[i];
if (child.nodeName == "#text"){
o.removeChild(child);
}
}
}

Et utilisons-la dans la fonction HtmlEditTable.prototype.Transform pour nettoyer la table des nœuds texte parasites :

HtmlEditTable.prototype.Transform v1.2

Transform: function(args){
this.columns = [];
this.lines = [];
this.headers = [];
this.control = args[0].table;
this.caption = this.control.caption;
for (var i=0, imax=this.control.rows[0].cells.length; i<imax; i++){
this.columns.push("" + i);
}
for (var i=0, imax=this.control.rows.length; i<imax; i++){
this.lines.push("" + i);
}

if (this.control && this.control.tBodies){
for (var i=0, imax=this.control.tBodies.length; i<imax; i++){
var tBody = this.control.tBodies[i];
for (var j=0, jmax=tBody.rows.length; j<jmax; j++){
for (var k=0, kmax=tBody.rows[j].cells.length; k<kmax; k++){
var cell = tBody.rows[j].cells[k];
cell.tabIndex = 0;
HtmlEditTableHelper.CellInitialize(this, cell);
}
}
}
}

if (this.control){
Tools.RemoveTextNode(this.control);
if (this.control.tHead){
var tHead = this.control.tHead;
Tools.RemoveTextNode(tHead);
for (var i=0, imax=tHead.rows.length; i<imax; i++){
Tools.RemoveTextNode(tHead.rows[i]);
}
}
if (this.control.tBodies){
for (var i=0, imax=this.control.tBodies.length; i<imax; i++){
var tBody = this.control.tBodies[i];
Tools.RemoveTextNode(tBody);
for (var j=0, jmax=tBody.rows.length; j<jmax; j++){
Tools.RemoveTextNode(tBody.rows[j]);
}
}
}
if (this.control.tFoot){
var tFoot = this.control.tFoot;
Tools.RemoveTextNode(tFoot);
for (var i=0, imax=tFoot.rows.length; i<imax; i++){
Tools.RemoveTextNode(tFoot.rows[i]);
}
}
}
}

Vous allez rire, mais… c'est fini :) voir la démo - télécharger les sources

Cette fonction peut être optimisée au niveau des boucles. Cependant j'ai préféré séparer le « nettoyage » de la table de l'initialisation des cellules, surtout pour le côté pédagogique de l'article. Libre à vous d'optimiser la fonction si vous pensez avoir des problèmes de performances à ce niveau.

III - Navigation au clavier

Pour l'instant les seules interactions que l'on ait avec la table sont :

- mise en édition d'une cellule par double-clic ;

- écriture dans la zone de texte d'une cellule en édition ;

- fin de l'édition par perte du focus de la cellule ou pression sur la touche Entrée.

Ce qui serait franchement intéressant, c'est de pouvoir saisir les valeurs dans notre table sans avoir recours à la souris pour passer d'une cellule à l'autre. Il faut donc ajouter des événements supplémentaires à ceux déjà mis en place. Pour que l'utilisation de l'HtmlEditTable soit conviviale, nous aurons donc besoin de pouvoir :

- initier l'édition d'une cellule par pression de la touche Entrée ;

- annuler l'édition d'une cellule par pression de la touche Echap ;

- naviguer d'une cellule à l'autre à l'aide des flèches ;

- valider une saisie et mettre la cellule suivante dans la même colonne en édition par pression sur la touche Tab.

III-A - Prémisses

Naviguer dans la table c'est bien, mais il faut pouvoir le visualiser. C'est-à-dire que la cellule active doit être mise en évidence! Pour cela il faut être capable de déterminer qu'elle est la cellule active et modifier sa classe CSS.

Modifions le constructeur de la classe HtmlEditTable pour y introduire la propriété activeCell initialisée à null et qui référencera la cellule active.

Constructeur HtmlEditTable - v1.2

HtmlEditTable = function(){
if (arguments.length > 0 && arguments[0].table){
this.Transform(arguments);
}
else{
this.control = document.createElement("table");
if (arguments.length > 0){
this.Build(arguments[0]);
}
}
this.control.cellSpacing = 0;
this.control.cellPadding = 0;
this.control.className = "HtmlEditTable";
this.activeCell = null;
};

Implémentons la fonction HtmlEditTable.prototype.ActiveCellChange() qui sera appelée lorsque l'on change de cellule active.

HtmlEditTable.prototype.ActiveCellChange

ActiveCellChange: function(cell, focus){
if (this.activeCell){
this.activeCell.className = null;
}
this.activeCell = cell;
if (cell){
this.activeCell.className = "activeCell";
if (focus){
this.activeCell.focus();
}
}
}
}

Si une cellule était active avant le changement, alors sa classe CSS avait été modifiée. Donc on réinitialise cette valeur. Ensuite on référence la nouvelle cellule active qui est passée en paramètre, on lui donne la classe CSS activeCell ainsi que le focus suivant le booléen passé en paramètre.

.HtmlEditTable .activeCell

.HtmlEditTable .activeCell
{
background-color: RGB(201,200,181);
}

Maintenant que nous avons presque tous les outils nécessaires à la reconnaissance et à la mise en évidence de la cellule active, utilisons-les lors du double-clic sur une cellule pour l'édition. Ce qu'il nous manque, c'est une référence sur l'objet HtmlEditTable accessible par les cellules. En effet, lorsque l'on va effectuer une action sur une cellule, il faut pouvoir appeler la fonction HtmlEditTable.prototype.ActiveCellChange(). Pour ce faire, ajoutons à chaque cellule la propriété grid lors de l'appel à la fonction :

HtmlEditTableHelper.CellInitialize v1.1

CellInitialize: function (grid, cell, value){
if (cell){
cell.grid = grid;
cell.ondblclick = HtmlEditTableHelper.DblClickHandler;
if (typeof value != "undefined"){
cell.appendChild(document.createTextNode(value));
}
}
}

Tous les appels à la fonction HtmlEditTableHelper.CellInitialize() dans les fonctions de la classe HtmlEditTable doivent être modifiés en passant this en premier paramètre de cette fonction :

Appels de HtmlEditTableHelper.CellInitialize() dans la classe HtmlEditTable

HtmlEditTableHelper.CellInitialize(this, ...);

La fonction loseFocus locale au constructeur de la classe HtmlEdit est modifiée de la façon suivante :

Constructeur HtmlEdit v1.1

var loseFocus = function(e){
var src = Tools.Target(e);
var cell = Tools.Node(src, "TD");
var grid = cell.grid;
Tools.Purge(cell);
HtmlEditTableHelper.CellInitialize(grid, cell, src.value);
};

Finalement, la fonction HtmlEditTable.prototype.ActiveCellChange() peut être appelée lors du double-clic sur une cellule.

HtmlEditTableHelper.DblClickHandler v1.2

DblClickHandler: function(e){
var src = Tools.Node(Tools.Target(e), "TD");
if (!src){
Tools.Event(e).returnValue = false;
return false;
}
var htmlEdit = new HtmlEdit(src.firstChild.data);
htmlEdit.AppendTo(src);
src.ondblclick = null;
src.grid.ActiveCellChange(src, false);
}

La nouveauté vient de la dernière instruction de la fonction, l'appel à HtmlEditTable.prototype.ActiveCellChange(). Vous constaterez que maintenant, lorsque vous double-cliquez sur une cellule puis validez par pression sur la touche Entrée, la cellule active a changé de couleur. Si vous éditez une autre cellule, la précédente reprend son aspect normal et la nouvelle cellule active a changé de couleur à la fin de l'édition.

Il faudrait aussi que la cellule active soit désignée comme la cellule sur laquelle on clique (simple clic). Ce sera le rôle de la fonction HtmlEditTableHelper.ClickHandler().

HtmlEditTableHelper.ClickHandler

ClickHandler: function(e){
var src = Tools.Node(Tools.Target(e), "TD");
if (!src){
Tools.Event(e).returnValue = false;
return false;
}
src.grid.ActiveCellChange(src, true);
}

Son implémentation est identique à celle de HtmlEditTableHelper.DblClickHandler(), moins la mise en édition de la cellule. Il ne reste qu'à l'assigner au gestionnaire d'événement onclick de chaque cellule via la fonction HtmlEditTableHelper.CellInitialize().

HtmlEditTableHelper.CellInitialize v1.2

CellInitialize: function (grid, cell, value){
if (cell){
cell.grid = grid;
cell.onclick = HtmlEditTableHelper.ClickHandler;
cell.ondblclick = HtmlEditTableHelper.DblClickHandler;
if (typeof value != "undefined"){
cell.appendChild(document.createTextNode(value));
}
}
}

voir la démo - télécharger les sources

III-B - Initier l'édition d'une cellule

Jusqu'ici on pouvait éditer une cellule en double-cliquant dessus. Maintenant nous sommes prêts à ajouter de nouvelles fonctionnalités de navigation au clavier! Commençons par la touche Entrée. Il serait intéressant de pouvoir éditer la cellule en appuyant sur cette touche.

Affectons un callback HtmlEditTableHelper.KeydownHandler() au gestionnaire d'événement keydown sur les cellules dans la fonction HtmlEditTableHelper.CellInitialize() :

HtmlEditTableHelper.CellInitialize v1.3

CellInitialize: function (grid, cell, value){
if (cell){
cell.grid = grid;
cell.onclick = HtmlEditTableHelper.ClickHandler;
cell.ondblclick = HtmlEditTableHelper.DblClickHandler;
cell.onkeydown = HtmlEditTableHelper.KeydownHandler;
if (typeof value != "undefined"){
cell.appendChild(document.createTextNode(value));
}
}
}

HtmlEditTableHelper.KeydownHandler v1.1

KeydownHandler: function(e){
var src = Tools.Node(Tools.Target(e), "TD");
if (!src){
Tools.Event(e).returnValue = false;
return false;
}
switch(Tools.KeyCode(e)){
case KeyCodes.RETURN: alert("return"); break;
}

returnValue = false;
return false;
}

Les deux dernières instructions du callback empêchent l'événement de se propager au reste du document.

N'oublions de mettre à jour également la fonction HtmlEditTableHelper.CellNeutralizeEvents() :

HtmlEditTableHelper.CellNeutralizeEvents v1.2

CellNeutralizeEvents: function(cell){
if (cell){
cell.onclick = null;
cell.ondblclick = null;
cell.onkeydown = null;
}
}

voir la démo - télécharger les sources

Si vous testez à ce stade vous remarquerez que cela fonctionne sur Internet Explorer (IE6 au moins), mais pas par exemple ni sur Firefox ni sur Chrome. Kêzako? En fait il existe des éléments capables naturellement d'être accessibles au clavier et d'autres non. Les tables, lignes et cellules font partie de la seconde catégorie d'éléments… Ce qui n'empêche nullement Internet Explorer de gérer les événements claviers sur ces éléments (sacré farceur!). Cependant il en faut plus pour les autres navigateurs.

Pour que la gestion des événements clavier soit possible, il faut que l'élément soit tabulable. C'est le cas des contrôles de formulaire et des liens. Pour les autres éléments, il suffit de donner une valeur supérieure ou égale à zéro à la propriété tabIndex.

Comportement des éléments suivant le tabIndex

valeur de tabIndex

focus souris ou JavaScript

navigable

non renseigné

comportement par défaut spécifique à l'élément

comportement par défaut spécifique à l'élément

négatif

oui

non (le focus via JavaScript doit être donné pour que cela soit possible)

zéro

oui

oui

positif

oui

oui

Nous allons donner la valeur zéro au tabIndex des cellules du HtmlEditTable afin de les rendre tabulables. Une valeur positive entrainera une modification de l'ordre de tabulation dans la page, les éléments avec un tabIndex supérieur à zéro ayant préséance sur les éléments naturellement tabulables et ceux dont le tabIndex est à zéro. En mettant le tabIndex à zéro, on garde l'ordre de tabulation naturel, celui-ci étant imposé par l'ordre d'apparition des éléments dans le DOM du document.

La valeur du tabIndex est à fixer une fois à l'initialisation de la cellule lors de sa création ou lors de l'extension d'une table HTML en HtmlEditTable. Les fonctions impactées sont donc les suivantes :

- HtmlEditTable.prototype.Transform() ;

- HtmlEditTable.prototype.Populate() ;

- HtmlEditTable.prototype.Line() ;

- HtmlEditTable.prototype.Column().

Dans chacune de ces fonctions, initialisons le tabIndex des cellules avant l'appel à la fonction HtmlEditTableHelper.CellInitialize() :

Rendre les cellules tabulables

[...]
cell.tabIndex = 0;
HtmlEditTableHelper.CellInitialize(this, cell[, ...]);
[...]

Et cette fois ça marche! Pour Firefox comme pour Chrome, lorsqu'une cellule est sélectionnée et que l'on presse sur Entrée, l'alerte s'affiche. Il ne nous reste plus qu'à troquer l'alerte contre le comportement désiré, à savoir l'édition de la cellule. L'action a réalisé est la même que celle effectuée sur le double-clic de la cellule, donc :

HtmlEditTableHelper.KeydownHandler v1.2

KeydownHandler: function(e){
var src = Tools.Node(Tools.Target(e), "TD");
if (!src){
Tools.Event(e).returnValue = false;
return false;
}
switch(Tools.KeyCode(e)){
case KeyCodes.RETURN: HtmlEditTableHelper.DblClickHandler(e); break;
}

returnValue = false;
return false;
}

voir la démo - télécharger les sources

III-C - Annuler l'édition d'une cellule

Une convention largement répandue est que la touche Echap annule l'action en cours. Dans notre cas il serait intéressant de pouvoir annuler l'édition d'une cellule, c'est-à-dire de revenir en mode visualisation de la cellule sans que sa valeur ne soit modifiée.

L'implémentation de cette fonctionnalité est très simple. Il suffit d'enrichir le constructeur de la classe HtmlEdit. D'abord, renommons la fonction loseFocus en returnEvent, sauvegardons la valeur initiale de la zone de texte dans la propriété initialValue de cette dernière et donnons le focus à la cellule à la fin de l'édition. Implémentons la fonction escapeEvent sur le modèle de returnEvent, mais en passant la valeur de la propriété initialValue au lieu de value. Il ne reste plus qu'à appeler cette nouvelle fonction sur le keydown de la zone de texte.

Constructeur HtmlEdit v1.2

var HtmlEdit = function(value){
var returnEvent = function(e){
var src = Tools.Target(e);
var cell = Tools.Node(src, "TD");
var grid = cell.grid;
Tools.Purge(cell);
HtmlEditTableHelper.CellInitialize(grid, cell, src.value);
cell.focus();
};

var escapeEvent = function(e){
var src = Tools.Target(e);
var cell = Tools.Node(src, "TD");
var grid = cell.grid;
Tools.Purge(cell);
HtmlEditTableHelper.CellInitialize(grid, cell, src.initialValue);
cell.focus();
};

this.control = document.createElement("input");
this.control.type = "text";
this.control.className = "HtmlEdit";
this.control.onblur = returnEvent;
this.control.onkeydown = function(e){
switch(Tools.KeyCode(e)){
case KeyCodes.RETURN: returnEvent(e); break;
case KeyCodes.ESCAPE: escapeEvent(e); break;
}
};

this.control.value = value;
this.control.initialValue = value;
};

voir la démo - télécharger les sources

III-D - Supprimer le contenu d'une cellule

Voilà sûrement la fonctionnalité nécessitant le moins de code :) Pour supprimer le contenu d'une cellule en lecture lorsqu'elle est active, il suffit de gérer la touche DEL dans le gestionnaire de l'événement keydown de la façon suivante :

HtmlEditTableHelper.KeydownHandler v1.3

KeydownHandler: function(e){
var src = Tools.Node(Tools.Target(e), "TD");
if (!src){
Tools.Event(e).returnValue = false;
return false;
}

var NavDel = function(cell){
cell.firstChild.data = "";
};

switch(Tools.KeyCode(e)){
case KeyCodes.RETURN: HtmlEditTableHelper.DblClickHandler(e); break;
case KeyCodes.DELETE: NavDel(src); break;
}

returnValue = false;
return false;
}

Pour ne pas qu'une ligne dont toutes les valeurs ont été supprimées ne prenne une taille nulle, ajoutons la classe CSS suivante au fichier CSS :

.HtmlEditTable tbody tr

.HtmlEditTable tbody tr
{
height: 1em;
}

voir la démo - télécharger les sources

III-E - Déplacement à l'aide des flèches

Je vois que ça vous amuse, vous cliquez partout comme des p'tits fous pour changer la couleur de la cellule cliquée :) Quelle candeur, quelle fraîcheur. Ô joie et inconscience de la jeunesse! (pétage de câble…) Bref, allons plus loin et voyons comment nous passer de la souris.

Le top du top serait de pouvoir passer d'une cellule à l'autre facilement en ne bougeant qu'imperceptiblement nos petites mimines. Économisons nos forces ;). Du coup, l'idéal serait de pouvoir utiliser les flèches du clavier! Très simple une fois encore…

Modifions le callback HtmlEditTableHelper.KeydownHandler() :

HtmlEditTableHelper.KeydownHandler v1.4

KeydownHandler: function(e){
var src = Tools.Node(Tools.Target(e), "TD");
if (!src){
Tools.Event(e).returnValue = false;
return false;
}

var NavDel = function(cell){
cell.firstChild.data = "";
};

switch(Tools.KeyCode(e)){
case KeyCodes.RETURN: HtmlEditTableHelper.DblClickHandler(e); break;
case KeyCodes.DELETE: NavDel(src); break;
case KeyCodes.LEFT: alert("left"); break;
case KeyCodes.UP: alert("up"); break;
case KeyCodes.RIGHT: alert("right"); break;
case KeyCodes.DOWN: alert("down"); break;
}

returnValue = false;
return false;
}

Pour finaliser la navigation par les flèches du clavier, il suffit de remplacer les alertes dans la fonction HtmlEditTableHelper.KeydownHandler() par les fonctions adéquates :

- NavLeft : active la cellule à gauche de la cellule courante s'il y en a une ;

- NavRight : active la cellule à droite de la cellule courante s'il y en a une ;

- NavUp : active la cellule au-dessus de la cellule courante s'il y en a une ;

- NavDown : active la cellule au-dessous de la cellule courante s'il y en a une.

La fonction HtmlEditTableHelper.KeydownHandler() devient donc :

HtmlEditTableHelper.KeydownHandler v1.5

KeydownHandler: function(e){
var src = Tools.Node(Tools.Target(e), "TD");
if (!src){
Tools.Event(e).returnValue = false;
return false;
}

var NavDel = function(cell){
cell.firstChild.data = "";
};

var NavLeft = function(cell){
cell = Tools.Node(cell.previousSibling, "TD");
if (cell){
cell.grid.ActiveCellChange(cell, true);
}
};

var NavRight = function(cell){
cell = Tools.Node(cell.nextSibling, "TD");
if (cell){
cell.grid.ActiveCellChange(cell, true);
}
};

var NavUp = function(cell){
var row = cell.parentNode.previousSibling;
if (row){
cell = row.cells[cell.cellIndex];
cell.grid.ActiveCellChange(cell, true);
}
};

var NavDown = function(cell){
var row = cell.parentNode.nextSibling;
if (row){
cell = row.cells[cell.cellIndex];
cell.grid.ActiveCellChange(cell, true);
}
};

switch(Tools.KeyCode(e)){
case KeyCodes.RETURN: HtmlEditTableHelper.DblClickHandler(e); break;
case KeyCodes.DELETE: NavDel(src); break;
case KeyCodes.LEFT: NavLeft(src); break;
case KeyCodes.UP: NavUp(src); break;
case KeyCodes.RIGHT: NavRight(src); break;
case KeyCodes.DOWN: NavDown(src); break;
}

returnValue = false;
return false;
}

voir la démo - télécharger les sources

III-F - Déplacement à l'aide de la touche de tabulation

Souvent dans les tableurs, Excel par exemple, on peut passer à la cellule suivante de la même colonne par pression de la touche tabulation. Maintenant que nous avons réalisé la navigation à l'aide des flèches, il est trivial d'implémenter la tabulation. Tabuler correspond à naviguer de la même façon qu'avec la flèche bas jusqu'à la dernière cellule de la colonne, puis à passer ensuite à la première cellule de la colonne suivante. Allons même plus loin en considérant la combinaison de touche SHIFT+TAB pour passer à la cellule précédente !

D'où la nouvelle mouture de la fonction HtmlEditTableHelper.KeydownHandler() :

HtmlEditTableHelper.KeydownHandler v1.6

KeydownHandler: function(e){
var src = Tools.Node(Tools.Target(e), "TD");
if (!src){
Tools.Event(e).returnValue = false;
return false;
}

var NavDel = function(cell){
cell.firstChild.data = "";
};

var NavLeft = function(cell){
cell = Tools.Node(cell.previousSibling, "TD");
if (cell){
cell.grid.ActiveCellChange(cell, true);
}
};

var NavRight = function(cell){
cell = Tools.Node(cell.nextSibling, "TD");
if (cell){
cell.grid.ActiveCellChange(cell, true);
}
};

var NavUp = function(cell){
var row = cell.parentNode.previousSibling;
if (row){
cell = row.cells[cell.cellIndex];
cell.grid.ActiveCellChange(cell, true);
}
};

var NavDown = function(cell){
var row = cell.parentNode.nextSibling;
if (row){
cell = row.cells[cell.cellIndex];
cell.grid.ActiveCellChange(cell, true);
}
};

var NavTab = function(cell){
var shiftKey = Tools.SpecialKeys(e).ShiftKey;
var row = shiftKey ? cell.parentNode.previousSibling : cell.parentNode.nextSibling;
if (row){
cell = row.cells[cell.cellIndex];
cell.grid.ActiveCellChange(cell, true);
}
else{
row = cell.parentNode;
tBody = row.parentNode;
if (shiftKey && cell.cellIndex>0){
row = tBody.lastChild;
cell = row.cells[cell.cellIndex-1];
cell.grid.ActiveCellChange(cell, true);
}
if (!shiftKey && row.cells.length-1 > cell.cellIndex){
row = tBody.firstChild;
cell = row.cells[cell.cellIndex+1];
cell.grid.ActiveCellChange(cell, true);
}
}
};

switch(Tools.KeyCode(e)){
case KeyCodes.RETURN: HtmlEditTableHelper.DblClickHandler(e); break;
case KeyCodes.DELETE: NavDel(src); break;
case KeyCodes.LEFT: NavLeft(src); break;
case KeyCodes.UP: NavUp(src); break;
case KeyCodes.RIGHT: NavRight(src); break;
case KeyCodes.DOWN: NavDown(src); break;
case KeyCodes.TAB: NavTab(src); break;
}

returnValue = false;
return false;
}

Maintenant on peut passer en lecture d'une cellule à l'autre par tabulation. Il nous reste à gérer la touche de tabulation sur la zone de texte pour pouvoir faire de même en édition. Pour se faire, on modifie une fois de plus le constructeur de la classe HtmlEdit et on ajoute un nouveau cas dans le gestionnaire d'événement onkeydown de la zone de texte. La fonction tabEvent() commence par réaliser l'action returnEvent (comme lorsqu'on presse la touche Entrée), puis active la cellule suivante (au sens de la tabulation dans un tableur) ou précédente dans le même style que la navigation par les flèches, et finalement ajoute la zone de texte dans la nouvelle cellule active.

Constructeur HtmlEdit v1.3

var HtmlEdit = function(value){
var returnEvent = function(e){
var src = Tools.Target(e);
var cell = Tools.Node(src, "TD");
var grid = cell.grid;
Tools.Purge(cell);
HtmlEditTableHelper.CellInitialize(grid, cell, src.value);
cell.focus();
};

var escapeEvent = function(e){
var src = Tools.Target(e);
var cell = Tools.Node(src, "TD");
var grid = cell.grid;
Tools.Purge(cell);
HtmlEditTableHelper.CellInitialize(grid, cell, src.initialValue);
cell.focus();
};

var tabEvent = function(e){
var src = Tools.Target(e);
var cell = Tools.Node(src, "TD");
var shiftKey = Tools.SpecialKeys(e).ShiftKey;
var row = shiftKey ? cell.parentNode.previousSibling : cell.parentNode.nextSibling;

returnEvent(e);

if (row){
cell = row.cells[cell.cellIndex];
cell.grid.ActiveCellChange(cell, true);

var htmlEdit = new HtmlEdit(cell.firstChild.data);
htmlEdit.AppendTo(cell);
}
else{
row = cell.parentNode;
tBody = row.parentNode;
if (shiftKey && cell.cellIndex>0){
row = tBody.lastChild;
cell = row.cells[cell.cellIndex-1];
cell.grid.ActiveCellChange(cell, true);
var htmlEdit = new HtmlEdit(cell.firstChild.data);
htmlEdit.AppendTo(cell);
}
else if (!shiftKey && row.cells.length-1 > cell.cellIndex){
row = tBody.firstChild;
cell = row.cells[cell.cellIndex+1];
cell.grid.ActiveCellChange(cell, true);
var htmlEdit = new HtmlEdit(cell.firstChild.data);
htmlEdit.AppendTo(cell);
}
}
};

this.control = document.createElement("input");
this.control.type = "text";
this.control.className = "HtmlEdit";
this.control.onblur = returnEvent;
this.control.onkeydown = function(e){
switch(Tools.KeyCode(e)){
case KeyCodes.RETURN: returnEvent(e); break;
case KeyCodes.ESCAPE: escapeEvent(e); break;
case KeyCodes.TAB: tabEvent(e); break;
}
};

this.control.value = value;
this.control.initialValue = value;
};

voir la démo - télécharger les sources

IV - Remerciements

Je remercie :

- jacques_jean pour sa relecture attentive et ses corrections ;

- drumtof qui m'a plusieurs fois sollicité pour obtenir les évolutions de la v2.0 ;

- SpaceFrog, Arnaud F., Gatsu35 et kernelfailure pour leurs contributions à la discussion relatives à l'article de la v1.0 du HtmlEditTable qui m'ont permis d'apporter quelques améliorations.

V - Toutes les publications

V-A - Espaces de noms (ou namespace) en JavaScript

Cet article s'adresse à une population de développeurs déjà familière avec la problématique du développement Web et le langage JavaScript. Dans cet article, vous apprendrez comment packager vos bibliothèques en simulant en JavaScript ce que l'on appelle un espace de noms (« namespace » en anglais). Vous pouvez participer à la discussion relative à cet article sur le site communautaire www.developpez.com. Commentez

lire l'article

V-B - Conception de contrôles Web avancés : Créer une table HTML éditable - HtmlEditTable v1.0

Ce tutoriel est le premier d'une série consacrée au développement de contrôles Web avancés en JavaScript. Vous allez apprendre au cours de votre lecture à penser et concevoir des outils plus complexes au travers d'une étude de cas : la conception d'une table éditable. Vous pouvez participer à la discussion relative à cet article sur le site communautaire www.developpez.com. Commentez

lire l'article

V-C - Conception de contrôles Web avancés : Créer une table HTML éditable - HtmlEditTable v2.0

Dans un précédent tutoriel, nous avons conçu une table HTML éditable en JavaScript : HtmlEditTable v1.0. Pour la v2.0, nous allons réviser un peu le code et ajouter les fonctionnalités suivantes :

- création d'un HtmlEditTable à partir d'une table HTML existante ;

- navigation au clavier.

Vous pouvez participer à la discussion relative à cet article sur le site communautaire www.developpez.com. Commentez

lire l'article

V-D - Bibliothèque d'effets JavaScript : Smile FX

En cours…

voir la démo

(1)Contribution de Gatsu35