Знайдіть усі правила CSS, що застосовуються до елемента


87

Багато інструментів / API надають способи вибору елементів певних класів або ідентифікаторів. Також є можливість перевірити необроблені таблиці стилів, завантажені браузером.

Однак, щоб браузери відображали елемент, вони компілюють усі правила CSS (можливо, з різних файлів таблиць стилів) і застосовують його до елемента. Це те, що ви бачите у Firebug або WebKit Inspector - повному дереві успадкування CSS для елемента.

Як я можу відтворити цю функцію в чистому JavaScript, не вимагаючи додаткових плагінів браузера?

Можливо, приклад може дати деякі пояснення щодо того, що я шукаю:

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

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

Тут до елемента опису p # застосовано два правила CSS: червоний колір та розмір шрифту 20 пікселів.

Я хотів би знайти джерело, звідки беруть початок ці обчислювані правила CSS (колір - правило p тощо).



Переглянути в браузері та користуватися інструментами розробника браузера (наприклад, вкладка Елементи в Chrome)?
Ронні Ройстон,

Відповіді:


77

Оскільки це запитання наразі не має легкої (не бібліотечної) відповіді, сумісної з переглядачами, я спробую надати її:

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/

Виклик css(document.getElementById('elementId'))поверне масив з елементом для кожного правила CSS, що відповідає переданому елементу. Якщо ви хочете дізнатись більш конкретну інформацію про кожне правило, перегляньте документацію до об’єкта CSSRule .


1
a.matchesвизначається в цьому рядку: a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector. Це означає, що якщо вже існує (стандартний) метод "збігів" для DOM-вузлів, він буде використовувати цей, інакше він намагається використовувати специфічний Webkit (webkitMatchesSelector), а потім - Mozilla, Microsoft та Opera. Детальніше про це ви можете прочитати тут: developer.mozilla.org/en/docs/Web/API/Element/matches
SB

3
На жаль, я думаю, що ця альтернатива не виявляє всіх правил CSS, які каскадуються з батьківських елементів у дітей. Скрипка: jsfiddle.net/t554xo2L У цьому випадку правило UL (яке застосовується до елемента) не відповідає if (a.matches(rules[r].selectorText))умові захисту.
funforums

2
Я ніколи не стверджував, що в ньому перелічені / успадковані / правила CSS - все, що він робить, це перелік правил CSS, які відповідають переданому елементу. Якщо ви також хочете отримати успадковані правила для цього елемента, вам, мабуть, потрібно пройти DOM вгору і викликати css()кожен з батьківських елементів.
SB

2
Я знаю :-) Я просто хотів зазначити це, оскільки люди, які могли б розглянути це питання, могли б припустити, що він отримує "всі правила css, що застосовуються до елемента", як сказано в назві запитання, але це не так .
funforums

3
Якщо ви хочете, щоб усі правила, які в даний час застосовуються до елемента, включаючи успадковані, вам слід використовувати getComputedStyle. У світлі цього, я вважаю, що ця відповідь правильна і правильно не включати стилі, успадковані від батьків (наприклад, колір тексту, присвоєний батькові). Однак сюди не входять правила, які застосовуються умовно до медіа-запитів.
трембі

23

EDIT: Ця відповідь застаріла і більше не працює в Chrome 64+ . Залишаючи для історичного контексту. Фактично цей звіт про помилку посилається на це питання, щоб знайти альтернативні варіанти його використання.


Здається, мені вдалося відповісти на власне запитання після чергової години досліджень.

Це так просто:

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

(Працює в WebKit / Chrome, можливо і в інших)


4
Ну, це не надто корисно, якщо це підтримується лише хромом. Це буде працювати менш ніж на 5% усіх відвідувачів (залежно від демографічних показників).
Томасі,

5
@diamandiev: Станом на червень 2012 року частка використання Chrome зросла до понад 32% (і трохи вище, ніж використання IE!). gs.statcounter.com
Рой Тінкер,

