HTML : range

Representer graphiquement des intervalles en HTML.

Spécifications

[MIN|FROM-TO|MAX]
Syntaxe wiki pour la déclaration d'un intervalle.
[MIN|FROM1-TO1,FROM2-TO2|MAX]
Syntaxe wiki pour la déclaration d'un intervalle à plusieurs valeurs.
min ≤ from ≤ to ≤ max
Règles définissant les valeurs min, from, to et max.
<span class="range" aria-valuemin="0" aria-valuemax="10">
    <span class="value">
        <span class="from" aria-valuemin="0" aria-valuemax="7" aria-valuenow="3">3</span>
        <span class="to" aria-valuemin="3" aria-valuemax="10" aria-valuenow="7">7</span>
    </span>
</span>
Syntaxe HTML pour la déclaration d'un intervalle.

3 7

Rendu d'un intervalle.

API

Elément range

Nom
min
Description
Valeur minimum de l'intervalle.
type
double
default
0.0
Nom
max
Description
Valeur maximale de l'intervalle.
type
double
default
1
Nom
orientation
Description
Orientation de l'élément (vertical|horizontal).
type
string
default
horizontal
method
HTMLSpanElement addValue ( from , to )
Description
Ajouter une valeur d'intervalle.
param
int from Début de l'intervalle.
param
int to Fin de l'intervalle.
return
HTMLSpanElement Element valeur créé.
method
HTMLSpanElement getValue ( index )
Description
Récupérer la valeur à un index donné.
param
int index Index de la valeur.
return
HTMLSpanElement Element valeur
method
void setValue ( index , from , to )
Description
Définir la valeur d'intervalle à un index donné.
param
int index Index de la valeur.
param
int from Début de l'intervalle.
param
int to Fin de l'intervalle.

Element valeur

Nom
from
Description
Valeur de début de l'intervalle de valeurs.
type
double
Nom
to
Description
Valeur de fin de l'intervalle de valeurs.
type
double

Code

