HTML : meter

Introduction à l'utilisation du nouvel élément HTML 5 meter et à la manière de lui assurer un rendu sur les navigateurs qui ne le supportent pas.

Présentation

The meter element represents a scalar measurement within a known range, or a fractional value; for example disk usage, the relevance of a query result, or the fraction of a voting population to have selected a particular candidate.

ref-html5-meter

L'élément meter devrait donc être utilisé quand nous avons une mesure, quelle qu'elle soit, qui peut prendre une valeur dans un intervalle défini. Autrement dit : « une jauge ». Pour évaluer la « nature » de cette mesure, nous disposons de 2 séries de propriétés.

Propriétés pour gérer l'intervalle de valeurs
Propriétés Fonctions
min Valeur minimale qui peut prendre la mesure (0 par défaut).
max Valeur maximale qui peut prendre la mesure (1 par défaut).
value Valeur courante.
Propriétés pour définir les zones basse, médiane et haute de l'intervalle de valeurs
Propriétés Fonctions
low Valeur supérieure de la zone basse de la jauge (min par défaut).
high Valeur inférieure de la zone haute de la jauge (max par défaut).
optimum Valeur optimum de la jauge (médiane entre min et max par défaut).

Toutes ces propriétés sont des nombres flottants qui doivent obéir aux règles suivantes :

  • minvaluemax
  • minlowmax
  • minhighmax
  • minoptimummax
  • lowhigh
Règles arithmétiques liant les propriétés de meter.

Images

Représentation graphique des propriétés de l'élément meter.

En fonction de la position de la valeur optimum, les zones délimitées par les valeurs de low et high seront définies comme « optimale », « sub-optimale » ou « encore moins bonne ».

Images

Rprésentation graphiques des zones d'un l'élément meter.

Notons au passage qu'il aurait été peut-être plus judicieux de définir la valeur par défaut de optimum comme la valeur médiane de la zone délimitée par low et high et non pas celle délimitée par min et max.

Rendu par défaut de l'élément

Prenons pour exemple un élément meter définissant une échelle de valeurs allant de 0 à 100.

<meter min="0" max="100" low="25" high="80" value="40">40%</meter>
Code HTML d'un élément meter.

Pour illustrer les différents états de la jauge, créons une série d'éléments pour lesquels nous ferons varier la valeur et/ou l'optimum :

Value: 40 (low: 25 ; high: 80; optimum: 50)
40%
Value: 20 (low: 25 ; high: 80; optimum: 50)
20%
Value: 90 (low: 25 ; high: 80; optimum: 50)
90%
Value: 90 (low: 25 ; high: 80; optimum: 20)
90%
Rendu d'élément meter.

Si vous affichez cette page avec Chromium ≥ 17, Opera ≥ 11 ou Firefox ≥ 16, vous verrez une jauge graphique en lieu et place du texte des éléments meter ; une jauge verte pour les valeurs dans la zone optimale, jaune pour celles se trouvant dans une zone sub-optimale et rouge si elles sont dans une zone encore moins bonne. Seul le texte est visible avec les autres navigateurs.

Capture d'écran

Affichage des éléments meter ci-dessus avec le navigateur web Opera.

Implémenter un rendu

La première implémentation de rendu en 2012 avait consisté à suppléer les lacunes de certains navigateurs web en injectant du code quand le rendu graphique le nécessitait et en développant un modèle de données permettant de gérer un intervalle de valeurs. L'implémentation actuelle aborder les choses différemment en décorant l'élément meter par un élément span qui hérite de ces propriétés afin (1) d'harmoniser le rendu graphique et (2) de gérer une propriété qui n'existe pas encore : l'orientation.

Le code

<meter value="3" min="0" max="5">3/5</meter>
Exemple de code HTML initial.
<span class="meter">
    <span class="value">3/5</span>
    <meter value="3" min="0" max="5" aria-hidden="true">3/5</meter>
