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>
Ecoute du click sur un paragraphe.

/// <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

Démonstration.

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
};
Stopper le comportement par défaut du click sur un label.

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
    }
};
S'assurer que le click n'est pas délégué par un label.
/**
 *  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;
};
Méthode

Ressources et références

chapter
4.10.6 The label element
chapter
4.10.2 Categories
Commentaire

Liste des eléments « labélisables ».