Encontre todas as regras CSS que se aplicam a um elemento

87

Muitas ferramentas / APIs fornecem maneiras de selecionar elementos de classes ou IDs específicos. Também é possível inspecionar as folhas de estilo brutas carregadas pelo navegador.

No entanto, para os navegadores renderizarem um elemento, eles compilarão todas as regras CSS (possivelmente de arquivos de folha de estilo diferentes) e as aplicarão ao elemento. Isso é o que você vê no Firebug ou no WebKit Inspector - a árvore de herança CSS completa para um elemento.

Como posso reproduzir esse recurso em JavaScript puro sem exigir plug-ins de navegador adicionais?

Talvez um exemplo possa fornecer algum esclarecimento sobre o que procuro:

<style type="text/css">
    p { color :red; }
    #description { font-size: 20px; }
</style>

<p id="description">Lorem ipsum</p>

Aqui, o elemento p # description tem duas regras CSS aplicadas: uma cor vermelha e um tamanho de fonte de 20 px.

Eu gostaria de encontrar a fonte de onde essas regras CSS calculadas se originam (a cor vem da regra p e assim por diante).

cgbystrom
fonte
Visualizar em um navegador e usar as ferramentas de desenvolvedor do navegador (por exemplo, guia Elementos no Chrome)?
Ronnie Royston

Respostas:

77

Como esta pergunta atualmente não tem uma resposta leve (não biblioteca) compatível com vários navegadores, tentarei fornecer uma:

function css(el) {
    var sheets = document.styleSheets, ret = [];
    el.matches = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector 
        || el.msMatchesSelector || el.oMatchesSelector;
    for (var i in sheets) {
        var rules = sheets[i].rules || sheets[i].cssRules;
        for (var r in rules) {
            if (el.matches(rules[r].selectorText)) {
                ret.push(rules[r].cssText);
            }
        }
    }
    return ret;
}

JSFiddle: http://jsfiddle.net/HP326/6/

A chamada css(document.getElementById('elementId'))retornará uma matriz com um elemento para cada regra CSS que corresponda ao elemento passado. Se você deseja descobrir informações mais específicas sobre cada regra, verifique a documentação do objeto CSSRule .

SB
fonte
1
a.matchesé definido nesta linha: a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector. Isso significa que se já existe um método (padrão) de "correspondências" para nós DOM, ele o usará, caso contrário, tentará usar o específico do Webkit (webkitMatchesSelector), depois os do Mozilla, Microsoft e Opera. Você pode ler mais sobre isso aqui: developer.mozilla.org/en/docs/Web/API/Element/matches
SB
3
Infelizmente, acho que essa alternativa não detecta todas as regras CSS que se propagam a partir de elementos pais em filhos. Fiddle: jsfiddle.net/t554xo2L Nesse caso, a regra UL (que se aplica ao elemento) não corresponde à if (a.matches(rules[r].selectorText))condição de proteção.
funforums de
2
Eu nunca afirmei que ele listou / herdou / regras CSS - tudo o que ele faz é listar regras CSS que correspondem ao elemento passado. Se você quiser obter as regras herdadas para esse elemento também, provavelmente precisará percorrer o DOM para cima e chamar css()cada um dos elementos pai.
SB
2
Eu sei :-) Eu só queria apontar isso, já que as pessoas que podem analisar esta questão podem presumir que ela recebe 'todas as regras de css que se aplicam a um elemento', como o título da pergunta diz, o que não é o caso .
funforums de
3
Se você quiser que todas as regras atualmente aplicadas ao elemento, incluindo aquelas herdadas, você deve usar getComputedStyle. Diante disso, acho que essa resposta está correta e é correta não incluir estilos herdados dos pais (cor do texto atribuída ao pai, por exemplo). O que não inclui, no entanto, são regras aplicadas condicionalmente com consultas de mídia.
tremby
23