define(function () {
    'use strict';
    
    var CLASS_RANGE = 'range';
    
    var CLASS_VALUE = 'value';
    
    /**
     *  Liste des arttributs pour les noeuds de valeurs.
     *  @private
     */
    var VALUE_ATTRS = ['from', 'to'];
    
    /**
     *  Liste de propriétés pour les noeuds de valeurs.
     *  @private
     */
    var VALUE_PROPS = {
        /**
         *  Descripteur de la propriété 'from'.
         */
        from: {
            enumerable: true,
            configurable: false,
            get: function () {
                return getFloatValue(this.firstElementChild, 'aria-valuenow');
            },
            set: function (newValue) {
                if (Number.isNaN(newValue) || newValue === this.from) {
                    return;
                }
                if (newValue === null) {
                    resetValue(this);
                    return;
                }
                var min = this.parentNode.min,
                    to = this.to;
                if (newValue < min) {
                    newValue = min;
                } else if (to !== null && newValue > to) {
                    newValue = to;
                }
                this.firstElementChild.setAttribute('aria-valuenow', newValue);
                this.lastElementChild.setAttribute('aria-valuemin', newValue);
                render(this.parentNode);
            }
        },
        
        /**
         *  Descripteur de la propriété 'to'.
         */
        to: {
            enumerable: true,
            configurable: false,
            get: function () {
                return getFloatValue(this.lastElementChild, 'aria-valuenow');
            },
            set: function (newValue) {
                if (Number.isNaN(newValue) || newValue === this.to) {
                    return;
                }
                if (newValue === null) {
                    resetValue(this);
                    return;
                }
                var max = this.parentNode.max, 
                    from = this.from;
                if (newValue > max) {
                    newValue = max;
                } else if (from !== null && newValue < from) {
                    newValue = from;
                }
                this.lastElementChild.setAttribute('aria-valuenow', newValue);
                this.firstElementChild.setAttribute('aria-valuemax', newValue);
                render(this.parentNode);
            }
        }
    };
    
    /**
     *  Liste des attributs à gérer.
     *  
     *  @private
     *  @type String[]
     */
    var ATTRS = ['orientation', 'min', 'max', 'getValue', 'setValue', 'addValue'];
    
    /**
     *  Liste des descripteurs de propriétés.
     *  
     *  min ≤ from ≤ to ≤ maximum
     *  
     *  @private
     *  @type Object
     */
    var PROPS = {
    
        getValue: {
            enumerable: false,
            configurable: false,
            value: function (index) {
                if (Number.isNaN(index) || !Number.isInteger(index) || index < 0) {
                    return null;
                }
                return this.children[index];
            }
        },
        
        setValue: {
            enumerable: false,
            configurable: false,
            value: function (index, from, to) {
                if (Number.isNaN(index) || !Number.isInteger(index) || index < 0) {
                    //throw new Error('Wrong index');
                    return;
                }
                var element = this.children[index];
                if (!element) {
                    //throw new Error('Out of bound exception');
                    return;
                }
                element.from = from;
                element.to = to;
                renderValue(element);
            }
        },
        
        addValue: {
            enumerable: false,
            configurable: false,
            value: function (from, to) {
                var element = createValue();
                initValue(element);
                this.appendChild(element);
                element.from = from;
                element.to = to;
                renderValue(element);
            }
        },
        
        /**
         *  Descripteur de la propriété 'orientation'.
         */
        orientation: {
            enumerable: true,
            configurable: false,
            get: function () {
                var value = this.getAttribute('aria-orientation') || 'horizontal';
                if (value !== 'vertical' && value !== 'horizontal') {
                    this.setAttribute('aria-orientation', 'horizontal');
                }
                return value;
            },
            set: function (newValue) {
                if (newValue !== 'vertical') {
                    newValue = 'horizontal';
                }
                if (newValue === this.orientation) {
                    return;
                }
                this.setAttribute('aria-orientation', newValue);
                render(this);
            }
        },
        
        /**
         *  Descripteur de la propriété 'min'.
         */
        min: {
            enumerable: true,
            configurable: false,
            get: function () {
                return getFloatValue(this, 'aria-valuemin', 0);
            },
            set: function (newValue) {
                if (Number.isNaN(newValue) || newValue === this.min) {
                    return;
                }
                var max = this.max;
                if (newValue > max) {
                    newValue = max;
                }
                this.setAttribute('aria-valuemin', newValue);
                // FIXME Valider les valeurs
//                this.firstElementChild.firstElementChild.setAttribute('aria-valuemin',
//                        newValue);
                render(this);
            }
        },
        
        /**
         *  Descripteur de la propriété 'max'.
         */
        max: {
            enumerable: true,
            configurable: false,
            get: function () {
                return getFloatValue(this, 'aria-valuemax', 1);
            },
            set: function (newValue) {
                if (Number.isNaN(newValue) || newValue === this.max) {
                    return;
                }
                var min = this.min;
                if (newValue < min) {
                    newValue = min;
                }
                this.setAttribute('aria-valuemax', newValue);
                // FIXME Valider les valeurs
//                this.firstElementChild.lastElementChild.setAttribute('aria-valuemax',
//                        newValue);
                render(this);
            }
        }
    };
    
    /**
     *  @private
     *  @param HTMLSpanElement Element de valeur.
     */
    var resetValue = function (valueElement) {
        // élément from
        valueElement.firstElementChild.removeAttribute('aria-valuenow');
        valueElement.firstElementChild.setAttribute('aria-valuemax', 
                valueElement.parentNode.getAttribute('aria-valuemax'));
        // élément to
        valueElement.lastElementChild.removeAttribute('aria-valuenow');
        valueElement.lastElementChild.setAttribute('aria-valuemin',
                valueElement.parentNode.getAttribute('aria-valuemin'));
    };
    
    /**
     *  Récupérer la valeur d'un attribut.
     *  
     *  @private
     *  @param HTMLSpanElement element Elément à analyser.
     *  @param String attribute Attribut à lire
     *  @param mixed [defaultValue=null] Valeur par défaut à retourner.
     *  @return Float
     */
    var getFloatValue = function (element, attribute, defaultValue) {
        var value = element.getAttribute(attribute),
            def = defaultValue !== undefined 
                ? defaultValue
                : null;
        if (value === null) {
            return def;
        }
        value = Number.parseFloat(value);
        if (Number.isNaN(value)) {
            return def;
        }
        return value;
    };
    
    /**
     *  Rendre l'état graphique de l'élément.
     *  
     *  L'intervalle (from - to) peut être non défini. Si un seul des
     *  attributs est défini, ne pas calculer non plus.
     *  
     *  @private
     *  @param HTMLSpanElement element Elément range.
     */
    var render = function (element) {
        var values = element.getElementsByClassName(CLASS_VALUE),
            i = 0,
            n = values.length,
            posRule = 'left',
            sizeRule = 'width',
            oppositeSizeRule = 'height',
            oppositePosRule = 'bottom';
            
        if (element.orientation === 'vertical') {
            posRule = 'bottom';
            sizeRule = 'height';
            oppositeSizeRule = 'width';
            oppositePosRule = 'left';
        }
        element.dataset.posRule = posRule;
        element.dataset.sizeRule = sizeRule;
        element.dataset.oppositePosRule = oppositePosRule;
        element.dataset.oppositeSizeRule = oppositeSizeRule;
        
        for (i = 0; i < n; i += 1) {
            renderValue(values[i]);
        }
    };
    
    /**
     *  @private
     *  @return {HTMLSpanElement}
     */
    var createValue = function () {
        var element = document.createElement('span');
        element.className = CLASS_VALUE;
        element.appendChild(document.createElement('span'));
        element.firstChild.className = 'from';
        element.appendChild(document.createElement('span'));
        element.lastChild.className = 'to';
        return element;
    };

    var initValue = function (element) {
        VALUE_ATTRS.forEach(function (attr) {
            Object.defineProperty(element, attr, VALUE_PROPS[attr]);
        });
    };
    
    /**
     *  @private
     */
    var renderValue = function (element) {
        var parent = element.parentNode,
            from = element.from,
            to = element.to,
            min = parent.min,
            max = parent.max,
            delta = max - min + 1,
            posValue = 0,
            sizeValue = 0,
            value = 0;
            
        if (from !== null && to !== null) {
            value = to - from + 1;
            sizeValue = (value / delta) * 100;
            posValue = ((from - min) / delta) * 100;
        }

        // traiter les 2 cas car on peut avoir un switch d'orientation.
        element.style[parent.dataset.posRule] = posValue + '%';
        element.style[parent.dataset.sizeRule] = sizeValue + '%';
        element.style[parent.dataset.oppositePosRule] = '';
        element.style[parent.dataset.oppositeSizeRule] = '';
    };
    
    /**
     *  Créer un élément range.
     *  
     *  @private
     *  @return HTMLSpanElement
     */
    var create = function () {
        var element = document.createElement('span');
        element.className = CLASS_RANGE;
        return element;
    };
    
    /**
     *  Méthode de validation du HTML.
     *  FIXME Faire quelque chose comme test des classes des enfants.
     *  
     *  @private
     *  @param HTMLELement element Elément à tester;
     *  @return Boolean Si la structure HTML est valide.
     */
    var validate = function (element) {
        return true;
    };
    
    /**
     *  Initialisation d'un élément range.
     *  
     *  @private
     *  @param HTMLSpanElement element Elément à initialiser.
     *  @return HTMLSpanElement
     */
    var init = function (element) {
        if (element.classList.contains('nojs') ||
                element.getAttribute('data-rnbjs') === 'true') {
            return;
        }
        if (!validate(element)) {
            return;
        }
        element.classList.add(CLASS_RANGE);
        element.setAttribute('data-rnbjs', 'true');
        ATTRS.forEach(function (attr) {
            Object.defineProperty(element, attr, PROPS[attr]);
        });
        
        var values = element.getElementsByClassName(CLASS_VALUE),
            n = values.length,
            i = 0;
        for (i = 0; i < n; i += 1) {
            initValue(values[i]);
        }
        
        render(element);
        return element;
    };
    
    /**
     *  Module range permettant d'inialiser des éléments ``range`` dans le 
     *  document courant.
     *  
     *  ----
     *      <span class="range" aria-valuemin="0" aria-valuemax="10" aria-orinetation="vertical">
     *          <span class="value">
     *              <span aria-valuemin="0" aria-valuemax="7" aria-valuenow="3">3</span>
     *              <span aria-valuemin="3" aria-valuemax="10" aria-valuenow="7">7</span>
     *          </span>
     *      </span>
     *  ----
     *  Exemple de code HTML d'un élément range
     */
    return {
        /**
         *  Initialiser le document. La méthode est appelée automatiquement par
         *  la librairie rnb.
         */
        initDocument: function () {
            var elements = document.getElementsByClassName(CLASS_RANGE),
                n = elements.length,
                i = 0;
            for (i = 0; i < n; i += 1) {
                init(elements[i]);
            }
        },
        
        /**
         *  Initialiser un élément.
         *  
         *  @param HTMLSpanElement element Elément à initialiser.
         *  @return HTMLSpanElement
         */
        initElement: function (element) {
            return init(element);
        },
        
        /**
         *  Créer un  élément range.
         *  
         *  @param double [min=0] Valeur minimale de l'intervalle.
         *  @param double [max=1] Valeur maximale de l'intervalle.
         *  @param Object[] [values] Valeurs (objets avec propriétés from et to).
         *  @return HTMLSpanElement L'élément range.
         */
        createElement: function (min, max, values) {
            var element = create();
            init(element);
            element.min = min;
            element.max = max;
            if (values) {
                values.forEach(function (value) {
                    element.addValue(value.from, value.to);
                });
            }
            return element;
        }
    };
});
Code javascript.
/**
 *  range et meter
 *  ============================
 *  
 *  Pour une représentation d'un intervalle de valeur ou la représentation d'une valeur.
 *  
 *  ----
 *  °°stx-html°°
 *      <span class="range" aria-valuemin="0" aria-valuemax="10">
 *          <span class="value">
 *              <span class="from" aria-valuemin="0" aria-valuemax="7" aria-valuenow="3">3</span>
 *              <span class="to" aria-valuemin="3" aria-valuemax="10" aria-valuenow="7">7</span>
 *          </span>
 *      </span>
 *  ----
 *  Code HTML pour un .range
 *  
 *  ----
 *  °°stx-html°°
 *      <span class="meter">
 *          <span class="value">3/5</span>
 *          <meter value="3" min="0" max="5" aria-hidden="true">3/5</meter>
 *      </span>
 *  ----
 *  Code HTML pour .meter
 */

