Select : gérer la sélection multiple

L'élément HTML select manque cruellement de fonctionnalités à partir du moment où l'on active la sélection multiple et que l'on souhaite par exemple limiter le nombre de sélection ou connaître l'ordre des sélections… Voilà donc quelques pistes de réflexions / expérimentations.

Limiter le nombre de sélections

D'abord concernant la limitation du nombre de sélections : aucune API n'est disponible ; une fois que l'élément HTML possède l'attribut « multiple » ou si l'objet DOM select à sa propriété multiple à true, l'utilisateur peut sélectionner autant d'options qu'il le souhaite. Comme par ailleurs il ne semble pas possible d'interdire la sélection, il faut se mettre à l'écoute de l'événement change et désélectionner toutes les options surnuméraires.

/**
 *  Limiter le nombre de sélections permises dans un élément HTML select
 *  à sélection multiple.
 *  
 *  @param {HTMLSelectElement} select Elément qu'il faut gérer.
 *  @param {int} [limit = 1] Nombre de sélection maximum. Si le paramètre
 *  n'est pas fournit, le nombre de sélection maximum peut être définit sur
 *  l'élément 'select' avec l'attribut 'data-max-selection'.
 */
var maxSelection = function(select, limit) {
    if (!select.multiple) {
        return;
    }
    if (!limit) {
        if (select.dataset.maxSelection) {
            limit = parseInt(select.dataset.maxSelection, 10);
            if (isNaN(limit)) {
                limit = 1;
            }
        } else {
            limit = 1;
        }
    }
    
    /**
     *  Liste des options sélectionnées.
     *  @type HTMLOptionElement[]
     */
    var selectedOptions = null;
    
    /**
     *  Ecouteur d'évenement change sur le select
     *  @param {Event} e Change event
     */
    var onChange = function(e) {
        var options = e.target.selectedOptions,
            n = options.length,
            i = 0,
            queueLength = 0,
            queueOptions = [],
            addedOptions = [];
        if (n > limit) {
            // Cas où on n'a pas encore atteint la limite mais on
            // sélectionne plusieurs options qui la font dépasser
            // (shift + click). On ajoute les options jusqu'à remplir
            // la liste.
            if (selectedOptions.length < limit) {
                queueLength = limit - selectedOptions.length;
                for (i = 0; i < n; i++) {
                    if (selectedOptions.indexOf(options[i]) === -1) {
                        if (selectedOptions.length < limit) { 
                            selectedOptions.push(options[i]);
                        } else {
                            break;
                        }
                    }
                }
            }
            // Désélectionner les options surnuméraires.
            for (i = 0; i < n; i++) {
                if (selectedOptions.indexOf(options[i]) === -1) {
                    addedOptions.push(options[i]);
                }
            }
            if (addedOptions.length > 0) {
                n = addedOptions.length;
                for (i = 0; i < n; i++) {
                    addedOptions[i].selected = false;
                }
            }
        } else {
            selectedOptions = Array.prototype.slice.call(options);
        }
    };
    
    // Attache de l'écouteur d'événement 'change'.
    select.addEventListener('change', onChange, false);
};
Fonction pour limiter le nombre de sélections possible dans un élément select.

Démonstration

Remarques complémentaires :

  • On ne peut pas se baser sur un écouteur d'événement click qui désélectionne l'option cliquée si la limite de sélections a été atteinte car cela n'aurait aucun effet sur une sélection au clavier.
  • On aurait pu aussi désactiver les options de la liste une fois que le nombre maximum de sélections a été atteint ; cela demanderait cependant une manipulation du DOM supplémentaire un peu lourde si le nombre d'options est important.

Retrouver l'ordre de sélection

Passons maintenant à l'ordre des sélections. L'objet DOM select possède une propriété selectedOptions qui stocke la liste des options sélectionnées dans l'ordre du code HTML, pas dans l'ordre de sélection. Il faut donc créer un objet sépcifique capable de stocker cet ordre.

/**
 *  Objet permettant de conserver la trace de l'ordre de sélection
 *  des options dans un élément select à sélection multiple.
 *
 *  @example
 *      var selectionOrder = new SelectionOrder(mySelect);
 *      mySelect.addEventListener('change', function(e) {
 *          // options list in selected order.
 *          var orderedOptions = selectionOrder.getOptions();
 *      }, false);
 *  
 *  @constructor
 *  @param {HTMLSelectElement} select Elément 'select' à gérer.
 */
var SelectionOrder = function(select) {
    /**
     *  Liste des options sélectionnées dans l'ordre
     *  de sélection
     *  @private
     *  @type HTMLOptionElement[]
     */
    this.orderedOptions = [];
    /**
     *  Elément 'select' à gérer
     *  @private
     *  @type HTMLSelectElement
     */
    this.element = select;
    
    this.element.addEventListener('change', this.onChange.bind(this), false);
};

SelectionOrder.prototype = {
    // fix constructor
    constructor: SelectionOrder,
    
    /**
     *  Récupérer la liste des options sélectionnées dans l'ordre de
     *  sélection.
     *  @return HTMLOptionElement[];
     */
    getOptions: function() {
        return this.orderedOptions;
    },
    
    /**
     *  Supprimer les options enregistrées qui ne sont plus sélectionnées.
     *  @private
     */
    removeUnselected: function() {
        var options = Array.prototype.slice.call(this.element.selectedOptions),
            newOrderedOptions = this.orderedOptions.filter(function(item) {
                return options.indexOf(item) !== -1;
            });
        this.orderedOptions = newOrderedOptions;
    },
    
    /**
     *  Ecouteur d'événement 'change' sur le select.
     *  @param {Event} e Change event.
     */
    onChange: function(e) {
        var options = e.target.selectedOptions,
            n = options.length,
            i = 0;
        if (n === 0) {
            this.orderedOptions = [];
        } else {
            this.removeUnselected();
            if (n > this.orderedOptions.length) {
                for (i = 0; i < n; i++) {
                    if (this.orderedOptions.indexOf(options[i]) === -1) {
                        this.orderedOptions.push(options[i]);
                    }
                }
            }
        }
    }
};
Objet pour enregistrer l'ordre de sélection dans un élément select.

Démonstration

Ressources et références

HTMLSelectElement. Mozilla Developer Network,

<select>. Mozilla Developer Network,