EDITAR: esta resposta está obsoleta e não funciona mais no Chrome 64+ . Saindo para o contexto histórico. Na verdade, esse relatório de bug leva de volta a esta questão para soluções alternativas para usar isso.


Parece que consegui responder minha própria pergunta depois de mais uma hora de pesquisa.

É tão simples quanto isto:

window.getMatchedCSSRules(document.getElementById("description"))

(Funciona no WebKit / Chrome, possivelmente em outros também)

cgbystrom
fonte
4
Bem, isso não é muito útil se for suportado apenas pelo cromo. Funcionará com menos de 5% de todos os visitantes (dependendo da demografia).
Tomasi
5
@diamandiev: Em junho de 2012, a participação no uso do Chrome aumentou para mais de 32% (e é um pouco maior do que o uso do IE!). gs.statcounter.com
Roy Tinker
6
getMatchedCSSRules NÃO mostra os estilos finais que se aplicam ao elemento. Ele retorna uma matriz de todos os objetos CSSStyleRule que se aplicam na ordem em que aparecem. Se você faz design responsivo pela Web por meio de consultas de mídia CSS ou carrega mais de uma folha de estilo (como uma para o IE), ainda precisa fazer um loop em cada um dos estilos retornados e calcular a especificidade do css para cada regra. Em seguida, calcule as regras finais que se aplicam. Você precisa reproduzir o que o navegador faz naturalmente. Para provar isso em seu exemplo, acrescente "p {color: blue! Important}" ao início de sua declaração de estilo.
mrbinky3000
24
Isso agora está obsoleto no Chrome 41. Consulte code.google.com/p/chromium/issues/detail?id=437569#c2 .
Daniel Darabos
5
Isso foi finalmente removido no Chrome 63 (postagem oficial do blog - que aponta para esta questão)
brichins
19

Dê uma olhada nesta biblioteca, que faz o que foi pedido: http://www.brothercake.com/site/resources/scripts/cssutilities/

Ele funciona em todos os navegadores modernos desde o IE6, pode fornecer coleções de regras e propriedades como o Firebug (na verdade, é mais preciso do que o Firebug) e também pode calcular a especificidade relativa ou absoluta de qualquer regra. A única ressalva é que, embora entenda os tipos de mídia estática, não entende as consultas de mídia.

brothercake
fonte
Este módulo é realmente ótimo, só espero que receba mais atenção do autor.
mr1031011
17

Versão curta 12 de abril de 2017

Challenger aparece.

var getMatchedCSSRules = (el, css = el.ownerDocument.styleSheets) => 
    [].concat(...[...css].map(s => [...s.cssRules||[]])) /* 1 */
    .filter(r => el.matches(r.selectorText));            /* 2 */

Line /* 1 */constrói uma matriz simples de todas as regras.
A linha /* 2 */descarta as regras não correspondentes.

Baseado na funçãocss(el) de @SB na mesma página.

Exemplo 1

var div = iframedoc.querySelector("#myelement");
var rules = getMatchedCSSRules(div, iframedoc.styleSheets);
console.log(rules[0].parentStyleSheet.ownerNode, rules[0].cssText);

Exemplo 2

var getMatchedCSSRules = (el, css = el.ownerDocument.styleSheets) => 
    [].concat(...[...css].map(s => [...s.cssRules||[]]))
    .filter(r => el.matches(r.selectorText));

function Go(big,show) {
    var r = getMatchedCSSRules(big);
PrintInfo:
    var f = (dd,rr,ee="\n") => dd + rr.cssText.slice(0,50) + ee;
    show.value += "--------------- Rules: ----------------\n";
    show.value += f("Rule 1:   ", r[0]);
    show.value += f("Rule 2:   ", r[1]);
    show.value += f("Inline:   ", big.style);
    show.value += f("Computed: ", getComputedStyle(big), "(…)\n");
    show.value += "-------- Style element (HTML): --------\n";
    show.value += r[0].parentStyleSheet.ownerNode.outerHTML;
}

