简体   繁体   中英

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?

How to dynamically modify CSS rule set (eg 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

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 .

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;
                }
            }
        }
    }
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM