Cómo obtener todas las propiedades personalizadas en una página en JavaScript »Wiki Ùtil Programar Plus

Podemos usar JavaScript para obtener el valor de una propiedad personalizada de CSS. Robin escribió una explicación detallada sobre esto en Obtener un valor de propiedad personalizado de CSS con JavaScript. Para revisar, digamos que hemos declarado una sola propiedad personalizada en el elemento HTML:

html {
  --color-accent: #00eb9b;
}

En JavaScript, podemos acceder al valor con getComputedStyle y getPropertyValue:

const colorAccent = getComputedStyle(document.documentElement)
  .getPropertyValue('--color-accent'); // #00eb9b

Perfecto. Ahora tenemos acceso a nuestro color de acento en JavaScript. ¿Sabes qué es genial? Si cambiamos ese color en CSS, ¡también se actualiza en JavaScript! Práctico.

Sin embargo, ¿qué sucede cuando no es solo una propiedad a la que necesitamos acceder en JavaScript, sino un montón de ellas?

html {
  --color-accent: #00eb9b;
  --color-accent-secondary: #9db4ff;
  --color-accent-tertiary: #f2c0ea;
  --color-text: #292929;
  --color-divider: #d7d7d7;
}

Terminamos con JavaScript que se ve así:

const colorAccent = getComputedStyle(document.documentElement).getPropertyValue('--color-accent'); // #00eb9b
const colorAccentSecondary = getComputedStyle(document.documentElement).getPropertyValue('--color-accent-secondary'); // #9db4ff
const colorAccentTertiary = getComputedStyle(document.documentElement).getPropertyValue('--color-accent-tertiary'); // #f2c0ea
const colorText = getComputedStyle(document.documentElement).getPropertyValue('--color-text'); // #292929
const colorDivider = getComputedStyle(document.documentElement).getPropertyValue('--color-text'); // #d7d7d7

Nos repetimos mucho. Podríamos acortar cada una de estas líneas abstrayendo las tareas comunes a una función.

const getCSSProp = (element, propName) => getComputedStyle(element).getPropertyValue(propName);
const colorAccent = getCSSProp(document.documentElement, '--color-accent'); // #00eb9b
// repeat for each custom property...

Eso ayuda a reducir la repetición de código, pero todavía tenemos una situación menos que ideal. Cada vez que agregamos una propiedad personalizada en CSS, tenemos que escribir otra línea de JavaScript para acceder a ella. Esto puede funcionar y funciona bien si solo tenemos algunas propiedades personalizadas. He usado esta configuración en proyectos de producción antes. Pero también es posible automatizar esto.

Analicemos el proceso de automatización haciendo algo que funcione.

Que estamos haciendo

Crearemos una paleta de colores, que es una característica común en las bibliotecas de patrones. Generaremos una cuadrícula de muestras de color a partir de nuestras propiedades personalizadas de CSS.

Aquí está la demostración completa que crearemos paso a paso.

Una vista previa de nuestra paleta de colores personalizada basada en propiedades de CSS.  Se muestran seis tarjetas, una para cada color, incluido el nombre de la propiedad personalizada y el valor hexadecimal en cada tarjeta.Esto es lo que pretendemos.

Preparemos el escenario. Usaremos una lista desordenada para mostrar nuestra paleta. Cada muestra es una <li> elemento que renderizaremos con JavaScript.

<ul class="colors"></ul>

El CSS para el diseño de la cuadrícula no es pertinente para la técnica en esta publicación, por lo que no lo veremos en detalle. Está disponible en la demostración de CodePen.

Ahora que tenemos nuestro HTML y CSS en su lugar, nos centraremos en JavaScript. Aquí hay un resumen de lo que haremos con nuestro código:

  1. Obtenga todas las hojas de estilo en una página, tanto externas como internas
  2. Descarte cualquier hoja de estilo alojada en dominios de terceros
  3. Obtenga todas las reglas para las hojas de estilo restantes
  4. Descarta las reglas que no sean reglas de estilo básicas.
  5. Obtenga el nombre y el valor de todas las propiedades CSS
  6. Descartar propiedades CSS no personalizadas
  7. Construya HTML para mostrar las muestras de color

Hagámoslo.

Paso 1: obtenga todas las hojas de estilo en una página

Lo primero que debemos hacer es obtener todas las hojas de estilo internas y externas en la página actual. Las hojas de estilo están disponibles como miembros del documento global.

document.styleSheets

