Change a CSS stylesheet's selectors' properties - javascript

Here's how we traditionally change the style of a recurring element.
Applying the style to each element
function changeStyle(selector, prop, val) {
var elems = document.querySelectorAll(selector);
Array.prototype.forEach.call( elems, function(ele) {
ele.style[prop] = val;
});
}
changeStyle('.myData', 'color', 'red');
Using classes to supersede the existing style
function addClass(selector, newClass) {
var elems = document.querySelectorAll(selector);
for (let i=0; i<elems.length; i++) {
elems[i].classList.add(newClass);
};
}
addClass('.myData', 'redText');
Instead, I want to change the actual stylesheet's selectors' properties (such as directly modifying a class). I don't want to loop through the elements that match my selector and apply the CSS directly nor add a modifier class to the elements.

Use an external stylesheet
Identify its order on the page
Modify the properties of the rules
Here's how to do that:
// ssMain is the stylesheet's index based on load order. See document.styleSheets. E.g. 0=reset.css, 1=main.css.
var ssMain = 1;
var cssRules = (document.all) ? 'rules': 'cssRules';
function changeCSSStyle(selector, cssProp, cssVal) {
for (i=0, len=document.styleSheets[ssMain][cssRules].length; i<len; i++) {
if (document.styleSheets[ssMain][cssRules][i].selectorText === selector) {
document.styleSheets[ssMain][cssRules][i].style[cssProp] = cssVal;
return;
}
}
}
Make sure that the rule that you want to modify already exist in the CSS file and are in the correct cascading order, even if they're empty. Otherwise, if a selector doesn't have a rule, you would have to use document.styleSheets[index].insertRule() for which you would have to specify where in the list of rules should the rule be inserted.
changeCSSStyle('.warning', 'color', 'red');
changeCSSStyle('td.special', 'fontSize', '14px');

Related

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.

Show/hide in javascript when no html access

