gal007 gal007 - 1 month ago 6
Javascript Question

Firefox SDK Add-on with a sidebar on both the right and left at the same time

I'm programming a Firefox Add-on SDK based extension. I need to use both a left and a right-side sidebar, at the same time. By default, I can display one on the left side. I've already read about changing between having the ui/sidebar on the left and the right by using the CSS:

@namespace url(http://www.mozilla.org/keymaster/gat...re.is.only.xul);
hbox#browser {
direction: rtl !important;
}
hbox#browser > vbox {
direction: ltr !important;
}


But, that seems to be something of the old-school, because I didn't define .xul files.

Do you know any to have a sidebar on both the left and right of the browser at the same time?

Answer

Your question is not clear as to exactly what you want. Thus, I have made some assumptions.

Ultimately, there is no officially supported way to have more than one "sidebar" displayed at a time. By default, only a single "sidebar" exist within the Firefox Browser windows. While there is no supported API which provides you with more than one sidebar, it is possible to create multiple sidebars (interface panels) by modifying the XUL DOM for the Firefox Browser windows.

The code below creates user interface panels (side-bars, top-bars, bottom-bars and windows) which are on the top, bottom, left, right, of the browser window, or in a separate window. The interface panels can be created such that they are with respect to the window (they stay visible when you switch tabs; like a bookmarks/history sidebar), or are with respect to the tab (they on only visible when the tab in which they were created is displayed; like the developer tools).

The createInterfacePanelIframe() method creates an interface panel containing an <iframe> (or <browser>) for which you can supply the URL and all attributes. The elements for the <iframe> and the <splitter> are returned to the caller so you can perform other operations on them, or remove them from the Firefox window's DOM to delete/hide them. This method creates <iframe> and calls createInterfacePanel() to insert it into the Firefox browser in the location specified.

The createInterfacePanel() method will put the XUL element that you pass to it (use a Document Fragment to pass multiple elements) into the Firefox Browser window's DOM, along with a <splitter> in the location specified (left, right, top, bottom, window and relative to either the window or the tab). You can specify the window and/or tab you desire the interface panel to be within, or, by default, the interface panel is inserted in the current window/tab. If an interface panel already exists where you are specifying one to be inserted, another panel will be created adjacent to the existing one. There is no inherent limit to the number you can create (e.g. you could have 10 panels on the right, if you wanted).

In addition, a demo Firefox Add-on SDK extension is below which adds 6 different buttons. The first 5 buttons create or destroy interface panels. The badge indicates which panel the button will affect (left, right, top, bottom, and window). A green badge indicates an interface panel will be created. A red badge indicates clicking will destroy the already existing panel. The sixth button toggles the other buttons between Tab relative interface panels, and Window relative interface panels. If you create all the panels your will have 8 panels (4 for the Window and 4 for the current tab) along with two separate windows (per Browser Window).

The following is what the demo add-on looks like. The panels shown in the image below are narrow and/or short to allow them to be displayed on this page. The code below allows you to make the panels whatever size you desire and for the user to be able to resize them.
Demo Add-on in action

This is the code which creates the sidebars (interface panels):

sidebars.js:

