Znajdź wszystkie reguły CSS, które mają zastosowanie do elementu


87

Wiele narzędzi / interfejsów API udostępnia sposoby wyboru elementów określonych klas lub identyfikatorów. Istnieje również możliwość przejrzenia surowych arkuszy stylów załadowanych przez przeglądarkę.

Jednak aby przeglądarki mogły renderować element, skompilują wszystkie reguły CSS (prawdopodobnie z różnych plików arkuszy stylów) i zastosują je do elementu. Oto, co widzisz w Firebug lub WebKit Inspector - pełnym drzewie dziedziczenia CSS dla elementu.

Jak mogę odtworzyć tę funkcję w czystym języku JavaScript bez konieczności stosowania dodatkowych wtyczek do przeglądarki?

Być może przykład może wyjaśnić, czego szukam:

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

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

Tutaj element p # description ma zastosowane dwie reguły CSS: kolor czerwony i rozmiar czcionki 20 pikseli.

Chciałbym znaleźć źródło, z którego pochodzą te obliczone reguły CSS (kolor pochodzi z reguły p itd.).



Wyświetlić w przeglądarce i użytkować narzędzia programistyczne przeglądarki (np. Zakładkę Elementy w przeglądarce Chrome)?
Ronnie Royston,

Odpowiedzi:


77

Ponieważ to pytanie nie ma obecnie lekkiej (nie będącej biblioteką) odpowiedzi zgodnej z różnymi przeglądarkami, spróbuję podać jedną:

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/

Wywołanie css(document.getElementById('elementId'))zwróci tablicę z elementem dla każdej reguły CSS, która pasuje do przekazanego elementu. Jeśli chcesz uzyskać bardziej szczegółowe informacje o każdej regule, zapoznaj się z dokumentacją obiektu CSSRule .


1
a.matchesjest zdefiniowana w tym wierszu: a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector. Oznacza to, że jeśli istnieje już (standardowa) metoda "dopasowań" dla węzłów DOM, użyje jej, w przeciwnym razie spróbuje użyć specyficznego dla Webkita (webkitMatchesSelector), a następnie Mozilli, Microsoft i Opera. Możesz przeczytać więcej na ten temat tutaj: developer.mozilla.org/en/docs/Web/API/Element/matches
SB

3
Niestety myślę, że ta alternatywa nie wykrywa wszystkich reguł CSS, które są kaskadowe z elementów nadrzędnych u dzieci. Fiddle: jsfiddle.net/t554xo2L W tym przypadku reguła UL (która dotyczy elementu) nie jest dopasowywana do if (a.matches(rules[r].selectorText))warunku ochronnego.
funforum

2
Nigdy nie twierdziłem, że zawiera listę / dziedziczenie / reguły CSS - wszystko, co robi, to lista reguł CSS, które pasują do przekazanego elementu. Jeśli chcesz uzyskać odziedziczone reguły również dla tego elementu, prawdopodobnie musisz przejść przez DOM w górę i wywołać css()każdy z elementów nadrzędnych.
SB

2
Wiem :-) Chciałem tylko zwrócić na to uwagę, ponieważ osoby, które mogłyby przyjrzeć się temu pytaniu, mogą założyć, że dostaje „wszystkie reguły CSS, które mają zastosowanie do elementu”, jak mówi tytuł pytania, a tak nie jest .
funforum

3
Jeśli chcesz, aby wszystkie reguły były obecnie zastosowane do elementu, w tym dziedziczone, powinieneś używać getComputedStyle. W związku z tym uważam, że ta odpowiedź jest poprawna i słuszna jest nieuwzględnianie stylów odziedziczonych po rodzicach (na przykład kolor tekstu przypisany rodzicowi). Nie obejmuje jednak reguł stosowanych warunkowo w zapytaniach o media.
tremby

24

EDYTUJ: ta odpowiedź została wycofana i nie działa już w przeglądarce Chrome 64+ . Pozostawiając kontekst historyczny. W rzeczywistości raport o błędzie zawiera odnośniki z powrotem do tego pytania w celu znalezienia alternatywnych rozwiązań.


Wydaje się, że po kolejnej godzinie badań udało mi się odpowiedzieć na własne pytanie.

To takie proste:

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

(Działa w WebKit / Chrome, prawdopodobnie także w innych)


4
Cóż, nie jest to zbyt użyteczne, jeśli jest obsługiwane tylko przez Chrome. Będzie działać dla mniej niż 5% wszystkich odwiedzających (w zależności od demografii).
Tomasi

5
@diamandiev: Od czerwca 2012 r. udział użytkowania Chrome wzrósł do ponad 32% (i jest nieco wyższy niż w IE!). gs.statcounter.com
Roy Tinker

