Use browser to parse CSS without touching the DOM/styles - javascript

I have something like:
const someCSS = `
.foo {
padding: 20px;
background-color: #ddf;
width: 100px;
}
.bar {
height: 100px;
}
.foo {
padding-top: 30px; /* this overrides the previous one */
}
`;
I can add this do the DOM and get back all selectors with each rule like this (jsFiddle):
const style = document.createElement('style');
style.innerHTML = someCSS;
document.head.append(style);
const styleSheet = Array.from(document.styleSheets).find(ss => ss.ownerNode == style);
const rules = Array.from(styleSheet.rules).map(rule => rule.cssText);
function styleToObject(rules, mergeWith = {}) {
return [...rules].reduce(
(obj, rule) => (obj[rule] = rules[rule], obj), mergeWith
);
}
const styleObject = Array.from(styleSheet.rules).reduce(
(obj, rule) => (obj[rule.selectorText] = styleToObject(rule.style, obj[rule.selectorText]), obj), {}
);
document.querySelector('pre').appendChild(
document.createTextNode(JSON.stringify(styleObject, null, '\t'))
);
and get something like this:
{
".foo": {
"padding-top": "30px",
"padding-right": "20px",
"padding-bottom": "20px",
"padding-left": "20px",
"background-color": "rgb(221, 221, 255)",
"width": "100px"
},
".bar": {
"height": "100px"
}
}
Is there another way to have the browser do this, without touching the DOM? I mean have a CSS text parsed by the browser (no regex) without actually styling anything in the page.
I though about adding it to a iFrame, but before its appended to the DOM the document is not available...

The short answer is no, you can't without changing the DOM.
If your concern is the added element triggering a redraw or the loaded style influencing the page in any way, you could add a "never-matching" media rule to the <style>-element you create.
For example:
style.setAttribute('media', '(max-width: 0)');
Working fiddle
EDIT
Was working on an example utilising this trick/hack/solution, you can find it here. Only now noticed the update to the question which is rather similar in mechanics (although my sample will work in less green browsers (not part of the question, I know)).
I've checked some sources which I've come across when I was trying to do a similar thing, most notably MDN - CSSStylesheets is very thorough and states:
A CSSStyleSheet object is created and inserted into the document's
styleSheets list automatically by the browser, when a style sheet is
loaded for a document. As the document.styleSheets list cannot be
modified directly, there's no useful way to create a new CSSStyleSheet
object manually (although Constructable Stylesheet Objects might get
added to the Web APIs at some point). To create a new stylesheet,
insert a or element into the document.
(Emphasis mine. CSO already mentioned by #Ouroborus)
I haven't done a lot of testing on various browsers, but I haven't seen redraws and/or reflows by adding the (media queried) style node to the <head>, unlike adding an <iframe>.
I'm curious if someone out here knows of a solution which relies on the (cross-)browser for processing CSS without hitting the DOM, as I haven't found it and ended up building a Tokenizer/Lexer to create such a tree).

Related

What's the most efficient way to style multiple html elements when generating them with JS?

I'm currently building a web app for some popular software on steam and decided to generate the elements with JS. The reason for doing this is because I'm going to force a refresh after the user confirms a settings menu, and then check localStorage on each customizable element and act accordingly.
The problem is that while rendering the HTML elements I'm styling them individually, compared to just using a class in standard css. I looked around for a potential solution and the only one I could find was by defining the class in the css file and then applying that class through js, but I'll end up with a lot of wasted classes this way.
for(let i=0; i<title.length; i++) {
// creation
const a = document.createElement("a");
// styling
a.style.display = "block";
a.style.width = "90%";
a.style.margin = "auto";
a.style.padding = "20px";
a.style.backgroundColor = "#2f2f2f";
// attaching it to element as child
sets.appendChild(a);
}
This is currently how I have all my elements being created, which is less than ideal. I want to know whether or not it's possible to create a css class from within js, or at least the best method of achieving the end result. It might not be a problem at the current stage, but I'm likely going to have hundreds of elements sharing the same styling in the near future.
**thanks for the help in advance.
Use setAttribute
a.setAttribute('class', 'my-css-class');
You can then add the styles with css:
.my-css-class {
background-color: #2f2f2f;
}
In terms of pure efficiency, I don't think you can do a lot better than this except maybe generate an HTML string and use innerHTML but I wouldn't do that as it might save a few miliseconds but it makes the code less maintainable in the long run. The fragment is already a better alternative as suggested by #fcalderan. You can also use a CSS class as pointed by #Sirence. Also, if you care about code-style, it's a little loss on performance but a gain in readability, you can use forEach() instead of a classic for loop.
CSS:
.set-title {
display: block;
width: 90%;
margin: auto;
padding: 20px;
backgroun-color: #2f2f2f;
}
JS:
let fragment = new DocumentFragment();
title.forEach(t => {
const a = document.createElement("a");
a.className = 'set-title';
fragment.appendChild(a);
});
sets.appendChild(fragment);