/**
 * createInterfacePanelIframe(location,options)
 *   Creates an <iframe> based panel within the current tab, within the
 *   current window, or opens a window, for use as an user interface
 *   box.  If it is not a window, it is associated with the current
 *   browser tab, or the current browser window depending on the
 *   byWindow option.

 * @param location 
 *   Placement of the panel [right|left|top|bottom|window]
 *   The default location is 'right'.
 * @param options
 *   An Object containing optional parameters. 
 *     height
 *       The height of a top or bottom sidebar
 *       Default is 200.
 *     width
 *       The width of a left or right sidebar
 *       Default is 200.
 *     size
 *       Width if on left or right. Height if top or bottom.
 *       Both width and height if location='window' unless
 *       features is a string. 
 *       Default is 200.
 *     id
 *       The ID to assign to the iframe. Default is
 *       'makyen-interface-panel'
 *       The <splitter> will be assigned the
 *       ID = id + '-splitter'
 *     url
 *       This is the chrome://  URL to use for the contents
 *       of the iframe or the window.
 *       the default is:
 *       'chrome://devtools/content/framework/toolbox.xul'
 *     iframeAttributes
 *       An Object
 *       Contains key/value pairs which are applies as attributes
 *       of the iframe. These will override any defaults or other
 *       attributes derived from other options (e.g. id, height,
 *       width, type, etc.). If the value of the property is null
 *       then that attribute will be removed.
 *     useBrowser
 *       If true, a <browser> element is used instead of an <iframe>.
 *     byWindow
 *       If true then the created sidebar is for the window
 *       not the tab.
 *     tab
 *       The tab for which to create the sidebar. If not
 *       specified, the current tab is used.
 *     window
 *       The window for which to create the sidebar. If not
 *       specified, the current window is used.
 *     features
 *       The features string for the window. See:
 *       https://developer.mozilla.org/en-US/docs/Web/API/Window.open
 *
 * returns [splitterEl, iframeEl]
 *   The elements for the <splitter> and <iframe>.
 *
 * Copyright 2014-2016 by Makyen.
 * Released under the MPL 2.0. http://mozilla.org/MPL/2.0/.
 **/
function createInterfacePanelIframe(location,options){
    //options
    let size,width,height,id,chromeUrl;
    if(typeof options === 'object'){
        size = options.size;
        width = options.width;
        height = options.height;
        id = options.id;
        chromeUrl = options.url;
    }
    if(!width && !height && size){
        width = size;
        height = size;
    }
    [width,height] = getSizeWithDefaults(location,width,height);

    //defaults
    id = typeof id !== 'string' ? 'makyen-interface-panel' : id;
    chromeUrl = typeof chromeUrl !== 'string'
        ? 'chrome://devtools/content/framework/toolbox.xul'
        : chromeUrl;

    //Create some common variables if they do not exist.
    //This gets the currently active Firefox XUL window.
    //  Add/remove a '/' to comment/un-comment the code appropriate for your add-on type.
    //* Add-on SDK:
    let activeWindow = options.window ?
            options.window : require('sdk/window/utils').getMostRecentBrowserWindow();
    //*/
    /* Overlay and bootstrap (from almost any context/scope):
    Components.utils.import('resource://gre/modules/Services.jsm');//Services
    let activeWindow = options.window ?
            options.window : Services.wm.getMostRecentWindow('navigator:browser');
    //*/
    let mainDocument = activeWindow.document;

    //Create the <iframe> use
    //mainDocument for the XUL namespace.
    let iframeEl;
    if(options.useBrowser){
        iframeEl = mainDocument.createElement('browser');
    } else {
        iframeEl = mainDocument.createElement('iframe');
    }
    iframeEl.id = id;
    iframeEl.setAttribute('src',chromeUrl);
    iframeEl.setAttribute("tooltip", "aHTMLTooltip");
    iframeEl.setAttribute("autocompleteenabled", true);
    iframeEl.setAttribute("autocompletepopup", "PopupAutoComplete");
    iframeEl.setAttribute("disablehistory",true);
    iframeEl.setAttribute('type', 'content');
    if(typeof height === 'number'){
        iframeEl.setAttribute('height', height.toString());
    }
    if(typeof width === 'number'){
        iframeEl.setAttribute('width', width.toString());
    }
    if(typeof options.iframeAttributes === 'object'){
        let attrs = options.iframeAttributes;
        for(let attr in attrs){
            if(attrs.hasOwnProperty(attr)) {
                if(attrs[attr]===null){
                    iframeEl.removeAttribute(attr);
                }else{
                    iframeEl.setAttribute(attr, attrs[attr]);
                }
            }
        }
    }

    //Call createInterfacePanel
    let splitterEl;
    let newOptions = {};
    if(height) {
        newOptions.height = height;
    }
    if(width) {
        newOptions.width = width;
    }
    newOptions.url = chromeUrl;
    if(options.tab){
        newOptions.tab = options.tab;
    }
    if(options.window){
        newOptions.window = options.window;
    }
    if(options.features){
        newOptions.features = options.features;
    }
    if(options.byWindow){
        newOptions.byWindow = options.byWindow;
    }
    newOptions.id = id + '-splitter';
    splitterEl = createInterfacePanel(location, iframeEl, newOptions)
    return [splitterEl, iframeEl];
}