6
getMatchedCSSRules НЕ показує вам остаточні стилі, що застосовуються до елемента. Він повертає масив усіх об'єктів CSSStyleRule, які застосовуються в тому порядку, в якому вони з'являються. Якщо ви виконуєте адаптивний веб-дизайн за допомогою медіа-запитів CSS або завантажуєте більше однієї таблиці стилів (наприклад, для IE), вам все одно потрібно прокрутити кожен із повернутих стилів та обчислити специфіку css для кожного правила. Потім обчисліть остаточні правила, які застосовуються. Вам потрібно відтворити те, що браузер робить природним шляхом. Щоб довести це у своєму прикладі, додайте "p {color: blue! Important}" до початку декларації стилю.
mrbinky3000

24
Наразі це застаріло в Chrome 41. Див. Code.google.com/p/chromium/issues/detail?id=437569#c2 .
Даніель Дарабос,

5
Це нарешті було видалено в Chrome 63 (офіційна публікація в блозі - що вказує на це питання)
brichins

19

Погляньте на цю бібліотеку, яка виконує те, про що просили: http://www.brothercake.com/site/resources/scripts/cssutilities/

Він працює у всіх сучасних браузерах ще до IE6, може надати вам колекції правил та властивостей, таких як Firebug (насправді це точніше, ніж Firebug), а також може обчислити відносну або абсолютну специфічність будь-якого правила. Єдине застереження полягає в тому, що, хоча він розуміє статичні типи носіїв, він не розуміє медіа-запити.


Цей модуль справді чудовий, сподіваюся, він отримає більше любові від автора.
mr1031011

18

Коротка версія 12 квітня 2017 р

З'являється претендент.

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

Рядок /* 1 */будує плоский масив усіх правил.
Рядок /* 2 */відкидає невідповідні правила.

На основі функціїcss(el) @SB на тій самій сторінці.

Приклад 1

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

Приклад 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>

Недоліки

  • Ні обробки медіа, немає @import, @media.
  • Немає доступу до стилів, завантажених із міждоменних таблиць стилів.
  • Відсутність сортування за “специфічністю” селектора (порядок важливості).
  • Жодних стилів, успадкованих від батьків.
  • Може не працювати зі старими або елементарними браузерами.
  • Не впевнений, як він справляється з псевдокласами та псевдоселекторами, але, здається, це нормально.

Можливо, колись я усуну ці недоліки.

Довга версія 12 серпня 2018 року

Ось набагато повніша реалізація, взята з чиєїсь сторінки GitHub (розгалужена з цього оригінального коду через Bugzilla ). Написано для Gecko та IE, але, за чутками, працює також із Blink.

4 травня 2017 р .: Калькулятор специфічності мав критичні помилки, які я зараз виправив. (Я не можу повідомити авторів, оскільки у мене немає облікового запису GitHub.)

12 серпня 2018 року: Останні оновлення Chrome, схоже, роз’єднали область об’єкта ( this) від методів, призначених незалежним змінним. Тому виклик matcher(selector)перестав працювати. Заміна його на matcher.call(el, selector)це вирішила.

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

Виправлені помилки

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

У getSheetRules мені довелося додати if (stylesheet.cssRules === null) {return []}, щоб він працював для мене.
Gwater17,

Перевірено "довгу версію". Працює для мене. Шкода, що getMatchedCSSRules () ніколи не стандартизувався браузерами.
colin moock

Як це обробляє два селектори з однаковими особливостями, такі як h1 та h1, div - де слід використовувати той, який оголошено останнім?
Стеллан

PE Можливо, ми можемо отримати якусь ідею щодо обробки псевдо тут? github.com/dvtng/jss/blob/master/jss.js
mr1031011

4

