Web Extension:How to Use "Browser_Style = True"

Web Extension : How do I use browser_style = true?

Using "browser_style": true results in the chrome://browser/content/extension.css file being applied to your HTML (on OSX chrome://browser/content/extension-mac.css is applied instead).

Mozilla has a Style Guide which you can peruse to see how various elements and classes are used. The link to this Style Guide is in the browser_style entry within the "Syntax" section of the browser_action documentation page. A similar link is in the same location on the page_action MDN documentation page. Personally, I would find it more appropriate for the information contained in the Style Guide to be hosted directly on MDN rather than on firefoxux.github.io.

If you are just interested in the elements and classes, you can find examples under the Components section.

Note: Under some conditions, Firefox also attempts to apply chrome://browser/content/extension-win-panel.css or chrome://browser/content/extension-mac-panel.css neither of which exist.

Using browser_style in addon on Firefox (v57)

Styles are correctly applied, you are probably just using the wrong classes.
Note that the old style guide is now deprecated in favor of the new Photon Design System.

These are the used stylesheets, just go to these URLs in Firefox to see the full source:

  • On Windows: chrome://browser/content/extension.css
  • On Mac: chrome://browser/content/extension-mac.css

Most of the styles assume you use the browser-style class. For example, here are some of the styles for the button element (on Windows):

/* stylelint-disable property-no-vendor-prefix */
/* Buttons */
button.browser-style,
select.browser-style {
background-color: #fbfbfb;
border: 1px solid #b1b1b1;
box-shadow: 0 0 0 0 transparent;
font: caption;
height: 24px;
outline: 0 !important;
padding: 0 8px 0;
transition-duration: 250ms;
transition-property: box-shadow, border;
}

Let's verify if the styles are actually applied.

Example, given an extension with the following manifest.json:

{
"name": "Options page",
"manifest_version": 2,
"version": "0.0.1",
"description": "Sample options page",
"options_ui": {
"page": "options.html",
"browser_style": true
}
}

And the following options.html:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div>Just a text example page</div>
<div>
<input checked id="switch1" type="checkbox" class="visually-hidden">
<label for="switch1"></label>
<button class="browser-style">Test button</button>
</div>
</body>
</html>

This is the rendered options page:

options page

Inspect the options page to verify the applied styles:

  • Paste about:debugging into the address bar
  • Select "Enable add-on debugging" on top
  • Click on the Debug link
  • Click "Ok" when prompted for allowing incoming connection

debug

Now switch back to the Options page and inspect the "Test button" we added

inspect

As you can see, the button is correctly styled with the browser stylesheet.

Mozilla Web Extensions: Insert UI component into page

I can't believe I didn't notice it for so long, but I finally got it all working as I need. While doing that I even came up with an alternative approach, so here goes.

But first here's the main reason why I dismissed all other possible approaches besides using iframe:

  • I needed the UI elements added by extension to use their own UI styles and wanted to take advantage of modern frameworks (e.g. jQuery and Bootstrap) and I didn't want to run into problems of conflicting CSS and JavsScript later.
    And I actually noticed early on that CSS in the page that I'm embedding into do override Bootstrap styles.
  • Preferably I also didn't want to affect century old markup of the page that I'm embedding into.

Option A - IFRAME with external source file

In the end it turned out that the only thing I was missing is the web_accessible_resources setting in the manifest.json. Once I added the html file used as source for the iframe into that list, it all just started working.

// manifest.json:
{
"manifest_version": 2,
...
"web_accessible_resources": [
"data/my.html"
],
"content_scripts": [
{
"matches": ["*://*"],
"js": ["js/my.js"]
}
]
}

// js/my.js
var addonFrame = document.createElement ("IFRAME");
addonFrame.style = "position: absolute; bottom: 0; right: 0; width: 150px; height: 38px;";
addonFrame.frameBorder = "0";
addonFrame.src = chrome.extension.getURL ("data/my.html");
document.body.appendChild (addonFrame);

Option B - IFRAME with inline HTML in JS

Before I finally got the first approach working, my experimentation led me to another working approach - inserting HTML into the iframe directly in the content script.

