How I can make a browser action button that looks and acts like a toggle
Asked Answered
K

1

5

The target is get a WebExtension for Firefox that can be activated/deactivated by a user in the toolbar, like an on/off switch.

I'm using a background.js with this code:

browser.browserAction.onClicked.addListener(function (tab) {
    switch (button) {
        case 'turn-on':
            enable();
            break;
        case 'turn-off':
            disable();
            break;
    }
});
function enable() {
    browser.browserAction.setIcon({ path: '/ui/is-on.png', });
    browser.browserAction.setPopup({ popup: '/ui/turn-off.js', });
    browser.webNavigation.onCommitted.addListener(onTabLoad); 
} 
function disable() {
    browser.browserAction.setIcon({ path: '/ui/is-off.png', });
    browser.browserAction.setPopup({ popup: '/ui/turn-on.js', });
    browser.webNavigation.onCommitted.removeListener(onTabLoad); 
}
function onTabLoad(details) {
    browser.tabs.executeScript(details.tabId, {
        file: '/gc.js',
        allFrames true,
    }); 
}
enable(); // enable or disable by default

Obviously I'm doing something wrong. I'm kind newbie in coding. This is a personal project I'm trying to finish.

Kantianism answered 15/11, 2016 at 6:45 Comment(7)
Obviaslly I'm doing somenthing wrong - no, it isn't obvious, because you haven't said if there's a problem, what the problem is, any debugging information from the console ... nothing, so, no, it isn't obvious at allSanitary
though, there DOES seem to be a missing switch statement for the case statements on line 2 and 3 - perhaps even more than that missingSanitary
Sorry. I made this reading in a lot of websites, but I haven't found any concrete about make a button. Let me read about switch. ThanksKantianism
I add browser.browserAction.onClicked.addListener(function(tab) { switch (button) { case 'turn-on': enable(); break; case 'turn-off': disable(); break; } });Kantianism
@rafael1138, Please edit the question with such information. While comments help, all information that is relevant to answering the Question should be in the Question.Bavaria
@rafael1138, Once you have everything else working, if your issue is that your content script code is not executing, then the problem is that you are running into what I consider a bug in Firefox. Trying to inject a script immediately from within a webNavigation.onCommitted listener using tabs.executeScript() will fail (it does not fail on Chrome). See the Browser Console (Ctrl-Shift-J, or Cmd-Shift-J on OSX) for error information.Bavaria
@rafael1138, this will not work with the browserAction.setPopup() calls. Those will need to be removed. Even if you were to use them, they need to be to HTML pages, not JavaScript.Bavaria
B
3

Your code

While you added a switch statement to switch on button, you never defined button, nor changed its state. You also did not have a default case, just in case the button variable was not one of the values for which you were testing in your case statements.

You should not be using browserAction.setPopup() to set a popup. Setting a popup will result in the popup being opened instead of your background page receiving a click event. In addition, the popup needs to be an HTML page, not JavaScript.

See the section below for the Firefox bug which you need to work around in onTabLoad().

Listening to webNavigation.onCommitted is not sufficient to cover all cases of when your script will need to be injected. In other words, webNavigation.onCommitted does not fire every time a page is loaded. To fully cover every situation where your script will need to be injected is something that you will need to ask in another question.

var nextButtonState;
browser.browserAction.onClicked.addListener(function (tab) {
    switch (nextButtonState) {
        case 'turn-on':
            enable();
            break;
        case 'turn-off':
        default:
            disable();
            break;
    }
});
function enable() {
    browser.browserAction.setIcon({ path: '/ui/is-on.png', });
    //browser.browserAction.setPopup({ popup: '/ui/turn-off.js', });
    browser.webNavigation.onCommitted.addListener(onTabLoad); 
    nextButtonState = 'turn-off';
} 
function disable() {
    browser.browserAction.setIcon({ path: '/ui/is-off.png', });
    //browser.browserAction.setPopup({ popup: '/ui/turn-on.js', });
    browser.webNavigation.onCommitted.removeListener(onTabLoad); 
    nextButtonState = 'turn-on';
}
function onTabLoad(details) {
    //Add a setTimout to avoid a Firefox bug that Firefox is not quite ready to 
    //  have tabs.executeScript() inject a script when the onCommitted event fires.
    setTimeout(function(){
        chrome.tabs.executeScript(details.tabId, {
            file: '/gc.js',
            allFrames true,
        }); 
    },0);
}
enable(); // enable or disable by default

Workaround for a Firefox webNavigation.onCommitted bug

There is a change needed to your onTabLoad() code for using a webNavigation.onCommitted listener to inject scripts using tabs.executeScript() in Firefox (this is not needed in Chrome). This is due to a bug in Firefox which causes tabs.executeScript() to fail if executed immediately from a webNavigation.onCommitted listener. The workaround I use is to inject the script after a setTimeout(function,0) delay. This allows Firefox to execute the code needed to set up the environment necessary for executeScript() to be functional.

function onTabLoad(details) {
    //Add a setTimout to avoid a Firefox bug that Firefox is not quite ready to 
    //  have tabs.executeScript() inject a script when the onCommitted event fires.
    setTimeout(function(){
        chrome.tabs.executeScript(details.tabId, {
            file: '/gc.js',
            allFrames true,
        }); 
    },0);
}