/**
 * createInterfacePanel(location,objectEl,options)
 *   Creates a panel within the current tab, or opens a window, for use as a
 *   user interface box. If not a window, it is associated with the current
 *   browser tab.
 * @param location 
 *   Placement of the panel [right|left|top|bottom|window]
 *   The default location is 'right'.
 * @param objectEl
 *   The element of an XUL object that will be inserted into
 *   the DOM such that it is within the current tab.
 *   Some examples of possible objects are <iframe>,
 *   <browser>, <box>, <hbox>, <vbox>, etc.
 *   If the location='window' and features is not a string
 *   and this is a number then it is used as the width of the
 *   window.
 * @param options
 *   An Object containing optional parameters. 
 *     height
 *       The height of a top or bottom sidebar
 *       Default is 200.
 *     width
 *       The width of a left or right sidebar
 *       Default is 200.
 *     size
 *       Width if on left or right. Height if top or bottom.
 *       Both width and height if location='window' unless
 *       features is a string. 
 *       Default is 200.
 *       If none of height, width or size is specified, then the
 *       size of the sidebar should be specified within the XUL
 *       elements referenced by objectEl.
 *     sizeEl
 *       The element that is to contain attributes of 'width' and 
 *       'height'. If location=='left'|'right' then the 
 *       'height' attribute is removed prior to the objectEl
 *       being inserted into the DOM.
 *       This is an optional spearate reference for the size element
 *       in case the objectEl is a documentFragment containing
 *       multiple elements. However, normal usage is for
 *       objectEl === sizeEl (which is default if unspecified)
 *       when location != 'window'.
 *       When location == 'window' and features is not a string,
 *       and sizeEl is a number then it is used as the height
 *       of the window.
 *       If features is a string, it is assumed the height is
 *       set in that, or elsewhere (e.g. in the XUL).
 *     id
 *       The ID to assign to the <splitter>. The default is:
 *       'makyen-interface-panel-splitter'.
 *     url
 *       This is the chrome://  URL to use for the contents
 *       of the window.
 *       the default is:
 *       'chrome://devtools/content/framework/toolbox.xul'
 *     byWindow
 *       If true then the created sidebar is for the window
 *       not the tab.
 *     tab
 *       The tab for which to create the sidebar. If not
 *       specified, the current tab is used.
 *     window
 *       The window for which to create the sidebar. If not
 *       specified, the current window is used.
 *     features
 *       The features string for the window. See:
 *       https://developer.mozilla.org/en-US/docs/Web/API/Window.open
 *       If features is a string, it is assumed the width is
 *       set in that, or elsewhere (e.g. in the XUL).
 *
 * returns
 *   if location != 'window':
 *     splitterEl, The element for the <splitter>.
 *   if location == 'window':
 *     The windowObjectReference returned by window.open().
 *
 * Copyright 2014-2016 by Makyen.
 * Released under the MPL 2.0. http://mozilla.org/MPL/2.0/.
 **/