Go(...document.querySelectorAll("#big,#show"));
.red {color: red;}
#big {font-size: 20px;}
<h3 id="big" class="red" style="margin: 0">Lorem ipsum</h3>
<textarea id="show" cols="70" rows="10"></textarea>

Deficiências

  • No manuseio de mídia, não @import, @media.
  • Sem acesso a estilos carregados de folhas de estilo de vários domínios.
  • Sem classificação pelo seletor “especificidade” (ordem de importância).
  • Nenhum estilo herdado dos pais.
  • Pode não funcionar com navegadores antigos ou rudimentares.
  • Não tenho certeza de como ele lida com pseudo-classes e pseudo-seletores, mas parece estar bem.

Talvez eu resolva essas deficiências um dia.

Versão longa 12 de agosto de 2018

Aqui está uma implementação muito mais abrangente tirada da página GitHub de alguém (bifurcada deste código original , via Bugzilla ). Escrito para Gecko e IE, mas há rumores de que funcione também com o Blink.

4 de maio de 2017: A calculadora de especificidade apresentou bugs críticos que já corrigi. (Não posso notificar os autores porque não tenho uma conta no GitHub.)

12 de agosto de 2018: as atualizações recentes do Chrome parecem ter desacoplado o escopo do objeto ( this) dos métodos atribuídos a variáveis ​​independentes. Portanto, a invocação matcher(selector)parou de funcionar. Substituí-lo por matcher.call(el, selector)resolveu.

