Wextensible

Experimentar CSS3, pero ¿usando prefijos?

En mi último artículo sobre los vendor prefixes o prefijos propietarios expliqué que estos se usan para aquellas características que se implementan antes de que exista un estándar. Mientras tanto si los navegadores no han unificado su comportamiento es posible que una propiedad CSS no se comporte igual en todos ellos. Lo más práctico es no usar nuevas características que aún no se han consolidado. Pero esta actitud no es adecuada para afrontar los cambios en Internet, donde debemos siempre estar aprendiendo y aplicando nuevas tecnologías. Aunque eso suponga equivocarse y verse obligado a rectificar o retocar cosas que hemos puesto en nuestro sitio. Con las propiedades con prefijo tenemos el problema de que hay que escribir muchas declaraciones CSS para los diferentes navegadores. Con el riesgo de que en un futuro todo ese material resulte, al menos, pesado de mantener. Hay técnicas ya funcionando para gestionar las nuevas propiedades de CSS3, algo que ya intenté hace tiempo y ahora estoy perfeccionando.

Sobre esas técnicas que ya existen para preprocesar o gestionar CSS3 no totalmente soportado no voy a exponer nada aquí, pues quiero construirme mi propio manejador o gestor de CSS3. Si buscas software relacionado con esto puedes mirar en W3C CSS Software, con una larga lista de software relacionado con la gestión de edición de CSS.

Recuerdo hace unos años que intentaba aplicar una efecto de transparencia con IE8 y que a la vez funcionase en otros navegadores. Se trata de la propiedad opacity que ese navegador implementa con filter. En la aplicación de mi formulario emergente necesitaba crear una capa de transparencia para lo cual inicialmente dotaba con este estilo (incluida en el archivo form-emerge.css):

div#IDPREpantalla {
    opacity: 0.6;
    -ms-filter: "alpha(opacity=60)";
    ...
}

Con esto inicialmente me aseguraba los navegadores que permitiesen opacity y también IE8 con -ms-filter. Sin embargo esa disposición pudiera no ser adecuada en el caso de que el navegador sobrescriba las propiedades. IE9 permite opacity al mismo tiempo que -ms-filter, por lo que esta última "debería" sobrescribir la anterior (digo "debería" porque realmente no lo hace, pero es lo esperado). Recuerda que cuando un navegador encuentra varias veces una misma declaración siempre la última sobrescribe las anteriores. Aquí hay un ejemplo en ejecución:

<div style="
    background-color: red;      
    background-color: yellow;
    background-color: blue;
    width: 200px;
    height: 100px;
    color: white;"
>Esto deberá tener un fondo azul</div> 

Ejemplo:

Esto deberá tener un fondo azul
Estos ejemplos (cuadros con borde verde) son ejecutados en línea. El estilo lo declaro en el elemento, aspecto que hago para que pueda consultarlo con más comodidad en el código fuente. Pero no se debe olvidar que siempre es recomendable ponerlo en el elemento <style> de la página, o mejor, en un archivo .css separado.

De los tres background-color sólo el último es representado. La herramienta inspeccionar elemento de Chrome nos expone el estilo, indicando las sobrescrituras:

Sobreescribir estilo CSS

Además el estilo computado (computed style) que es el que realmente se va a usar tiene el último valor:

Sobreescribir estilo CSS

Así que el orden de escritura en mi capa de transparencia debería haber sido como sigue, pues manteniendo la propiedad estándar (sin prefijos) al final aseguramos que se use en última instancia la actual si es soportada.

div#IDPREpantalla {
    -ms-filter: "alpha(opacity=60)";
    opacity: 0.6;
    ...
}

De todas formas posteriormente omití las declaraciones de la anterior capa de transparencia en el archivo form-emerge.css porque en aquellos momentos estaba obsesionado por el cumplimiento de estándares. Incluso validaba los documentos y no me permitía ninguna propiedad no estándar. Así que no podía (o no quería) usar prefijos CSS directamente desde la declaración de la página, por lo que me vi obligado a dotar las propiedades no estándar desde JavaScript. Lo cual dicho sea de paso es incongruente, pues validar el CSS en el documento pero no el que se incorpora desde JavaScript no es realmente sino una validación parcial. Bueno, pero eso es otro asunto. Sea como sea creé una función estiloNoCss(propiedad, valor) con este código:

function estiloNoCss(propiedad, valor) {
    var estilo = "";
    switch (propiedad){
        case "opacity": {//según CSS3 opacity
            if (esNavegador("msie")){//IE
                estilo = "filter: alpha(opacity=" + (valor * 100) + ")";            
            } else {//FF, OP, SA?
                estilo = "opacity: " + valor;
            }
            break;
        }
        case "user-select": {//CSS3 user-select
            if (esNavegador("firefox")){//Sólo FF
                estilo = "-moz-user-select: " + valor;
            }
            break; 
        }

    }    
    return estilo;    
}