Asigning CSS property to variable in JavaScript [Polymer]

I have simple question about asigning values of CSS properties to some variables is JavaScript in Polymer app.
Assume I have one div with width:200px;. In some JavaScript function i want to change width to 200px+10px.
I know i can apply this in JS in this way div.style.top = '210px';, but this is not what I need!
I want to changing this width property, and have full control about this.
So I readed i can make some custom CSS variable to save my width:
:host {
--my-width: 200px;
}
div{
width: var(--my-width);
}
This is nice because now I have one CSS variable, and I can set this attribute to few selectors, elements.
The question is - how to get this variable in JS and change it in that way (pseudocode):
--my-width = --my-width + 10px
I know i can use this
this.updateStyles({
'--my-width': '210px'
});
to replace value, but I want to code something like this:
this.updateStyles({
'--my-width': '--my-width'.value + 10px
});
So that I could changing this width by adding some values (+10px) , not defining new (= 210px)
I'm asking about how to make this and about some good practices in polymer, how to do that.
You can use window.getComputedStyle and getPropertyValue:
const styles = window.getComputedStyle(this);
const myWidth = styles.getPropertyValue('--my-width');
const newWidth = `${parseFloat(myWidth) + 10}px`;
this.updateStyles({ '--my-width': newWidth });
It's worth reading the Polymer docs on custom properties. Although I'm not sure they're 100% up-to-date, they have some useful information re: Shady DOM.
An alternative, depending on your use case and the browsers you're targeting, is the CSS calc() function:
div {
width: calc(var(--my-width) + 10px);
}
You could do the same with updateStyles, of course.

How to detect resize of any element in HTML5