I have this:
<div id="myid1">Text 1</div>
<div id="myid2">Text 2</div>
<div id="myid3">Text 3</div>
and I would hide all these elements by default. Then when I click on a link, I would like show all them at once. I looked for some solution in Javascript but it seem is not possible to declare multiple ID when using document.getElementById.
Precision: I seen many solution who suggest to use class instead ID. The problem is I work with an external application integrated in my site and I have access partially to html, but I can set javascript code inside a dedicated JS file.
You could create a function that retrieves several elements by their id, and simply iterate over that collection of elements to hide or show them:
function getElementsByIds(idArray) {
// initialise an array (over which we'll iterate, later)
var elements = [];
// if no arguments have been passed in, we quit here:
if (!arguments) {
return false;
}
else {
/* we're running a basic check to see if the first passed-argument
is an array; if it is, we use it: */
if (Object.prototype.toString.call(arguments[0]) === '[object Array]') {
idArray = idArray;
}
/* if a string has been passed-in (rather than an array), we
make an array of those strings: */
else if ('string' === typeof arguments[0]) {
idArray = [].slice.call(arguments);
}
// here we iterate over the array:
for (var i = 0, len = idArray.length; i < len; i++) {
// we test to see if we can retrieve an element by the id:
if (document.getElementById(idArray[i])) {
/* if we can, we add that found element to the array
we initialised earlier: */
elements.push(document.getElementById(idArray[i]));
}
}
}
// returning the elements:
return elements;
}
// here we toggle the display of the elements (between 'none' and 'block':
function toggle (elems) {
// iterating over each element in the passed-in array:
for (var i = 0, len = elems.length; i < len; i++) {
/* if the current display is (exactly) 'none', we change to 'block'
otherwise we change it to 'none': */
elems[i].style.display = elems[i].style.display === 'none' ? 'block' : 'none';
}
}
function hide (nodes) {
// iterating over the passed-in array of nodes
for (var i = 0, len = nodes.length; i < len; i++) {
// setting each of their display properties to 'none':
nodes[i].style.display = 'none';
}
}
// getting the elements:
var relevantElements = getElementsByIds('myid1','myid2','myid3'),
toggleButton = document.getElementById('buttonThatTogglesVisibilityId');
// binding the click-handling functionality of the button:
toggleButton.onclick = function(){
toggle (relevantElements);
};
// initially hiding the elements:
hide (relevantElements);
JS Fiddle demo.
References:
arguments keyword.
Array.prototype.push().
Array.protoype.slice().
document.getElementById().
Function.prototype.call().
Object.prototype.toString().
typeof.
Three options (at least):
1) Wrap them all in a parent container if this is an option. Then you can just target that, rather than the individual elements.
document.getElementById('#my_container').style.display = 'block';
2) Use a library like jQuery to target multiple IDs:
$('#myid1, #myid2, #myid3').show();
3) Use some ECMA5 magic, but it won't work in old browsers.
[].forEach.call(document.querySelectorAll('#myid1, #myid2, #myid3'), function(el) {
el.style.display = 'block'; //or whatever
});
If id="myid< increment-number >" then you can select these elements very easily.
Below code will select all elements that START WITH "myid".
$("div[id^='myid']").each(function (i, el) {
//do what ever you want here
}
See jquery doc
http://api.jquery.com/attribute-contains-selector/

Native Javascript Selections

I need to use native Javascript and for some of these I need to select more than one attribute (ex. a div with a class and id). Here is a code sample of what I've got so far. The example has all single selections.
var $ = function (selector) {
var elements = [];
var doc = document, i = doc.getElementsByTagName("div"),
iTwo = doc.getElementById("some_id"), // #some_id
iThree = doc.getElementsByTagName("input"),
// ^ Lets say I wanted to select an input with ID name as well. Should it not be doc.getElementsByTagName("input").getElementById("idname")
iFour = doc.getElementsByClassName("some_class"); // some_class
elements.push(i,iTwo,iThree,iFour);
return elements;
};
Oh yes, I forgot to mention I cannot use querySelector at all...
It depends on the properties you want to select on. For example, you might pass an object like:
{tagname: 'div', class: 'foo'};
and the function might be like:
function listToArray(x) {
for (var result=[], i=0, iLen=x.length; i<iLen; i++) {
result[i] = x[i];
}
return result;
}
function getByProperties(props) {
var el, elements;
var baseProps = {id:'id', tagName:'tagName'};
var result = [];
if ('tagName' in props) {
elements = listToArray(document.getElementsByTagName(props.tagName));
} else if ('id' in props) {
elements = [document.getElementById(props.id)];
}
for (var j=0, jLen=elements.length; j<jLen; j++) {
el = elements[j];
for (var prop in props) {
// Include all with tagName as used above. Avoids case sensitivity
if (prop == 'tagName' || (props.hasOwnProperty(prop) && props[prop] == el[prop])) {
result.push(el);
}
}
}
return result;
}
// e.g.
getByProperties({tagName:'div', className:'foo'});
However it's a simplistic approach, it won't do things like child or nth selectors.
You can perhaps look at someone else's selector function (there are a few around) and follow the fork to support non–qSA browsers. These are generally based on using a regular expression to tokenise a selector, then apply the selector manually similar to the above but more extensivly.
They also allow for case sensitivity for values (e.g. tagName value) and property names to some extent, as well as map HTML attribute names to DOM property names where required (e.g. class -> className, for -> htmlFor, etc.).

how to find common in inline styles?

I have an array of sequential dom element nodes which may or may not have inline styles. I need to end up with an object or array with only keys and values common to all the nodes. Needs to work in IE8+, chrome and FF.
I can't even get one nodes styles into an array without a bunch of other stuff being included as well.
I've tried to use node[x].style but it seems to return a lot of extraneous stuff and other problems.
//g is node array
s=[];
for(k in g)
{
if(g.hasOwnProperty(k) && g[k]) s[k]=g[k];
}
console.log(s);
gives me ["font-weight", cssText: "font-weight: bold;", fontWeight: "bold"] which is close but I only want fontWeight: "bold" in the array. In any case, this only works in chrome.
The only idea I have at the moment that might work is using the cssText and splitting on semi-colons and splitting again on colons but that seems an ugly and slow way to do it especially as I then need to compare to a bunch of nodes and do the same to their styles.
So, I'm hoping someone can come up with a simple elegant solution to the problem posed in the first paragraph.
If you truly want ONLY styles that are specified inline in the HTML for the object, then you will have to deal with text of the style attribute as you surmised.
The .style property will show you more styles than were specified on the object itself (showing you default values for some styles) so you can't use that.
Here's a function that takes a collection of DOM nodes and returns a map of common styles (styles that are specified inline and are the same property and value on every object):
function getCommonStyles(elems) {
var styles, styleItem, styleCollection = {}, commonStyles = {}, prop, val;
for (var i = 0; i < elems.length; i++) {
var styleText = elems[i].getAttribute("style");
if (styleText) {
// split into an array of individual style strings
styles = styleText.split(/\s*;\s*/);
for (var j = 0; j < styles.length; j++) {
// split into the two pieces of a style
styleItem = styles[j].split(/\s*:\s*/);
// only if we found exactly two pieces should we count this one
if (styleItem.length === 2) {
prop = styleItem[0];
val = styleItem[1];
// if we already have this style property in our collection
if (styleCollection[prop]) {
// if same value, then increment the cntr
if (styleCollection[prop].value === val) {
++styleCollection[prop].cntr;
}
} else {
// style tag didn't exist so add it
var newTag = {};
newTag.value = val;
newTag.cntr = 1;
styleCollection[prop] = newTag;
}
}
}
}
}
// now go through the styleCollection and put the ones in the common styles
// that were present for every element
for (var prop in styleCollection) {
if (styleCollection[prop].cntr === elems.length) {
commonStyles[prop] = styleCollection[prop].value;
}
}
return(commonStyles);
}
Working demo: http://jsfiddle.net/jfriend00/JW7CZ/

How can I get width of div, which is written in CSS

I want width of a div element which is specified by developer.
Means if you write $('body').width(), it will provide you width in px , whether you have specified it or not.
I need width of div specified in CSS/JavaScript but not which is calculated by jQuery (which was not actually specified but was inherited).
If not width is specified then I need to know.
The code below will loop through all stylesheets as well as the matching element's style property. This function will return an array of widths.
function getCSSWidths( id ) {
var stylesheets = document.styleSheets;
var widths = [];
var styleWidth;
// loop through each stylesheet
$.each( stylesheets, function( i, sheet ) {
var len = ( sheet.rules.length || sheet.cssRules.length );
var rules = sheet.rules || sheet.cssRules;
// loop through rules
for (var i=0; i < len; i++) {
var rule = rules[i];
// if width is specified in css add to widths array
if (rule.selectorText.toLowerCase() == "#" + id.toLowerCase() ) {
widths.push( rule.style.getPropertyValue("width"));
}
}
});
var el = document.getElementById( id );
// get width specified in the html style attribute
if( el && el.style && el.style.width ) {
widths.push( el.style.width );
}
return widths;
}
getCSSWidths( "test" );
Fiddle here
There is one issue that I can see though as multiple selectors can be specified in the selectorText i.e.
#id1, #id2, .class1 {
// properties
}
In these cases the id passed in as an argument will not match the selectorText property. Maybe someone can come up with a regex expression that will match the specified id :)
<style type="text/css">
.largeField {
width: 65%;
}
</style>
<script>
var rules = document.styleSheets[0].rules || document.styleSheets[0].cssRules;
for (var i=0; rules.length; i++) {
var rule = rules[i];
if (rule.selectorText.toLowerCase() == ".largefield") {
alert(rule.style.getPropertyValue("width"));
}
}
</script>
Possibly duplicate with get CSS rule's percentage value in jQuery or Getting values of global stylesheet in jQuery
For the Javascript specified width, you can try this:
document.getElementById("myDivElement").style.width
If the above returns an empty string, it means it has not been specified by Javascript.
As for rules specified through CSS, find the class name(s) of the element and then the rules specified for that class:
var className = document.getElementById("myDivElement").className;
var cssWidth = getWidthFromClassname(className);
Now you need to define getWidthFromClassname:
function getWidthFromClassname(className)
{
var reqWidth;
var classes = document.styleSheets[0].rules || document.styleSheets[0].cssRules
for(var x=0;x<classes.length;x++) {
if(classes[x].selectorText==className) {
reqWidth = classes[x].style.getPropertyValue("width");
break;
}
}
return reqWidth;
}
document.getElementById('divid').style.width
You could use the clientWidth property, it calculates the actual width of the element, regardless if set through CSS or if it's the natural flow of the document:
document.getElementById('divId').clientWidth
If you want the full content width with margins/paddings/border you can use offsetWidth:
document.getElementById('divId').offsetWidth
https://developer.mozilla.org/en-US/docs/DOM/element.offsetWidth
https://developer.mozilla.org/en-US/docs/DOM/element.clientWidth

Categories

Resources