// polyfill window.getMatchedCSSRules() in FireFox 6+
if (typeof window.getMatchedCSSRules !== 'function') {
    var ELEMENT_RE = /[\w-]+/g,
            ID_RE = /#[\w-]+/g,
            CLASS_RE = /\.[\w-]+/g,
            ATTR_RE = /\[[^\]]+\]/g,
            // :not() pseudo-class does not add to specificity, but its content does as if it was outside it
            PSEUDO_CLASSES_RE = /\:(?!not)[\w-]+(\(.*\))?/g,
            PSEUDO_ELEMENTS_RE = /\:\:?(after|before|first-letter|first-line|selection)/g;
        // convert an array-like object to array
        function toArray(list) {
            return [].slice.call(list);
        }

        // handles extraction of `cssRules` as an `Array` from a stylesheet or something that behaves the same
        function getSheetRules(stylesheet) {
            var sheet_media = stylesheet.media && stylesheet.media.mediaText;
            // if this sheet is disabled skip it
            if ( stylesheet.disabled ) return [];
            // if this sheet's media is specified and doesn't match the viewport then skip it
            if ( sheet_media && sheet_media.length && ! window.matchMedia(sheet_media).matches ) return [];
            // get the style rules of this sheet
            return toArray(stylesheet.cssRules);
        }

        function _find(string, re) {
            var matches = string.match(re);
            return matches ? matches.length : 0;
        }

        // calculates the specificity of a given `selector`
        function calculateScore(selector) {
            var score = [0,0,0],
                parts = selector.split(' '),
                part, match;
            //TODO: clean the ':not' part since the last ELEMENT_RE will pick it up
            while (part = parts.shift(), typeof part == 'string') {
                // find all pseudo-elements
                match = _find(part, PSEUDO_ELEMENTS_RE);
                score[2] += match;
                // and remove them
                match && (part = part.replace(PSEUDO_ELEMENTS_RE, ''));
                // find all pseudo-classes
                match = _find(part, PSEUDO_CLASSES_RE);
                score[1] += match;
                // and remove them
                match && (part = part.replace(PSEUDO_CLASSES_RE, ''));
                // find all attributes
                match = _find(part, ATTR_RE);
                score[1] += match;
                // and remove them
                match && (part = part.replace(ATTR_RE, ''));
                // find all IDs
                match = _find(part, ID_RE);
                score[0] += match;
                // and remove them
                match && (part = part.replace(ID_RE, ''));
                // find all classes
                match = _find(part, CLASS_RE);
                score[1] += match;
                // and remove them
                match && (part = part.replace(CLASS_RE, ''));
                // find all elements
                score[2] += _find(part, ELEMENT_RE);
            }
            return parseInt(score.join(''), 10);
        }

        // returns the heights possible specificity score an element can get from a give rule's selectorText
        function getSpecificityScore(element, selector_text) {
            var selectors = selector_text.split(','),
                selector, score, result = 0;
            while (selector = selectors.shift()) {
                if (matchesSelector(element, selector)) {
                    score = calculateScore(selector);
                    result = score > result ? score : result;
                }
            }
            return result;
        }

        function sortBySpecificity(element, rules) {
            // comparing function that sorts CSSStyleRules according to specificity of their `selectorText`
            function compareSpecificity (a, b) {
                return getSpecificityScore(element, b.selectorText) - getSpecificityScore(element, a.selectorText);
            }

            return rules.sort(compareSpecificity);
        }

        // Find correct matchesSelector impl
        function matchesSelector(el, selector) {
          var matcher = el.matchesSelector || el.mozMatchesSelector || 
              el.webkitMatchesSelector || el.oMatchesSelector || el.msMatchesSelector;
          return matcher.call(el, selector);
        }

        //TODO: not supporting 2nd argument for selecting pseudo elements
        //TODO: not supporting 3rd argument for checking author style sheets only
        window.getMatchedCSSRules = function (element /*, pseudo, author_only*/) {
            var style_sheets, sheet, sheet_media,
                rules, rule,
                result = [];
            // get stylesheets and convert to a regular Array
            style_sheets = toArray(window.document.styleSheets);

            // assuming the browser hands us stylesheets in order of appearance
            // we iterate them from the beginning to follow proper cascade order
            while (sheet = style_sheets.shift()) {
                // get the style rules of this sheet
                rules = getSheetRules(sheet);
                // loop the rules in order of appearance
                while (rule = rules.shift()) {
                    // if this is an @import rule
                    if (rule.styleSheet) {
                        // insert the imported stylesheet's rules at the beginning of this stylesheet's rules
                        rules = getSheetRules(rule.styleSheet).concat(rules);
                        // and skip this rule
                        continue;
                    }
                    // if there's no stylesheet attribute BUT there IS a media attribute it's a media rule
                    else if (rule.media) {
                        // insert the contained rules of this media rule to the beginning of this stylesheet's rules
                        rules = getSheetRules(rule).concat(rules);
                        // and skip it
                        continue
                    }

                    // check if this element matches this rule's selector
                    if (matchesSelector(element, rule.selectorText)) {
                        // push the rule to the results set
                        result.push(rule);
                    }
                }
            }
            // sort according to specificity
            return sortBySpecificity(element, result);
        };
}

Bugs corrigidos

  • = match+= match
  • return re ? re.length : 0;return matches ? matches.length : 0;
  • _matchesSelector(element, selector)matchesSelector(element, selector)
  • matcher(selector)matcher.call(el, selector)
7vujy0f0hy
fonte
Em getSheetRules, tive que adicionar if (stylesheet.cssRules === null) {return []} para fazê-lo funcionar para mim.
Gwater17
Testado a "versão longa." Funciona para mim. Pena que getMatchedCSSRules () nunca foi padronizado pelos navegadores.
colin moock
Como isso lida com dois seletores com as mesmas especificidades, como h1 e h1, div - onde o declarado por último deve ser usado?
Stellan
PEr talvez possamos ter alguma idéia de como lidar com pseudo aqui? github.com/dvtng/jss/blob/master/jss.js
mr1031011
4