6
getMatchedCSSRules NIE pokazuje ostatecznych stylów, które mają zastosowanie do elementu. Zwraca tablicę wszystkich obiektów CSSStyleRule, które mają zastosowanie w kolejności, w jakiej się pojawiają. Jeśli projektujesz responsywne strony internetowe za pomocą zapytań o media CSS lub ładujesz więcej niż jeden arkusz stylów (na przykład jeden dla IE), nadal musisz przejrzeć każdy ze zwróconych stylów i obliczyć specyficzność CSS dla każdej reguły. Następnie oblicz ostateczne zasady, które mają zastosowanie. Musisz odtworzyć to, co robi przeglądarka w sposób naturalny. Aby to udowodnić w swoim przykładzie, dodaj „p {color: blue! Important}” na początku deklaracji stylu.
mrbinky3000

24
To jest teraz wycofane w Chrome 41. Zobacz code.google.com/p/chromium/issues/detail?id=437569#c2 .
Daniel Darabos,

5
To zostało ostatecznie usunięte w Chrome 63 (oficjalny post na blogu - co wskazuje na to pytanie)
brichins

20

Wersja skrócona 12 kwietnia 2017 r

Pojawia się Challenger.

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

Line /* 1 */tworzy płaską tablicę wszystkich reguł.
Linia /* 2 */odrzuca niezgodne reguły.

Na podstawie funkcjicss(el) @SB na tej samej stronie.

Przykład 1

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

Przykład 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>

Niedociągnięcia

  • Nie manipulacja mediów, no @import, @media.
  • Brak dostępu do stylów ładowanych z arkuszy stylów międzydomenowych.
  • Brak sortowania według „specyfiki” selektora (kolejność ważności).
  • Brak stylów odziedziczonych po rodzicach.
  • Może nie działać ze starymi lub podstawowymi przeglądarkami.
  • Nie jestem pewien, jak radzi sobie z pseudoklasami i pseudo-selektorami, ale wydaje się, że działa dobrze.

Może kiedyś zajmę się tymi niedociągnięciami.

Wersja długa 12 sierpnia 2018 r

Oto znacznie bardziej kompleksowa implementacja zaczerpnięta z czyjejś strony GitHub (rozwidlona z tego oryginalnego kodu za pośrednictwem Bugzilli ). Napisany dla Gecko i IE, ale podobno działa również z Blink.

4 maja 2017: Kalkulator specyficzności zawierał krytyczne błędy, które teraz naprawiłem. (Nie mogę powiadomić autorów, ponieważ nie mam konta GitHub).

12 sierpnia 2018: Wydaje się, że w ostatnich aktualizacjach Chrome oddzielono zakres obiektu ( this) od metod przypisanych do zmiennych niezależnych. Dlatego inwokacja matcher(selector)przestała działać. Zastąpienie go matcher.call(el, selector)rozwiązało.

// 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);
        };
}

Naprawione błędy

  • = match+= match
  • return re ? re.length : 0;return matches ? matches.length : 0;
  • _matchesSelector(element, selector)matchesSelector(element, selector)
  • matcher(selector)matcher.call(el, selector)

W getSheetRules musiałem dodać if (stylesheet.cssRules === null) {return []}, aby działało dla mnie.
Gwater17

Przetestowano „wersję długą”. Pracuje dla mnie. Szkoda, że ​​metoda getMatchedCSSRules () nigdy nie została ujednolicona przez przeglądarki.
colin moock

Jak to radzi sobie z dwoma selektorami o tej samej specyfice, jak h1 i h1, div - gdzie powinien być używany ten zadeklarowany jako ostatni?
Stellan

PE Może tu wpadniemy na jakiś pomysł na obsługę pseudo? github.com/dvtng/jss/blob/master/jss.js
mr1031011

19

Zajrzyj do tej biblioteki, która robi to, o co proszono: http://www.brothercake.com/site/resources/scripts/cssutilities/

Działa we wszystkich nowoczesnych przeglądarkach aż do IE6, może udostępniać zbiory reguł i właściwości, takie jak Firebug (w rzeczywistości jest dokładniejsze niż Firebug), a także może obliczyć względną lub absolutną specyficzność dowolnej reguły. Jedynym zastrzeżeniem jest to, że chociaż rozumie statyczne typy mediów, nie rozumie zapytań o media.


Ten moduł jest naprawdę świetny, miej tylko nadzieję, że autor będzie go bardziej doceniał.
mr1031011

4

Oto wersja odpowiedzi SB, która również zwraca reguły dopasowania w ramach pasujących zapytań o media. I usunęliśmy *.rules || *.cssRuleskoalescencji i .matchesFinder realizacji; dodaj wypełnienie lub dodaj te linie z powrotem, jeśli ich potrzebujesz.

