Find all CSS rules that apply to an element - javascript

Many tools/APIs provide ways of selecting elements of specific classes or IDs. There's also possible to inspect the raw stylesheets loaded by the browser.
However, for browsers to render an element, they'll compile all CSS rules (possibly from different stylesheet files) and apply it to the element. This is what you see with Firebug or the WebKit Inspector - the full CSS inheritance tree for an element.
How can I reproduce this feature in pure JavaScript without requiring additional browser plugins?
Perhaps an example can provide some clarification for what I'm looking for:
<style type="text/css">
p { color :red; }
#description { font-size: 20px; }
</style>
<p id="description">Lorem ipsum</p>
Here the p#description element have two CSS rules applied: a red color and a font size of 20 px.
I would like to find the source from where these computed CSS rules originate from (color comes the p rule and so on).

Since this question currently doesn't have a lightweight (non-library), cross-browser compatible answer, I'll try to provide one:
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/
Calling css(document.getElementById('elementId')) will return an array with an element for each CSS rule that matches the passed element.
If you want to find out more specific information about each rule, check out the CSSRule object documentation.

Short version12 April 2017
Challenger appears.
var getMatchedCSSRules = (el, css = el.ownerDocument.styleSheets) =>
[].concat(...[...css].map(s => [...s.cssRules||[]])) /* 1 */
.filter(r => el.matches(r.selectorText)); /* 2 */
Line /* 1 */ builds a flat array of all rules.
Line /* 2 */ discards non-matching rules.
Based on function css(el) by #S.B. on the same page.
Example 1
var div = iframedoc.querySelector("#myelement");
var rules = getMatchedCSSRules(div, iframedoc.styleSheets);
console.log(rules[0].parentStyleSheet.ownerNode, rules[0].cssText);
Example 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>
Shortcomings
No media handling, no #import, #media.
No access to styles loaded from cross-domain stylesheets.
No sorting by selector “specificity” (order of importance).
No styles inherited from parents.
May not work with old or rudimentary browsers.
Not sure how it copes with pseudo-classes and pseudo-selectors but seems to fare okay.
Maybe I will address these shortcomings one day.
Long version12 August 2018
Here’s a much more comprehensive implementation taken from someone’s GitHub page
(forked from this original code, via Bugzilla). Written for Gecko and IE, but is rumoured to work also with Blink.
4 May 2017: The specificity calculator has had critical bugs which I have now fixed. (I can’t notify the authors because I don’t have a GitHub account.)
12 August 2018: Recent Chrome updates seem to have decoupled object scope (this) from methods assigned to independent variables. Therefore invocation matcher(selector) has stopped working. Replacing it by matcher.call(el, selector) has solved it.
// 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);
};
}
Fixed bugs
= match → += match
return re ? re.length : 0; → return matches ? matches.length : 0;
_matchesSelector(element, selector) → matchesSelector(element, selector)
matcher(selector) → matcher.call(el, selector)

EDIT: This answer is now deprecated and no longer works in Chrome 64+. Leaving for historical context. In fact that bug report links back to this question for alternative solutions to using this.
Seems I managed to answer my own question after another hour of research.
It's as simple as this:
window.getMatchedCSSRules(document.getElementById("description"))
(Works in WebKit/Chrome, possibly others too)

Have a look at this library, which does what was asked for: http://www.brothercake.com/site/resources/scripts/cssutilities/
It works in all modern browsers right back to IE6, can give you rule and property collections like Firebug (in fact it's more accurate than Firebug), and can also calculate the relative or absolute specificity of any rule. The only caveat is that, although it understands static media types, it doesn't understand media-queries.

Here is my version of getMatchedCSSRules function which support #media query.
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
}

Here's a version of S.B.'s answer which also returns matching rules within matching media queries. I've removed the *.rules || *.cssRules coalescence and the .matches implementation finder; add a polyfill or add those lines back in if you need them.
This version also returns the CSSStyleRule objects rather than the rule text. I think this is a little more useful, since the specifics of the rules can be more easily probed programmatically this way.
Coffee:
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;
}

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>

