Last active
June 10, 2024 19:52
-
-
Save stracker-phil/4ba0b9b9ad67dff268c9130f2fdbb473 to your computer and use it in GitHub Desktop.
Highly optimised and accurate alternative to the deprecated `getMatchedCSSRules()` method. StackOverflow question: https://stackoverflow.com/questions/66078682/get-the-css-definition-of-an-element/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Scans all CSS rules in the current document to find the most | |
* specific definiton of a single CSS property for a given element. | |
* | |
* Usage: getStyleDef('#my-element', 'width'); | |
* --> returns the most specific "width" defintiion, e.g. "27em". | |
* | |
* @param {HTMLElement} element - The HTML Element to inspect. | |
* @param {string} prop - The CSS property to inspect. | |
* @return {string} The most specific CSS definition, | |
* or an empty string. | |
*/ | |
function getStyleDef(element, prop) { | |
let result = ''; | |
let highestSpecificity = false; | |
const allRules = []; | |
const relevantRules = []; | |
// Translate a selector string into an HTMLElement. | |
if ('string' === typeof element) { | |
element = document.querySelector(element); | |
} | |
// Translate a jQuery element to a HTMLElement. | |
if (element && element instanceof jQuery) { | |
element = element.get(0); | |
} | |
// Bail, if no valid element is specified. | |
if (!element || !(element instanceof HTMLElement)) { | |
return ''; | |
} | |
// When an inline style is set, always return it. | |
if (element.style && '' !== element.style[prop]) { | |
return element.style[prop]; | |
} | |
// Parse all stylesheet entries to find relevant CSS rules. | |
_getRelevantRules(element); | |
// Loop all relevant rules to find the most specific one. | |
for (let i = relevantRules.length - 1; i >= 0; i--) { | |
// Is there a rule for the required property? | |
if ('' === relevantRules[i].style[prop]) { | |
continue; | |
} | |
// Determine the specificity of the given CSS selector. | |
const specificity = _getCssSpecificity( | |
relevantRules[i].selectorText, | |
relevantRules[i].style[prop] | |
); | |
// Find the most specific CSS definition. | |
if (_compareCssSpecificity(specificity, highestSpecificity) > 0) { | |
highestSpecificity = specificity; | |
result = relevantRules[i].style[prop]; | |
} | |
} | |
return result; | |
// -- Helper functions follow -- | |
/** | |
* Convert an array-like object to array. | |
* | |
* @param {Iterable} list - A value-list that can be converted to an array. | |
* @return {array} An array representation of the list, or an empty array. | |
*/ | |
function _toArray(list) { | |
if ('undefined' === typeof list || null === list) { | |
return []; | |
} | |
try { | |
if ('function' === typeof Array.from) { | |
return Array.from(list); | |
} else { | |
return [].slice.call(list); | |
} | |
} catch (exception) { | |
// In case the list cannot be converted, return an empty array. | |
return []; | |
} | |
} | |
/** | |
* Handles extraction of `cssRules` as an `Array` from a stylesheet | |
* or something that behaves the same. | |
* | |
* The rules are prepended to the allRules array. This function does | |
* not return a value. | |
* | |
* @param {CSSStyleSheet} stylesheet - The stylesheet to parse. | |
*/ | |
function _extractSheetRules(stylesheet) { | |
try { | |
// Skip disabled rules. | |
if (stylesheet.disabled) { | |
return; | |
} | |
// Skip rules that do not match the current viewport. | |
const media = stylesheet.media; | |
if (media && !matchMedia(media.mediaText).matches) { | |
return; | |
} | |
// Prepend the rules to the `allRules` array. | |
Array.prototype.unshift.apply(allRules, _toArray(stylesheet.cssRules)); | |
} catch (exception) { | |
/* | |
* CORS prevents us from accessing rules from other domains, such | |
* as google font styles. | |
*/ | |
} | |
} | |
/** | |
* Parses all stylesheets and populates the `relevantRules` array with CSS | |
* rules that match the given element AND the current viewport size. | |
* | |
* @param {HTMLElement} element - The element to inspect. | |
* @private | |
*/ | |
function _getRelevantRules(element) { | |
let rule; | |
// assuming the browser hands us stylesheets in order of appearance | |
// we iterate them from the beginning to follow proper cascade order | |
for (let i = 0; i < document.styleSheets.length; i++) { | |
// Extract the style rules of this sheet into `allRules`. | |
_extractSheetRules(document.styleSheets[i]); | |
// Loop the rules in order of appearance. | |
while (rule = allRules.shift()) { | |
if (rule.styleSheet) { | |
// Insert the `@import`ed stylesheet's rules at the | |
// beginning of this stylesheet's rules. | |
_extractSheetRules(rule.styleSheet); | |
// ... and skip the rest of this rule. | |
continue; | |
} | |
// If there's no stylesheet attribute BUT there IS a | |
// media attribute it's a media rule. | |
if (rule.media) { | |
// insert the contained rules of this media rule to | |
// the beginning of this stylesheet's rules. | |
_extractSheetRules(rule); | |
// ... and skip the rest it. | |
continue; | |
} | |
// check if this element matches this rule's selector | |
if (element.matches(rule.selectorText)) { | |
// push the rule to the results set | |
relevantRules.push(rule); | |
} | |
} | |
} | |
} | |
/** | |
* Calculates the CSS specificity of a CSS selector. | |
* | |
* @param input - The CSS selector. | |
* @param attribValue - Optional. A specific CSS attribute value. Only used | |
* to properly recognize `!important` values. | |
* @return {array} Always an array with 4 elements. | |
*/ | |
function _getCssSpecificity(input, attribValue) { | |
const token = input.split(','); | |
if (token.length > 1) { | |
let result = []; | |
let singleSpecificity; | |
for (let i = 0; i < token.length; i++) { | |
singleSpecificity = _getCssSpecificity(token[i]); | |
if (_compareCssSpecificity(singleSpecificity, result) > 0) { | |
result = singleSpecificity; | |
} | |
} | |
return result; | |
} | |
let selector = input, | |
findMatch, | |
typeCount = { | |
'a': 0, | |
'b': 0, | |
'c': 0 | |
}, | |
// The following regular expressions assume that selectors matching the | |
// preceding regular expressions have been removed. | |
attributeRegex = /(\[[^\]]+\])/g, | |
idRegex = /(#[^\#\s\+>~\.\[:\)]+)/g, | |
classRegex = /(\.[^\s\+>~\.\[:\)]+)/g, | |
pseudoElementRegex = /(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi, | |
// A regex for pseudo classes with brackets - :nth-child(), | |
// :nth-last-child(), :nth-of-type(), :nth-last-type(), :lang() | |
// The negation pseudo class (:not) is filtered out because specificity | |
// is calculated on its argument. | |
// :global and :local are filtered out - they look like pseudo classes | |
// but are an identifier for CSS Modules. | |
pseudoClassWithBracketsRegex = /(:(?!not|global|local)[\w-]+\([^\)]*\))/gi, | |
// A regex for other pseudo classes, which don't have brackets | |
pseudoClassRegex = /(:(?!not|global|local)[^\s\+>~\.\[:]+)/g, | |
elementRegex = /([^\s\+>~\.\[:]+)/g, | |
isImportant; | |
isImportant = 'string' === typeof attribValue && attribValue.indexOf('!important') > 0; | |
// Find matches for a regular expression in a string and push their details | |
// to parts. Type is "a" for IDs, "b" for classes, attributes and pseudo- | |
// classes and "c" for elements and pseudo-elements. | |
findMatch = function (regex, type) { | |
let matches, i, len, match, index, length; | |
if (regex.test(selector)) { | |
matches = selector.match(regex); | |
for (i = 0, len = matches.length; i < len; i += 1) { | |
typeCount[type] += 1; | |
match = matches[i]; | |
index = selector.indexOf(match); | |
length = match.length; | |
// Replace this simple selector with whitespace so it won't be | |
// counted in further simple selectors. | |
selector = selector.replace(match, Array(length + 1).join(' ')); | |
} | |
} | |
}; | |
// Replace escaped characters with plain text, using the "A" character | |
// https://www.w3.org/TR/CSS21/syndata.html#characters | |
(function () { | |
const replaceWithPlainText = function (regex) { | |
let matches, i, len, match; | |
if (regex.test(selector)) { | |
matches = selector.match(regex); | |
for (i = 0, len = matches.length; i < len; i += 1) { | |
match = matches[i]; | |
selector = selector.replace(match, Array(match.length + 1).join('A')); | |
} | |
} | |
}, | |
// Matches a backslash followed by six hexadecimal digits followed | |
// by an optional single whitespace character. | |
escapeHexadecimalRegex = /\\[0-9A-Fa-f]{6}\s?/g, | |
// Matches a backslash followed by fewer than six hexadecimal digits | |
// followed by a mandatory single whitespace character. | |
escapeHexadecimalRegex2 = /\\[0-9A-Fa-f]{1,5}\s/g, | |
// Matches a backslash followed by any character. | |
escapeSpecialCharacter = /\\./g; | |
replaceWithPlainText(escapeHexadecimalRegex); | |
replaceWithPlainText(escapeHexadecimalRegex2); | |
replaceWithPlainText(escapeSpecialCharacter); | |
}()); | |
// Remove anything after a left brace in case a user has pasted in a rule, not just a | |
// selector | |
(function () { | |
let regex = /{[^]*/gm, | |
matches, i, len, match; | |
if (regex.test(selector)) { | |
matches = selector.match(regex); | |
for (i = 0, len = matches.length; i < len; i += 1) { | |
match = matches[i]; | |
selector = selector.replace(match, Array(match.length + 1).join(' ')); | |
} | |
} | |
}()); | |
// Add attribute selectors to parts collection (type b) | |
findMatch(attributeRegex, 'b'); | |
// Add ID selectors to parts collection (type a) | |
findMatch(idRegex, 'a'); | |
// Add class selectors to parts collection (type b) | |
findMatch(classRegex, 'b'); | |
// Add pseudo-element selectors to parts collection (type c) | |
findMatch(pseudoElementRegex, 'c'); | |
// Add pseudo-class selectors to parts collection (type b) | |
findMatch(pseudoClassWithBracketsRegex, 'b'); | |
findMatch(pseudoClassRegex, 'b'); | |
// Remove universal selector and separator characters | |
selector = selector.replace(/[\*\s\+>~]/g, ' '); | |
// Remove any stray dots or hashes which aren't attached to words | |
// These may be present if the user is live-editing this selector | |
selector = selector.replace(/[#\.]/g, ' '); | |
// Remove the negation pseudo-class (:not) but leave its argument because | |
// specificity is calculated on its argument. Remove non-standard :local and | |
// :global CSS Module identifiers because they do not effect the specificity. | |
selector = selector.replace(/:not/g, ' '); | |
selector = selector.replace(/:local/g, ' '); | |
selector = selector.replace(/:global/g, ' '); | |
selector = selector.replace(/[\(\)]/g, ' '); | |
// The only things left should be element selectors (type c) | |
findMatch(elementRegex, 'c'); | |
return [isImportant ? 1 : 0, typeCount.a, typeCount.b, typeCount.c]; | |
} | |
/** | |
* Compares two CSS Specificity terms to determine, which one is more | |
* specific. | |
* | |
* @param a - The first term | |
* @param b - The second term. | |
* @returns {number} 0 if both are equally specific. +1 if a is more | |
* specific, -1 if b is more specific. | |
*/ | |
function _compareCssSpecificity(a, b) { | |
for (let i = 0; i < 4; i += 1) { | |
const valA = parseInt(isNaN(a[i]) ? 0 : a[i]); | |
const valB = parseInt(isNaN(b[i]) ? 0 : b[i]); | |
if (valA < valB) { | |
return -1; | |
} else if (valA > valB) { | |
return 1; | |
} | |
} | |
return 0; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Error:
jQuery is not defined
Alternative: https://stackoverflow.com/questions/2952667/find-all-css-rules-that-apply-to-an-element