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.
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.
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
{
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 :
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 :
HtmlEditTableHelper.CellInitialize
(
this,
...
);
La fonction loseFocus locale au constructeur de la classe HtmlEdit est modifiée de la façon suivante :
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.
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().
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().
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));
}
}
}
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() :
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));
}
}
}
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() :
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.
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() :
[...]
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 :
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;
}
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.
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;
};
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 :
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
{
height:
1
em;
}
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() :
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 :
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;
}
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() :
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.
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;
};