.meter,
.range {
    position: relative;
    display: inline-block;
    box-sizing: border-box;
    overflow: hidden;
/*    vertical-align: -0.2em;*/
/*    vertical-align: middle;*/
    border-width: 1px;
    border-style: solid;
    border-radius: 4px;
    padding: 0;
    height: 1rem;
    width: 5rem;
    /* typo */
    line-height: 1;
    /* colors */
    transition: all 0.1s;
    border-color: rgba(0, 0, 0, 0.2);
    background-color: rgba(0, 0, 0, 0.03);
/*    background-image: linear-gradient(#e6e6e6, #e6e6e6, #eee 20%, #ccc 45%, #ccc 55%);*/
    /*box-shadow: inset 2px 1px 5px rgba(0,0,0,0.3);*/
}

.meter > .value,
.range > .value {
    position: absolute;
    /* box */
    display: block;
    height: 100%;
    width: 0;
    overflow: hidden;
    /* typo */
    font-size: 0;
    line-height: 0;
    /* colors */
    transition: all 0.2s;
    background-color: #7a3;
/*    background-image: linear-gradient(#ad7, #ad7, #cea 20%, #7a3 45%, #7a3 55%);*/
}

.meter > .optimum-value {
/*    background: linear-gradient(#ad7, #ad7, #cea 20%, #7a3 45%, #7a3 55%);*/
    background-color: #7a3;
}
.meter > .sub-optimum-value {
/*  background: linear-gradient(#fe7, #fe7, #ffc 20%, #db3 45%, #db3 55%);*/
  background-color: #db3;
}
.meter > .even-less-good-value {
/*  background: linear-gradient(#f77, #f77, #fcc 20%, #d44 45%, #d44 55%);*/
  background-color: #d44;
}