Aqui está uma versão da resposta de SB que também retorna regras de correspondência dentro de consultas de mídia correspondentes. Eu removi a *.rules || *.cssRulescoalescência e o.matches localizador de implementação; adicione um polyfill ou adicione essas linhas de volta se precisar delas.

Esta versão também retorna os CSSStyleRuleobjetos em vez do texto da regra. Acho que isso é um pouco mais útil, uma vez que as especificações das regras podem ser mais facilmente testadas programaticamente dessa maneira.

Café:

getMatchedCSSRules = (element) ->
  sheets = document.styleSheets
  matching = []

  loopRules = (rules) ->
    for rule in rules
      if rule instanceof CSSMediaRule
        if window.matchMedia(rule.conditionText).matches
          loopRules rule.cssRules
      else if rule instanceof CSSStyleRule
        if element.matches rule.selectorText
          matching.push rule
    return

  loopRules sheet.cssRules for sheet in sheets

  return matching

JS:

function getMatchedCSSRules(element) {
  var i, len, matching = [], sheets = document.styleSheets;

  function loopRules(rules) {
    var i, len, rule;

    for (i = 0, len = rules.length; i < len; i++) {
      rule = rules[i];
      if (rule instanceof CSSMediaRule) {
        if (window.matchMedia(rule.conditionText).matches) {
          loopRules(rule.cssRules);
        }
      } else if (rule instanceof CSSStyleRule) {
        if (element.matches(rule.selectorText)) {
          matching.push(rule);
        }
      }
    }
  };

  for (i = 0, len = sheets.length; i < len; i++) {
    loopRules(sheets[i].cssRules);
  }

  return matching;
}
trêmulo
fonte
Como isso poderia ser alterado para ser usado também em filhos de falecidos element?
Kragalon
2
Qual é o seu caso de uso? Eu realmente não vejo onde isso seria útil, uma vez que as regras que se aplicam aos filhos não se aplicam necessariamente aos pais. Você acabaria com uma pilha de regras sem nada em particular em comum. Se você realmente quiser isso, pode simplesmente recursar nos filhos e executar esse método para cada um, e construir uma matriz de todos os resultados.
tremendo de
Estou apenas tentando fazer cloneNode(true)funcionalidade, mas com a clonagem profunda de estilo também.
Kragalon
1
esta condição: if (window.matchMedia (rule.conditionText) .matches) {...} impediu uma correspondência no meu caso, pois "rule.conditionText" estava indefinido. Sem isso, funcionou. Você pode tentar e testar isso em news.ycombinator.com . "span.pagetop b" tem uma regra de consulta de mídia que não corresponde à sua função como ela é.
Ayal Gelles
1
O Chrome não oferece suporte à propriedade conditionText em instâncias CSSMediaRule.
Macil,
3

Aqui está minha versão de getMatchedCSSRulesfunção que suporta @mediaconsulta.

const getMatchedCSSRules = (el) => {
  let rules = [...document.styleSheets]
  rules = rules.filter(({ href }) => !href)
  rules = rules.map((sheet) => [...(sheet.cssRules || sheet.rules || [])].map((rule) => {
    if (rule instanceof CSSStyleRule) {
      return [rule]
    } else if (rule instanceof CSSMediaRule && window.matchMedia(rule.conditionText)) {
      return [...rule.cssRules]
    }
    return []
  }))
  rules = rules.reduce((acc, rules) => acc.concat(...rules), [])
  rules = rules.filter((rule) => el.matches(rule.selectorText))
  rules = rules.map(({ style }) => style)
  return rules
}
user3896501
fonte
1

var GetMatchedCSSRules = (elem, css = document.styleSheets) => Array.from(css)
  .map(s => Array.from(s.cssRules).filter(r => elem.matches(r.selectorText)))
  .reduce((a,b) => a.concat(b));

