Modifier les régles CSS de sa page en JavaScript

Je viens de trouver une petite astuce intéressante afin de manipuler sa page.

Mon besoin est le suivant: j'ai une application Web dont on voudrait customiser l'affichage, en l’occurrence les couleurs, les fonts, certaines icônes, etc... Le tout injecter en tant que query parameters.

Le but est de pouvoir facilement paramétrer cela.

Le plus facile en tant que développeur serait de demander un fichier CSS et de l'injecter dans sa page. Mais cela implique au développeur utilisateur de connaître la structuration du CSS de mon application. Et de la mettre à jour en fonction de son évolution.

Une second solution serait d'injecter un fichier de variables Less avec les valeurs, et que l'application la charge et compile le Less "on the fly". Mais c'est très très lent.

Une autre serait d'écouter les "MutationEvent" et de modifier les éléments. Mais non seulement c'est très, mais c'est un traitement quasi en continue: à chaque fois que quelque chose se modifie dans la page, je devrais modifier les valeurs CSS.

Après une petit réflexion, vue que désormais les moteurs Web HTML5 permettant au JavaScript de plus en plus de servir du moteur CSS pour se faciliter la vie (comme pour la méthode querySelectorAll ou encore matchMedia), est-ce qu'il n'existe pas un moyen de récupérer les règles CSS de son application et de les modifier. Et la réponse est oui !

Il existe sur l'objet "document" une propriété "styleSheets" qui contient la liste des styles déclarées via la balise "link" ou la balise "style". Cette liste de "CSSSyleSheet" va contenir des propriétés "cssRules" qui vont contenir nos règles.

Attention toutefois: seules les styles déclarés via "style" et les balises "link" sur le même nom de domaine seront modifiables. Les autres auront une propriété "cssRules" à "null" (donc les styles chargés via CDN ne seront pas modifiables, sauf s'ils sont sur le même domaine).

Observons d'un peu plus prêt. Imaginons que nous avons:

<style title="My style tag">
    .my-css-class {
        color: red;
    }

    .my-css-class2 {
        background-color: red;
        color: white;
        height: 100px;
        width: 300px;
    }

    @media (max-width: 1024px) {
        .my-css-class2 {
            background-color: white;
            border: 1px solid red;
            color: red;
        }
    }
</style>


Cela va me donner sur la propriété "styleSheets":


Comme on peut le voir, sur la propriété "cssRules", j'ai un tableau de régles de styles:



Là nous avons deux types d’éléments: "CSSStyleRule" et "CSSMediaRule". Le premier correspond concrètement à une règle CSS. Le second correspond à un groupe de règles CSS regroupé par rapport à un média (souvent utilisé pour les media-queries et les aspects responsives).

Et si nous ouvrons notre "CSSStyleRule", nous voyons bien la règle CSS que nous écrite plus haut:





Du coup, rien ne nous empêche d'écrire un petit script pour modifier une fois que la page est chargée, où sur une action notre style de notre page.


Voyons un petit exemple. Soit notre page HTML suivante:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Override styling rules</title>


<style title="My style tag">
    .my-css-class {
        color: red;
    }

    .my-css-class2 {
        background-color: red;
        color: white;
        height: 100px;
        width: 300px;
    }

    @media (max-width: 1024px) {
        .my-css-class2 {
            background-color: white;
            border: 1px solid red;
            color: red;
        }
    }

</style>
    </head>

    <body>
        <h1>Listing of declared stylehseets</h1>
        <ul name="declaredStylesheets"></ul>

        <hr />

        <h1>Listing of computed CSS rules</h1>
        <ul name="computedCssRules"></ul>

        <hr />

        <h1>Examples</h1>
        <p class="my-css-class">My text should be in red color</p>
        <p class="my-css-class2">My text should be in white color with
red background color</p>
        <button type="button" name="blueButton">Change red to blue</button>

        <!-- Scripts -->        <script src="./babel-browser-build.js"></script>
        <script src="./browser-es-module-loader.js"></script>
        <script type="module" src="./index.js"></script>
    </body>
</html>


Et notre fichier index.js associé:

'use strict';

// Get DOM elementsconst declaredStylesheetsElement 
    document.querySelector('[name="declaredStylesheets"]');
const computedCssRules = document.querySelector('[name="computedCssRules"]');
const blueButton = document.querySelector('[name="blueButton"]');

// Private functions
function replace(cssRule, oldValue, newValue) {
    if (cssRule.style) {
        for (let i = 0; i < cssRule.style.length; ++i) {
            let styleName = cssRule.style[i];
            let styleValue = cssRule.style[styleName];

            if (styleValue === oldValue) {
                cssRule.style[styleName] = newValue;
            }
        }

    } else {
        if (cssRule.cssRules) {
            Array
                .from(cssRule.cssRules)
                .forEach(subCssRule => replace(subCssRule, oldValue, newValue));
        }
    }
}
// Fill the DOM elementsArray .from(document.styleSheets) .forEach(styleSheet => { let liElement = document.createElement('li'); liElement.innerText
            `${styleSheet} - ${styleSheet.type} - ${styleSheet.title}`;

        declaredStylesheetsElement.appendChild(liElement);
    });