Ensuring IE9+, I wrote a function which calculates CSS for requested element and its children, and gives possibility to save it to a new className if needed in snippet below.
/**
* #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;
}
Usage
getElementStyles(document.getElementByClassName('.my-class'), '.dummy-class', 'width:100%;opaity:0.5;transform:scale(1.5);');

I think the answer from S.B. should be the accepted one at this point but it is not exact. It is mentioned a few times that there will be some rules that may be missed. Faced with that, I decided to use document.querySelectorAll instead of element.matches. The only thing is that you would need some kind of unique identification of elements to compare it to the one you are looking for. In most cases I think that is achievable by setting its id to have a unique value. That's how you can identify the matched element being yours. If you can think of a general way to match the result of document.querySelectorAll to the element you are looking for that would essentially be a complete polyfill of getMatchedCSSRules.
I checked the performance for document.querySelectorAll since it probably is slower than element.matches but in most cases it should not be a problem. I see that it takes about 0.001 milliseconds.
I also found CSSUtilities library that advertises that it can do this but I feel its old and has not been updated in a while. Looking at its source code, it makes me think there may be cases that it misses.

As the linked question is closed as a duplicate of this, I add an answer here instead.
The unanswered part 2: "Once I found the computed style, I want to know where it comes from"
By looping over the document.styleSheets, and looking at the getComputedStyle() before and after you modify it, you can detect what stylesheet is in use.
It's far from optimal, but at least it can detect if the rule you looking at is in use or not.
Here is an exemple:
<html><head>
<title>CSS Test</title>
<style id="style-a">
li {color: #333; font-size: 20px !important;}
li.bb {color: #600; font-size: 10px;}
p {margin: 5px;}
p {margin-bottom: 10px;}
</style>
<script>
window.addEventListener('DOMContentLoaded', async () => {
const selector = 'li';
// const selector = 'li.bb';
const exempleValues = {
'color': ['rgb(0, 0, 0)', 'rgb(255, 255, 255)'],
'font-size': ['10px', '12px'],
};
const delay = (t) => new Promise((k, e) => {setTimeout(k, t)});
for(const element of document.querySelectorAll(selector)) {
const elementCss = document.defaultView.getComputedStyle(element);
for(const sheet of document.styleSheets) {
for(const rule of sheet.cssRules) {
if(rule.selectorText !== selector) {
continue;
}
for(const properyName of rule.style) {
const currentValue = rule.style[properyName];
const priority = rule.style.getPropertyPriority(properyName)
if(!exempleValues[properyName]) {
console.warn('no exemple values for', properyName);
continue;
}
const exempleValue = exempleValues[properyName][exempleValues[properyName][0] === currentValue ? 1 : 0];
rule.style.setProperty(properyName, exempleValue, priority);
await delay(100);
if(exempleValue === elementCss[properyName]) {
console.log(selector, properyName, currentValue, priority || false, true, 'in use', element, sheet.ownerNode);
} else {
console.log(selector, properyName, currentValue, priority || false, false, 'overrided', element);
}
rule.style.setProperty(properyName, currentValue, priority);
await delay(100);
}
}
}
}
}, {once: true});
</script>
</head><body>
<h1>CSS Test</h1>
<p>html-file for testing css</p>
<ul>
<li>AAAA</li>
<li class="bb">BBBB</li>
<li>CCCC</li>
</ul>
</body></html>

Related

Can I use Javascript to change page styling? [duplicate]

So far I have to do this:
elem.classList.add("first");
elem.classList.add("second");
elem.classList.add("third");
While this is doable in jQuery, like this
$(elem).addClass("first second third");
I'd like to know if there's any native way to add or remove.
elem.classList.add("first");
elem.classList.add("second");
elem.classList.add("third");
is equal
elem.classList.add("first","second","third");
The new spread operator makes it even easier to apply multiple CSS classes as array:
const list = ['first', 'second', 'third'];
element.classList.add(...list);
You can do like below
Add
elem.classList.add("first", "second", "third");
// OR
elem.classList.add(...["first","second","third"]);
Remove
elem.classList.remove("first", "second", "third");
// OR
elem.classList.remove(...["first","second","third"]);
Reference
TLDR;
In the straight forward case above removal should work. But in case of removal, you should make sure class exists before you remove them
const classes = ["first","second","third"];
classes.forEach(c => {
if (elem.classList.contains(c)) {
element.classList.remove(c);
}
})
The classList property ensures that duplicate classes are not unnecessarily added to the element. In order to keep this functionality, if you dislike the longhand versions or jQuery version, I'd suggest adding an addMany function and removeMany to DOMTokenList (the type of classList):
DOMTokenList.prototype.addMany = function(classes) {
var array = classes.split(' ');
for (var i = 0, length = array.length; i < length; i++) {
this.add(array[i]);
}
}
DOMTokenList.prototype.removeMany = function(classes) {
var array = classes.split(' ');
for (var i = 0, length = array.length; i < length; i++) {
this.remove(array[i]);
}
}
These would then be useable like so:
elem.classList.addMany("first second third");
elem.classList.removeMany("first third");
Update
As per your comments, if you wish to only write a custom method for these in the event they are not defined, try the following:
DOMTokenList.prototype.addMany = DOMTokenList.prototype.addMany || function(classes) {...}
DOMTokenList.prototype.removeMany = DOMTokenList.prototype.removeMany || function(classes) {...}
Since the add() method from the classList just allows to pass separate arguments and not a single array, you need to invoque add() using apply. For the first argument you will need to pass the classList reference from the same DOM node and as a second argument the array of classes that you want to add:
element.classList.add.apply(
element.classList,
['class-0', 'class-1', 'class-2']
);
Here is a work around for IE 10 and 11 users that seemed pretty straight forward.
var elem = document.getElementById('elem');
['first','second','third'].forEach(item => elem.classList.add(item));
<div id="elem">Hello World!</div>
Or
var elem = document.getElementById('elem'),
classes = ['first','second','third'];
classes.forEach(function(item) {
elem.classList.add(item);
});
<div id="elem">Hello World!</div>
To add class to a element
document.querySelector(elem).className+=' first second third';
UPDATE:
Remove a class
document.querySelector(elem).className=document.querySelector(elem).className.split(class_to_be_removed).join(" ");
Newer versions of the DOMTokenList spec allow for multiple arguments to add() and remove(), as well as a second argument to toggle() to force state.
At the time of writing, Chrome supports multiple arguments to add() and remove(), but none of the other browsers do. IE 10 and lower, Firefox 23 and lower, Chrome 23 and lower and other browsers do not support the second argument to toggle().
I wrote the following small polyfill to tide me over until support expands:
(function () {
/*global DOMTokenList */
var dummy = document.createElement('div'),
dtp = DOMTokenList.prototype,
toggle = dtp.toggle,
add = dtp.add,
rem = dtp.remove;
dummy.classList.add('class1', 'class2');
// Older versions of the HTMLElement.classList spec didn't allow multiple
// arguments, easy to test for
if (!dummy.classList.contains('class2')) {
dtp.add = function () {
Array.prototype.forEach.call(arguments, add.bind(this));
};
dtp.remove = function () {
Array.prototype.forEach.call(arguments, rem.bind(this));
};
}
// Older versions of the spec didn't have a forcedState argument for
// `toggle` either, test by checking the return value after forcing
if (!dummy.classList.toggle('class1', true)) {
dtp.toggle = function (cls, forcedState) {
if (forcedState === undefined)
return toggle.call(this, cls);
(forcedState ? add : rem).call(this, cls);
return !!forcedState;
};
}
})();
A modern browser with ES5 compliance and DOMTokenList are expected, but I'm using this polyfill in several specifically targeted environments, so it works great for me, but it might need tweaking for scripts that will run in legacy browser environments such as IE 8 and lower.
A very simple, non fancy, but working solution that I would have to believe is very cross browser:
Create this function
function removeAddClasses(classList,removeCollection,addCollection){
for (var i=0;i<removeCollection.length;i++){
classList.remove(removeCollection[i]);
}
for (var i=0;i<addCollection.length;i++){
classList.add(addCollection[i]);
}
}
Call it like this:
removeAddClasses(node.classList,arrayToRemove,arrayToAdd);
...where arrayToRemove is an array of class names to remove:
['myClass1','myClass2'] etcetera
...and arrayToAdd is an array of class names to add:
['myClass3','myClass4'] etcetera
The standard definiton allows only for adding or deleting a single class. A couple of small wrapper functions can do what you ask :
function addClasses (el, classes) {
classes = Array.prototype.slice.call (arguments, 1);
console.log (classes);
for (var i = classes.length; i--;) {
classes[i] = classes[i].trim ().split (/\s*,\s*|\s+/);
for (var j = classes[i].length; j--;)
el.classList.add (classes[i][j]);
}
}
function removeClasses (el, classes) {
classes = Array.prototype.slice.call (arguments, 1);
for (var i = classes.length; i--;) {
classes[i] = classes[i].trim ().split (/\s*,\s*|\s+/);
for (var j = classes[i].length; j--;)
el.classList.remove (classes[i][j]);
}
}
These wrappers allow you to specify the list of classes as separate arguments, as strings with space or comma separated items, or a combination. For an example see http://jsfiddle.net/jstoolsmith/eCqy7
Assume that you have an array of classes to being added, you can use ES6 spread syntax:
let classes = ['first', 'second', 'third'];
elem.classList.add(...classes);
A better way to add the multiple classes separated by spaces in a string is using the Spread_syntax with the split:
element.classList.add(...classesStr.split(" "));
I found a very simple method which is more modern and elegant way.
const el = document.querySelector('.m-element');
// To toggle
['class1', 'class2'].map((e) => el.classList.toggle(e));
// To add
['class1', 'class2'].map((e) => el.classList.add(e));
// To remove
['class1', 'class2'].map((e) => el.classList.remove(e));
Good thing is you can extend the class array or use any coming from API easily.
I liked #rich.kelly's answer, but I wanted to use the same nomenclature as classList.add() (comma seperated strings), so a slight deviation.
DOMTokenList.prototype.addMany = DOMTokenList.prototype.addMany || function() {
for (var i = 0; i < arguments.length; i++) {
this.add(arguments[i]);
}
}
DOMTokenList.prototype.removeMany = DOMTokenList.prototype.removeMany || function() {
for (var i = 0; i < arguments.length; i++) {
this.remove(arguments[i]);
}
}
So you can then use:
document.body.classList.addMany('class-one','class-two','class-three');
I need to test all browsers, but this worked for Chrome.
Should we be checking for something more specific than the existence of DOMTokenList.prototype.addMany? What exactly causes classList.add() to fail in IE11?
Another polyfill for element.classList is here. I found it via MDN.
I include that script and use element.classList.add("first","second","third") as it's intended.
One of the best solution is to check if an element exists and then proceed to add or possibly remove and above all if the element is empty, delete it.
/**
* #description detect if obj is an element
* #param {*} obj
* #returns {Boolean}
* #example
* see below
*/
function isElement(obj) {
if (typeof obj !== 'object') {
return false
}
let prototypeStr, prototype
do {
prototype = Object.getPrototypeOf(obj)
// to work in iframe
prototypeStr = Object.prototype.toString.call(prototype)
// '[object Document]' is used to detect document
if (
prototypeStr === '[object Element]' ||
prototypeStr === '[object Document]'
) {
return true
}
obj = prototype
// null is the terminal of object
} while (prototype !== null)
return false
}
/*
* Add multiple class
* addClasses(element,['class1','class2','class3'])
* el: element | document.querySelector(".mydiv");
* classes: passing:: array or string : [] | 'cl1,cl2' | 'cl1 cl2' | 'cl1|cl2'
*/
function addClasses(el, classes) {
classes = Array.prototype.slice.call(arguments, 1);
if ( isElement(el) ){ //if (document.body.contains(el)
for (var i = classes.length; i--;) {
classes[i] = Array.isArray(classes[i]) ? classes[i]: classes[i].trim().split(/\s*,\s*|\s+/);
for (var j = classes[i].length; j--;)
el.classList.add(classes[i][j]);
}
}
}
/*
* Remove multiple class
* Remove attribute class is empty
* addClasses(element,['class1','class2','class3'])
* el: element | document.querySelector(".mydiv");
* classes: passing:: array or string : [] | 'cl1,cl2' | 'cl1 cl2' | 'cl1|cl2'
*/
function removeClasses(el, classes) {
classes = Array.prototype.slice.call(arguments, 1);
if ( isElement(el) ) {
for (var i = classes.length; i--;) {
classes[i] = Array.isArray(classes[i]) ? classes[i]: classes[i].trim().split(/\s*,\s*|\s+/);
for (var j = classes[i].length; j--;)
el.classList.remove(classes[i][j]);
let cl = el.className.trim();
if (!cl){
el.removeAttribute('class');
}
}
}
}
var div = document.createElement("div");
div.id = 'test'; // div.setAttribute("id", "test");
div.textContent = 'The World';
//div.className = 'class';
// possible use: afterend, beforeend
document.body.insertAdjacentElement('beforeend', div);
// Everything written above you can do so:
//document.body.insertAdjacentHTML('beforeend', '<div id="text"></div>');
var el = document.querySelector("#test");
addClasses(el,'one,two,three,four');
removeClasses(el,'two,two,never,other');
el.innerHTML = 'I found: '+el.className;
// return : I found: four three one
#test {
display: inline-block;
border: 1px solid silver;
padding: 10px;
}