En principio el objetivo era ir agregando nuevas propiedades a medida que las necesitara. Esta función aún está en uso en este sitio, pero hay un detalle que no me gusta. Diferenciar distintos comportamientos por el nombre del navegador no es la mejor táctica. Por ejemplo, si el navegador es un "msie" puede ser cualquier versión de Internet Explorer. ¿Qué pasa si IE9 o posteriores soportan ya opacity y, tal vez, dejen de soportar filter. Es obvio que esta no es la mejor forma de proceder. Debemos basarnos en la existencia de la característica independientemente del navegador que se esté usando.

Octubre 2013: Ya he eliminado definitivamente la función estiloNoCss() de este sitio. Ahora con el gestor de vendor prefixes VpForCss que expondré en el siguiente tema puedo determinar si un navegador soporta una determinada propiedad, en cuyo caso puedo obtener su prefijo si lo llevara o bien un valor null si no la soporta.

Valor especificado, computado, usado, real y resuelto

Antes de seguir creo que conviene aclararnos con el concepto de estilo computado. El estilo final de un elemento es un proceso que comprende varias etapas. Es el resultado de combinar hojas de estilo (en archivos externos), el que proviene del elemento <style>, el ubicado en línea en el atributo style y, en su caso, el incorporado mediante JavaScript. En ese proceso se realizan tareas de sobrescrituras, descarte de propiedades no conocidas por el navegador, aplicación del mecanismo de cascada, computación a absolutos de los valores relativos (por ejemplo 10em se transforman en 22px), etc. Para entenderlo mejor hay que ver las definiciones oficiales de los valores especificados, computados, usados y reales según la especificación.

Este último valor real no debe confundirse con el valor actual (current style) de IE8. En todo caso las herramientas de los navegadores nos dan el estilo computado, que al igual que el método getComputedStyle() debe entenderse generalmente como valor usado en lugar de computado. Esto es lo que expone la especificación del Modelo de Objetos CSS (CSSOM) aún en fase de borrador acerca del valor resuelto (resolved value):

El método getComputedStyle() fue históricamente definido para devolver el valor computado de un elemento. Sin embargo el concepto de valor computado ha cambiado en las distintas revisiones de CSS, mientras que la implementación de ese método no ha modificado su nombre para mantener la compatibilidad. Por lo tanto se crea el concepto de valor resuelto que se determina como sigue:

Esa especificación del CSSOM aún está en fase de borrador, con fecha de Marzo de 2012, por lo que puede sufrir variaciones.

Hay un tema que conviene conocer acerca del valor real. En el siguiente ejemplo ponemos varios elementos con un relleno (padding) de 1 píxel, pero forzándolos a un posición absoluta hacemos que su ancho (width) y alto (height) sean cero como valores iniciales. También anulamos el borde (border) y contorno (outline). Al final el grueso del punto quedará determinado por el padding. Si a cada elemento le ponemos un fondo de color resultará que debe formar un pequeño punto de 2×2 píxeles. Para formar un punto de 1×1 píxel entonces tendríamos que poner un relleno de 0.5 píxeles. Se trata de posicionarlos uno junto a otro para que formen una línea recta compuesta con puntos tal que cada uno de ellos es un elemento HTML. ¿Que harán los navegadores con este valor padding:0.5px?, pues en principio los píxeles se deben dar en unidades enteras ¿o no?. En este ejemplo he dispuesto dos líneas horizontales azules de 10 píxeles de ancho, una más gruesa que la otra, aunque en algunos navegadores sólo verá una de las líneas:

Ejemplo:

Punto con HTML Esta imagen es la captura de Firefox 18.0, que al igual que IE8 representa dos líneas azules de puntos HTML. La más gruesa está formada por cinco elementos <i> con padding:1px y posicionados uno junto a otro para formar una línea de 10 píxeles. La segunda línea se forma con diez elementos con un padding:0.5px, lo que también da lugar a 10 píxeles de ancho pues cada elemento tiene un ancho efectivo de 0.5+0.5=1px dotados única y exclusivamente por el padding, pues border, width y outline son cero. Pero Chrome 18.0, Safari 5.1 y Opera 11.62 computa este relleno a 0px por lo que la línea fina no se verá. Este problema se me planteó al implementar el trazador de gráficas matemáticas usando sólo elementos HTML para presentar los puntos, viéndome forzado a usar puntos de 2px en lugar de 1px que resultaría lo más apropiado. Por lo tanto pensemos siempre que el valor computado es el valor final (real) de la presentación del elemento y que a veces no coincide en todos los navegadores.