// js/my.js
var addonFrame = document.createElement ("IFRAME");
addonFrame.style = "position: absolute; bottom: 0; right: 0; width: 150px; height: 38px;";
addonFrame.frameBorder = "0";

var addonHtml = "<!DOCTYPE html>\n\
<html>\n\
<head>\n\
<meta charset='UTF-8'>\n\
<title>Title of the document</title>\n\
</head>\n\
<body>\n\
...\n\
</body>\n\
</html>";

addonFrame.src = 'data:text/html;charset=utf-8,' + encodeURI (addonHtml);
document.body.appendChild (addonFrame);

Even though I ended up using option A in the end, let me outline some pros and cons:

  1. Option A is obviously more canon: view (html) is clearly separated from behavior (js) and all files have content appropriate for their type (except for small exception of building iframe element in JS).
    So it should be easier to support going forward.
  2. Option A doesn't allow to use inline scripts in the frame (https://developer.chrome.com/extensions/contentSecurityPolicy). This makes prototyping harder but ultimately should be a plus.
  3. For some reason that is still unclear to me, I cannot use # in the inserted html in option B.
  4. Option B makes doing ajax calls from the add-on frame to the original server easier since the frame source is considered to be from the same domain as the original web page.
    In option A, I had to use Window.postMessage in the frame in order to ask my content script inserted into the original page to make an ajax request and give me back the response (the second part was especially hard since there's nothing like jQuery or Prototype available there).

Firefox WebExtension settings page

Don't use XUL in a WebExtension add-on:

If you are using XUL from within a WebExtension, then something is probably wrong. If you are writing a WebExtension, and you start to do XUL: take a step back. Make sure that is really what you are supposed to be doing. One of the points of WebExtensions is that it insulates the add-on from the internals of Firefox (i.e. from XUL).

Options panels and pages:
In general, options should be stored in storage.local (or storage.sync, when supported). If you are attempting to communicate from an options page, or a panel back to your main background script there are, at least, 4 somewhat different ways to do so:

  1. Options are stored to storage.local in the options.js code. The background code listens for events from storage.onChanged. No need to pass messages back and forth. No need for your options/panel code to specifically notify the background page that changes have occurred.
  2. Options are stored to storage.local in the options.js code. Then, the options.js directly invokes the a function in your background script to have the background script re-read the options. In the code below, the getOptions() function in the background.js file is directly invoked by the options.js code.
  3. Options are stored to storage.local in the options.js code. Use chrome.runtime.sendMessage() to send a message to your background page that the options have changed. In the code below: After storing the options in storage.local, the options.js sends an optionsUpdated message to the background script that the options have been updated. The background script then re-reads the options. [The message can be whatever you want that indicates this. optionsUpdated is merely what I chose as the message in the code below.]
  4. Use chrome.runtime.sendMessage() to send a message with all options data to the background page. In the code below: An optionsData message is sent from the options.js code to the background page when the options are change which contains a data payload with all of the options. The options are then stored to storage.local in the background script. Once the options are stored, the background script sends a optionsStored message back to the options.js code. The options.js code then indicates to the user that the options have been saved. [The message can be whatever you want that indicates this. optionsData and optionsStored is merely what I chose as the message in the code below.]

Below is a WebExtension that demonstrates those four different methods of getting the changed option information back to the background script.

Note: The code below uses a browser_action button to bring up a panel with the exact same options as are used for options_ui. This is done for the purpose of demonstration. If you are going to have a button that opens your options, it may be better to directly open your options_ui page with runtime.openOptionsPage(). Which you do depends on the user interface you want to present to the user.

background.js:

var useDirect=0; //Holds the state of how we communicate with options.js
var emailAddress=''; //The email address from options.
const useDirectTypes=[ 'Listen to chrome.storage changes'
,'Directly invoke functions in the background script from'
+ ' the options/panel code'
,'Send a message that data in storage.local was updated'
,'Send a message with all options data' ];

//Register the message listener
chrome.runtime.onMessage.addListener(receiveMessage);

function receiveMessage(message,sender,sendResponse){
//Receives a message that must be an object with a property 'type'.
// This format is imposed because in a larger extension we may
// be using messages for multiple purposes. Having the 'type'
// provides a defined way for other parts of the extension to
// both indicate the purpose of the message and send arbitrary
// data (other properties in the object).
console.log('Received message: ',message);
if(typeof message !== 'object' || !message.hasOwnProperty('type')){
//Message does not have the format we have imposed for our use.
//Message is not one we understand.
return;
}
if(message.type === "optionsUpdated"){
//The options have been updated and stored by options.js.
//Re-read all options.
getOptions();
}
if(message.type === "optionsData"){
saveOptionsSentAsData(message.data,function(){
//Callback function executed once data is stored in storage.local
console.log('Sending response back to options page/panel');
//Send a message back to options.js that the data has been stored.
sendResponse({type:'optionsStored'});
//Re-read all options.
getOptions();
});
//Return true to leave the message channel open so we can
// asynchronously send a message back to options.js that the
// data has actually been stored.
return true;
}
}

function detectStorageChange(change){
//Ignore the change information. Just re-get all options
console.log('Background.js: Detected storage change');
getOptions();
}

function listenToStorageChanges(){
chrome.storage.onChanged.addListener(detectStorageChange);
}

function stopListeningToStorageChanges(){
chrome.storage.onChanged.removeListener(detectStorageChange);
}

function getOptions(){
//Options are normally in storage.sync (sync'ed across the profile).
//This example is using storage.local.
//Firefox does not currently support storage.sync.
chrome.storage.local.get({
useDirect: 0,
emailAddress: ''
}, function(items) {
if(typeof items.useDirect !== 'number' || items.useDirect <0
|| items.useDirect >= useDirectTypes.length) {
items.useDirect=0;
}
useDirect = items.useDirect;
emailAddress = items.emailAddress;
console.log('useDirect=' + useDirectTypes[useDirect]);
console.log('email address=' + emailAddress);
});
}

function saveOptionsSentAsData(data,callback) {
//Options data received as a message from options.js is
// stored in storeage.local.
chrome.storage.local.set(data, function() {
//Invoke a callback function if we were passed one.
if(typeof callback === 'function'){
callback();
}
});
}

//Read the options stored from prior runs of the extension.
getOptions();

//On Firefox, open the Browser Console:
//To determine if this is Chrome, multiple methods which are not implemented
// in Firefox are checked. Multiple ones are used as Firefox will eventually
// support more APIs.
var isChrome = !! window.InstallTrigger
|| (!!chrome.extension.setUpdateUrlData
&& !!chrome.runtime.reload
&& !!chrome.runtime.restart);
if(!isChrome) {
//In Firefox cause the Browser Console to open by using alert()
window.alert('Open the console. isChrome=' + isChrome);
}

options.js:

// Saves options to chrome.storage.local.
// It is recommended by Google that options be saved to chrome.storage.sync.
// Firefox does not yet support storage.sync.
function saveOptions(data, callback) {
chrome.storage.local.set(data, function() {
if(typeof callback === 'function'){
callback();
}
// Update status to let user know options were saved.
notifyOptionsSaved();
});
}

function optionsChanged() {
//Get the selected option values from the DOM
let useDirectValue = document.getElementById('useDirect').value;
let email = document.getElementById('email').value;
useDirectValue = +useDirectValue; //Force to number, not string
//Put all the option data in a single object
let optionData = {
useDirect: useDirectValue,
emailAddress: email
}
setBackgroundPageNotListeningToStorageChanges();
if(useDirectValue == 0 ) {
//Use listening to storage changes
//console.log('Going to listen for storage changes');
setBackgroundPageListeningToStorageChanges();
saveOptions(optionData);
} else if (useDirectValue == 1) {
//We save the options in the options page, or popup
saveOptions(optionData, function(){
//After the save is complete:
//The getOptions() functon already exists to retrieve options from
// storage.local upon startup of the extension. It is easiest to use that.
// We could remove and add the listener here, but that code already
// exists in background.js. There is no reason to duplicate the code here.
let backgroundPage = chrome.extension.getBackgroundPage();
backgroundPage.getOptions();
});
} else if (useDirectValue == 2) {
//We save the options in the options page, or popup
saveOptions(optionData, function(){
//Send a message to background.js that options in storage.local were updated.
chrome.runtime.sendMessage({type:'optionsUpdated'});
});
} else {
//Send all the options data to background.js and let it be dealt with there.
chrome.runtime.sendMessage({
type:'optionsData',
data: optionData
}, function(message){
//Get a message back that may indicate we have stored the data.
if(typeof message === 'object' && message.hasOwnProperty('type')){
if(message.type === 'optionsStored') {
//The message received back indicated the option data has
// been stored by background.js.
//Notify the user that the options have been saved.
notifyOptionsSaved();
}
}
});
}
}

function setBackgroundPageListeningToStorageChanges(){
//Normally the listener would be set up once, and only once, within the
// background page script. We are doing it here to demonstrate switing
// between the different methods of notification.
let backgroundPage = chrome.extension.getBackgroundPage();
//either add the listener directly:
chrome.storage.onChanged.addListener(backgroundPage.detectStorageChange);
//or let the background page add it:
//backgroundPage.listenToStorageChanges();
}

function setBackgroundPageNotListeningToStorageChanges(){
//Normally the listener would be set up once, and only once, within the
// background page script. We are doing it here to demonstrate switing
// between the different methods of notification.
let backgroundPage = chrome.extension.getBackgroundPage();
//either remove the listener directly:
chrome.storage.onChanged.removeListener(backgroundPage.detectStorageChange);
//or let the background page add it:
//backgroundPage.stopListeningToStorageChanges();
}

// Restores select box and input using the preferences
// stored in chrome.storage.
function useStoredOptionsForDisplayInDOM() {
chrome.storage.local.get({
useDirect: 0,
emailAddress: ''
}, function(items) {
//Store retrieved options as the selected values in the DOM
document.getElementById('useDirect').value = items.useDirect;
document.getElementById('email').value = items.emailAddress;
});
//notifyStatusChange('Option read');
}

function notifyOptionsSaved(callback){
//Notify the user that the options have been saved
notifyStatusChange('Options saved.',callback);
}

function notifyStatusChange(newStatus,callback){
let status = document.getElementById('status');
status.textContent = newStatus;
//Clear the notification after a second
setTimeout(function() {
status.textContent = '';
if(typeof callback === 'function'){
callback();
}
}, 1000);
}

document.addEventListener('DOMContentLoaded', useStoredOptionsForDisplayInDOM);
document.getElementById('optionsArea').addEventListener('change',optionsChanged);
//In order to track the change if this is open in _both_ the browser_action panel
// and the add-on options page, we need to listen for changes to storage.
// There is already a function which reads all of the options, so don't
// use the information as to what changed, just that a change occurred.
chrome.storage.onChanged.addListener(useStoredOptionsForDisplayInDOM);

options.html:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebRequest Logging Options</title>
<style>
body: { padding: 10px; }
</style>
</head>

<body>
<div id="optionsArea">
Communication with background page:
<select id="useDirect">
<option value="0">Listen for storage changes</option>
<option value="1">Directly call background page functions</option>
<option value="2">Send a Message Updated storage.local</option>
<option value="3">Send a Message with all Data</option>
</select>
<div>Email:
<input id="email"></input>
</div>
</div>
<div id="status" style="top:0px;display:inline-block;"></div>

<script src="options.js"></script>
</body>
</html>

manifest.json:

{
"description": "Demonstrate an email field in options with various ways of communicating with the background script.",
"manifest_version": 2,
"name": "email-in-options-demo",
"version": "0.1",

"applications": {
"gecko": {
//Firefox: must define id to use option_ui:
"id": "email-in-options-demo@example.example",
"strict_min_version": "48.0"
}
},

"permissions": [
"storage"
],

"background": {
"scripts": [
"background.js"
]
},

"browser_action": {
"default_icon": {
"48": "myIcon.png"
},
"default_title": "Show panel",
"browser_style": true,
"default_popup": "options.html"
},

"options_ui": {
"page": "options.html",
"chrome_style": true
}
}

The code above is based on code in my answer to Update WebExtension webRequest.onBeforeRequest listener URL settings from separate script.



Related Topics



Leave a reply



Submit