How to get a list of custom properties (CSS variables) ? - something like `getPropertyEntries()` [duplicate]

I have JS library and I have this issue: I'm creating temporary element for calculating size of character using monospace font. Right now I'm copying inlie style, but I need all styles from original including css variables. I don't want to clone the element, because there are elements, that are inside, that I don't need. Also element may have id set by the user, not sure how this will behave when there will be two elements with same id, so it would be better (I think) to just copy each style to new temporary element.
I have code based on these:
Accessing a CSS custom property (aka CSS variable) through JavaScript
Set javascript computed style from one element to another
My code look like this:
function is_valid_style_property(key, value) {
//checking that the property is not int index ( happens on some browser
return typeof value === 'string' && value.length && value !== parseInt(value);
}
function copy_computed_style(from, to) {
var computed_style_object = false;
computed_style_object = from.currentStyle || document.defaultView.getComputedStyle(from, null);
if (!computed_style_object) {
return;
}
Object.keys(computed_style_object).forEach(function(key) {
var value = computed_style_object.getPropertyValue(key);
if (key.match(/^--/)) {
console.log({key, value}); // this is never executed
}
if (is_valid_style_property(key, value)) {
to.style.setProperty(key, value);
}
});
}
the problem is that getComputedStyle, don't return css variables. Is there any other solution to get list of css variables applied to element?
I need CSS variables because I have css that is applied to element that are inside of my temporary item, that is based on css variables. Is clone node the only way to copy CSS variables from one element to other?
EDIT:
this is not duplicate because css variable can also be set inline not only in style sheet per class. And my element can have style added by very different css selectors that I can't possibly know.
Based on this answer https://stackoverflow.com/a/37958301/8620333 I have created a code that rely on getMatchedCSSRules in order to retrieve all the CSS and then extract the CSS custom properties. Since Custom properties are inherited we need to gather the one defined within the element and the one defined on any parent element.
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);
};
}
var element = document.querySelector(".box");
/*Get element style*/
var obj = window.getMatchedCSSRules(element)[0];
var all_css = obj.parentStyleSheet.cssRules;
for(var i=0;i < all_css.length;i++) {
var rules = all_css[i].cssText.substring(all_css[i].cssText.indexOf("{")+1,all_css[i].cssText.indexOf("}"));
rules = rules.split(";");
for(var j=0;j<rules.length;j++) {
if(rules[j].trim().startsWith("--")) {
console.log(rules[j]);
}
}
}
/*get inline style*/
var rules = element.getAttribute("style").trim().split(";");
for(var j=0;j<rules.length;j++) {
if(rules[j].trim().startsWith("--")) {
console.log(rules[j]);
}
}
:root {
--b: 20px;
}
.box {
background: red;
height: 100px;
--c: blue;
border: 1px solid var(--c);
}
.element {
--e:30px;
padding:var(--e);
}
<div class="box element" style="color:blue;--d:10ch;border-radius:20px;">
</div>
Here is the relevant part of the code1:
var element = document.querySelector(".box");
/*Get external styles*/
var obj = window.getMatchedCSSRules(element)[0];
var all_css = obj.parentStyleSheet.cssRules;
for(var i=0;i < all_css.length;i++) {
var rules = all_css[i].cssText.substring(all_css[i].cssText.indexOf("{")+1,all_css[i].cssText.indexOf("}"));
rules = rules.split(";");
for(var j=0;j<rules.length;j++) {
if(rules[j].trim().startsWith("--")) {
console.log(rules[j]);
}
}
}
/*Get inline styles*/
var rules = element.getAttribute("style").trim().split(";");
for(var j=0;j<rules.length;j++) {
if(rules[j].trim().startsWith("--")) {
console.log(rules[j]);
}
}
As you can see, this will print the needed values. You can easily adjust the code to store the values in an array or an Object.
1: This code is not optimized as it may gather non needed CSS in some cases. Will keep editing it.