Ось версія відповіді SB, яка також повертає правила відповідності у відповідних медіа-запитах. Я видалив *.rules || *.cssRulesзрощення та .matchesпошук реалізації; додайте поліфіл або додайте ці рядки назад, якщо вони вам потрібні.

Ця версія також повертає CSSStyleRuleоб'єкти, а не текст правила. Я думаю, це трохи корисніше, оскільки специфіку правил таким чином можна легше програмно дослідити.

Кава:

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

Як це можна було б змінити для використання і на дітях померлих element?
Крагалон

2
Який ви використовуєте? Я насправді не розумію, де це було б корисно, оскільки правила, які стосуються дітей, не обов'язково стосуються і батьків. Ви просто отримаєте купу правил, у яких нічого конкретно спільного. Якщо ви дійсно хочете, щоб ви могли просто повторити над дітьми і запустити цей метод для кожного, і створити масив усіх результатів.
трембі

Я просто намагаюся зробити cloneNode(true)функціональність, але також із глибоким клонуванням стилю.
Крагалон,

1
ця умова: якщо (window.matchMedia (rule.conditionText) .matches) {...} запобігло збігу в моєму випадку, оскільки "rule.conditionText" не було визначено. Без цього це спрацювало. Ви можете спробувати і протестувати це на news.ycombinator.com . "span.pagetop b" має правило медіа-запиту, яке не відповідає вашій функції, як воно є.
ayal gelles

1
Chrome не підтримує властивість conditionText у примірниках CSSMediaRule.
Macil 02

3

Ось моя версія getMatchedCSSRulesфункції, яка підтримує @mediaзапит.

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
Безглуздий дублікат старої версії моєї відповіді. Просто забруднює сторінку. Повна та сучасна версія: тут .
7vujy0f0hy

1

Забезпечуючи IE9 +, я написав функцію, яка обчислює CSS для запитуваного елемента та його дочірніх елементів і дає можливість зберегти його до нового className, якщо це потрібно, у фрагменті нижче.

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

Використання

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

2
1. Ви можете замінити всю computeStylesпідпрограму просто el => getComputedStyle(el).cssText. Доказ: скрипка . 2. '.' + element.className є несправною конструкцією, оскільки передбачає наявність однієї назви класу. Дійсне будівництво element.className.replace(/^| /g, '.'). 3. Ваша функція ігнорує можливість інших селекторів CSS, ніж просто класи. 4. Ваша рекурсія довільно обмежена одним рівнем (діти, але не онуки). 5. Використання: немає getElementByClassName, лише getElementsByClassName(повертає масив).
7vujy0f0hy

1

Я думаю, що відповідь SB повинна бути прийнятою на даний момент, але вона не є точною. Кілька разів згадується, що існуватимуть деякі правила, які можна пропустити. Зіткнувшись із цим, я вирішив використовувати document.querySelectorAll замість element.matches. Єдине, вам знадобиться якась унікальна ідентифікація елементів, щоб порівняти її з тією, яку ви шукаєте. У більшості випадків я думаю, що це можна досягти, встановивши його ідентифікатор як унікальне значення. Ось як ви можете визначити відповідний елемент вашим. Якщо ви можете придумати загальний спосіб зіставлення результату document.querySelectorAll з елементом, який ви шукаєте, це, по суті, було б повне заповнення getMatchedCSSRules.

Я перевірив продуктивність для document.querySelectorAll, оскільки він, ймовірно, повільніший за element.matches, але в більшості випадків це не повинно бути проблемою. Я бачу, що це займає близько 0,001 мілісекунд.

Я також знайшов бібліотеку CSSUtilities, яка рекламує, що вона може це зробити, але я відчуваю її стару і не оновлювалась деякий час. Дивлячись на його вихідний код, мені здається, що можуть бути випадки, коли він пропускає.


CSSUtilities дійсно старий, але він також повертає правила для псевдо станів (наприклад, він може повертати правила наведення). Я поки що не знайшов відповіді, яка стосується псевдо стану.
mr1031011
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.