let allCssRules = Array
    .from(document.styleSheets)
    .map(styleSheet => {
        return Array
            .from(styleSheet.cssRules)
            .map(cssRule => {
                if (cssRule.media) {
                    return Array.from(cssRule.cssRules).map(subCssRule =>
                      `${cssRule.media.mediaText} - ${subCssRule.selectorText}`);
                }

                return `${cssRule.selectorText}`;
            })
    })
    .reduce((previousValue, currentValue) => previousValue.concat(currentValue), []);

[ ...new Set(allCssRules)].forEach(selectorText => {
    let liElement = document.createElement('li');
    liElement.innerText = `${selectorText}`;

    computedCssRules.appendChild(liElement);
});

// Add some listenersblueButton.addEventListener('click', function () {
    Array
        .from(document.styleSheets)
        .forEach(styleSheet => {
            return Array
                .from(styleSheet.cssRules)
                .forEach(cssRule => {
                    replace(cssRule, 'red', 'blue');
                });
        });
}, false);



Nous avons à l'affichage:


Le code suivant permet tout simplement de lister (de manière simple), les règles CSS:

Array
    .from(document.styleSheets)
    .forEach(styleSheet => {
        let liElement = document.createElement('li');
        liElement.innerText = `${styleSheet} - ${styleSheet.type} - ${styleSheet.title}`;

        declaredStylesheetsElement.appendChild(liElement);
    });

let allCssRules = Array
    .from(document.styleSheets)
    .map(styleSheet => {
        return Array
            .from(styleSheet.cssRules)
            .map(cssRule => {
                if (cssRule.media) {
                    return Array.from(cssRule.cssRules).map(subCssRule => `${cssRule.media.mediaText} - ${subCssRule.selectorText}`);
                }

                return `${cssRule.selectorText}`;
            })
    })
    .reduce((previousValue, currentValue) => previousValue.concat(currentValue), []);

[ ...new Set(allCssRules)].forEach(selectorText => {
    let liElement = document.createElement('li');
    liElement.innerText = `${selectorText}`;

    computedCssRules.appendChild(liElement);
});


Et le code suivant permet de modifier la couleur rouge en blue sur le click du bouton:
Array
    .from(document.styleSheets)
    .forEach(styleSheet => {
        return Array
            .from(styleSheet.cssRules)
            .forEach(cssRule => {
                replace(cssRule, 'red', 'blue');
            });
    });


Déjà première constation: la propriété "styleSheets" et ses sous-propriétés ne sont pas des tableaux tels que nous les connaissons. Nous pouvons faire un classique "for" dessus, mais pas question d'un "for-of" ou d'utiliser les méthodes de l'objet "Array". Nous devons donc utiliser "Array.from" pour "convertir la propriété de type "List" en "Array".

Ensuite examinons la méthode "replace":

function replace(cssRule, oldValue, newValue) {
    if (cssRule.style) {
        for (let i = 0; i < cssRule.style.length; ++i) {
            let styleName = cssRule.style[i];
            let styleValue = cssRule.style[styleName];

            if (styleValue === oldValue) {
                cssRule.style[styleName] = newValue;
            }
        }

    } else {
        if (cssRule.cssRules) {
            Array
                .from(cssRule.cssRules)
                .forEach(subCssRule => replace(subCssRule, oldValue, newValue));
        }
    }
}

Comme vous pouvez le voir, c'est une fonction récursive (à cause des règles CSS media que nous pouvons déclarer). Egalement, la propriété "cssRules" (qui apparaît quand nous sommes dans un "CSSMediaRule") n'est pas un tableau.

Quand nous avons un "CSSSyleRule", la propriété "style" va contenir toutes les propriétés CSS déclarées. En revanche, ce n'est pas non plus un tableau: nous la parcourons classiquement via un "for". Par contre, libre à nous de la modifier comme nous en avons envie !

Et au clique, voyez le résultat:




Il y a toutefois deux petites choses à savoir:
  • les noms de propriétés css ne sont pas en "dash-case" mais en "came-case"

  • Les valeurs de couleurs peuvent être convetie. Si vous utiliser une valeur hexadécimal, nous retrouvez votre valeur dans les "CSSStyleRule" en "rgb".



Voilà, j'espère que vous trouverez cela intéressant

Commentaires

Posts les plus consultés de ce blog

ISO: liens & outils utiles

NodeJs et SSL: une petite analyse

Créer sa commande slack en quelques minutes