Analizar el estilo computado de un elemento: El objeto CSSStyleDeclaration

Aparte de usar las herramientas de desarrollo del navegador que permiten inspeccionar un elemento y ver todas las propiedades de estilo computadas, ya hace tiempo que me construí un analizador del DOM para este cometido. Se trata de un módulo JavaScript cuya función analizaDOM() nos devuelve, entre otras cosas, una lista de las propiedades de estilo computado. Hice una prueba de uso en una aplicación de un formulario emergente, pero veamos aquí otro ejemplo en funcionamiento:

Ejemplo:

Para llenar el cuadro con borde verde con las propiedades y estilo de ese elemento simplemente hacemos esto:

<div class="ejemplo-linea" id="diver"
style="height: 10em; overflow: auto">
</div>
<script>
    var diver = document.getElementById("diver");
    diver.innerHTML = analizaDOM(diver);
</script>

La función analizaDOM(elemento) utiliza objetoEstiloActual() y propiedadEstiloActual() ubicadas en el módulo /res/inc/general.js que contiene funciones de uso general para el sitio (este script lo vinculo en todas las páginas del sitio). Se trata de obtener el estilo computado (computed style) del elemento con document.defaultView.getComputedStyle:

function objetoEstiloActual(elemento){
    try {
        if (elemento != null) {
            if (document.defaultView.getComputedStyle) {//FF, OP, CH, SA
                return document.defaultView.getComputedStyle(elemento, null);
            } else {
                return null;
            }
        } else {
            return null;
        }
    } catch(e){
        //IE8 no soporta getComputedStyle y da error,
        //salimos por aquí devolviendo el currentStyle
        try {
            return elemento.currentStyle;
        } catch(e){}
        return null;
    }         
}
Inicialmente nombré estas funciones finalizando con ...EstiloActual, condicionado a que sólo IE8 no aceptaba el estilo computado y extraía algo parecido que llama current style o estilo actual. Pero en todo caso lo que debemos buscar es el estilo que finalmente registra el navegador para aplicar a los elementos.

Usando el objetoEstilo devuelto con la función anterior podemos consultar el valor de una propiedad o, si no existe, nos devolverá nulo, usando esta función:

function propiedadEstiloActual(objetoEstilo, propiedad){
    try {    
        if (objetoEstilo && (propiedad != "")) {
            var prop = propiedad;
            if (propiedad.indexOf("-")>-1) prop = cambiaGuiones(propiedad);
            var valor = objetoEstilo[prop];
            if ((valor==undefined)||(valor==null)){
                valor = objetoEstilo[propiedad];
                if ((valor==undefined)||(valor==null)){
                    return null;
                } else {
                    return valor;
                }
            } else {
                return valor;   
            }
        } else {
            return null;
        }
    } catch(e){
        return null;
    }     
}

El objeto devuelto con document.defaultView.getComputedStyle, o en su caso con elemento.currentStyle para IE8, es una recopilación de todas las propiedades de estilo en un objeto de tipo CSSStyleDeclaration, a cuyos valores podemos acceder con propiedadEstiloActual(objetoEstilo, propiedad) también del módulo general.js. Hay una especificación en fase de borrador con fecha 14 de Marzo de 2012 para el objeto CSSStyleDeclaration que puede ver en W3C The CSSStyleDeclaration Interface. Hay cosas aún sin aclarar a fecha de hoy (Mayo 2012) en ese documento, como el método item(unsigned long index) donde no se especifica lo que tendría que devolver, aunque supuestamente será el índice de las propiedades disponibles. Chrome y Firefox disponen en este objeto de dos grupos, uno son parejas número:nombre_propiedad donde se relacionanan todas las propiedades de estilo disponibles en el elemento. El otro grupo son parejas nombre_propiedad:valor de aquellas que se computan en el momento actual en que el elemento es presentado. Opera 11.62 sólo tiene el segundo grupo, aunque dispone del método item(). Veámos este ejemplo para extraer la propiedad número 35:

Ejemplo:

En Chrome y Safari obtenemos con item(35) la propiedad clip y Firefox nos da cursor. En Opera se obtiene una cadena vacía (¿sin implementar?). Podemos obtener el valor de una propiedad cualquiera como color obteniendo rgb(0,128,0) con objCss.getPropertyValue("color"), aunque para que funcione en IE8 y en los otros es suficiente con objCss["color"].

Por lo tanto si en el CSSStyleDeclaration está todo el estilo posible que un elemento puede tomar, nos puede servir para consultar si el navegador soporta una propiedad con o sin prefijo. En el siguiente tema intentaré plantear un gestor de vendor prefixes para evitar tener que escribir las propiedades con todos los prefijos de los distintos navegadores.