Ta wersja zwraca również CSSStyleRuleobiekty, a nie tekst reguły. Myślę, że jest to trochę bardziej przydatne, ponieważ specyfikę reguł można w ten sposób łatwiej zbadać programowo.

Kawa:

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;
}

Jak można to zmienić, aby zastosować je również na dzieciach minionych lat element?
Kragalon

2
Jaki jest Twój przypadek użycia? Naprawdę nie widzę, gdzie to byłoby przydatne, ponieważ zasady, które dotyczą dzieci, niekoniecznie odnoszą się do rodzica. Po prostu skończyłbyś ze stosem zasad, które nie mają ze sobą nic wspólnego. Jeśli naprawdę chcesz, możesz po prostu powtórzyć operacje potomne i uruchomić tę metodę dla każdego z nich, a następnie zbudować tablicę wszystkich wyników.
tremby

Po prostu staram się uzyskać cloneNode(true)funkcjonalność, ale z głębokim klonowaniem stylizacji.
Kragalon

1
ten warunek: jeśli (window.matchMedia (rule.conditionText) .matches) {...} uniemożliwiło dopasowanie w moim przypadku, ponieważ „rule.conditionText” była niezdefiniowana. Bez tego to działało. Możesz spróbować i przetestować to na news.ycombinator.com . „span.pagetop b” zawiera regułę zapytania o media, która nie pasuje do Twojej funkcji.
ayal gelles

1
Chrome nie obsługuje właściwości conditionText w instancjach CSSMediaRule.
Macil

3

Oto moja wersja getMatchedCSSRulesfunkcji obsługującej @mediazapytanie.

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
}

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>


3
Bezcelowy duplikat starej wersji mojej odpowiedzi. Po prostu zanieczyszcza stronę. Kompletna i aktualna wersja: tutaj .
7vujy0f0hy

1

Zapewniając IE9 +, napisałem funkcję, która oblicza CSS dla żądanego elementu i jego dzieci oraz daje możliwość zapisania go do nowej nazwy klasy w razie potrzeby we fragmencie poniżej.

/**
  * @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;
}

Stosowanie

getElementStyles(document.getElementByClassName('.my-class'), '.dummy-class', 'width:100%;opaity:0.5;transform:scale(1.5);');

2
1. Możesz zastąpić cały computeStylespodprogram zwykłym el => getComputedStyle(el).cssText. Dowód: skrzypce . 2. '.' + element.className jest konstrukcją wadliwą, ponieważ zakłada istnienie jednej nazwy klasy. Prawidłowa konstrukcja to element.className.replace(/^| /g, '.'). 3. Twoja funkcja ignoruje możliwości innych selektorów CSS niż tylko klasy. 4. Twoja rekursja jest arbitralnie ograniczona do jednego poziomu (dzieci, ale nie wnuki). 5. Sposób użycia: nie ma getElementByClassName, tylko getElementsByClassName(zwraca tablicę).
7vujy0f0hy

1

Myślę, że odpowiedź SB powinna być w tym momencie akceptowana, ale nie jest dokładna. Wspomina się kilka razy, że będą pewne zasady, których można przeoczyć. W obliczu tego zdecydowałem się użyć document.querySelectorAll zamiast element.matches. Jedyną rzeczą jest to, że potrzebowałbyś jakiejś unikalnej identyfikacji elementów, aby porównać go z tym, którego szukasz. W większości przypadków wydaje mi się, że można to osiągnąć, ustawiając jego identyfikator na unikalną wartość. W ten sposób możesz zidentyfikować dopasowany element jako Twój. Jeśli możesz pomyśleć o ogólnym sposobie dopasowania wyniku document.querySelectorAll do elementu, którego szukasz, byłby to w zasadzie kompletny polyfill getMatchedCSSRules.

Sprawdziłem wydajność dla document.querySelectorAll, ponieważ prawdopodobnie jest wolniejsza niż element.matches, ale w większości przypadków nie powinno to stanowić problemu. Widzę, że zajmuje to około 0,001 milisekundy.

Znalazłem również bibliotekę CSSUtilities, która reklamuje, że może to zrobić, ale czuję, że jest stara i od jakiegoś czasu nie była aktualizowana. Patrząc na jego kod źródłowy, wydaje mi się, że mogą być przypadki, w których brakuje.


CSSUtilities jest naprawdę stary, ale zwraca również reguły dla pseudostanów (na przykład może zwrócić reguły najechania kursorem). Nie znalazłem tutaj żadnej odpowiedzi, która dotyczyłaby pseudo stanu.
mr1031011
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.