Eso devuelve un objeto similar a una matriz. Queremos usar métodos de matriz, así que lo convertiremos en una matriz. Pongamos esto también en una función que usaremos a lo largo de esta publicación.

const getCSSCustomPropIndex = () => [...document.styleSheets];

Demostración de CodePen

Cuando invocamos getCSSCustomPropIndex, vemos una matriz de CSSStyleSheet objetos, uno para cada hoja de estilo externa e interna en la página actual.

La salida de getCSSCustomPropIndex, una matriz de objetos CSSStyleSheet

Paso 2: descarte las hojas de estilo de terceros

Si nuestro script se ejecuta en https://example.com, cualquier hoja de estilo que queramos inspeccionar también debe estar en https://example.com. Esta es una característica de seguridad. De los documentos de MDN para CSSStyleSheet:

En algunos navegadores, si se carga una hoja de estilo desde un dominio diferente, acceder cssRules resultados en SecurityError.

Eso significa que si la página actual se vincula a una hoja de estilo alojada en https://some-cdn.com, no podemos obtener propiedades personalizadas, ni ningún estilo, de ella. El enfoque que estamos adoptando aquí solo funciona para hojas de estilo alojadas en el dominio actual.

CSSStyleSheet los objetos tienen un href propiedad. Su valor es la URL completa de la hoja de estilo, como https://example.com/styles.css. Las hojas de estilo internas tienen un href propiedad, pero el valor será null.

Escribamos una función que descarte hojas de estilo de terceros. Lo haremos comparando las hojas de estilo. href valor para el current location.origin.

const isSameDomain = (styleSheet) => {
  if (!styleSheet.href) {
    return true;
  }


  return styleSheet.href.indexOf(window.location.origin) === 0;
};

Ahora usamos isSameDomain como un filtro endocument.styleSheets.

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain);

Demostración de CodePen

Con las hojas de estilo de terceros descartadas, podemos inspeccionar el contenido de las restantes.

Paso 3: obtenga todas las reglas para las hojas de estilo restantes

Nuestro objetivo para getCSSCustomPropIndex es producir una matriz de matrices. Para llegar allí, usaremos una combinación de métodos de matriz para recorrer, encontrar los valores que queremos y combinarlos. Demos un primer paso en esa dirección produciendo una matriz que contenga todas las reglas de estilo.

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(...sheet.cssRules), []);

Demostración de CodePen

Usamos reduce y concat porque queremos producir una matriz plana en la que cada elemento de primer nivel sea lo que nos interese. En este fragmento, iteramos sobre CSSStyleSheet objetos. Para cada uno de ellos, necesitamos su cssRules. De los documentos de MDN:

El de solo lectura CSSStyleSheet propiedad cssRules devuelve una CSSRuleList que proporciona una lista actualizada en tiempo real de todas las reglas CSS que componen la hoja de estilo. Cada elemento de la lista es un CSSRule definiendo una sola regla.

Cada regla CSS es el selector, las llaves y las declaraciones de propiedad. Usamos el operador de propagación ...sheet.cssRules para eliminar todas las reglas del cssRules objeto y colóquelo en finalArr. Cuando registramos la salida de getCSSCustomPropIndex, obtenemos una matriz de un solo nivel de CSSRule objetos.

Salida de ejemplo de getCSSCustomPropIndex que produce una matriz de objetos CSSRule

Esto nos da todas las reglas CSS para todas las hojas de estilo. Queremos descartar algunos de esos, así que sigamos adelante.

Paso 4: descarte las reglas que no sean reglas de estilo básicas

Las reglas CSS vienen en diferentes tipos. Las especificaciones de CSS definen cada uno de los tipos con un nombre constante y un número entero. El tipo de regla más común es la CSSStyleRule. Otro tipo de regla es la CSSMediaRule. Los usamos para definir consultas de medios, como @media (min-width: 400px) {}. Otros tipos incluyen CSSSupportsRule, CSSFontFaceRule, y CSSKeyframesRule. Consulte la sección de constantes de tipo de los documentos de MDN para CSSRule para la lista completa.

Solo nos interesan las reglas en las que definimos propiedades personalizadas y, para los fines de esta publicación, nos centraremos en CSSStyleRule. Eso deja fuera el CSSMediaRule tipo de regla donde sea válido para definir propiedades personalizadas. Podríamos usar un enfoque similar al que estamos usando para extraer propiedades personalizadas en esta demostración, pero excluiremos este tipo de regla específica para limitar el alcance de la demostración.

Para limitar nuestro enfoque a las reglas de estilo, escribiremos otro filtro de matriz:

