5

I have the following code and I want to get the desired CSS property of the element(even if the style is set by js, i.e. for file created by SPA like react) and not the computed/applied ones. For example the computedStyle.width should have an undefined value. How can I accomplish this?

let ele = document.querySelector(".hello");
let computedStyle = window.getComputedStyle(ele);

console.log(computedStyle.backgroundColor);
console.log(computedStyle.height);
console.log(computedStyle.width); // <- This should be undefined or ""
.hello {
  height: 10px;
  background-color: red;
}
<div class="hello">
  helloInnerText
</div>
Krueger
  • 1,178
  • 3
  • 11
  • 26
  • 2
    If you want to get the "desired css property of the element" then don't use `getComputedStyle`, its sole purpose is to give all properties after applying/resolving all stylesheets. Just access directly, like `ele.style.width`. – bryce Jun 22 '22 at 16:27
  • But then the `ele.style.height` ist undefined too. – Krueger Jun 22 '22 at 16:32
  • Selectively getting explicitly defined style values and filtering computed ones isn't supported as far as I know. What's your use case for this? – bryce Jun 22 '22 at 16:38
  • Want to generate a normalised css & html file for a website. – Krueger Jun 22 '22 at 16:45
  • Does this answer your question? [How to get CSS class property in Javascript?](https://stackoverflow.com/questions/20377835/how-to-get-css-class-property-in-javascript) – isherwood Jun 24 '22 at 20:21
  • Google one of the CSS reset stylesheets. Different browsers have slightly different base styles. Using a CSS reset stylesheet mitigates this and provides a good base to work from. – Ouroborus Jun 24 '22 at 20:28
  • what do you get if you add `width: auto` instead of not having a width? Everything has an implicit width. – chovy Jun 27 '22 at 12:03

2 Answers2

2

You can do this by checking all the stylesheets for rules that apply to the element under investigation. When the rule applies, you have a css property and it's value as desired for this element by the developer who wrote the stylesheet(s).

Since the rule that is more specific takes precedence, you have to check each rule's specificity against other rules that apply.

Note that I wouldn't recommend this for production, since it is rather slow.

See the comments in the code below:

/* Say, we want to know the width, height, and background-color */
let propertiesWeWant = ['width', 'height', 'background-color', 'color', 'margin', 'font-size'];
let ele = document.querySelector(".hello");
/* First we have to prepare the stylesheets, in order to avoid CORS problems, 
 * since css rules of external stylesheets cannot be accessed. 
 * We loop through all stylesheets and inject external sheets into the head, 
 * so we can read them in the next step.
 */
for(let i = 0; i < document.styleSheets.length; i++){
    prepareStylesheet(document.styleSheets[i]);
}

setTimeout(function(){
    /* We get all the styles for this element, as found in the stylesheets. */
    let givenStyles = getStylesFromStylesheets(ele);
    /* And filter the results, so we only have the properties we want. */
    let propertyList = {};
    for(let i = 0; i < propertiesWeWant.length; i++){
        let property = propertiesWeWant[i];
        if(typeof(givenStyles[property]) !== "undefined"){
            propertyList[property] = givenStyles[property].style;
        }
        else{
            propertyList[property] = "";
        }
    }
    /* prints all given properties. 
    *  Object { width: "1px", height: "4px", "background-color": "pink", color: "turquoise", margin: "", "font-size": "17.5px" }
    *  */
    console.log(propertyList);
},2000);

/* Taken from her: https://discourse.mozilla.org/t/webextensions-porting-access-to-cross-origin-document-stylesheets-cssrules/18359 
 * If cssRules cannot be accessed because of CORS issues:
 * 1. remove the stylesheet
 * 2. fetch it using its url
 * 3. insert the rules in the header
 */
function prepareStylesheet(sheet){
    try {
        sheet.cssRules;
    } catch (e) {
        if (e.name === 'SecurityError') {
            let nextElement = sheet.ownerNode.nextSibling;
            let parentNode = sheet.ownerNode.parentNode;
            sheet.ownerNode.parentNode.removeChild(sheet.ownerNode);
            fetch(sheet.href).then(resp => resp.text()).then(css => {
                let style = document.createElement('style');
                style.innerText = css;
                //document.head.appendChild(style);
                if(nextElement === null){
                    parentNode.appendChild(style);
                }
                else{
                    parentNode.insertBefore(style, nextElement);
                }
                
            });  
            
            
        }
    }
}
/**
 * Search through stylesheets for rules that apply to a given element.
 * Returns object with css properties name and css property value 
 */
function getStylesFromStylesheets(elementToInvestigate){
    let givenStyles = {};
    //alert(document.styleSheets.length);
    for(let i = 0; i < document.styleSheets.length; i++){
        let rules = document.styleSheets[i].cssRules;
        /* Loop through every rule in the stylesheet and see wether it applies to this element */
        for(let j = 0; j < rules.length; j++){
            let rule = rules[j];
            /* Ignore media rules. Can be added, but needs some logic that you have to write. */
            if(typeof(rule.selectorText) === "undefined"){
                continue;
            }
            let split = rule.selectorText.split(",");
            for(let l = 0; l < split.length; l++){
                let selector = split[l];
                let elements = document.querySelectorAll(selector);
                let applies = false;
                for(let k = 0; k < elements.length; k++){
                    if(elements[k] === elementToInvestigate){
                        applies = true;
                        break;
                    }
                }
                if(applies === true){
                    /* If the rule applies to this element, loop through the css properties of this css rule. */
                    let styles = rule.style;
                    for(let k = 0; k < styles.length; k++){
                        /* For each css property, check if it was already added earlier. */ 
                        let styleName = styles[k];
                        let styleValue = styles[styleName];
                        let newSpecificity = calculateSingle( selector);
                        if(typeof(givenStyles[styleName]) !== "undefined"){
                            /* If the property was already added, compare the specificity of the new one to the one added earlier. */
                            let earlierSelector = givenStyles[styleName].selector;
                            let earlierSpecificity = givenStyles[styleName].specificity;
                            let newHasMoreSpecificity = compareSpecifities( newSpecificity, earlierSpecificity);
                            if(newHasMoreSpecificity === true){
                                givenStyles[styleName] = {
                                        style: styleValue,
                                        specificity: newSpecificity
                                }
                            }
                            else{
                                /* Leave givenStyles as it was, since the former tule had higher specificity. */
                            }
                        }
                        else{
                            /* If the property was not yet seen, add it to the givenStyles. */
                            givenStyles[styleName] = {
                                    style: styleValue,
                                    specificity: newSpecificity
                            }
                        }
                    }
                }
            }
        }
        /* If the element has inline styles, overwrite the found properties since inline always takes precedence. */
        if(elementToInvestigate.style.length > 0){
            for(let j = 0; j < elementToInvestigate.style.length; j++){
                let styleName = elementToInvestigate.style[j];
                let styleValue = elementToInvestigate.style[styleName];
                givenStyles[styleName] = {
                        style: styleValue,
                        specificity: [1,1,1,1] //not needed here
                }
            }
        }
        var inlineStyles;
    }
    return givenStyles;
}
/* Taken from here and adjusted the output: https://raw.githubusercontent.com/keeganstreet/specificity/master/specificity.js */
/**
 * Calculates the specificity of CSS selectors
 * http://www.w3.org/TR/css3-selectors/#specificity
 *
 * Returns specificity: e.g. 0,1,0,0
 */
function calculateSingle(input) {
    var selector = input,
        findMatch,
        typeCount = {
            'a': 0,
            'b': 0,
            'c': 0
        },
        parts = [],
        // The following regular expressions assume that selectors matching the preceding regular expressions have been removed
        attributeRegex = /(\[[^\]]+\])/g,
        idRegex = /(#[^\#\s\+>~\.\[:\)]+)/g,
        classRegex = /(\.[^\s\+>~\.\[:\)]+)/g,
        pseudoElementRegex = /(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi,
        // A regex for pseudo classes with brackets - :nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-type(), :lang()
        // The negation psuedo class (:not) is filtered out because specificity is calculated on its argument
        // :global and :local are filtered out - they look like psuedo classes but are an identifier for CSS Modules
        pseudoClassWithBracketsRegex = /(:(?!not|global|local)[\w-]+\([^\)]*\))/gi,
        // A regex for other pseudo classes, which don't have brackets
        pseudoClassRegex = /(:(?!not|global|local)[^\s\+>~\.\[:]+)/g,
        elementRegex = /([^\s\+>~\.\[:]+)/g;

    // Find matches for a regular expression in a string and push their details to parts
    // Type is "a" for IDs, "b" for classes, attributes and pseudo-classes and "c" for elements and pseudo-elements
    findMatch = function(regex, type) {
        var matches, i, len, match, index, length;
        if (regex.test(selector)) {
            matches = selector.match(regex);
            for (i = 0, len = matches.length; i < len; i += 1) {
                typeCount[type] += 1;
                match = matches[i];
                index = selector.indexOf(match);
                length = match.length;
                parts.push({
                    selector: input.substr(index, length),
                    type: type,
                    index: index,
                    length: length
                });
                // Replace this simple selector with whitespace so it won't be counted in further simple selectors
                selector = selector.replace(match, Array(length + 1).join(' '));
            }
        }
    };

    // Replace escaped characters with plain text, using the "A" character
    // https://www.w3.org/TR/CSS21/syndata.html#characters
    (function() {
        var replaceWithPlainText = function(regex) {
                var matches, i, len, match;
                if (regex.test(selector)) {
                    matches = selector.match(regex);
                    for (i = 0, len = matches.length; i < len; i += 1) {
                        match = matches[i];
                        selector = selector.replace(match, Array(match.length + 1).join('A'));
                    }
                }
            },
            // Matches a backslash followed by six hexadecimal digits followed by an optional single whitespace character
            escapeHexadecimalRegex = /\\[0-9A-Fa-f]{6}\s?/g,
            // Matches a backslash followed by fewer than six hexadecimal digits followed by a mandatory single whitespace character
            escapeHexadecimalRegex2 = /\\[0-9A-Fa-f]{1,5}\s/g,
            // Matches a backslash followed by any character
            escapeSpecialCharacter = /\\./g;

        replaceWithPlainText(escapeHexadecimalRegex);
        replaceWithPlainText(escapeHexadecimalRegex2);
        replaceWithPlainText(escapeSpecialCharacter);
    }());

    // Remove anything after a left brace in case a user has pasted in a rule, not just a selector
    (function() {
        var regex = /{[^]*/gm,
            matches, i, len, match;
        if (regex.test(selector)) {
            matches = selector.match(regex);
            for (i = 0, len = matches.length; i < len; i += 1) {
                match = matches[i];
                selector = selector.replace(match, Array(match.length + 1).join(' '));
            }
        }
    }());

    // Add attribute selectors to parts collection (type b)
    findMatch(attributeRegex, 'b');

    // Add ID selectors to parts collection (type a)
    findMatch(idRegex, 'a');

    // Add class selectors to parts collection (type b)
    findMatch(classRegex, 'b');

    // Add pseudo-element selectors to parts collection (type c)
    findMatch(pseudoElementRegex, 'c');

    // Add pseudo-class selectors to parts collection (type b)
    findMatch(pseudoClassWithBracketsRegex, 'b');
    findMatch(pseudoClassRegex, 'b');

    // Remove universal selector and separator characters
    selector = selector.replace(/[\*\s\+>~]/g, ' ');

    // Remove any stray dots or hashes which aren't attached to words
    // These may be present if the user is live-editing this selector
    selector = selector.replace(/[#\.]/g, ' ');

    // Remove the negation psuedo-class (:not) but leave its argument because specificity is calculated on its argument
    // Remove non-standard :local and :global CSS Module identifiers because they do not effect the specificity
    selector = selector.replace(/:not/g, '    ');
    selector = selector.replace(/:local/g, '      ');
    selector = selector.replace(/:global/g, '       ');
    selector = selector.replace(/[\(\)]/g, ' ');

    // The only things left should be element selectors (type c)
    findMatch(elementRegex, 'c');

    // Order the parts in the order they appear in the original selector
    // This is neater for external apps to deal with
    parts.sort(function(a, b) {
        return a.index - b.index;
    });

    return [0, typeCount.a, typeCount.b, typeCount.c];
};

/**
 * Compares two specificities
 *
 *  - it returns false if a has a lower specificity than b
 *  - it returns true if a has a higher specificity than b
 *  - it returns true if a has the same specificity than b
 */
function compareSpecifities(aSpecificity, bSpecificity) {

    for (var i = 0; i < 4; i += 1) {
        if (aSpecificity[i] < bSpecificity[i]) {
            return false;
        } else if (aSpecificity[i] > bSpecificity[i]) {
            return true;
        }
    }

    return true;
};
blockquote{
    margin:7px;
    border-left: 10000px;
}
#hello-id.hello{
    height: 1px;
}
#hello-id{
    height: 2px;
}
html #hello-id{
    height: 3px;
    color: green;
}
#hello-id.hello{
    height: 4px;
    color: turquoise;
}
.hello-wrapper .hello {
  height: 5px;
  background-color: blue;
}
.hello {
  height: 5px;
  background-color: red;
}
#bogus.bogus{
    height: 6px;
    background-color: orange;
}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css" >
<div class="hello-wrapper">
    <blockquote id="hello-id" class="hello" style="width:1px; background-color: pink;">
      helloInnerText
    </blockquote>
</div>
Klaassiek
  • 2,795
  • 1
  • 23
  • 41
  • That's a thorough answer. But this will only parse the stylesheet?? – Krueger Jun 26 '22 at 10:37
  • 1
    It uses the stylesheets the way the browser has parsed them. It checks for every rule and saves it if it applies to the element. You get a list of all the properties from the stylesheets that the browser has applied to the element. That’s what I call « desired css properties ». If that is not, what you need, edit your answer to be more clear about what you mean by « desired css properties ». – Klaassiek Jun 26 '22 at 16:06
  • Your answer is quite sound, but this will not register the changes made by a js library or a dom create by a js library like react? – Krueger Jun 27 '22 at 19:43
  • 1
    Yes. This will also register these changes as long as the scropt is run after the changes have been made. – Klaassiek Jun 28 '22 at 11:18
0

Just use js styleSheets[0].cssRules property. Here index 0 means the main style sheet (if you have 2 stylesheet for your page and you want the second one it will be styleSheets[1].cssRules for exemple).

As you can see in the snippet below, it returns the whole rules for every css element NOT COMPUTED with absolutely no exceptions, on the following format :

/**id:X**/ 
  "selectorText": "#idName/.className",
  #list of css properties

/**id:X**/ 
  "selectorText": "#idName/.className",
  #list of css properties

This method is fast and efficient even if you have a lot of useless data, thanksfully the css properties are ordered alphabetically. You can also apply conditions in your js to just get the properties you want like only the width, height, color...

To take back your exemple with the width, you can see that in the first div #test1 the value is "width" : "fit-content", while in the second one #test2 it's "width": "",.

To fully answere your question, this method isn't available when you define the style with js, because when you apply style with js you don't use a style sheet so the values have to be computed anyway. You still can try but you will have all the proprties values empty "".

I let you check this article for more informations.

function showCss(){
let ruleList = document.styleSheets[0].cssRules;

for (let rule of ruleList) {
  console.log(rule);
}
}
#test1{
width: fit-content;
border: 1px solid black;
height: none;
padding: 5px;
border-radius: 5px;
}

#test2{
width:;
color:white;
height: none;
padding: 5px;
border-radius: 5px;
background-color:red;
}
<div id="test1">
I'm a simple div :-)
</div>

<br>

<div id="test2">
I'm another simple div :-)
</div>

<br>

<input type="button" onclick="showCss()" value="Show properties">
hugomztl
  • 88
  • 11