4

How to dynamically modify CSS rule set (e.g. with a class selector) from JavaScript within Firefox Add-on using XUL, SDK or WebExtensions techniques? Trying to support Firefox 29.0b1 through 49.0a1.

Problem I'd like to solve (from within a XUL document within a Firefox add-on, which is sometimes different than a web page):

file.xul: (has this line at the top)

<?xml-stylesheet href="chrome://{GUID}/skin/text.css" type="text/css"?>

text.css: (relevant class within the file)

.myTextClass {
    max-width: 800px;
}

code:

// Modify the class selector rule set to add another rule,
// i.e. font-family: DejaVu Sans Mono

// Later on, after user input, change or remove this font in this one class

This may be in-part a duplicate of #20927075, which didn't have satisfactory answers, but I am additionally interested in SDK or WebExtensions techniques as well, to ensure the feature I want to add to an existing XUL based add-on can be implemented with the newer APIs.

I want to implement a rudimentary "font selector" UI feature, and have it apply the new font to the class. I have a simple CSS rule set with a class selector which defines one class with one rule (for now, but may have other rule sets manually/statically added), to which I would like to add another rule, and have the add-on's interface updated.

The options I am aware of, but which all seem too clumsy.

1) document.getElementById, set style attribute manually. I do this for the demo area, that's fine.

2) Similar as above, but getElementByClassName. However, this is NOT USING A CLASS! This is using a class just to find nodes, to which I change each element's attribute, either style or class. This seems rather slow. I could create a massive CSS file with a class every possible font, then change class attribute, but this is stupid. Worse than just changing style attribute.

3) There's the style sheet service, but I don't know if it's still in existence or will be obsoleted or removed (it's XPCOM based?), it's not part of Services.jsm, and it is a very blunt instrument that can merely load or unload an entire style sheet, when I merely want to modify one rule from one class definition.

4) There's jQuery, but, no. Just no. Do not even bother suggesting that. Too much overhead for a small add-on, and no idea if the technique works within XUL interface code.

5) There's things like document.styleSheets which seems right but doesn't get what I want (unless I am mistaken?). Everything seems read-only. More below.

6) There's manual clobbering of entire style sheets by fiddling with document.getElementsByTagName('head'), which again clobbers an entire css sheet rather than one rule of one class.

If there is really no other option, I may have to use method 2 above. But I was hoping for an option that allows fine grained control over CSS from within a XUL using JavaScript.

What I was trying, to understand how to do this:

for ( i = 0; i < document.styleSheets.length; i++ ) {
    styleSheet = document.styleSheets[ i ];

    console.log( 'style sheet: ' + i + ' ' + styleSheet.href );

    if ( styleSheet.href === 'chrome://{GUID}/skin/text.css' ) {
        for ( j = 0; j < styleSheet.cssRules.length; j++ ) {
            styleRule = styleSheet.cssRules[ j ];
            console.log( 'style sheet: ' + i + ' ' + 'styleRule: ' + styleRule.cssText );
        }
    }
}

During this edit, I now get the code to display the rule which defines the class, whereas last night I was getting an invalid property error. I had my terminology mixed up, thinking the rules were the line-items inside the class. But anyways, from that point, I was not sure how to proceed.

The above code (in file.js which is referenced from a file.xul <script> tag) outputs console.log similar to this (in Firefox 29.0b1):

"style sheet: 0 chrome://git-addonbar/skin/addonbar.css"                              file.js:189
09:49:47.101 "style sheet: 1 chrome://browser/content/browser.css"                    file.js:189
09:49:47.101 "style sheet: 2 chrome://browser/content/places/places.css"              file.js:189
09:49:47.101 "style sheet: 3 chrome://browser/skin/devtools/common.css"               file.js:189
09:49:47.101 "style sheet: 4 chrome://browser/skin/customizableui/panelUIOverlay.css" file.js:189
09:49:47.101 "style sheet: 5 chrome://browser/skin/browser.css"                       file.js:189
09:49:47.101 "style sheet: 6 chrome://browser/skin/browser-lightweightTheme.css"      file.js:189
09:49:47.101 "style sheet: 7 chrome://global/skin/global.css"                         file.js:189
09:49:47.101 "style sheet: 8 chrome://{GUID}/skin/text.css"                           file.js:189
09:49:47.102 "style sheet: 8 styleRule: .myTextClass { max-width: 800px; }"           file.js:194
09:49:47.102 "style sheet: 9 null"                                                    file.js:189
Community
  • 1
  • 1
  • 2
    You say "modify" and "css rules". But then you talk about the `style` attribute. That sounds like you want to affect particular elements' styles, not modify existing rules. Because you cannot change rule definitions through an element's style property, you can only add some to the cascading set which may or may not affect its final computed style depending on priorities. So maybe your question is poorly worded. You should say what you actually want to achieve and on which elements. – the8472 May 28 '16 at 11:58
  • I stated exactly what I wanted to achieve. I stated that I want to modify a single rule in a given class to change font (e.g. font-family). I stated that this class already has one other rule that I do not want to modify or overwrite. I know that style attributes do not modify classes, that is why I asked the question. Most people suggest just using getElementsByClassName, which as a code pattern, doesn't modify classes, just "easily" (from programmer's perspective) allows one to modify all style attributes. Can't be more clear, but will try to clarify the question further. –  May 28 '16 at 13:12
  • Sorry for any confusion, I had mixed up some terminology, and attempted to correct in the question. Trying to modify a `css rule set` with a use-case of a `class selector`, preferably at the level of individual `rules`. –  May 28 '16 at 14:15

