Click sur des labels
La délégation d'écoute d'événements est très préciseuse en javascript mais, dans certains cas, elle peut engendrer des comportements particuliers. C'est le cas si l'élément sur lequel on écoute les événements « click » possède un label
.
Problématique
Imaginons que nous écoutions les événements click sur un paragraphe de la manière suivante :
<p id="default">
<label for="foo">label</label>
<input type="text" id="foo">
</p>
<script>
document.getElementById('default').addEventListener('click', function (e) {
alert('click: ' + e.timeStamp);
}, false);
</script>
/// <div class="cadre"> <p id="target" style="background: #ccc; margin: 0; padding: 5px;"> <label for="foo" style="background: #eee; display:inline-block; padding:2px">label</label> <input type="text" id="foo" placeholder="champ texte"> </p> <script> var clickTime = 0; document.getElementById('target').addEventListener('click', function (e) { clickTime
Si vous cliquez n'importe où dans le paragraphe à l'exception du label, l'écouteur partira une fois ; si vous cliquez sur le label, l'écouteur partira 2 fois. Pourquoi ?
La raison en est simple : un label peut être associé soit à l'élément « labélisable » portant un identifiant de même valeur que son attribut « for », soit au premier élément labélisable qu'il contient. Cette association se traduit par le transfert de l'action, le « click », sur l'élément associé ; le label fait en quelque sorte de la délégation d'événement. Mais comment éviter de voir son écouteur de click appeler 2 fois dans des cas comme celui-ci ?
Solutions
Une première solution consiste à stopper le comportement par défaut du label lorsqu'il reçoit un click, c'est-à-dire le transfert de l'action vers son élément associé :
var listener = function (e) {
e.preventDefault();
// do something
};
La prévention peut être améliorée et restreinte en s'assurant que le click a bien eu lieu sur le label (ou un des ses descendants) mais, même ainsi, cela reste une solution radicale. Une autre technique consiste simplement à s'assurer que le click ne va pas être déléguer à un autre événement :
var listener = function (e) {
if (!labelClickDelegation(e.target)) {
// do something
}
};
/**
* Vérifier qu'un élément HTML est un label ou descendant de label
* et qu'un événement click peut être déléguer à un élément associé
* tel un champ input ou un bouton.
*
* @param {HTMLElement} el Elément HTML à tester.
* @return {Boolean} true si l'événement click peut être déléguer,
* false sinon.
*/
var labelClickDelegation = function (el)
{
var labelable = /^button|input|keygen|meter|output|progress|select|textarea$/i,
nodeName = el.nodeName.toLowerCase(),
parent,
i = 0,
n;
// Pas un label
if (nodeName !== 'label') {
// Elément labélisable
if (labelable.test(el.nodeName)) {
return false;
}
// Un descendant de label
parent = el.parentNode;
while (parent) {
nodeName = parent.nodeName.toLowerCase();
// Label parent trouvé
if (nodeName === 'label') {
break;
}
// XXX Formulaire parent: inutile d'aller plus haut ?
if (nodeName === 'form') {
return false;
}
parent = parent.parentNode;
}
// Pas de parent label
if (!parent) {
return false;
}
el = parent;
}
// Label avec un attribut 'for' pointant sur un élément existant
if (el.htmlFor && document.getElementById(el.htmlFor)) {
// XXX Vérifier que la cible est labelable ?
return true;
}
// Label sans enfants
if (el.children.length === 0) {
return false;
}
// label possédant un enfant 'activable'
n = el.children.length;
for (; i < n; i++) {
if (labelable.test(el.children[i].nodeName)) {
return true;
}
}
// Pas de délegation.
return false;
};
Ressources et références
- chapter
- 4.10.6 The label element
- chapter
- 4.10.2 Categories
- Commentaire
Liste des eléments « labélisables ».