Generalized solution for multi-state buttons (e.g. a toggle button)

The code I use to make a Browser Action button behave like a toggle is below. I have modified the browserButtonStates Object, which describes both what the buttons do and what they look like, to add and remove your webNavigation.onCommitted listener, onTabLoad(). See above for the issues with onTabLoad().

The code below is more complex than what you need. I wrote it intending to be able to move it from project to project with only needing to change the contents of the browserButtonStates object. Then, just by changing that object the icon, text, badge text, badge color, and action that is performed in each state (e.g. on/off) can be changed.

background.js

//The browserButtonStates Object describes the states the button can be in and the
//  'action' function to be called when the button is clicked when in that state.
//  In this case, we have two states 'on' and 'off'.
//  You could expand this to as many states as you desire.
//icon is a string, or details Object for browserAction.setIcon()
//title must be unique for each state. It is used to track the state.
//  It indicates to the user what will happen when the button is clicked.
//  In other words, it reflects what the _next_ state is, from the user's
//  perspective.
//action is the function to call when the button is clicked in this state.
var browserButtonStates = {
    defaultState: 'off',
    on: {
        icon         : '/ui/is-on.png'
        //badgeText  : 'On',
        //badgeColor : 'green',
        title        : 'Turn Off',
        action       : function(tab) {
                           chrome.webNavigation.onCommitted.removeListener(onTabLoad);
                       },
        nextState    : 'off'
    },
    off: {
        icon         : '/ui/is-off.png'
        //badgeText  : 'Off',
        //badgeColor : 'red',
        title        : 'Turn On',
        action       : function(tab) {
                           chrome.webNavigation.onCommitted.addListener(onTabLoad);
                       },
        nextState    : 'on'
    }
}

//This moves the Browser Action button between states and executes the action
//  when the button is clicked. With two states, this toggles between them.
chrome.browserAction.onClicked.addListener(function(tab) {
    chrome.browserAction.getTitle({tabId:tab.id},function(title){
        //After checking for errors, the title is used to determine
        //  if this is going to turn On, or Off.
        if(chrome.runtime.lastError){
            console.log('browserAction:getTitle: Encountered an error: ' 
                + chrome.runtime.lastError);
            return;
        }
        //Check to see if the current button title matches a button state
        let newState = browserButtonStates.defaultState;
        Object.keys(browserButtonStates).some(key=> {
            if(key === 'defaultState') {
                return false;
            }
            let state = browserButtonStates[key];
            if(title === state.title) {
                newState = state.nextState;
                setBrowserActionButton(browserButtonStates[newState]);
                if(typeof state.action === 'function') {
                    //Do the action of the matching state
                    state.action(tab);
                }
                //Stop looking
                return true;
            }
        });
        setBrowserActionButton(browserButtonStates[newState]);
    });
});

function setBrowserActionButton(tabId,details){
    if(typeof tabId === 'object' && tabId !== null){
        //If the tabId parameter is an object, then no tabId was passed.
        details = tabId;
        tabId       = null;
    }
    let icon   = details.icon;
    let title  = details.title;
    let text   = details.badgeText;
    let color  = details.badgeColor;

    //Supplying a tabId is optional. If not provided, changes are to all tabs.
    let tabIdObject = {};
    if(tabId !== null && typeof tabId !== 'undefined'){
        tabIdObject.tabId = tabId;
    }
    if(typeof icon === 'string'){
        //Assume a string is the path to a file
        //  If not a string, then it needs to be a full Object as is to be passed to
        //  setIcon().
        icon = {path:icon};
    }
    if(icon) {
        Object.assign(icon,tabIdObject);
        chrome.browserAction.setIcon(icon);
    }
    if(title) {
        let detailsObject = {title};
        Object.assign(detailsObject,tabIdObject);
        chrome.browserAction.setTitle(detailsObject);
    }
    if(text) {
        let detailsObject = {text};
        Object.assign(detailsObject,tabIdObject);
        chrome.browserAction.setBadgeText(detailsObject);
    }
    if(color) {
        let detailsObject = {color};
        Object.assign(detailsObject,tabIdObject);
        chrome.browserAction.setBadgeBackgroundColor(detailsObject);
    }
}

//Set the starting button state to the default state
setBrowserActionButton(browserButtonStates[browserButtonStates.defaultState]);

manifest.json:

{
    "description": "Demo Button toggle",
    "manifest_version": 2,
    "name": "Demo Button toggle",
    "version": "0.1",

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

    "browser_action": {
        "default_icon": {
            "32": "myIcon.png"
        },
        "default_title": "Turn On",
        "browser_style": true
    }
}
Bavaria answered 15/11, 2016 at 7:35 Comment(2)
@rafael1138, I have updated this code to reflect the doable portions of your code which was in the question. Some things which you appeared to be doing were not reasonable for the overall goals of what you were attempting to accomplish (e.g. calling browserAction.setPopup()). I have also modified your onTabLoad() to work around a Firefox bug which would prevent your content script from being injected.Bavaria
Very thorough! A note though. In Firefox for android, the on/off badge will be a checked/unchecked (or when unchecked, show no box) checkbox. As seen in - addons.mozilla.org/en-US/firefox/addon/fake-locationDisrupt

© 2022 - 2024 — McMap. All rights reserved.