3 Answers3

5

5) There's things like document.styleSheets which doesn't seem to get what I want (unless I am mistaken?). Everything seems read-only.

This is the correct option. While the styleSheets property is read-only (meaning you can't assign like this: document.styleSheets = val), the stylesheet object you get does allow modifications.

Since you only care about one (modern!) browser, this is easier than Changing a CSS rule-set from Javascript in a cross-browser way. A trivial example:

function changeCSS() {
  let myClassRule = document.styleSheets[0].cssRules[0];
  if (myClassRule.style.color == "red")
    myClassRule.style.color = "blue";
  else
    myClassRule.style.color = "red";
}
.my-class {
  color: red;
}
<p class="my-class">First paragraph</p>
<p class="my-class">Second paragraph</p>
<button onclick="changeCSS()">Change the stylesheet!</button>

You'll have to extend it to find the stylesheet and the rule you care about - the question linked above has a minimal example of doing that.

Community
  • 1
  • 1
Nickolay
  • 31,095
  • 13
  • 107
  • 185
  • Part of my confusion is from the MDN site which has incomplete, contradictory information, and said most of these objected were removed in v41-45. However, it seems to work fine in v29.0b1 - 49.0a1. The `.style.` object was well documented here: http://www.w3schools.com/jsref/dom_obj_style.asp . I saw no easy way to "delete", so I just set it to "inherit" for the default case of no user assigned value, or said value is unassigned by user. –  May 28 '16 at 18:10
  • @user314159 MDN is a wiki, so feel free to improve it! FWIW I don't see where it says most were removed in v41-45... – Nickolay May 29 '16 at 01:33
  • For a long while, all logins and account creations were disabled. I am still in a learning phase and relying upon documentation to educate me. Perhaps in some areas I am progressing to the stage where information will flow the opposite direction. But as yet I am not quite confident enough in my knowledge or experience to create formal docs for others. But I feel the task is larger than tweaking a few Wiki pages, and my impression of working with Mozilla Foundation to get any things changed is a very draining experience full of endless debates, entrenched ideas, rejection and stagnation. –  May 29 '16 at 02:18
  • https://developer.mozilla.org/en-US/docs/Web/API/CSSRule Look at the bottom. But, as I look around, there are maybe 2-3 or 4 other pages which describe fragments of the same thing. This particular page was one of the very few that search engines kept repeatedly spitting out even after variations of relevant keywords. There's a lot of Red Links / Undocumented things as well, here and other pages. Maybe one day I can revisit as an editor. But for now, I am still getting up to speed as I enter 3rd month learning about add-ons. –  May 29 '16 at 02:36
  • @user314159 in my experience, the folks working on MDN are friendly, even though overworked, so consider posting your feedback to [dev-mdc](https://developer.mozilla.org/en-US/docs/MDN/Community/Conversations). As you rightly notice, there are too many pages in the reference and making them all perfect or changing organization to reduce their number is daunting task. What I try to do (when I'm bored) is to find the "central" pages and make them clearer, and it's essential to get feedback like yours for that. – Nickolay May 29 '16 at 11:24
  • I updated [CSSStyleSheet](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet) and [CSSRule](https://developer.mozilla.org/en-US/docs/Web/API/CSSRule) a bit. If you have suggestions on making it clearer, I'd love to see them on dev-mdc. Re: CSSRule the problem is that the compatibility table only lists the removed sub-interfaces; finding information on browser support for other interfaces is too time-consuming. Can you come up with a comment that makes it clearer that the other interfaces work in Firefox and have a good chance of being supported in the other modern browsers? – Nickolay May 29 '16 at 11:29
2

To answer my own question, this is the code which ended up working and solved my problem. Key bits of information are as follows:

document.styleSheets[ i ].href
document.styleSheets[ i ].cssRules[ j ].selectorText
document.styleSheets[ i ].cssRules[ j ].style.fontFamily

This all starts with the document.styleSheets array-like object (array-like, because it is not actually a JavaScript array), and the rest is straightforward DOM. A generic function could be made. The href could be optional, depending upon if you know where the external sheet is loaded from and you know you can safely worry about only one unique selector, then you can avoid searching through the ~10 other sheets loaded by Firefox. This is the simplest use-case.

// Load font and apply style sheet class

// Am using the .href to find the sheet faster because location is known and rule is unique
var myCSShref = 'chrome://{GUID}/skin/text.css';
var mySelector = '.myTextClass';

// fonts stored as a string in a user pref, later split to array inline
// fonts could be stored as array during runtime, remove splits accordingly
// fonts get appended to a XUL menulist element
// list index 0 is for default, 1+ for fonts below
var fonts = 'monospace|Anonymous Pro|Apple Menlo|Bitstream Vera Sans Mono|Consolas|DejaVu Sans Mono|Droid Sans Mono|Everson Mono|Liberation Mono|Lucida Console|Source Code Pro';
var font_index = 2; // Anonymous Pro, the 2 is pulled from a user pref

// for loop vars
var i, styleSheet, j, cssRule, oldFontFamily, newFontFamily;

// console.logs for demonstration purposes only

// The following code only needed to be run once for me,
// but it can easily be changed to a reusable function.

if ( document.styleSheets ) {
    for ( i = 0; i < document.styleSheets.length; i++ ) {
        styleSheet = document.styleSheets[ i ];

        console.log( 'style sheet: ' + i + ' ' + styleSheet.href );

        if ( styleSheet.href === myCSShref ) {
            for ( j = 0; j < styleSheet.cssRules.length; j++ ) {
                cssRule = styleSheet.cssRules[ j ];
                console.log( 'style sheet: ' + i + ' cssRule.selectorText: ' + cssRule.selectorText );

                if ( cssRule && cssRule.selectorText === mySelector ) {
                    oldFontFamily = ( cssRule.style.fontFamily ) ? cssRule.style.fontFamily : '';
                    // font_index 0 is the menu default option, so we have 1 extra
                    newFontFamily = ( font_index === 0 ) ? 'inherit' : fonts.split( '|' )[ font_index - 1 ];
                    console.log( 'style sheet: ' + i + ' cssRule.style.fontFamily: ' + oldFontFamily + ' (before)' );
                    console.log( 'style sheet: ' + i + ' cssRule.style.fontFamily: ' + newFontFamily + ' (after)' );
                    cssRule.style.fontFamily = newFontFamily;
                }
            }
        }
    }
}
  • you shouldn't code-dump in an answer without some explaining prose – the8472 May 28 '16 at 18:39
  • 1
    As the original poster, it seemed obvious that I was answering my own question with the actual code I used. I did include a lot of prose, in the comments of the code. Nonetheless, I have included more verbose explanation, so that it should be very clear. Please remove the down vote if this is sufficient, or else suggest another improvement and I'll try again. Thank you for your suggestions and patience. –  May 28 '16 at 19:11
  • Yeah, but your particular code is tailored for your use-case. explaining key insights goes a long way towards avoiding cargo-culting and coding by copy&paste which plagues SO. – the8472 May 28 '16 at 22:01
  • Not familiar with the term cargo-culting. People who code by copy and paste have only themselves to answer to. I do not answer for them. My code is tailored towards my use-case because that is what answered my question. I've seen bits and pieces answered elsewhere but scarcely as thoroughly explained. My initial confusion stemmed from reliance on some horridly broken documentation at MDN which indicates this functionality is both read-only and removed from the browser. More generic code can be developed by others fueled with the key bits of info which eluded me, and so I gathered here. –  May 29 '16 at 02:00
  • After looking up the term, I am not sure if the cargo-culting comment was directed at me as an accusation or general impression by my initial code block post, but it was hand-written code. If the bulk of my other recent posts over the past few months are any indication, I tend to drive at the "why" question quite earnestly, and only after I understand why, do I ask, "how", but more at "how best" for my given situation (best in a subjective sense relative to my experience and project, etc.). Which again, all of my answer's initial code comments indicated as much. –  May 29 '16 at 02:09
0

3) There's the style sheet service, but I don't know if it's still in existence or will be obsoleted or removed (it's XPCOM based?), it's not part of Services.jsm, and it is a very blunt instrument that can merely load or unload an entire style sheet, when I merely want to modify one rule from one class definition.

Currently the other addon APIs are abstractions around the sheet service or windowutils.loadSheet. Of course mozilla may eventually remove it while maintaining the abstractions through other means. But as things look right now that still year(s) off in the the future.

The addon-sdk provides the option to load styles with its page-mod module

Webextensions support it either imperatively via tabs.insertCSS or declaratively in the manifest

But those are all meant for content frames. If you want to modify the browser chrome itself you'll have to use the xpcom APIs anyway, since there are no dedicated addon APIs targeting that.

the8472
  • 40,999
  • 5
  • 70
  • 122
  • Thank you for the forward-looking perspective. I am thinking that most of the user interface elements I will move to popups or panels or additional tabs or something, when I migrate to SDK, in which case I won't need to modify any chrome or be XPCOM dependent, just the JS/DOM/CSS, which turns out was mostly what I needed to modify the XUL element where data is now presented in status-bar context menus. Trying to play catch-up and learn the XUL and restartless/bootstrap and SDK and WedExtensions simultaneously so I can revive an old add-on. –  May 28 '16 at 18:47