function createInterfacePanel(location,objectEl,options) {
//function createInterfacePanel(location,objectEl,sizeEl,id,chromeUrl,features) {
    //options
    let size,width,height,sizeEl,id,chromeUrl,byWindow,features;
    if(typeof options === 'object'){
        size = options.size;
        width = options.width;
        height = options.height;
        //If a separate sizeEl is not specified, then use the ObjectEl for sizeEl.
        //  This is so we could pass a document fragment with multiple elements,
        //  But only one which should have a specified size.
        sizeEl = options.sizeEl? options.sizeEl:objectEl;
        id = options.id;
        chromeUrl = options.url;
        byWindow = options.byWindow;
        features = options.features;
    }
    if(!width && !height && size){
        width = size;
        height = size;
    }
    [width,height] = getSizeWithDefaults(location,width,height);

    //Set location default:
    location = typeof location !== 'string' ? 'right' : location;
    if(location == 'window') {
        if(typeof features !== 'string') {
            let widthText =  'width=' + width.toString() + ',';
            let heightText = 'height=' + height.toString() + ',';
            features = widthText + heightText
                       + 'menubar=no,toolbar=no,location=no,personalbar=no'
                       + ',status=no,chrome=yes,resizable,centerscreen';
        }
    }
    id = typeof id !== 'string' ? 'makyen-interface-panel-splitter' : id;
    chromeUrl = typeof chromeUrl !== 'string'
        ? 'chrome://devtools/content/framework/toolbox.xul'
        : chromeUrl;

    //Create some common variables if they do not exist.
    //This gets the currently active Firefox XUL window.
    //  Add/remove a '/' to comment/un-comment the code appropriate for your add-on type.
    //* Add-on SDK:
    let activeWindow = options.window ?
            options.window : require('sdk/window/utils').getMostRecentBrowserWindow();
    //*/
    /* Overlay and bootstrap (from almost any context/scope):
    Components.utils.import('resource://gre/modules/Services.jsm');//Services
    let activeWindow = options.window ?
            options.window : Services.wm.getMostRecentWindow('navigator:browser');
    //*/
    if (typeof gBrowser === 'undefined') {
        //If there is no gBrowser defined, get it
        var gBrowser = activeWindow.gBrowser;
    }

    //Get the tab & notification box (container for tab UI).
    let tab = options.tab?options.tab:gBrowser.selectedTab;
    let browserForTab = gBrowser.getBrowserForTab( tab );
    let notificationBox = gBrowser.getNotificationBox( browserForTab );
    let ownerDocument = gBrowser.ownerDocument;

    //Create a Document Fragment.
    //If doing multiple DOM additions, we should be in the habit
    //  of doing things in a way which causes the least number of reflows.
    //  We know that we are going to add more than one thing, so use a
    //  document fragment.
    let docFrag = ownerDocument.createDocumentFragment();

    //ownerDocument must be used here in order to have the XUL namespace
    //  or the splitter causes problems.
    //  createElementNS() does not work here.
    let splitterEl = ownerDocument.createElement('splitter');
    splitterEl.id =  id ;

    //Look for the child element with class='browserSidebarContainer'.
    //It is the element in procimity to which the <splitter>
    //and objectEl will be placed.
    let theChild = notificationBox.firstChild;
    while (!theChild.hasAttribute('class')
        || (theChild.getAttribute('class').indexOf('browserSidebarContainer') === -1)
    ) {
        theChild = theChild.nextSibling;
        if(!theChild) {
            //We failed to find the correct node.
            //This implies that the structure Firefox
            //  uses has changed significantly and it should 
            //  be assumed that the extension is no longer compatible.
            return null;
        }
    }
    let tabBrowser = ownerDocument.getElementById('content');
    let heightAttr='height';
    let widthAttr='width';
    if(byWindow) {
        notificationBox = ownerDocument.getElementById('browser');
        theChild = ownerDocument.getElementById('appcontent');
        //When Window referenced, where we need to put the elements is
        //  slightly different, but works out to just being a swapping
        //  of 'location' values.
        //Swap the width and height attributes and options.
        heightAttr='width';
        widthAttr='height';
        let foo = width;
        width = height;
        height = foo;
        foo = options.width;
        options.width = options.height;
        options.height = foo;
        switch(location) {
            case 'window'  :
                //no change
                break;
            case 'top'    :
                location = 'left'
                break;
            case 'bottom' :
                location = 'right'
                break;
            case 'left'   :
                location = 'top'
                break;
            case 'right'  :
            default       :
                location = 'bottom'
                break;
        }
    }

    switch(location) {
        case 'window'    :
            return activeWindow.open(chromeUrl,'_blank',features);
            break;
        case 'top'    :
            if(options.size || options.height) {
                //Don't mess with the height/size unless it was specified
                sizeEl.removeAttribute(widthAttr);
                sizeEl.setAttribute(heightAttr,height);
            }
            docFrag.appendChild(objectEl);
            docFrag.appendChild(splitterEl);
            //Inserting the document fragment results in the same
            //  DOM structure as if you Inserted each child of the
            //  fragment separately. (i.e. the document fragment
            //  is just a temporary container).
            //Insert the interface prior to theChild.
            notificationBox.insertBefore(docFrag,theChild);
            break;
        case 'bottom' :
            if(options.size || options.height) {
                //Don't mess with the height/size unless it was specified
                sizeEl.removeAttribute(widthAttr);
                sizeEl.setAttribute(heightAttr,height);
            }
            docFrag.appendChild(splitterEl);
            docFrag.appendChild(objectEl);
            //Insert the interface just after theChild.
            notificationBox.insertBefore(docFrag,theChild.nextSibling);
            break;
        case 'left'   :
            if(options.size || options.width) {
                //Don't mess with the height/size unless it was specified
                sizeEl.removeAttribute(heightAttr);
                sizeEl.setAttribute(widthAttr,width);
            }
            docFrag.appendChild(objectEl);
            //Splitter is second in this orientaiton.
            docFrag.appendChild(splitterEl);
            if(byWindow) {
                //Insert the interface prior to the tabbrowser to put
                //  global notifications above the top sidebar.
                theChild.insertBefore(docFrag,tabBrowser);
            }else{
                //Insert the interface as the first child of theChild.
                theChild.insertBefore(docFrag,theChild.firstChild);
            }
            break;
        case 'right'  :
        default       :
            //Right orientaiton, the default.
            if(options.size || options.width) {
                //Don't mess with the height/size unless it was specified
                sizeEl.removeAttribute(heightAttr);
                sizeEl.setAttribute(widthAttr,width);
            }
            docFrag.appendChild(splitterEl);
            docFrag.appendChild(objectEl);
            //Insert the interface as the last child of theChild.
            theChild.appendChild(docFrag);
            break;
    }
    return splitterEl;
}