How to remove :hover css rule from stylesheet

I am working on a project based on jQuery and javascript. I am trying to add the CSS rules using javascript insert/addRule properties. What I want to do is :
I have to create hover effect and insert them into the stylesheet using javascript css properties. For this I am creating two classes for example:
.dummy {.... }
and
.dummy:hover{.....}
I want that If I change the hover effect then it must delete the previous one (or duplicates ) from the stylesheet and insert the new rules to the stylesheet.
But the problem is that If I try to remove or delete the CSS rules using deleteRule or removeRule it removes only
.dummy{...} rule but not the .dummy:hover{...}
Here is my code that can help you to understand what I am doing:
function createNewStyle() {
var style = document.createElement("style");
style.setAttribute('id', 'd-set-stylesheet');
style.appendChild(document.createTextNode(""));
document.head.appendChild(style);
} // this function might be not required but included here so that you may understand the program flow.
//the main issue is with this function.
function removeIfExists(class_name) {
var styleTag = document.getElementById("d-set-stylesheet");
var styleRef = styleTag.sheet ? styleTag.sheet : styleTag.styleSheet;
if (styleRef.rules) { //all browsers except IE 9-
console.log(styleRef);
for (var i in styleRef.cssRules) {
if (styleRef.cssRules[i].selectorText === "." + class_name+":hover" ||styleRef.cssRules[i].selectorText === "." + class_name )
styleRef.deleteRule(i);
}
} else {
var i;
for (i = 0; i < styleRef.rules.length; i++) {
//if(styleRef.rules[i].selectorText === "."+class_name || styleRef.rules[i].selectorText === "."+class_name+":hover")
styleRef.removeRule(i);
}
}
return styleRef;
}
//this function maybe not related with the issue but if it is you can check it.
function setNewClass(element, class_name, classSelector, styleObject) {
var stylesheet = removeIfExists(class_name);
if (element.data('hover-class') == null)
element.data('hover-class', class_name);
let count = 0;
var style = [];
var property = [{
"append": ":hover"
}, {
"append": ""
}];
for (j = 0; j < 2; j++) {
style.push({});
style[j].sheet = stylesheet;
style[j].selector = class_name;
style[j].type = classSelector;
style[j].property = property[j]["append"];
style[j].style = "";
}
for (i in styleObject) {
if (styleObject.hasOwnProperty(i))
if (count % 2 == 0)
style[0].style += styleObject[i] + ":";
else
style[0].style += styleObject[i] + ";";
count++;
}
style[1].style = "transition:" + styleObject.t_property_value;
addCSSRule(style);
}
function addCSSRule(styleSheet) {
console.log(styleSheet);
for (i = 0; i < styleSheet.length; i++)
if (styleSheet[i].sheet.insertRule)
styleSheet[i].sheet.insertRule(styleSheet[i].type + styleSheet[i].selector + styleSheet[i].property + "{" + styleSheet[i].style + "}", styleSheet[i].sheet.cssRules.length);
else
styleSheet[i].sheet.addRule(styleSheet[i].type + styleSheet[i].selector + styleSheet[i].property, styleSheet[i].sheet.style, -1);
}
Here are the images that of console.log
In the first image the hover and without hover class is added to CSS rules.
In the second image the previous hover class is not deleted while without hover class is deleted. I want hover transition must be deleted. Before adding new Rules to prevent duplication of classes and more than one transition on hovering the element should be prevented.
Thank you.
I found the answer today. I will explain it so that it can help others in future to understand the problem and solution.
As through my question, you can understand the issue. The problem was with this function only
function removeIfExists(class_name) {
......
if (styleRef.rules) { //all browsers except IE 9-
for (var i in styleRef.cssRules) {
.....
styleRef.deleteRule(i); //problem is with this.
.....
}
}
.......
return styleRef;
}
When I check the deleteRule on the basis of some test I found that when the deleteRule is called following things happens:
The css rule is deleted on the basis of index.
(The most important, as there is no documentation or clarification or explanation about this) The content get updated with their indexing. So that my function will no longer be able to delete the correct rules on the basis of old indexing.
To understand it better I will give you an example:
Suppose, I have added two rules in css such as: .dummy:hover{...} and .dummy{...}
Now the indexing of these rules will be as follows:
[0]: .dummy:hover rule
[1]: .dummy rule
When i call the delete rule what happen is :
// at i =0;
deleteRule(0);/* will be called and it will remove [0]: .dummy:hover rule
and then it will update the rules indexing again, which means
[1]: .dummy rule => [0]: .dummy rule */
// now when i = 1;
deleteRule(1); /* will be called and it will try to remove [1]: .dummy rule which is not here anymore and it will just skip the removal of [1]: .dummy rule.
So what I do to remove it is starting to remove the rule from highest indexing or in reverse order which means the i = 1 rule will be deleted first then i=0; rule so that there would not be any consequences.
and the function would changed as this:
function removeIfExists(class_name)
{
...
var styleRef = styleTag.sheet ? styleTag.sheet : styleTag.styleSheet;
var len = styleRef.cssRules ? styleRef.cssRules.length : styleRef.rules.length;
if(styleRef.rules){ //all browsers except IE 9-
for(i=len;i>0;i--)
{
if (styleRef.cssRules[i-1].selectorText === "." + class_name+":hover" ||styleRef.cssRules[i-1].selectorText === "." + class_name )
styleRef.deleteRule(i-1);
}
}
......
.....
}
return styleRef;
}
Note : The similar procedure would be followed for IE but I have not included it here.

How to get the applied style from an element, excluding the default user agent styles

How in JavaScript do you retrieve the styles that have been applied to an element, excluding the default user agent styles (so inline + stylesheet styles only).
Basically, all the user styles you can see in the Computed tab of your favorite developer tool:
No framework please, IE8+, Edge, Chrome and Firefox.
I am expecting the answer to be the result of getComputedStyle minus getDefaultComputedStyle, but in a cross browser way. Seeing that all developer tools are capable of doing it, there must be a solution :)
There is a read only property of document called 'styleSheets'.
var styleSheetList = document.styleSheets;
https://developer.mozilla.org/en-US/docs/Web/API/Document/styleSheets
By using this, you can reach all the styles which are applied by the author.
There is a similar question about this but not a duplicate, in here:
Is it possible to check if certain CSS properties are defined inside the style tag with Javascript?
You can get the applied style from an element, excluding the default user agent styles using the accepted answer of that question i just mentioned.
That answer didn't supply the element's own style attribute content, so i have improved the code a bit:
var proto = Element.prototype;
var slice = Function.call.bind(Array.prototype.slice);
var matches = Function.call.bind(proto.matchesSelector ||
proto.mozMatchesSelector || proto.webkitMatchesSelector ||
proto.msMatchesSelector || proto.oMatchesSelector);
// Returns true if a DOM Element matches a cssRule
var elementMatchCSSRule = function(element, cssRule) {
return matches(element, cssRule.selectorText);
};
// Returns true if a property is defined in a cssRule
var propertyInCSSRule = function(prop, cssRule) {
return prop in cssRule.style && cssRule.style[prop] !== "";
};
// Here we get the cssRules across all the stylesheets in one array
var cssRules = slice(document.styleSheets).reduce(function(rules, styleSheet) {
return rules.concat(slice(styleSheet.cssRules));
}, []);
var getAppliedCss = function(elm) {
// get only the css rules that matches that element
var elementRules = cssRules.filter(elementMatchCSSRule.bind(null, elm));
var rules =[];
if(elementRules.length) {
for(i = 0; i < elementRules.length; i++) {
var e = elementRules[i];
rules.push({
order:i,
text:e.cssText
})
}
}
if(elm.getAttribute('style')) {
rules.push({
order:elementRules.length,
text:elm.getAttribute('style')
})
}
return rules;
}
function showStyle(){
var styleSheetList = document.styleSheets;
// get a reference to an element, then...
var div1 = document.getElementById("div1");
var rules = getAppliedCss(div1);
var str = '';
for(i = 0; i < rules.length; i++) {
var r = rules[i];
str += '<br/>Style Order: ' + r.order + ' | Style Text: ' + r.text;
}
document.getElementById("p1").innerHTML = str;
}
#div1 {
float:left;
width:100px;
}
div {
text-align:center;
}
<div id="div1" style="font-size:14px;">
Lorem ipsum
</div>
<br/>
<br/>
Show me the style.
<p id="p1"><p>
All developer tools can cheat, because they have access to the default rules the browser they are built into applies.
I thought that the following approach might work.
Construct an element of exactly the same type (say, a div or p) as the one we are interested in.
Append this element somewhere on the page so that only default browser rules are applied. We can do so by putting it in an iframe.
If you are sure that you do not have rules targeting any p element, for example, then appending to the body may be more efficient.
Check the difference in styles and only report values that differ.
Clean up temporary element(s).
It seems to work reasonably well in practice. I have only tested this in Firefox and Chrome, but I think that it should work in other browsers too - except maybe for the fact that I used for...in and for...of, but one could easily rewrite that. Note that not just the properties you specify are reported, but also some properties that are influenced by the properties you do specify. For example, the border color matches the text color by design and is hence reported as different even when you only set color: white.
To summarize, I have taken the example you posted in one of your comments and added a getNonDefaultStyles function to it that I think does what you want. It can of course be modified to cache default styles of say, div elements and thus be more efficient in repeated calls (because modifying the DOM is expensive), but it shows the gist.
The below snippet shows how the version can be implemented that appends an element to the body. Due to limitations on StackOverflow, it is not possible to show the iframe version in a snippet. It is possible on JSFiddle. The below snippet can also be found in a Fiddle.
var textarea = document.getElementById("textarea"),
paragraph = document.getElementById("paragraph");
/**
* Computes applied styles, assuming no rules targeting a specific element.
*/
function getNonDefaultStyles(el) {
var styles = {},
computed = window.getComputedStyle(el),
notTargetedContainer = document.createElement('div'),
elVanilla = document.createElement(el.tagName);
document.body.appendChild(notTargetedContainer);
notTargetedContainer.appendChild(elVanilla);
var vanilla = window.getComputedStyle(elVanilla);
for (let key of computed) {
if (vanilla[key] !== computed[key]) {
styles[key] = computed[key];
}
}
document.body.removeChild(notTargetedContainer);
return styles;
}
var paragraphStyles = getNonDefaultStyles(paragraph);
for (let style in paragraphStyles) {
textarea.value += style + ": " + paragraphStyles[style] + "\n";
}
#paragraph {
background: red;
}
textarea {
width: 300px;
height: 400px;
}
<p id="paragraph" style="color: white">
I am a DIV
</p>
<p>
User styles:
</p>
<textarea id="textarea"></textarea>
Here's a function that gets all the CSS rules that have been applied to an element from either inline styles (HTML style attribute) or stylesheets on the page. It also grabs relevant keyframes for CSS animations and the :active, :hover, ::before, and ::after selectors.
function getAppliedCssData(el) {
// we create a unique id so we can generate unique ids for renaming animations
let uniqueId = "id" + Math.random().toString().slice(2) + Math.random().toString().slice(2);
let allRules = [...document.styleSheets].map(s => {
let rules = [];
try { rules.push(...s.cssRules) } catch(e) {} // we ignore cross-domain stylesheets with restrictive CORs headers
return rules;
}).flat();
let styleRules = allRules.filter(rule => rule.type === CSSRule.STYLE_RULE)
let fontFaceRules = allRules.filter(rule => rule.type === CSSRule.FONT_FACE_RULE);
let keyframesRules = allRules.filter(rule => rule.type === CSSRule.KEYFRAMES_RULE);
let matchingDefaultRules = styleRules.filter(rule => el.matches(rule.selectorText));
let nonMatchingRules = styleRules.filter(rule => !el.matches(rule.selectorText));
let matchingHoverRules = nonMatchingRules.filter(rule => el.matches(rule.selectorText.replace(/ :/g, " *:").replace(/([^(])(:hover)\b/g, "$1")));
let matchingActiveRules = nonMatchingRules.filter(rule => el.matches(rule.selectorText.replace(/ :/g, " *:").replace(/([^(])(:active)\b/g, "$1")));
let matchingBeforeRules = nonMatchingRules.filter(rule => el.matches(rule.selectorText.replace(/ :/g, " *:").replace(/::before\b/g, "")));
let matchingAfterRules = nonMatchingRules.filter(rule => el.matches(rule.selectorText.replace(/ :/g, " *:").replace(/::after\b/g, "")));
let allMatchingStyleRules = [...matchingActiveRules, ...matchingDefaultRules, ...matchingHoverRules, ...matchingBeforeRules, ...matchingAfterRules];
let matchingAnimationNames = allMatchingStyleRules.map(rule => rule.style.animationName).filter(n => n.trim());
let matchingKeyframeRules = keyframesRules.filter(rule => matchingAnimationNames.includes(rule.name));
// make name changes before actually grabbing the style text of each type
allMatchingStyleRules.forEach(rule => rule.style.animationName = rule.style.animationName+uniqueId);
matchingKeyframeRules.forEach(rule => rule.name = rule.name+uniqueId);
let matchingDefaultStyles = matchingDefaultRules.map(rule => rule.cssText).map(r => r.split(/[{}]/g)[1].trim()).join(" ") + (el.getAttribute('style') || ""); // important to add these last because inline styles are meant to override stylesheet styles (unless !important is used)
let matchingHoverStyles = matchingHoverRules.map(rule => rule.cssText).map(r => r.split(/[{}]/g)[1].trim()).join(" ");
let matchingActiveStyles = matchingActiveRules.map(rule => rule.cssText).map(r => r.split(/[{}]/g)[1].trim()).join(" ");
let matchingBeforeStyles = matchingBeforeRules.map(rule => rule.cssText).map(r => r.split(/[{}]/g)[1].trim()).join(" ");
let matchingAfterStyles = matchingAfterRules.map(rule => rule.cssText).map(r => r.split(/[{}]/g)[1].trim()).join(" ");
let matchingKeyframeStyles = matchingKeyframeRules.map(rule => rule.cssText).join(" ");
// undo the rule name changes because this actually affects the whole document:
matchingKeyframeRules.forEach(rule => rule.name = rule.name.replace(uniqueId, ""));
allMatchingStyleRules.forEach(rule => rule.style.animationName = rule.style.animationName.replace(uniqueId, ""));
let data = {
uniqueId,
defaultStyles: matchingDefaultStyles,
hoverStyles: matchingHoverStyles,
activeStyles: matchingActiveStyles,
keyframeStyles: matchingKeyframeStyles,
beforeStyles: matchingBeforeStyles,
afterStyles: matchingAfterStyles,
}
return data;
}
The :focus, :focus-within and :visited selectors are not included, but could be easily added.
You can calculate the user-applied (non-default) styles by comparing them against a "default" HTML element of the same tag name, which is rendered in an isolated <iframe> so no styles from the document "leak" into the default element.
This solution is the same as #Just a student, but adds these improvements:
the <iframe> is a hidden HTML element so the user won't see it
for performance, default styles are cached and we wait to clean up the <iframe> until the end when we call removeSandbox
it accounts for inheritance (i.e. if you provide parentElement it will list a style even when the parent sets it and the element overrides it back to the default)
it accounts for situations where a default style's initial value and computed value don't match (for more info, see note [1] in this PR)
// usage:
element = document.querySelector('div');
styles = getUserComputedStyles(element);
styles = getUserComputedStyles(element, parentElement);
// call this method when done to cleanup:
removeSandbox();
function getUserComputedStyles(element, parentElement = null) {
var defaultStyle = getDefaultStyle(element.tagName);
var computedStyle = window.getComputedStyle(element);
var parentStyle =
parentElement ? window.getComputedStyle(parentElement) : null;
var styles = {};
[...computedStyle].forEach(function(name) {
// If the style does not match the default, or it does not match the
// parent's, set it. We don't know which styles are inherited from the
// parent and which aren't, so we have to always check both.
// This results in some extra default styles being returned, so if you
// want to avoid this and aren't concerned about omitting styles that
// the parent set but the `element` overrides back to the default,
// call `getUserComputedStyles` without a `parentElement`.
const computedStyleValue = computedStyle[name];
if (computedStyleValue !== defaultStyle[name] ||
(parentStyle && computedStyleValue !== parentStyle[name])) {
styles[name] = computedStyleValue;
}
});
return styles;
}
var removeDefaultStylesTimeoutId = null;
var sandbox = null;
var tagNameDefaultStyles = {};
function getDefaultStyle(tagName) {
if (tagNameDefaultStyles[tagName]) {
return tagNameDefaultStyles[tagName];
}
if (!sandbox) {
// Create a hidden sandbox <iframe> element within we can create
// default HTML elements and query their computed styles. Elements
// must be rendered in order to query their computed styles. The
// <iframe> won't render at all with `display: none`, so we have to
// use `visibility: hidden` with `position: fixed`.
sandbox = document.createElement('iframe');
sandbox.style.visibility = 'hidden';
sandbox.style.position = 'fixed';
document.body.appendChild(sandbox);
// Ensure that the iframe is rendered in standard mode
sandbox.contentWindow.document.write(
'<!DOCTYPE html><meta charset="UTF-8"><title>sandbox</title><body>');
}
var defaultElement = document.createElement(tagName);
sandbox.contentWindow.document.body.appendChild(defaultElement);
// Ensure that there is some content, so properties like margin are applied
defaultElement.textContent = '.';
var defaultComputedStyle =
sandbox.contentWindow.getComputedStyle(defaultElement);
var defaultStyle = {};
// Copy styles to an object, making sure that 'width' and 'height' are
// given the default value of 'auto', since their initial value is always
// 'auto' despite that the default computed value is sometimes an absolute
// length.
[...defaultComputedStyle].forEach(function(name) {
defaultStyle[name] = (name === 'width' || name === 'height')
? 'auto' : defaultComputedStyle.getPropertyValue(name);
});
sandbox.contentWindow.document.body.removeChild(defaultElement);
tagNameDefaultStyles[tagName] = defaultStyle;
return defaultStyle;
}
function removeSandbox() {
if (!sandbox) {
return;
}
document.body.removeChild(sandbox);
sandbox = null;
if (removeDefaultStylesTimeoutId) {
clearTimeout(removeDefaultStylesTimeoutId);
}
removeDefaultStylesTimeoutId = setTimeout(() => {
removeDefaultStylesTimeoutId = null;
tagNameDefaultStyles = {};
}, 20 * 1000);
}
Even with these improvements, for block elements, some default styles are still listed because their initial and computed values don't match, namely width, height, block-size, inset-block, transform-origin, and perspective-origin (see note in #4). This solution in dom-to-image-more (the getUserComputedStyle function) is able to trim away even more of these, although the calculation is slower.
I've used this function in the past...
function get_style(obj,nam) { //obj = HTML element, nam = style property
var val = "";
if(document.defaultView && document.defaultView.getComputedStyle) {
nam = nam.replace(/[A-Z]/g,function(str) { //convert name into hypenated
return "-"+str.toLowerCase();
});
val = document.defaultView.getComputedStyle(obj,"").getPropertyValue(nam); //get current style
}
else if(obj.currentStyle) {
nam = nam.replace(/\-(\w)/g,function(str,p1) { //convert name into camel case
return p1.toUpperCase();
});
val = obj.currentStyle[nam]; //get current style
}
return val;
}
It allows you to pass in the style property as either hypenated (background-color) or camel case (backgroundColor) and replaces it depending on the method it uses.
This cover older browsers as well, even old IE!

Using javascript to reference current node?

So I'm doing a redesign for a site with an unhelpful CMS where I don't have full access to the markup (or ftp access). There's a node in particular which is hindering my progress significantly - its written with inline style & no class or ID and I have to change it (not remove it). Its looking like:
<div style="background-color: blue">
<div class="editablecontent">
(stuff I can edit in the CMS goes here)
</div>
</div>
I don't think getElementsByClassName is going to work here? but what sort of works is the very ugly defining an empty div with an ID then and document.getElementById.parentNode.parentNode.style.backgroundColor="white" which is obviously filthy and doesn't work in IE anyways and I need IE8 support (at least). I am not using a framework but do have access to the header.
Thanks in advance.
Assuming this content is unique in your document (e.g. there's only one of them), you can use this to find it:
function changeColor() {
var divs = document.getElementsByTagName("div");
for (var i = 0, len = divs.length; i < len; i++) {
if (divs[i].style.backgroundColor === "blue" &&
divs[i + 1] &&
divs[i + 1].parentNode === divs[i] &&
divs[i + 1].className === "editablecontent") {
divs[i].style.backgroundColor = "white";
return;
}
}
}
This does the following steps:
Get all divs in the document
Look for one with backgroundColor set to blue with an inline style setting
When you find one check to see if there is another div after it
If that next div is a child
If that next div has a className of "editablecontent"
Then, change its background color.
If you wanted any further checks based on other criteria to make sure you were finding the right object, you could add those to the logic, though these checks are all you have disclosed to us.
Working example: http://jsfiddle.net/jfriend00/NmxUn/
Another approach is to use getElementsByClassName and install a polyfill like this one to make it work in older browsers:
// Add a getElementsByClassName function if the browser doesn't have one
// Limitation: only works with one class name
// Copyright: Eike Send http://eike.se/nd
// License: MIT License
if (!document.getElementsByClassName) {
document.getElementsByClassName = function(search) {
var d = document, elements, pattern, i, results = [];
if (d.querySelectorAll) { // IE8
return d.querySelectorAll("." + search);
}
if (d.evaluate) { // IE6, IE7
pattern = ".//*[contains(concat(' ', #class, ' '), ' " + search + " ')]";
elements = d.evaluate(pattern, d, null, 0, null);
while ((i = elements.iterateNext())) {
results.push(i);
}
} else {
elements = d.getElementsByTagName("*");
pattern = new RegExp("(^|\\s)" + search + "(\\s|$)");
for (i = 0; i < elements.length; i++) {
if ( pattern.test(elements[i].className) ) {
results.push(elements[i]);
}
}
}
return results;
}
}
And, then you could simply do this, even on older versions of IE:
var items = document.getElementsByClassName("editablecontent");
if (items.length) {
items[0].parentNode.style.backgroundColor = "white";
}

Categories

Resources