const isStyleRule = (rule) => rule.type === 1;

Cada CSSRule tiene un type propiedad que devuelve el número entero para ese tipo constante. Usamos isStyleRule Filtrar sheet.cssRules.

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(
    [...sheet.cssRules].filter(isStyleRule)
  ), []);

Demostración de CodePen

Una cosa a tener en cuenta es que estamos envolviendo ...sheet.cssRules entre paréntesis para que podamos usar el filtro del método de matriz.

Nuestra hoja de estilo solo tenía CSSStyleRules por lo que los resultados de la demostración son los mismos que antes. Si nuestra hoja de estilo tiene consultas de medios o font-face declaraciones, isStyleRule los descartaría.

Paso 5: obtenga el nombre y el valor de todas las propiedades

Ahora que tenemos las reglas que queremos, podemos obtener las propiedades que las componen. CSSStyleRule Los objetos tienen una propiedad de estilo que es CSSStyleDeclaration objeto. Se compone de propiedades CSS estándar, como color, font-family, y border-radius, además de propiedades personalizadas. Agreguemos eso a nuestro getCSSCustomPropIndex función para que observe todas las reglas, construyendo una matriz de matrices a lo largo del camino:

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(
    [...sheet.cssRules]
      .filter(isStyleRule)
      .reduce((propValArr, rule) => {
        const props = []; /* TODO: more work needed here */
        return [...propValArr, ...props];
      }, [])
  ), []);

Si invocamos esto ahora, obtenemos una matriz vacía. Tenemos más trabajo por hacer, pero esto sienta las bases. Como queremos terminar con una matriz, comenzamos con una matriz vacía usando el acumulador, que es el segundo parámetro de reduce. En el cuerpo del reduce función de devolución de llamada, tenemos una variable de marcador de posición, props, donde recogeremos las propiedades. El return declaración combina la matriz de la iteración anterior – el acumulador – con el actual props formación.

En este momento, ambos son matrices vacías. Necesitamos usar rule.style para completar los accesorios con una matriz para cada propiedad / valor en la regla actual:

const getCSSCustomPropIndex = () => [...document.styleSheets]
  .filter(isSameDomain)
  .reduce((finalArr, sheet) => finalArr.concat(
    [...sheet.cssRules]
      .filter(isStyleRule)
      .reduce((propValArr, rule) => {
        const props = [...rule.style].map((propName) => [
          propName.trim(),
          rule.style.getPropertyValue(propName).trim()
        ]);
        return [...propValArr, ...props];
      }, [])
  ), []);

Demostración de CodePen

rule.style es similar a una matriz, por lo que usamos el operador de extensión nuevamente para poner cada miembro en una matriz que recorremos con map. En el map devolución de llamada, devolvemos una matriz con dos miembros. El primer miembro es propName (que incluye color, font-family, --color-accent, etc.). El segundo miembro es el valor de cada propiedad. Para conseguir eso, usamos el getPropertyValue método de CSSStyleDeclaration. Toma un solo parámetro, el nombre de cadena de la propiedad CSS.

Usamos trim tanto en el nombre como en el valor para asegurarnos de que no incluimos ningún espacio en blanco inicial o final que a veces se quede atrás.

Ahora cuando invocamos getCSSCustomPropIndex, obtenemos una matriz de matrices. Cada matriz secundaria contiene un nombre de propiedad CSS y un valor.

Salida de getCSSCustomPropIndex que muestra una matriz de matrices que contiene cada nombre y valor de propiedad

¡Esto es lo que estamos buscando! Bueno, casi. Estamos obteniendo todas las propiedades además de las propiedades personalizadas. Necesitamos un filtro más para eliminar esas propiedades estándar porque todo lo que queremos son las propiedades personalizadas.

Paso 6: descarte las propiedades no personalizadas

Para determinar si una propiedad es personalizada, podemos mirar el nombre. Sabemos que las propiedades personalizadas deben comenzar con dos guiones (--). Eso es único en el mundo de CSS, por lo que podemos usarlo para escribir una función de filtro:

([propName]) => propName.indexOf("--") === 0)

Luego lo usamos como filtro en el props formación:

const getCSSCustomPropIndex = () =>
  [...document.styleSheets].filter(isSameDomain).reduce(
    (finalArr, sheet) =>
      finalArr.concat(
        [...sheet.cssRules].filter(isStyleRule).reduce((propValArr, rule) => {
          const props = [...rule.style]
            .map((propName) => [
              propName.trim(),
              rule.style.getPropertyValue(propName).trim()
            ])
            .filter(([propName]) => propName.indexOf("--") === 0);


          return [...propValArr, ...props];
        }, [])
      ),
    []
  );