function getSizeWithDefaults(location,width,height){
    let defaultSize = 200;
    switch(location) {
        case 'window'    :
            width = ( (typeof width !== 'number') || width<1) ? defaultSize : width; 
            height = ( (typeof height !== 'number') || height<1) ? defaultSize : height; 
            break;
        case 'top'    :
        case 'bottom' :
            width = null;
            height = ( (typeof height !== 'number') || height<1) ? defaultSize : height; 
            break;
        case 'left'   :
        case 'right'  :
        default       :
            width = ( (typeof width !== 'number') || width<1) ? defaultSize : width; 
            height = null;
            break;
    }
    return [width,height];
}

exports.createInterfacePanel=createInterfacePanel;
exports.createInterfacePanelIframe=createInterfacePanelIframe;

The demo Firefox Add-on SDK extension is:

package.json:

{
    "title": "Demo Sidebars",
    "name": "demo-sidebars",
    "version": "0.0.1",
    "description": "Demo creating Window related sidebars",
    "main": "index.js",
    "author": "Makyen",
    "permissions": {"private-browsing": true},
    "engines": {
        "firefox": ">=38.0a1",
        "fennec": ">=38.0a1"
    },
    "license": "MIT",
    "keywords": [
        "jetpack"
    ]
}

data/sidebar.html:

<html>
<head>
    <meta charset="utf-8">
</head>
<body style="background-color:white;">
    This is a Window.
</body>
</html>

index.js:

var utils = require('sdk/window/utils');
var tabs = require('sdk/tabs');
var tabsUtils = require('sdk/tabs/utils');
var self = require('sdk/self');

//For testing: Open the Browser Console
var activeWin = utils.getMostRecentBrowserWindow();
activeWin.document.getElementById('menu_browserConsole').doCommand();

var mySidebars = require('./sidebars.js');
var sidebarSize = 100;  //Width & height to use

var sidebarByWindow = false;
var sidebars = {};
//The buttons
var buttons = {
    '◀': {where:'Left'},
    '▶': {where:'Right'},
    '▲': {where:'Top'},
    '▼': {where:'Bottom'},
    '☐': {where:'Window'}
};

//Create Buttons
var sdkActionButtons     = require('sdk/ui/button/action');

for(let badge in buttons){
    buttons[badge].button = sdkActionButtons.ActionButton({
        id: 'openSidebar' + buttons[badge].where,
        label: 'Open ' + buttons[badge].where + ' Sidebar',
        badge: badge,
        badgeColor: 'green',
        icon: './icons/Aurora-icon64.png',
        onClick: handleButtonClick
    });
}