</span>
Exemple de code HTML final.
define(function () {
    'use strict';
    
    /**
     *  Flag indiquant si les éléments meter sont suportés.
     *  @private
     *  @type Boolean
     */
    var SUPPORTED = ('value' in document.createElement('meter'));

    /**
     *  Flag pour l'état optimum.
     *  @private
     *  @type String
     */
    var OPTIMUM = 'optimum-value';

    /**
     *  Flag pour l'état sub-optimum.
     *  @private
     *  @type String
     */
    var SUB_OPTIMUM = 'sub-optimum-value';

    /**
     *  Flag pour l'état even-less-good
     *  @private
     *  @type String
     */
    var EVEN_LESS_GOOD = 'even-less-good-value';

    /**
     *  Regex pour identifier la classe définissant l'état optimum
     *  @private
     *  @type RegExp
     */
    var OPT_REGEX = new RegExp('(' + OPTIMUM + '|' + SUB_OPTIMUM +
            '|' + EVEN_LESS_GOOD + ')');
    
    /**
     *  Liste des attributs spécifiques à meter.
     *  @private
     *  @type Array
     */
    var BASE_ATTRS = ['min', 'max', 'low', 'high', 'optimum', 'value'];
    
    /**
     *  Liste des attributs gérés par le wrapper.
     *  @private
     *  @type Array
     */
    var ATTRS = BASE_ATTRS.concat(['orientation']);
    
    /**
     *  Liste de propriétés à déclarer sur le décorateur et mappées sur celles
     *  de l'élément meter géré.
     *  @private
     *  @type Object
     */
    var PROPERTIES = {
        /**
         *  Descripteur de la propriété 'orientation'.
         *  @private
         *  @type Object
         */
        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é 'value' mappée sur celle du meter.
         *  @private
         *  @type Object
         */
        value: {
            enumerable: true,
            configurable: false,
            get: function () {
                return this.lastElementChild.value;
            },
            set: function (newValue) {
                this.lastElementChild.value = newValue;
                render(this);
            }
        },
        /**
         *  Descripteur de la propriété 'min' mappée sur celle du meter.
         *  @private
         *  @type Object
         */
        min: {
            enumerable: true,
            configurable: false,
            get: function () {
                return this.lastElementChild.min;
            },
            set: function (newValue) {
                if (this.lastElementChild.min !== newValue) {
                    this.lastElementChild.min = newValue;
                    render(this);
                }
            }
        },
        /**
         *  Descripteur de la propriété 'max' mappée sur celle du meter.
         *  @private
         *  @type Object
         */
        max: {
            enumerable: true,
            configurable: false,
            get: function () {
                return this.lastElementChild.max;
            },
            set: function (newValue) {
                if (this.lastElementChild.max !== newValue) {
                    this.lastElementChild.max = newValue;
                    render(this);
                }
            }
        },
        /**
         *  Descripteur de la propriété 'low' mappée sur celle du meter.
         *  @private
         *  @type Object
         */
        low: {
            enumerable: true,
            configurable: false,
            get: function () {
                return this.lastElementChild.low;
            },
            set: function (newValue) {
                if (this.lastElementChild.low !== newValue) {
                    this.lastElementChild.low = newValue;
                    render(this);
                }
            }
        },
        /**
         *  Descripteur de la propriété 'high' mappée sur celle du meter.
         *  @private
         *  @type Object
         */
        high: {
            enumerable: true,
            configurable: false,
            get: function () {
                return this.lastElementChild.high;
            },
            set: function (newValue) {
                if (this.lastElementChild.high !== newValue) {
                    this.lastElementChild.high = newValue;
                    render(this);
                }
            }
        },
        /**
         *  Descripteur de la propriété 'optimum' mappée sur celle du meter.
         *  @private
         *  @type Object
         */
        optimum: {
            enumerable: true,
            configurable: false,
            get: function () {
                return this.lastElementChild.optimum;
            },
            set: function (newValue) {
                if (this.lastElementChild.optimum !== newValue) {
                    this.lastElementChild.optimum = newValue;
                    render(this);
                }
            }
        }
    };
    
    /**
     *  Définition des propriétés d'un élément meter pour les navigateurs qui 
     *  ne le supporte pas.
     *  @private
     */
    var BASE_PROPERTIES = {
        /**
         *  Descripteur de la propriété 'value'.
         *  @private
         *  @type Object
         */
        value: {
            enumerable: true,
            configurable: false,
            get: function () {
                return getFloatValue(this, 'value');
            },
            set: function (newValue) {
                if (!isNaN(newValue) || newValue === this.value) {
                    return;
                }
                var min = this.min,
                    max = this.max;
                if (newValue < min) {
                    newValue = min;
                } else if (newValue > max) {
                    newValue = max;
                }
                this.setAttribute('value', newValue);
            }
        },
        /**
         *  Descripteur de la propriété 'min'.
         *  @private
         *  @type Object
         */
        min: {
            enumerable: true,
            configurable: false,
            get: function () {
                return getFloatValue(this, 'min', 0);
            },
            set: function (newValue) {
                if (!Number.isNaN(newValue) || newValue === this.min) {
                    return;
                }
                var max = this.max;
                if (newValue > max) {
                    newValue = max;
                }
                this.setAttribute('value', newValue);
                recalculateBoundaries(this);
            }
        },
        /**
         *  Descripteur de la propriété 'max'. (min ≤ low ≤ high ≤ max)
         *  @private
         *  @type Object
         */
        max: {
            enumerable: true,
            configurable: false,
            get: function () {
                return getFloatValue(this, 'max', 1);
            },
            set: function (newValue) {
                if (!Number.isNaN(newValue) || newValue === this.max) {
                    return;
                }
                var min = this.min;
                if (newValue < min) {
                    newValue = min;
                }
                this.setAttribute('value', newValue);
                recalculateBoundaries(this);
            }
        },
        /**
         *  Descripteur de la propriété 'low'. (min ≤ low ≤ high ≤ max)
         *  @private
         *  @type Object
         */
        low: {
            enumerable: true,
            configurable: false,
            get: function () {
                return getFloatValue(this, 'low', this.min);
            },
            set: function (newValue) {
                if (!Number.isNaN(newValue) || newValue === this.low) {
                    return;
                }
                var max = this.max,
                    min = this.min,
                    high = this.high;
                if (newValue < min) {
                    newValue = min;
                } else if (high !== null) {
                    if (newValue > high) {
                        newValue = high;
                    }
                } else if (newValue > max) {
                    newValue = max;
                }
                this.setAttribute('low', newValue);
            }
        },
        /**
         *  Descripteur de la propriété 'high'. (min ≤ low ≤ high ≤ max)
         *  @private
         *  @type Object
         */
        high: {
            enumerable: true,
            configurable: false,
            get: function () {
                return getFloatValue(this, 'high', this.max);
            },
            set: function (newValue) {
                if (!Number.isNaN(newValue) || newValue === this.high) {
                    return;
                }
                var max = this.max,
                    min = this.min,
                    low = this.low;
                if (newValue > max) {
                    newValue = max;
                } else if (low !== null) {
                    if (newValue < low) {
                        newValue = low;
                    }
                } else if (newValue < min) {
                    newValue = min;
                }
                this.setAttribute('high', newValue);
            }
        },
        /**
         *  Descripteur de la propriété 'optimum'. (min ≤ optimum ≤ max)
         *  @private
         *  @type Object
         */
        optimum: {
            enumerable: true,
            configurable: false,
            get: function () {
                var value = getFloatValue(this, 'optimum');
                if (value === null) {
                    value = (this.max - this.min) / 2;
                }
                return value;
            },
            set: function (newValue) {
                if (!Number.isNaN(newValue) || newValue === this.optimum) {
                    return;
                }
                var min = this.min,
                    max = this.max;
                if (newValue < min) {
                    newValue = min;
                } else if (newValue > max) {
                    newValue = max;
                }
                this.setAttribute('optimum', newValue);
            }
        }
    };
    
    /**
     *  Récupérer la valeur d'un attribut quand meter n'est pas géré nativement.
     *  
     *  @private
     *  @param HTMLMeterElement element Elément à analyser.
     *  @param String attribute Attribut à lire (value, min, max, low, high, optimum)
     *  @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;
    };
    
    /**
     *  Redéfinir les bornes internes lorsque min ou max ont été modifiés et 
     *  que meter n'est pas géré nativement.
     *  
     *  * min ≤ low ≤ high ≤ max
     *  * min ≤ optimum ≤ max
     *  
     *  @private
     *  @param HTMLMeterElement element Elément à analyser.
     */
    var recalculateBoundaries = function (element) {
        var min = element.min,
            max = element.max,
            optimum = element.optimum,
            low = element.low,
            high = element.high;
        // optimum value
        if (optimum !== null) {
            if (optimum < min) {
                element.optimum = min;
            } else if (optimum > max) {
                element.optimum = max;
            }
        }
        // low value
        if (low < min) {
            element.low = min;
        } else if (low > high) {
            element.low = high;
        } else if (low > max) {
            element.low = max;
        }
        // high value
        if (high > max) {
            element.high = max;
        } else if (high < low) {
            element.high = low;
        } else if (high < min) {
            element.high = min;
        }
    };
    
    /**
     *  Création du décorateur.
     *  @private
     *  @param HTMLMeterElement meter Elément meter à encapsuler.
     *  @return HTMLSpanElement L'élément décorateur.
     */
    var createDecorator = function (meter) {
        var element = document.createElement('span');
        // copy meter data
        element.className = meter.className;
        meter.className = '';
        element.id = meter.id;
        meter.id = '';
        element.title = meter.title;
        // add data
        element.classList.add('meter');
        element.appendChild(document.createElement('span'));
        element.firstChild.className = 'value';
        element.setAttribute('aria-orientation', 
                meter.getAttribute('aria-orientation') || meter.orientation);
        ATTRS.forEach(function (attr) {
            Object.defineProperty(element, attr, PROPERTIES[attr]);
        });
        if (!SUPPORTED) {
            BASE_ATTRS.forEach(function (attr) {
                Object.defineProperty(meter, attr, BASE_PROPERTIES[attr]);
            });
        }
        return element;
    };
    
    /**
     *  Rendu de l'état de l'élément.
     *  @private
     *  @param HTMLSpanElement element Elément à dessiner.
     */
    var render = function (element) {
        var value = element.value,
            cls = OPTIMUM,
            optimum = element.optimum,
            low = element.low,
            high = element.high,
            target = element.firstElementChild,
            current = target.className.match(OPT_REGEX),
            sizeValue = (value / (element.max - element.min)) * 100,
            sizeRule = 'width',
            oppositeRule = 'height';
        
        if (element.orientation === 'vertical') {
            sizeRule = 'height';
            oppositeRule = 'width';
        }
        // Taille de l'élément valeur.
        target.style[sizeRule] = sizeValue + '%';
        target.style[oppositeRule] = '';
        // gestion du look en fonction de la position de la valeur
        if (optimum >= low && optimum <= high) {
            if (value < low || value > high) {
                cls = SUB_OPTIMUM;
            }
        } else if (optimum < low) {
            if (value > high) {
                cls = EVEN_LESS_GOOD;
            } else if (value > low) {
                cls = SUB_OPTIMUM;
            }
        } else {
            if (value < low) {
                cls = EVEN_LESS_GOOD;
            } else if (value < high) {
                cls = SUB_OPTIMUM;
            }
        }
        if (!current) {
            target.classList.add(cls);
        } else if (current[1] !== cls) {
            target.classList.remove(current[1]);
            target.classList.add(cls);
        }
    };
    
    /**
     *  Initialisation d'un élément meter.
     *  @private
     *  @param HTMLMeterElement element Element à gérer.
     *  @return HTMLSpanElement L'élément wrapper.
     */
    var init = function (element) {
        if (element.nodeName.toLowerCase() !== 'meter' 
                || element.classList.contains('nojs') 
                || element.getAttribute('data-rnbjs') === 'true') {
            return;
        }
        element.setAttribute('data-rnbjs', 'true');
        element.setAttribute('aria-hidden', 'true');
        var decorator = createDecorator(element);
        if (element.parentNode) {
            element.parentNode.insertBefore(decorator, element);
        }
        decorator.appendChild(element);
        render(decorator);
        
        return decorator;
    };
    
    /**
     *  Module permettant de gérer les élément ``meter``. La gestion consiste à
     *  « remplacer » l'élément par élément ``span`` qui possédera la même API 
     *  (value, min, max, low, high, optimum).
     *  
     *  ----
     *  °°stx-css°°
     *      <meter value="3" min="0" max="5">3/5</meter>
     *  ----
     *  Code HTML initial.
     *  
     *  ----
     *  °°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 final.
     *  
     *  @module rnb2/ui/meters
     */
    return {
        /**
         *  Initialiser tous les éléments meter du document. Si le module est 
         *  utiliser au sein de la librairie rnb, cette méthode est appelée
         *  automatiquement au chargement de la page.
         */
        initDocument: function () {
            var meters = document.getElementsByTagName('meter'),
                n = meters.length,
                i = 0;
            for (i = 0; i < n; i += 1) {
                init(meters[i]);
            }
        },
        
        /**
         *  Initialisation d'un élément ``meter``.
         *  
         *  @param HTMLMeterElement element Elément à gérer.
         *  @return HTMLSpanElement L'élément gérant le meter.
         */
        initElement: function (element) {
            return init(element);
        },
        
        /**
         *  Créer un élément de surcharge d'un élément ``meter``.
         *  
         *  @param number value La valeur
         *  @param number [min=0] Valeur minimale
         *  @param number [max=1] Valeur maximale
         *  @param String [orientation] Orientation (horizontal|vertical)
         *  @return HTMLSpanElement L'élément gérant le meter.
         */
        createElement: function (value, min, max, orientation) {
            var meter = document.createElement('meter');
            meter.orientation = orientation || 'horizontal';
            meter.min = min || 0;
            meter.max = max || 1;
            if (value) {
                meter.value = value;
            }
            return init(meter);
        }
    };
});
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.
Value: 40 (low: 25 ; high: 80; optimum: 50)
40%
Value: 20 (low: 25 ; high: 80; optimum: 50)
20%
Value: 90 (low: 25 ; high: 80; optimum: 50)
90%
Value: 90 (low: 25 ; high: 80; optimum: 20)
90%
Implémentation du rendu de l'élément meter grâce au css et au javascript.
  • 40%
  • 20%
  • 90%
  • 90%

