Найдите все правила 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 # description применяются два правила CSS: красный цвет и размер шрифта 20 пикселей.

Я хотел бы найти источник, из которого происходят эти вычисленные правила CSS (цвет соответствует правилу p и так далее).

cgbystrom
источник
Просмотреть в браузере и использовать инструменты разработчика браузера (например, вкладку «Элементы» в 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 .

SB
источник
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, которые переходят из родительских элементов в дочерние элементы. Fiddle: jsfiddle.net/t554xo2L В этом случае правило UL (которое применяется к элементу) не соответствует if (a.matches(rules[r].selectorText))условию защиты.
funforums
2
Я никогда не утверждал, что он перечисляет / наследует / правила CSS - все, что он делает, это список правил CSS, соответствующих переданному элементу. Если вы хотите получить унаследованные правила для этого элемента, вам, вероятно, потребуется пройти по DOM вверх и вызвать css()каждый из родительских элементов.
SB
2
Я знаю :-) Я просто хотел указать на это, так как люди, которые могут изучить этот вопрос, могут предположить, что он получает «все правила css, которые применяются к элементу», как сказано в заголовке вопроса, что не так .
funforums
3
Если вы хотите, чтобы к элементу применялись все правила, в том числе унаследованные, вам следует использовать getComputedStyle. В свете этого, я думаю, что этот ответ верен и правильно не включает стили, унаследованные от родителей (например, цвет текста, назначенный родителю). Однако он не включает правила, условно применяемые к медиа-запросам.
tremby
23

РЕДАКТИРОВАТЬ: этот ответ устарел и больше не работает в Chrome 64+ . Уходя в исторический контекст. Фактически, этот отчет об ошибке ссылается на этот вопрос для поиска альтернативных решений для его использования.


Кажется, мне удалось ответить на свой вопрос после еще одного часа исследования.

Это так просто:

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

(Работает в WebKit / Chrome, возможно, и в других)

cgbystrom
источник
4
Что ж, это бесполезно, если поддерживается только хромом. Это будет работать менее чем для 5% всех посетителей (в зависимости от демографии).
Tomasi
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
17

Краткая версия 12 апреля 2017 г.

Появляется Челленджер.

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

Line /* 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)
7vujy0f0hy
источник
В getSheetRules мне пришлось добавить if (stylesheet.cssRules === null) {return []}, чтобы заставить его работать на меня.
Gwater17
Тестировал "Длинную версию". Работает для меня. Жаль, что getMatchedCSSRules () никогда не стандартизировался браузерами.
Colin Moock
Как это обрабатывает два селектора с одинаковыми характеристиками, например, h1 и h1, div - где следует использовать последний, объявленный?
Стеллан,
Может быть, здесь мы сможем получить представление о том, как обрабатывать псевдо? 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?
Kragalon
2
Какой у вас вариант использования? Я действительно не понимаю, где это было бы полезно, поскольку правила, применимые к детям, не обязательно применимы к родителям. В итоге вы получите кучу правил, в которых нет ничего общего. Если вы действительно хотите, чтобы вы могли просто рекурсивно перебирать дочерние элементы и запускать этот метод для каждого из них, а также создавать массив всех результатов.
tremby
Я просто пытаюсь сделать cloneNode(true)функциональность, но с глубоким клонированием стиля.
Kragalon
1
это условие: if (window.matchMedia (rule.conditionText) .matches) {...} предотвратило совпадение в моем случае, поскольку "rule.conditionText" не было определено. Без него все заработало. Вы можете попробовать и протестировать это на news.ycombinator.com . "span.pagetop b" имеет правило медиа-запроса, которое не соответствует вашей функции как таковой.
аял гельлес
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
}
user3896501
источник
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, которая рекламирует, что она может это делать, но я чувствую, что она старая и давно не обновлялась. Глядя на его исходный код, я думаю, что могут быть случаи, когда он упускается.

cagdas_ucar
источник
CSSUtilities действительно старый, но он также возвращает правила для псевдосостояний (например, он может возвращать правила наведения). Я еще не нашел здесь ответа, касающегося псевдосостояния.
mr1031011