function Go(paragraph, print) {
  var rules = GetMatchedCSSRules(paragraph);
PrintInfo:
  print.value += "Rule 1: " + rules[0].cssText + "\n";
  print.value += "Rule 2: " + rules[1].cssText + "\n\n";
  print.value += rules[0].parentStyleSheet.ownerNode.outerHTML;
}

Go(document.getElementById("description"), document.getElementById("print"));
p {color: red;}
#description {font-size: 20px;}
<p id="description">Lorem ipsum</p>
<textarea id="print" cols="50" rows="12"></textarea>

Thomas
fonte
3
Duplicação inútil de uma versão antiga da minha resposta. Apenas poluindo a página. Versão completa e atualizada: aqui .
7vujy0f0hy
1

Garantindo o IE9 +, escrevi uma função que calcula CSS para o elemento solicitado e seus filhos, e dá a possibilidade de salvá-lo em um novo className se necessário no snippet abaixo.

/**
  * @function getElementStyles
  *
  * Computes all CSS for requested HTMLElement and its child nodes and applies to dummy class
  *
  * @param {HTMLElement} element
  * @param {string} className (optional)
  * @param {string} extras (optional)
  * @return {string} CSS Styles
  */
function getElementStyles(element, className, addOnCSS) {
  if (element.nodeType !== 1) {
    return;
  }
  var styles = '';
  var children = element.getElementsByTagName('*');
  className = className || '.' + element.className.replace(/^| /g, '.');
  addOnCSS = addOnCSS || '';
  styles += className + '{' + (window.getComputedStyle(element, null).cssText + addOnCSS) + '}';
  for (var j = 0; j < children.length; j++) {
    if (children[j].className) {
      var childClassName = '.' + children[j].className.replace(/^| /g, '.');
      styles += ' ' + className + '>' + childClassName +
        '{' + window.getComputedStyle(children[j], null).cssText + '}';
    }
  }
  return styles;
}

Uso

getElementStyles(document.getElementByClassName('.my-class'), '.dummy-class', 'width:100%;opaity:0.5;transform:scale(1.5);');
Shobhit Sharma
fonte
2
1. Você pode substituir toda a computeStylessub-rotina por apenas el => getComputedStyle(el).cssText. Prova: violino . 2. '.' + element.className é uma construção falha porque assume a existência de um nome de classe. A construção válida é element.className.replace(/^| /g, '.'). 3. Sua função ignora a possibilidade de outros seletores CSS além das classes. 4. Sua recursão é arbitrariamente limitada a um nível (filhos, mas não netos). 5. Uso: não há getElementByClassName, apenas getElementsByClassName(retorna uma matriz).
7vujy0f0hy
1

Acho que a resposta do SB deveria ser aceita neste momento, mas não é exata. É mencionado algumas vezes que algumas regras podem ser perdidas. Diante disso, decidi usar document.querySelectorAll em vez de element.matches. A única coisa é que você precisaria de algum tipo de identificação única de elementos para compará-lo com o que está procurando. Na maioria dos casos, acho que isso é possível definindo seu id para ter um valor único. É assim que você pode identificar o elemento correspondente sendo seu. Se você puder pensar em uma maneira geral de combinar o resultado de document.querySelectorAll com o elemento que está procurando, isso seria essencialmente um polyfill completo de getMatchedCSSRules.

Eu verifiquei o desempenho de document.querySelectorAll, pois provavelmente é mais lento do que element.matches, mas na maioria dos casos não deve ser um problema. Vejo que leva cerca de 0,001 milissegundos.

Eu também encontrei a biblioteca CSSUtilities que anuncia que pode fazer isso, mas eu sinto que é antiga e não é atualizada há algum tempo. Olhando para o seu código-fonte, me faz pensar que pode haver casos que ele pode perder.

cagdas_ucar
fonte
CSSUtilities é muito antigo, mas também retorna as regras para pseudo-estados (por exemplo, pode retornar regras de flutuação). Não encontrei nenhuma resposta aqui que trate do pseudo estado ainda.
mr1031011