Demostración de CodePen

En la firma de la función, tenemos ([propName]). Allí, estamos usando la desestructuración de matrices para acceder al primer miembro de cada matriz secundaria en props. A partir de ahí, hacemos un indexOf Verifique el nombre de la propiedad. Si -- no está al principio del nombre del accesorio, entonces no lo incluimos en el props formación.

Cuando registramos el resultado, tenemos el resultado exacto que estamos buscando: una matriz de matrices para cada propiedad personalizada y su valor sin otras propiedades.

La salida de getCSSCustomPropIndex que muestra una matriz de matrices que contiene cada propiedad personalizada y su valor

Mirando más hacia el futuro, la creación del mapa de propiedad / valor no tiene por qué requerir tanto código. Hay una alternativa en el borrador de nivel 1 del modelo de objetos con tipo CSS que usa CSSStyleRule.styleMap. El styleMap property es un objeto en forma de matriz de cada propiedad / valor de una regla CSS. Todavía no lo tenemos, pero si lo tuviéramos, podríamos acortar nuestro código anterior eliminando el map:

// ...
const props = [...rule.styleMap.entries()].filter(/*same filter*/);
// ...

Demostración de CodePen

En el momento de escribir este artículo, Chrome y Edge tienen implementaciones de styleMap pero ningún otro navegador importante lo hace. Porque styleMap está en un borrador, no hay garantía de que realmente lo obtengamos, y no tiene sentido usarlo para esta demostración. Aún así, ¡es divertido saber que es una posibilidad futura!

Tenemos la estructura de datos que queremos. Ahora usemos los datos para mostrar muestras de color.

Paso 7: compile HTML para mostrar las muestras de color

Conseguir que los datos tuvieran la forma exacta que necesitábamos fue un trabajo duro. Necesitamos un poco más de JavaScript para renderizar nuestras hermosas muestras de color. En lugar de registrar la salida de getCSSCustomPropIndex, vamos a almacenarlo en variable.

const cssCustomPropIndex = getCSSCustomPropIndex();

Aquí está el HTML que usamos para crear nuestra muestra de color al comienzo de esta publicación:

<ul class="colors"></ul>

Usaremos innerHTML para completar esa lista con un elemento de lista para cada color:

document.querySelector(".colors").innerHTML = cssCustomPropIndex.reduce(
  (str, [prop, val]) => `${str}<li class="color">
    <b class="color__swatch" style="--color: ${val}"></b>
    <div class="color__details">
      <input value="${prop}" readonly />
      <input value="${val}" readonly />
    </div>
   </li>`,
  "");

Demostración de CodePen

Usamos reduce para iterar sobre el índice de prop personalizado y construir una única cadena de aspecto HTML para innerHTML. Pero reduce no es la única forma de hacer esto. Nos vendría bien un map y join o forEach. Cualquier método de construcción de la cadena funcionará aquí. Esta es solo mi forma preferida de hacerlo.

Quiero resaltar un par de bits específicos de código. En el reduce firma de devolución de llamada, estamos usando la desestructuración de matrices nuevamente con [prop, val], esta vez para acceder a ambos miembros de cada matriz secundaria. Luego usamos el prop y val variables en el cuerpo de la función.

Para mostrar el ejemplo de cada color, usamos un b elemento con un estilo en línea:

<b class="color__swatch" style="--color: ${val}"></b>

Eso significa que terminamos con HTML que se ve así:

<b class="color__swatch" style="--color: #00eb9b"></b>

Pero, ¿cómo establece eso un color de fondo? En el CSS completo usamos la propiedad personalizada --color como el valor de background-color para cada .color__swatch. Dado que las reglas CSS externas heredan de los estilos en línea, --color es el valor que establecemos en el b elemento.

.color__swatch {
  background-color: var(--color);
  /* other properties */
}

¡Ahora tenemos una visualización HTML de muestras de color que representan nuestras propiedades personalizadas de CSS!

Esta demostración se centra en los colores, pero la técnica no se limita a los accesorios de color personalizados. No hay ninguna razón por la que no podamos expandir este enfoque para generar otras secciones de una biblioteca de patrones, como fuentes, espaciado, configuraciones de cuadrícula, etc. Todo lo que pueda almacenarse como una propiedad personalizada se puede mostrar en una página automáticamente usando esta técnica.