I'm writing a script that has access to a page's DOM, and I want to know what color would a particular link be if it had been visited. I'm aware that I cannot know whether the link has actually been visited, I don't care about that. I just want to know the color set for a:visited.
You can't get the :visited style with window.getComputedStyle according to the following MDN blog post because of privacy rules. So you can't programmatically tell if a user visited a link or not by checking the color.
But you can do it by walking through the stylesheets and search for a specific selector. Then if the selector is a match return the style.color property of that set style rule. This will not get the computed value but the value as set in the stylesheet.
const getStyleRuleColor = selector => {
for (const { rules } of document.styleSheets) {
for (const { style, selectorText } of rules) {
if (selectorText === selector) {
return style.color;
}
}
}
return null;
}
const selectorElement = 'a:visited';
const selectorId = '#someid:visited';
const elementVisitedColor = getStyleRuleColor(selectorElement);
const idVisitedColor = getStyleRuleColor(selectorId);
console.log(`${selectorElement} :`, elementVisitedColor);
console.log(`${selectorId} :`, idVisitedColor);
a {
color: black;
}
a:visited {
color: red;
}
#someid:visited {
color: green;
}
<a id="someid" href="http://someurl.com">I have a color</a>
Related
Let's say I want to create a custom element which bolds every other character. For example, <staggered-bold>Hello</staggered-bold> would become "Hello, where the H, l, and o are all bolded.
There's no nth-letter CSS selector, so as far as I know the only way to achieve this effect is to wrap each individual character with a span programmatically. To do that, I have an implementation that clones the text content into the Shadow Dom, so that the child content as specified by the user is not changed.
Unfortunately, by doing so, something like <staggered-bold><span class="red">red</span></staggered-bold> no longer works, because by cloning the content into the Shadow Dom, the class CSS declarations for the wrapped span no longer apply.
Here's a proof-of-concept implementation, showcasing that the red and blue text are in fact not red and blue:
customElements.define('staggered-bold', class extends HTMLElement {
constructor() {
super()
this
.attachShadow({ mode: 'open' })
.appendChild(document.getElementById('staggered-bold').content.cloneNode(true))
}
connectedCallback() {
// this is a shadow dom element
const text = this.shadowRoot.getElementById('text')
this.shadowRoot.querySelector('slot').assignedNodes().forEach(node => {
const content = node.textContent.split('').map((char) => {
return `<span class="char">${char}</span>`
}).join('')
const newNode = node.nodeType === Node.TEXT_NODE ? document.createElement('span') : node.cloneNode(true)
newNode.innerHTML = content
text.appendChild(newNode)
})
}
})
.red { color: red; }
.blue { color: blue; }
<p><staggered-bold>Some text</staggered-bold></p>
<p><staggered-bold><span class="red">Red</span> <span class="blue">Blue</span></staggered-bold></p>
<template id="staggered-bold">
<style>
.hide { display: none; }
.char:nth-child(odd) {
font-weight: bold;
}
</style>
<span class="hide"><slot></slot></span>
<span id="text"></span>
</template>
My question is this: what is a good approach to styling each character in a custom element while preserving characteristics provided in the light dom?
One approach I've considered is to manipulate the light dom directly, but I have been avoiding that since I think of the light dom as being in full control of the usage-site (ie. things get complicated very quickly if external JS is manipulating the child of staggered-bold). I'm open to being convinced otherwise, especially there's no real alternative.
I've also considered cloning the content into a named slot so that the original text is preserved, and yet the content continues to live in the light dom. However, I feel like this is still kind of icky for the same reason as the previous paragraph.
You can't have the cake and eat it
Global CSS does NOT style shadowDOM (unless you use CSS properties)
Easier to not use shadowDOM at all.
With an extra safeguard: store the state so the element is properly redrawn on DOM moves.
Note: The setTimeout is always required,
because the connectedCallback fires early on the opening tag;
there is no parsed (innerHTML) DOM yet at that time.
So you have to wait for that DOM to be there.
If you do need a TEMPLATE and shadowDOM, dump the whole .innerHTML to the shadowRoot; but Global CSS still won't style it. Or <slot> it.
Do read: ::slotted CSS selector for nested children in shadowDOM slot
If you go with <slot> consider the slotchange Event
but be aware for an endless loop; changing lightDOM will trigger the slotchange Event again
<staggered-bold>Some text</staggered-bold>
<staggered-bold><span class="red">Red</span> <span class="blue">Blue</span></staggered-bold>
<style>
staggered-bold { display: block; font: 21px Arial }
staggered-bold .char:nth-child(even) { color: blue }
staggered-bold .char:nth-child(odd) { color: red; font-weight: bold }
</style>
<script>
customElements.define('staggered-bold', class extends HTMLElement {
connectedCallback() {
setTimeout(() => { // make sure innerHTML is all parsed
if (this.saved) this.innerHTML = this.saved;
else this.saved = this.innerHTML;
this.stagger();
})
}
stagger(node=this) {
if (node.children.length) {
[...node.children].forEach( n => this.stagger(n) )
} else {
node.innerHTML = node.textContent
.split('')
.map(ch => `<span class="char">${ch}</span>`)
.join('');
}
}
})
document.body.append(document.querySelector("staggered-bold"));//move in DOM
</script>
In the end I attempted a strategy I'm calling the mirror node. The idea is the custom element actually creates an adjacent node within which the split characters are placed.
The original node remains exactly as specified by the user, but is hidden from view
The mirror node actually displays the staggered bold text
The below implementation is incomplete, but gets the idea across:
class StaggeredBoldMirror extends HTMLElement {
constructor() {
super()
}
}
customElements.define('staggered-bold', class extends HTMLElement {
constructor() {
super()
this
.attachShadow({ mode: 'open' })
.appendChild(document.getElementById('staggered-bold').content.cloneNode(true))
}
connectedCallback() {
setTimeout(() => {
const mirror = new StaggeredBoldMirror()
mirror.innerHTML = this.divideIntoCharacters()
this.parentNode.insertBefore(mirror, this)
})
}
divideIntoCharacters = (node = this) => {
return [...node.childNodes].map(n => {
if (n.nodeType === Node.TEXT_NODE) {
return n.textContent
.split('')
.map(ch => `<span class="char">${ch}</span>`)
.join('')
} else {
const nn = n.cloneNode(false)
nn.innerHTML = this.divideIntoCharacters(n)
return nn.outerHTML
}
}).join('')
}
})
customElements.define('staggered-bold-mirror', StaggeredBoldMirror)
.red {
color: red;
}
.blue {
color: blue;
}
staggered-bold-mirror .char:nth-child(odd) {
font-weight: bold;
}
<p><staggered-bold>Some text</staggered-bold></p>
<p><staggered-bold><span class="red">Red</span> <span class="blue">Blue</span></staggered-bold></p>
<template id="staggered-bold">
<style>
.hide { display: none; }
</style>
<span class="hide"><slot></slot></span>
</template>
The vanilla component can be outfitted with a slotchange listener in order to rebuild its mirror whenever its inner content changes. The disconnectedCallback method can also ensure that when one node is removed, the other is too.
Of course, there are downsides to this approach, such has potentially having to also mirror events and the fact that it still manipulates the light dom.
Depending on the use case, either this or Danny's answer works.
Given the below css style, how can I reset the background-color with javascript? Not just for the currently highlighted button, but for all those highlighted in the future?
#bottom_buttons > div.bottom_button.highlighted {
background-color: #343434;
}
For instance:
$("div.bottom_button.highlighted").css("background-color", "#e67300");
this works, but is overwritten as soon as the page changes. IE loses focus or updates.
Similarly
document.getElementById("bottom_button_" + button).style.backgroundColor = "#e67300";
works to change the numbered button on click, but also doesn't last.
It's going to be difficult to have persistent changes in CSS from page to page with a refresh. That being said you can do it with local storage.
Here's how you do it :
function overwriteStyles(styles) {
var styleOverwrites = document.getElementById('style-overwrites');
if (styleOverwrites === null) {
styleOverwrites = document.createElement('style');
styleOverwrites.id = 'style-overwrites';
document.head.appendChild(styleOverwrites);
}
styleOverwrites.innerHTML = styles;
// Save styles to local storage
localStorage.setItem('overridden-styles', styles);
}
// Here styles are grabbed from local storage and loaded for persistence
document.addEventListener("DOMContentLoaded", function(event) {
var styles = localStorage.getItem('overriden-styles');
if(styles != null)
{
overwriteStyles(styles);
}
});
// Example of overwriting styles
overwriteStyles(`
#bottom_buttons > div.bottom_button.highlighted {
background-color: #e67300;
}
`);
On a site a CSS class is periodically added to an element by JavaScript.
I'd like not to show the visible effect of that class. In other words what I need is an effect similar to switching the class off in the development console of the browser. Let the class exist but without any consequences.
I understand that I can catch events and remove the class when it appears.
But maybe there is just a more simple way in my case?
In other words: there is a CSS class, I would like it to be present but without any visual effects. If it is impossible, that will also be an answer.
You could use something like this. Access the document's stylesheets and apply some sort of regex matching to figure out which rules are associated with the class in question. Then simply unset the styling on the rules. Note that just because the class name is found in the stylesheet rule doesn't mean it is the element being affected by the styles...but this should get you going in the right direction.
function removeClassStyling(clazz) {
var classRegex = new RegExp('\\.'+clazz.toLowerCase()+'\\b','i')
for (var s=0; s<document.styleSheets.length; ++s) {
var sheet = document.styleSheets[s];
for(var r=0; r<sheet.cssRules.length; ++r) {
var rule = sheet.cssRules[r];
if(rule.selectorText && rule.selectorText.match(classRegex)) {
var properties = Object.keys(rule.style);
for(var p=0; p<properties.length; ++p){
if(rule.style[properties[p]]) rule.style[properties[p]] = "";
}
console.log('removed styling for "'+clazz+'"');
}
}
}
}
setTimeout(function(){ removeClassStyling('unwanted-class') }, 1500)
.unwanted-class {
border: 1px solid red;
}
<div class="unwanted-class"> Test </div>
This should do the trick in most circumstances. I imagine there are circumstances that would evade this, but I can't think of them.
Basically you need to iterate document.styleSheets collection, then iterate each rule contained within and compare the CSSStyleRule.selectorText for each rule against a regular expression.
Regular expressions can be faulty, so I've included a check against an element with the supplied class name using the Element#matches() method. That method can also provide false positives in the case where the element matches some other part of the selector, so the two together should reasonably prevent any false positives.
Once you have a list of all the CSS rules that apply to a given class, you can simply delete them all. This can be done in the same step as finding them, but I've done it separately in for example's sake.
function findClassRules(name) {
const element = document.createElement('div')
element.classList.add(name)
const regex = new RegExp(`\\\.${ name }([^\w]|$)`, 'i')
const test = {
rule: rule => {
if('cssRules' in rule) {
return test.sheet(rule)
} else if('selectorText' in rule) {
const selector = rule.selectorText
return selector.match(regex) && element.matches(selector) && rule
}
},
sheet: sheet => {
const rules = Array.from(sheet.cssRules, test.rule).filter(Boolean)
return rules.length && { sheet, rules }
}
}
return Array.from(document.styleSheets, test.sheet).filter(Boolean)
}
function processSheet({ sheet, rules }) {
rules.forEach(rule => {
if('rules' in rule) {
processSheet(rule)
} else {
sheet.deleteRule(rule)
console.log(`Removed: ${ rule.cssText }`)
}
})
}
document.getElementById('clean').addEventListener('click', event => {
findClassRules('test').forEach(processSheet)
}, false)
.test { padding: 5px }
.test2 { padding: 10px }
#media screen {
.test { margin: 15px }
}
<p class="test">Hello world!</p>
<button id="clean">Remove CSS</button>
<style type="text/css">
.test { color: red }
</style>
<style type="text/css">
.test { border: 1px solid red }
</style>
How about you comment the class in your css file
I have a css file, where I place all my styles by page.
How I can get the selectors' class/id by searching for a property in css.
For example, I want to change the text color by all pages. I need to get an array with all the selectors that have a property of "color".
How I can do this?
so fun, i have answered a similar question (but for another problem). :)
=> see = https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables
let myGroup = document.querySelector('#my-Group');
let myGroupCSS = getComputedStyle(myGroup);
Bt_change_Color.onclick = function () {
let current_Color = myGroupCSS.getPropertyValue('--color_for_group');
console.log('first color', current_Color); // #305395
myGroup.style.setProperty('--color_for_group', '#95305a');
}
#my-Group {
--color_for_group: #305395;
}
h2 {
color: var(--color_for_group);
}
h4{
color: var(--color_for_group);
}
p {
color: var(--color_for_group);
}
<div id="my-Group">
<h2>title</h2>
<h4>sub tilte</h4>
<p>paragraph</p>
</div>
<button id="Bt_change_Color"> Change Color </button>
Alright, I'm creating a system for my webpage that allows users to change the theme. How I want to accomplish this is by having all the colors as variables, and the colors are set in the :root part of the CSS.
What I want to do is change those colors via JavaScript. I looked up how to do it, but nothing that I attempted actually worked properly. Here's my current code:
CSS:
:root {
--main-color: #317EEB;
--hover-color: #2764BA;
--body-color: #E0E0E0;
--box-color: white;
}
JS:
(Code to set the theme, it's ran on the click of a button) - I didn't bother adding the :root change to the other 2 themes since it doesn't work on the Dark theme
function setTheme(theme) {
if (theme == 'Dark') {
localStorage.setItem('panelTheme', theme);
$('#current-theme').text(theme);
$(':root').css('--main-color', '#000000');
}
if (theme == 'Blue') {
localStorage.setItem('panelTheme', 'Blue');
$('#current-theme').text('Blue');
alert("Blue");
}
if (theme == 'Green') {
localStorage.setItem('panelTheme', 'Green');
$('#current-theme').text('Green');
alert("Green");
}
}
(Code that is ran when the html is loaded)
function loadTheme() {
//Add this to body onload, gets the current theme. If panelTheme is empty, defaults to blue.
if (localStorage.getItem('panelTheme') == '') {
setTheme('Blue');
} else {
setTheme(localStorage.getItem('panelTheme'));
$('#current-theme').text(localStorage.getItem('panelTheme'));
}
}
It shows the alert, but does not actually change anything. Can someone point me in the right direction?
Thank you #pvg for providing the link. I had to stare at it for a little to understand what was going on, but I finally figured it out.
The magical line I was looking for was this:
document.documentElement.style.setProperty('--your-variable', '#YOURCOLOR');
That did exactly what I wanted it to do, thank you very much!
For those who want to modify the actual style sheet the following works:
var sheet = document.styleSheets[0];
sheet.insertRule(":root{--blue:#4444FF}");
More info at here: https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule
I think this is cleaner and easier to remember.
to set/get css variables to/from :root
const root = document.querySelector(':root');
// set css variable
root.style.setProperty('--my-color', 'blue');
// to get css variable from :root
const color = getComputedStyle(root).getPropertyValue('--my-color'); // blue
Example: setting multiple variables all at once
const setVariables = vars => Object.entries(vars).forEach(v => root.style.setProperty(v[0], v[1]));
const myVariables = {
'--color-primary-50': '#eff6ff',
'--color-primary-100': '#dbeafe',
'--color-primary-200': '#bfdbfe',
'--color-primary-300': '#93c5fd',
'--color-primary-400': '#60a5fa',
'--color-primary-500': '#3b82f6',
'--color-primary-600': '#2563eb',
'--color-primary-700': '#1d4ed8',
'--color-primary-800': '#1e40af',
'--color-primary-900': '#1e3a8a',
};
setVariables(myVariables);
To use the values of custom properties in JavaScript, it is just like standard properties.
// get variable from inline style
element.style.getPropertyValue("--my-variable");
// get variable from wherever
getComputedStyle(element).getPropertyValue("--my-variable");
// set variable on inline style
element.style.setProperty("--my-variable", 4);
I came here looking how to toggle the :root color-scheme with JavaScript, which sets the browser to dark mode (including the scroll bars) like this:
:root {
color-scheme: dark;
}
using the #Daedalus answer above, this is how I implemented my dark mode detection from user preference:
const userPrefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const preferredTheme = userPrefersDarkMode ? 'dark' : 'light';
document.documentElement.style.setProperty("color-scheme", preferredTheme);
or with saved toggle:
const savedTheme = localStorage.getItem('theme');
if (savedTheme == 'dark') {
thisTheme = 'light'
}
else {
thisTheme = 'dark'; // the default when never saved is dark
}
document.documentElement.style.setProperty("color-scheme", thisTheme);
localStorage.setItem('theme', thisTheme);
see also the optional meta tag in the header:
<meta name="color-scheme" content="dark light">
old jquery magic still working too
$('#yourStyleTagId').html(':root {' +
'--your-var: #COLOR;' +
'}');
TL;DR
A solution to the problem could be the below code:
const headTag = document.getElementsByTagName('head')[0];
const styleTag = document.createElement("style");
styleTag.innerHTML = `
:root {
--main-color: #317EEB;
--hover-color: #2764BA;
--body-color: #E0E0E0;
--box-color: white;
}
`;
headTag.appendChild(styleTag);
Explanation:
Although #Daedalus answer with document.documentElement does the job pretty well, a slightly better approach is to add the styling into a <style> HTML tag (solution proposed).
If you add document.documentElement.style then all the CSS variables are added into the html tag and they are not hidden:
On the other hand, with the proposed code:
const headTag = document.getElementsByTagName('head')[0];
const styleTag = document.createElement("style");
styleTag.innerHTML = `
:root {
--main-color: #317EEB;
--hover-color: #2764BA;
--body-color: #E0E0E0;
--box-color: white;
}
`;
headTag.appendChild(styleTag);
the HTML tag will be cleaner and if you inspect the HTML tag you can also see the :root styling as well.
Read only, retrieve all CSS --root rules in an array, without using .getComputedStyle().
This may allow to retrieve values before full DOM content load, to create modules that use global root theme variables, but not via CSS. (canvas context...)
/* Retrieve all --root CSS variables
* rules into an array
* Without using getComputedStyle (read string only)
* On this example only the first style-sheet
* of the document is parsed
*/
console.log(
[...document.styleSheets[0].rules]
.map(a => a.cssText.split(" ")[0] === ":root" ?
a.cssText.split("{")[1].split("}")[0].split("--") : null)
.filter(a => a !== null)[0]
.map(a => "--"+a)
.slice(1)
)
:root {
--gold: hsl(48,100%,50%);
--gold-lighter: hsl(48,22%,30%);
--gold-darker: hsl(45,100%,47%);
--silver: hsl(210,6%,72%);
--silver-lighter: hsl(0,0%,26%);
--silver-darker: hsl(210,3%,61%);
--bronze: hsl(28,38%,67%);
--bronze-lighter: hsl(28,13%,27%);
--bronze-darker: hsl(28,31%,52%);
}
/*My style*/
:root {
--hl-color-green: green;
}
/*My update*/
* {
--hl-color-green: #0cc120;
}