Search code examples
javascripthtmlfirefoxfirefox-addonfirefox-addon-sdk

Create a dynamic preferences panel using Firefox's Add-on SDK


I've created a simple Firefox add-on where each time the user selects some text on a page, a small popup with the user's search engines appears, which you can then click to search the web. It mimics plugins such as this one or FastestFox's popup panel.

The add-on uses all of the user's search engines by default, but I want to let the user hide certain engines from my panel, without affecting the browser's search engines manager. Using the SDK's simple-prefs, I can create settings in packages.json: int values, buttons (controls), checkboxes, dropdown menus, etc. However, search engines are different for each user and dynamic (they can be added or removed from the browser).

I added a button to my add-on's preferences menu:

Example of a simple-prefs button that does *something*

This button should open some window that shows all search engines and a checkbox behind each of them (to enable/disable them in my popup). I think I could save these extra settings using simple-storage. But now I don't know how to proceed from here.

This is what Adblock does:

Adblock's simple-prefs + button for extra settings window

I've also seen at least one add-on place all its settings in an HTML page.

Actual questions: Should I create a window for this? (What is the API I should use for that?) Or is it better to generate a webpage and handle everything through HTML and JavaScript? (Seems much harder!)

This is the first add-on I try developing for Firefox and I'm having a hard time finding info on the web. Everything in my add-on comes from the High-Level APIs of the Add-on SDK (No overlay extensions. No XUL.) Actually, having all these options confuses me.


Solution

  • As I didn't want to mix the Add-on SDK (high level modules) with XUL extension development, I ended up creating a function that is triggered when the "Select from available search engines" button is clicked. This function creates an HTML panel (sdk/panel) that is then populated with the available search engines, placing a checkbox behind each one. Some events are also set so that, when a checkbox is clicked, the engine's status (enabled/disabled) is saved using the sdk/simple-storage module.

    This is the code for the button in the add-on options (in package.json):

    ...
    "preferences": [
    {
        "name": "selectEnginesButton",
        "title": "Select from available search engines",
        "type": "control",
        "description": "Enable/disable the search engines that show on the popup panel.",
        "label": "Select"
    },
    ...
    

    Then, in main.js:

    var simplePrefs = require('sdk/simple-prefs');
    var simpleStorage = require("sdk/simple-storage");
    
    // ...some code for other things here...
    
    if (!simpleStorage.storage.searchEngines) {
        simpleStorage.storage.searchEngines = {};
    }
    
    simplePrefs.on("selectEnginesButton", function() {
        createSettingsPanel();
    });
    
    function createSettingsPanel()
    {
        var visibleEngines = searchService.getVisibleEngines({});
        var engines = visibleEngines.filter(function(engine) { return !engine.hidden; });
    
        // Create simple objects with engine information to pass to the panel's content script
        // This is because only main.js can work with the search engine objects themselves
        // i.e. the ones in "engines"
        var engineObjects = engines.map(function(engine) {
            var engineObject = {};
            engineObject.name = engine.name;
            engineObject.iconSpec = (engine.iconURI != null ? engine.iconURI.spec : null);
            engineObject.active = isEngineActive(engine);
            return engineObject;
        });
    
        var pan = panel.Panel({
            contentURL: data.url("engine-settings.html"),
            contentScriptFile: data.url("engine-settings.js"),
            width: 400,
            height: 225
        });
    
        pan.port.emit('setupPanel', engineObjects);
        pan.port.on("onSearchEngineToggle", onSearchEngineToggle);
    
        pan.show();
    }
    
    function onSearchEngineToggle(engine)
    {
        simpleStorage.storage.searchEngines[engine.name] = engine.active;
    }
    
    function isEngineActive(engine)
    {
        return simpleStorage.storage.searchEngines[engine.name];
    }
    

    Finally, the panel's HTML (engine-settings.html):

    <html>
    <head>
        <!-- some css and other stuff -->
    </head>
    <body>
        <div id="title">Select what search engines are shown on the popup panel:</div>
        <div id="engines"></div>
    </body>
    </html>
    

    ...and the panel's JavaScript (engine-settings.js):

    self.port.on('setupPanel', setupPanel);
    self.port.on('logInnerHTML', logInnerHTML);
    
    function setupPanel(engines)
    {
        engines.forEach(addEngineToLayout);
    }
    
    function addEngineToLayout(engine)
    {
        // WARNING: this might not be the best way to populate HTML from js. I'm certainly no expert here :)
    
        var element = document.createElement("div");
        var description = document.createTextNode(engine.name);
        var checkbox = document.createElement("input");
    
        checkbox.type = "checkbox";
        checkbox.value = engine.name;
        checkbox.checked = engine.active;
    
        element.appendChild(checkbox);
    
        var icon = document.createElement("img");
        // default.png is a png of mine, in case an engine has no icon
        icon.setAttribute("src", (engine.iconSpec != null ? engine.iconSpec : "default.png"));
        element.appendChild(document.createElement("span"));
        element.appendChild(icon);
    
        element.appendChild(document.createElement("span"));
        element.appendChild(description);
    
        document.getElementById('engines').appendChild(element);
    
        checkbox.addEventListener("mouseup", function(e) {
            engine.active = !engine.active;
            self.port.emit('onSearchEngineToggle', engine);
        });
    }
    

    Of course, this is not a perfect solution, but you get a simple panel that shows on top of the other settings when you click the button in the add-on preferences. It disappears when you press outside of it.

    Note that this code has engines disabled by default, as they will not be present in simpleStorage.storage.searchEngines. You can change this by doing this in isEngineActive:

    function isEngineActive(engine)
    {
        if (!simpleStorage.storage.searchEngines.hasOwnProperty(engine.name)) {
            simpleStorage.storage.searchEngines[engine.name] = true;
        }
        return simpleStorage.storage.searchEngines[engine.name];
    }
    

    When engines are removed from the browser, they'll still be forever present in simpleStorage.storage.searchEngines. You can easily fix this by going through everything in there when your main.js runs and removing engines that don't exist anymore.

    I hope this helps someone! :)