40% 20% 90% 90%

Rendu d'éléments meter verticaux.

Remarques

  • Toute l'API liée à la notion de jauge est déportée vers l'élément englobant (décorateur).
  • La notion d'orientation est portée par l'attribut aria « aria-orinetation ». La propriété « orient » n'est pour l'instant pas standardisée et uniquement reconnue par Firefox.

Autre chose qu'une jauge : notation

L'élément meter est rendu par défaut sous forme de jauge mais il peut très bien convenir à un système de notation sous forme d'étoiles par exemple grâce à des images de fond.

<meter class="rating" min="0" max="5" value="3">3/5</meter>
Code HTML d'un élément meter utilisé pour une notation (3/5).

3/5

Rendu de l'élement meter utilisé pour une notation.

Capture d'écran

Affichage d'un élément meter utilisé pour la notation sous Firefox.
Contenu indisponible.
Code css pour un système de notation.

Ressources et références

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

meter. Mozilla Developer Network,

Styling Form Controls Using Pseudo Classes. trac.webkit.org,

NIBAU, Rui. HTML : progress. Omacronides,

Historique

2015-07-07
  • upd Refonte complète avec abandon des vieux hacks.
  • add gestion de la propriété orientation.
2012-10-12
  • upd Hack générique pour Opera et Firefox 16.
2012-03-25
  • add Création de l'article.