What should the best practices to listen on element resize event?
I want to re-position an element (jQuery dialog in my case), once it's size changed. But I am now more interested to do in a general way to to listen to resize event, unaware of how the resize happens. It suppose to be simple until I found an element can be re-sized by
window resize
content text changes
children elements or their children elements resized
a sibling element resize (e.g. a cell in a table)
JavaScript changes it src(of img)/style attribute directly (or it's child's)
JavaScript rewrite CSS rules or stylesheet
native resize feature textarea or CSS3 resize
browser's zoom or text-enlarge
CSS transition or animations (by :hover or any other mean)
In the de-facto standard, there is a event window.onresize to subscribe resize on a window/frame.
But there is no a standard event on the HTML content or DOM Elements.
I come across the following thought
DOM Level 3 event target only on window/document type
IE has onresize for Elements but it is IE only implementation
MutationObserver which replace Mutation Events, but it does not fit the need of "onresize"
naive JavaScript polling
MutationObserver is close(inner DOM changes), but it does not (yet) cross browser (IE10 does not support) and it generate noise, not CSS aware.
A naive JavaScript polling should work in all case, but it generate either delay or CPU waste of many poll.
As of July 2020, ResizeObserver is still un-official in W3C nor WhatWG but it is already supported by all major browsers since support Safari 13.1 since 2020-Mar-24.
FYI, there's a spec for a new ResizeObserver API. Chrome seems to be the only browser that has implemented it as of Aug 2018 (see caniuse.com), but there's at least one polyfill you can use now (which uses MutationObserver).
Yes there is not simple solution, that's not good.
I've found something very useful for this.: cross browser event based element resize
It's tricky, appending some needed html to the element that have to be listened and detects scrolling event.
Some html example from that page:
<div class="resize-triggers">
<div class="expand-trigger"><div></div></div>
<div class="contract-trigger"></div>
</div>
Also Some JS:
var myElement = document.getElementById('my_element'),
myResizeFn = function(){
/* do something on resize */
};
addResizeListener(myElement, myResizeFn);
removeResizeListener(myElement, myResizeFn);
But it works for elements those are able to have children, not for self-closing tags.
You can see the demo http://jsfiddle.net/3QcnQ/67/
Well, there is a easy library for that. Although there's nothing official how to listen on dimension changes of all types of elements and only window supports it at the moment we have luckily a polifill for that that works very accurate and supports all browsers even inclusive IE6+.
https://github.com/marcj/css-element-queries
You can find there a class ResizeSensor. To setup a listener on a element you can just do:
new ResizeSensor($('.myelements'), function() {
console.log('changed');
});
Given yourelement, when the size changes (ex. a text translation took place) you can doyourstuff(), including
ro.unobserve(yourelement);
var inilen = yourelement.offsetWidth;
var ro = new ResizeObserver( entries => {
for (let entry of entries) {
const cr = entry.contentRect;
if (inilen !== cr.width) {
doyourstuff();
}
}
});
ro.observe(<your element>);
In the future, we may have the luxury of the ResizeObserver everywhere, but for less recent browsers in 2021 we need to make do with a workaround. This article has already been posted, but it's pretty old and I think the solution might be overly complicated for modern browsers.
Still, the core idea remains: add an <object> element as a child with width: 100%; height: 100%;, and set a resize listener on its inner window object.
Here's some demo code that works in the latest Chrome and Firefox:
const div = document.getElementById('demo')
const obj = document.createElement('object')
obj.className = 'resize-detector'
obj.type = 'text/html'
obj.data = 'about:blank'
obj.addEventListener('load', function() {
// Initialize once.
handleResize()
// Add resize handler on the <object>'s inner window.'
obj.contentWindow.addEventListener('resize', function() {
handleResize()
})
})
div.appendChild(obj)
function handleResize() {
document.getElementById('size').innerHTML = `${div.offsetWidth}×${div.offsetHeight}`
}
.resizable {
/* Make this the offset parent of the <object> */
position: relative;
}
#demo {
width: 100px;
height: 100px;
background-color: #def;
/* Allow user resizing, for testing. */
resize: both;
overflow: hidden;
}
object.resize-detector {
display: block;
visibility: hidden;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
<div id="demo" class="resizable">
<div id="size"></div>
</div>
It doesn't work inside the StackOverflow snippet because of some same-origin policy shenanigans, but here's a JSFiddle with the same code.

How to replace an any kind of element with a div of equal boxing?

I'm around trying to remove a DOM element (I'll put it elsewhere) and I need the position of the sibling elements do not change.
I tried some variations of this.
var elem = $("#theElement");
var ghost = $('<div></div>');
ghost.css({
width: elem.outerWidth(true),
height: elem.outerHeight(true),
margin: 0
});
elem.replaceWith(ghost);
But the document collapses slightly.
I know I can just change the visibility of the element, but not what I need. I'll put it somewhere else in the DOM and can not be duplicated.
The Question
How to replace any kind of element with a div that occupies the same space?
EDIT
Keep in mind that i can not change the source element attributes.
I do not know in advance which item and which properties it has, just take it out of where it is and move it elsewhere.
The jQuery documentation says:
.outerHeight(true): if the includeMargin argument is set to true, the margin (top and bottom) is also included.
.outerWidth(true): If includeMargin is omitted or false, the padding and border are included in the calculation; if true, the margin is also included.
plunker
That is because of the margin given by the browser, called user agent stylesheet in dev tools.
I have modified your plunk to have css like this
h1 {
color: red;
margin:0px !important;
}
Issue seemed to be resolved.
EDIT:
I have edited your code to be something like this:
$(function(){
var elem = $("h1");
var ghost = $('<div></div>');
ghost.css({
width: elem.outerWidth(),
height: elem.outerHeight(),
margin: 21
});
Since you can not modify the source, identify what styling the browser is putting onto it and give your ghost element the same styling.
To detect what css the browser is putting onto your element, refer
http://www.iecss.com
http://mxr.mozilla.org/mozilla-central/source/layout/style/html.css
http://trac.webkit.org/browser/trunk/Source/WebCore/css/html.css

How can I check if CSS calc() is available using JavaScript?

Is there a way to check if the CSS function calc is available using JavaScript?
I found lot of questions and articles about getting the same behaviour as calc using jQuery, but how can I only check if it's available?
In Modernizr you can find the test as css-calc currently in the non-core detects. They use the following code:
Modernizr.addTest('csscalc', function() {
var prop = 'width:';
var value = 'calc(10px);';
var el = document.createElement('div');
el.style.cssText = prop + Modernizr._prefixes.join(value + prop);
return !!el.style.length;
});
A bit late to the party but I figured I should share this because I was myself struggling with it. Came up with the idea of by using jQuery, I can create a div that uses the calc() value in a CSS property (such as width in this case) and also a fallback width in case the browser does not support calc(). Now to check whether it supports it or not, this is what I did:
Let's create the CSS style for the currently "non-existing" div element.
/* CSS3 calc() fallback */
#css3-calc {
width: 10px;
width: calc(10px + 10px);
display: none;
}
Now, if the browser does not support calc(), the element would return a width value of 10. If it does support it, it would return with 20. Doesn't matter what the values are exactly, but as long as the two width properties have different values in the end (in this case 10 and 20).
Now for the actual script. It's pretty simple and I suppose it's possible with regular JavaScript too, but here's the jQuery script:
// CSS3 calc() fallback (for unsupported browsers)
$('body').append('<div id="css3-calc"></div>');
if( $('#css3-calc').width() == 10) {
// Put fallback code here!
}
$('#css3-calc').remove(); // Remove the test element
Alternatively, native javascript check, and width:
#css3-calc { width: 1px; width: calc(1px + 1px); }
if( document.getElementById('css3-calc').clientWidth==1 ){
// clientHeight if you need height
/* ... */
}
Calc detection was added to modernizer according to their news page.
http://modernizr.com/news/
As well as tightening up support for existing tests, we've also added
a number of new detects, many submitted by the community:
[...]
css-calc
var cssCheck = document.createElement("DIV");
cssCheck.style.marginLeft = "calc(1px)";
if (cssCheck.style.getPropertyValue("margin-left"))
{
alert("calc is supported");
}
cssCheck = null;

Categories

Resources