function handleButtonClick(state){
    let where = buttons[state.badge].where.toLowerCase();
    let stateType = getSidebarByWindowText();
    let sidebarId = getSidebarId(state.badge,sidebarByWindow);
    //With this state being kept by window and tab, the checked property does
    //  not accurately track what we need to be doing, so use badgeColor and
    //  action buttons.
    if(sidebars[sidebarId]){
        //If we have a sidebar for this combo, then
        let elements = sidebars[sidebarId];
        if(elements){
            if(where==='window'){
                try{
                    elements[0].close();
                }catch(e){
                    //Do nothing. We should be tracking the state of the window so
                    //  users can use the close button. We are not, so trying to
                    //  close an already closed window could throw an error.
                }
            } else {
                elements.forEach(el => {el.remove();});
            }
        }
        delete sidebars[sidebarId];
    }else{
        //Create the sidebar and keep track of it so it can be removed.
        sidebars[sidebarId] = mySidebars.createInterfacePanelIframe(where,{
            url:self.data.url('sidebar.html'),
            byWindow:sidebarByWindow,
            size:sidebarSize,
            id:'makyen-interface-panel-' + stateType + '- ' + where
        });
        //Make the text reflect the sidebar
        if(where !== 'window'){
            setBodyText(sidebarId, 'This is a ' +stateType + ' ' + where + ' Sidebar.');
            sidebars[sidebarId][1].addEventListener('load', setBodyText.bind(null
                ,sidebarId
                ,'This is a ' + stateType.toUpperCase() + ' ' + where + ' Sidebar.'),true);
        }
    }
    updateButtonBadgeColors();
}

function setBodyText(sidebarId,text){
    let doc = sidebars[sidebarId][1].contentDocument; 
    doc.body.textContent = text;
}

function getSidebarId(badge,sidebarByWindow,domWin){
    let where = buttons[badge].where.toLowerCase();
    let stateType = getSidebarByWindowText();
    domWin = domWin?domWin:utils.getMostRecentBrowserWindow();
    let winId = utils.getOuterId(domWin);
    //This should get the tab ID from any window, not just the active window.
    let tabId = tabsUtils.getTabId(tabsUtils.getActiveTab(domWin));
    let id = sidebarByWindow?winId:tabId;
    return stateType+id+where;
}

function getSidebarByWindowText(){
    return sidebarByWindow?'window':'tab';
}

function updateButtonBadgeColors(){
    //Update the badge colors in all windows based on if there is a sidebar of the
    //  current type for the window/tab.
    let allWindows = utils.windows('navigator:browser',{includePrivate:true});
    for(let win of allWindows){
        for(let badge in buttons){
            let sidebarId = getSidebarId(badge,sidebarByWindow,win);
            buttons[badge].button.state(win,{
                badgeColor : sidebars[sidebarId]?'red':'green'
            });
        }
    }
}
//update badge colors each time the active tab changes.
tabs.on('activate',updateButtonBadgeColors);

//var sdkToggleButtons     = require('sdk/ui/button/toggle');
var windowTabLabelText = 'Sidebars are associated with ';
var windowTabToggleButton = sdkActionButtons.ActionButton({
    id: 'windowTabToggleButton',
    label: windowTabLabelText + getSidebarByWindowText(),
    icon: './icons/Aurora-icon64.png',
    onClick: handlewindowTabToggle
});

function handlewindowTabToggle(state){
    if(!state.badge){
        windowTabToggleButton.badge= '☐';
        windowTabToggleButton.badgeColor= 'blue';
        sidebarByWindow = true;
    } else {
        windowTabToggleButton.badge= '';
        sidebarByWindow = false;
    }
    windowTabToggleButton.label = windowTabLabelText + getSidebarByWindowText();
    updateButtonBadgeColors();
}

The code in this answer was adapted and significantly expanded from my answer to "Firefox Extension, Window related sidebar". That answer has significant additional information as how sidebars (interface panels) are structured within the Firefox Browser.