/*

vertical range and vertical meter
---------------------------------
*/

.meter[aria-orientation="vertical"],
.range[aria-orientation="vertical"] {
    height: 5em;
    width: 1em;
    background-color: #7a3;
/*    background-image: linear-gradient(90deg, #e6e6e6, #e6e6e6, #eee 20%, #ccc 45%, #ccc 55%);*/
}

.meter[aria-orientation="vertical"] > .value,
.range[aria-orientation="vertical"] > .value {
    width: 100%;
    height: 0;
    background-color: #7a3;
/*    background-image: linear-gradient(90deg, #ad7, #ad7, #cea 20%, #7a3 45%, #7a3 55%);*/
}
.meter[aria-orientation="vertical"] > .value {
    bottom: 0;
}
/*
.meter[aria-orientation="vertical"] > .optimum-value {
  background: linear-gradient(90deg, #ad7, #ad7, #cea 20%, #7a3 45%, #7a3 55%);
}
.meter[aria-orientation="vertical"] > .sub-optimum-value {
  background: linear-gradient(90deg, #fe7, #fe7, #ffc 20%, #db3 45%, #db3 55%);
}
.meter[aria-orientation="vertical"] > .even-less-good-value {
  background: linear-gradient(90deg, #f77, #f77, #fcc 20%, #d44 45%, #d44 55%);
}
*/
/*

.meter specific : hide meter element
------------------------------------
*/

.meter > meter {
    display: none;
}

/*

.range specific : define from and to position
----------------------------------------------

TODO: styles for mouse interaction with from|to elements.

*/

.range > .value > span {
    position: absolute;
    top: 0;
    bottom: 0;
}
.range[aria-orientation="vertical"] > .value > span {
    left: 0;
    right: 0;
}
.range[aria-orientation="vertical"] > .value > .from {
    top: auto;
}
.range[aria-orientation="vertical"] > .value > .to {
    bottom: auto;
}
Code CSS.

Ressources et références

HICKSON, Ian ; BERJON, Robin ; FAULKNER, Steve, et al.. HTML 5. W3C, . 4.10.15 The meter element

SCHEUHAMMER, Joseph ; COOPER, Michael. WAI-ARIA 1.0 Authoring Practices. W3C, . Slider (Multi-Thumb) (widget)