Tear off tab with SDK

I’m interested in accessing the tear off feature–the one where you can grab a tab, drag it off the window, and get a new window containing just that tab–in an SDK add-on. I have the window and tab objects in hand, but the high level docs say

A live list of tabs in this window. This property is read-only.

which sounds like it makes it impossible to move tabs between windows. But since Firefox has the detaching feature, and Firefox’s UI is entirely written in XUL and javascript and CSS, I am hopeful that there is a way, maybe buried in the low level API. I don’t know my way around XUL at all though, so I can’t figure this out on my own.

My thanks to anyone who shares any cluestick.

No low level API allows this. You have to go low low, to the XUL, which is to use docShell swapping. Currently, when you move a tab to another, that’s how it works. This way avoids reloading any of the resources and most importantly, maintains state (such as javascript dom changes).

Here is a long discussion on it, you can see it fires of off some events when you do drag it, so if you want to catch those, read here:

http://web.archive.org/web/20150604010137/https://forums.mozilla.org/viewtopic.php?f=7&t=21117&sid=6fe8e24fe97f960a7e1c58c0f17048a4

Here is a more concise solution, if you just want to move a tab yourself:

Edit: Actually below my solution, a user named @Duzun has shred a “low level api” way, using just the sdk api’s, very nice.

Excellent, thank you for jumping on this. I can see that you really put a lot of time into figuring that out! I thought it might have something to do with this gBrowser thing I was seeing mentioned but I didn’t know how.

Unfortunately Duzun’s code isn’t working for me. I’m getting “TypeError: gBrowser is undefined”. I’m onFirefox 44.0.2; did the SDK change between November and now?

My code is at https://github.com/kousu/disabletabs/blob/tear_off_tab/index.js

1 Like

So, maybe I’ve got my types wrong. I don’t know my way around XUL.

I found that hacking this into Duzun’s code made it run:

        -var gBrowser = aWin.gBrowser;
        +var gBrowser = getMostRecentBrowserWindow().gBrowser;

This has the right effect only because the window I want was just made with windows.open(): aWin == viewFor(windows.open("https://some-site.com"));

Since Duzun is calling viewFor can I assume I’m meant to pass one of the other high-level objects from sdk/windows? These objects don’t have a .gBrowser as far as I can tell, not immediately nor if I wait for the ‘open’ event.

viewFor returns the XUL browser window to a hogh-level sdk/windows Window.

I found my problem: gBrowser isn’t defined until ‘open’. So I have to add a callback layer:

var window = windows.open({'url': "about:blank", 'isPrivate': privateBrowsing.isPrivate(tab) });
window.on('open', function() {
        // tear off tab and put it on the new window
        // we have to wait for 'open' before viewFor(window).gBrowser exists
        tab.setWindow(window, 0);
});

Now I just need to figure out how to stop my handler triggering itself and I’ll be good.

1 Like

As you want to catch the window you have opened I would write the code this way:

let window = windows.open({
    url: "about:blank",
    isPrivate: privateBrowsing.isPrivate(tab),
    onOpen: function(window) {
        // tear off tab and put it on the new window
        // we have to wait for 'open' before viewFor(window).gBrowser exists
        tab.setWindow(window, 0);
  }
 });
3 Likes

@Duzun is an active member on stackoverflow, he made some comments on that topic. If you sign up and post a comment, Im sure he’ll gladly come here. In fact he posted a comment earlier pointing out a flaw in his code. I think it is what you are experiencing.

1 Like

Soooooooooooooooooooooo I introspected gBrowser and discovered replaceTabWithWindow() which is precisely the “tear off tab” API. It moves a given XUL tab (i.e. something you can get like viewFor(tab)) off its owning window and onto a new one, preserving state—it even preserves typed in passwords, which usually get erased as soon as a page is reloaded, so it must be legitimately moving the existing tab object.

Introspection is great especially since a lot of the low level API is apparently written in pure javascript, so we can even read its source:

function replaceTabWithWindow(aTab, aOptions) {

          
            if (this.tabs.length == 1)
              return null;

            var options = "chrome,dialog=no,all";
            for (var name in aOptions)
              options += "," + name + "=" + aOptions[name];

            // tell a new window to take the "dropped" tab
            return window.openDialog(getBrowserURL(), "_blank", options, aTab);
          
        
}

Here’s an example. Run jpm init, drop this into the index.js, do jpm run and make some multi-tab windows, do stuff to them (like typing in passwords or other form fields) and then click this button:

const { getMostRecentBrowserWindow } = require("sdk/window/utils");                                   
const { ActionButton } = require("sdk/ui/button/action");

var button = ActionButton({
  id: "tearofftabtab-button",
  label: "Tear Off Current Tab",
  icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACUElEQVQ4jaWTTWtTURCGjzc33CCpbVKN4kexC9EUY1Hov+iqPyDrbgtuCrViKUERqsWVguBGQaW4UiKiaEVxoShFGgnuBMUqNW3zce49Z+ZxUWtwoRR8YXbzPswM7xj+JgVEiXGsYVknxgII4Ltt5p8AB8RArOAUVQfqQJNtAFA8QgvF6i9PR1Dt0KbVBTjncM4hIni/OZv3HsRB+wvefiP2LcQnJIkQe49FEJFNQLPZZHh4mEwmQyqVoqenh3K5TGvlK1dOlageH+HG4DFar1/S0A6Lr99xdN8QxWKRXC6HGR0dJZvNMjk5Sb1ep1gskk6nuTo/D+/ec7dvkBdhP9cKeX7UXxEZQ2/YRxRFLC8vY+bm5qhUKnjvsdYyPj5OFEWcnTnHujiS5TfcPDbAw50h9w7u5f7UadLZFLVaDRHBiGzuY61lbGyMXC5HoVBgrbGGWAW/TvvxHR7s7udFKs/1oyfZ+PSRTqeDqm7eoFqtEoYhmUyG2dlZVJU4iREfI/WP3Nt9iMUdu7jdf5Anly5i0oaVlRWazSZmYWGBIAiIoohyucz09DQTExPMnJli9dlT5vcM8Kh3gFsHDuNqb9mb7yXMRBhjWFpawpRKJVKpFMYYgiAgDEOCIOD81BkunBjh8pEhKqUhGkvP6bQ/U//wgUP5/YRhSDabxbTbbVQV5xyq2q0kgR8NdOM7JKuo/Y5qggqIdPvMlnkrQCKCquJFsOrxeHAJxA48eFU6Xv4EqOpv41YqnQirqliv4MEmQtN7RBSs7wL+/gvb038DfgJnyUabbHzUbQAAAABJRU5ErkJggg==",
  onClick: function() {
    getMostRecentBrowserWindow().gBrowser.replaceTabWithWindow(getMostRecentBrowserWindow().gBrowser.selectedTab);
  }
});

There’s a comment in the swapBrowsersAndCloseOther() function that @noitidart used that mentions replaceTabWithWindow(), despite not being called by it. Maybe window.openDialog() in some roundabout way does end up relying on swapBrowsersAndCloseOther(), in which case @DUzun’s and @noitidart’s codes are valiant efforts at reimplementing replaceTabWithWindow().

The big advantage of this for me over @DUzun’s is that whatever black magic is working in the low-low levels doesn’t spawn a hidden temporary tab, so it doesn’t trigger tabs.on('open'), so I don’t get a recursive explosion.

Here a listing of all the methods available on gBrowser. Are these documented anywhere? They don’t seem to be in the Jetpack docs. Maybe it will help someone else:

addEventListener 
addProgressListener 
addTab 
addTabsProgressListener 
_adjustFocusAfterTabSwitch 
appendChild 
_appendStatusPanel 
attachFormFill 
_beginRemoveTab 
blur 
_blurTab 
_callProgressListeners 
click 
cloneNode 
closest 
compareDocumentPosition 
contains 
_createBrowser 
_createPreloadBrowser 
createShadowRoot 
createTooltip 
detachFormFill 
dispatchEvent 
doCommand 
duplicateTab 
_endRemoveTab 
enterTabbedMode 
focus 
_generateUniquePanelID 
getAnimations 
getAttribute 
getAttributeNode 
getAttributeNodeNS 
getAttributeNS 
getBoundingClientRect 
getBoundMutationObservers 
getBoxQuads 
getBrowserAtIndex 
getBrowserContainer 
getBrowserForContentWindow 
getBrowserForDocument 
getBrowserForOuterWindowID 
getBrowserForTab 
getBrowserIndexForDocument 
getClientRects 
getDestinationInsertionPoints 
getElementsByAttribute 
getElementsByAttributeNS 
getElementsByClassName 
getElementsByTagName 
getElementsByTagNameNS 
getEventHandler 
getFindBar 
getIcon 
getNotificationBox 
_getPreloadedBrowser 
getSidebarContainer 
getStatusPanel 
getStripVisibility 
_getSwitcher 
_getTabForBrowser 
getTabForBrowser 
_getTabForContentWindow 
getTabModalPromptBox 
getTabsToTheEndFrom 
getUserData 
getWindowTitleForBrowser 
goBack 
goForward 
goHome 
gotoIndex 
handleEvent 
_handleKeyDownEvent 
_handleKeyPressEventMac 
hasAttribute 
hasAttributeNS 
hasAttributes 
hasChildNodes 
hideTab 
insertAdjacentHTML 
insertBefore 
isDefaultNamespace 
isEqualNode 
isFailedIcon 
isFindBarInitialized 
_isPreloadingEnabled 
loadOneTab 
loadTabs 
loadURI 
loadURIWithFlags 
lookupNamespaceURI 
lookupPrefix 
matches 
moveTabBackward 
moveTabForward 
moveTabOver 
moveTabTo 
moveTabToEnd 
moveTabToStart 
mozMatchesSelector 
mozRequestFullScreen 
mozRequestPointerLock 
mozScrollSnap 
mTabProgressListener 
normalize 
observe 
openNonRemoteWindow 
pinTab 
previewTab 
querySelector 
querySelectorAll 
receiveMessage 
releaseCapture 
reload 
reloadAllTabs 
reloadTab 
reloadWithFlags 
remove 
removeAllTabsBut 
removeAttribute 
removeAttributeNode 
removeAttributeNS 
removeChild 
removeCurrentTab 
removeEventListener 
removeProgressListener 
removeTab 
removeTabsProgressListener 
removeTabsToTheEndFrom 
replaceChild 
replaceTabWithWindow 
scroll 
scrollBy 
scrollByNoFlush 
scrollIntoView 
scrollTo 
selectTabAtIndex 
setAttribute 
setAttributeNode 
setAttributeNodeNS 
setAttributeNS 
setCapture 
_setCloseKeyState 
setEventHandler 
setIcon 
setIsPrerendered 
setStripVisibilityTo 
setTabTitle 
setTabTitleLoading 
setUserData 
shouldLoadFavIcon 
showOnlyTheseTabs 
showTab 
stop 
_swapBrowserDocShells 
swapBrowsersAndCloseOther 
swapFrameLoaders 
swapNewTabWithBrowser 
_swapRegisteredOpenURIs 
_tabAttrModified 
unpinTab 
updateBrowserRemoteness 
updateBrowserRemotenessByURL 
updateCurrentBrowser 
updateTitlebar 
updateWindowResizers 
useDefaultIcon 
warnAboutClosingTabs 
webkitMatchesSelector 
1 Like

Fantastic research there! Thanks for sharing it!

Caveat: let me emphasize that the tab object must be a XUL tab, aka a “view”. If you don’t pass the right type you don’t get any error telling you what’s wrong.

Here is my latest code:

tabs.on('open', function(tab){
        viewFor(tab.window).gBrowser.replaceTabWithWindow(viewFor(tab));
});

Ah, this feels great. It’s so short and effective. Firefox does exactly what I tell it to and nothing more.

@Duzun couldn’t sign up yet so he wanted me to share this comment from stackoverflow:

I see @kousu found a better solution for moving a tab to a new window at discourse.mozilla-community.org/t/tear-off-tab-with-sdk/7085/9 using replaceTabWithWindow().
I can’t login there to comment, something is broken in the
authorization system :frowning: His solution doesn’t seem to work for moveing
to an existing window, but I believe there should be a better solution
then I used in setWindow() for the generic problem of movein tabs to different windows.
– DUzun

And I can’t comment over on SO because I don’t have enough reputation there. Awkward. Well, please relay this: Discourse is very nice, but the only login system they have unfortunately is Mozilla’s dying Persona system. I had the exactly same trouble until I turned off all my ad blockers—for some reason the login system didn’t list Persona, and I just had to allow everything globally. Once I got the cookie I was able to turn things back on, though.

I believe that tracing out the internal [quote=“kousu, post:9, topic:7085”]
return window.openDialog(getBrowserURL(), “_blank”, options, aTab);
[/quote]
will tell us how to setWindow() as well.

Message delivered :slight_smile: https://discourse.mozilla-community.org/t/tear-off-tab-with-sdk/7085/13?u=noitidart

There’s also the clone of Chrome’s API. It looks likewhen it’s ready you should be able to do something like:

// Tear off
browser.windows.create({tabId: aTab.id});


// Move 
browser.tabs.move(aTab.id, {windowId: aWin.id, index: 0});

WebExtensions are only in FF Developer/Nightly editions yet though.

Hi everyone!

I managed to login here only with Firefox.

I’ve spent a lot of time digging into SDK sources and trying to make it work (the setWindow() thing), it worked till FF 43.
It looks like we have to invest more time and energy to make it work properly, and still have to use low-level API, which is not guaranteed to work in newer versions of FF.
WebExtensions are coming soon (or later), which would fix this issue.
I’m wandering, is it worth the effort to somehow temporarily fix this issue?
I’m going to give it one last try…

1 Like

I’ve got your code here, @DUzun. (and I hereby MPL 2.0 it). Have you made any progress with your last try?

///////////  monkey-patch (model-side) Tab objects to be better ////////////////

/* Tab.detach(): tear off this tab and spawn a new window just for it.
 *
 * If the tab is the only one on its window nothing happens.
 * Private browsing is preserved: if this.window is a private browsing window, so is will the new window be.
 */
require("sdk/tabs/tab").Tab.prototype.detach = function() {
        // ((the single-tab check is handled by replaceTabWithWindow, so we don't need to do it))
        // ((as is the preservation of private browsing ))
        // ((really this is just a Jetpack SDK-friendly wrapper for XUL's replaceTabWithWindow())
        viewFor(this.window).gBrowser.replaceTabWithWindow(viewFor(this));
}

/* Tab.setWindow(window): Move tab to the given window.
 *
 * window should be a 'high level' or 'model' window object from the https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/windows module.
 * If window is null, a new window is created.
 *
 * if given, index specifies where in the list of tabs to insert. By default, insertion is done at the end. (XXX not implemented)
 *
 * precondition: unless it's null, window has passed the 'open' state
 *   That is, you cannot call this on a newly created window.
 *
 *   Incorrect:
 *   > tab.setWindow(windows.open("about:blank"));
 *   ( causes TypeError: viewFor(...).gBrowser is undefined )
 *
 *   Correct:
 *   > let win = windows.open("about:blank");
 *   > win.on('on', function() {
 *   >   tab.setWindow(win)
 *   > }));
 *
 * XXX how does this interact with private browsing? This could be bad...
 * XXX this doesn't handle the 'index' argument.
 * XXX write a similar method attached to the Window prototype: window.adopt(tab)
 */
require("sdk/tabs/tab").Tab.prototype.setWindow = function(window, index = -1) {
        console.log("index = " + index);
        if(window) {
                viewFor(window).gBrowser.tabContainer.appendChild(viewFor(this));
        } else {
                this.detach();
        }
}

It turns out it really is that simple. No need to spawn windows and swap docShells or anything: just append it to the .tabbrowser-tabs XUL element on the new window. Firefox recognizes that the tab has been moved and removes it from the original window – though that’s not promised anywhere in the docs, so maybe it’s not safe to rely upon.

1 Like

I figured this out by reading as much of the gBrowser source code as I could. Here’s gBrowser.addTab():

[object XULElement].addTab = function addTab(aURI, aReferrerURI, aCharset, aPostData, aOwner, aAllowThirdPartyFixup) {

          
            const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
            var aReferrerPolicy;
            var aFromExternal;
            var aRelatedToCurrent;
            var aSkipAnimation;
            var aAllowMixedContent;
            var aForceNotRemote;
            var aNoReferrer;
            var aUserContextId;
            if (arguments.length == 2 &&
                typeof arguments[1] == "object" &&
                !(arguments[1] instanceof Ci.nsIURI)) {
              let params = arguments[1];
              aReferrerURI          = params.referrerURI;
              aReferrerPolicy       = params.referrerPolicy;
              aCharset              = params.charset;
              aPostData             = params.postData;
              aOwner                = params.ownerTab;
              aAllowThirdPartyFixup = params.allowThirdPartyFixup;
              aFromExternal         = params.fromExternal;
              aRelatedToCurrent     = params.relatedToCurrent;
              aSkipAnimation        = params.skipAnimation;
              aAllowMixedContent    = params.allowMixedContent;
              aForceNotRemote       = params.forceNotRemote;
              aNoReferrer           = params.noReferrer;
              aUserContextId        = params.userContextId;
            }

            // if we're adding tabs, we're past interrupt mode, ditch the owner
            if (this.mCurrentTab.owner)
              this.mCurrentTab.owner = null;

            var t = document.createElementNS(NS_XUL, "tab");

            var uriIsAboutBlank = !aURI || aURI == "about:blank";

            if (aUserContextId)
              t.setAttribute("usercontextid", aUserContextId);
            t.setAttribute("crop", "end");
            t.setAttribute("onerror", "this.removeAttribute('image');");
            t.className = "tabbrowser-tab";

            // The new browser should be remote if this is an e10s window and
            // the uri to load can be loaded remotely.
            let remote = gMultiProcessBrowser &&
                         !aForceNotRemote &&
                         E10SUtils.canLoadURIInProcess(aURI, Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT);

            this.tabContainer._unlockTabSizing();

            // When overflowing, new tabs are scrolled into view smoothly, which
            // doesn't go well together with the width transition. So we skip the
            // transition in that case.
            let animate = !aSkipAnimation &&
                          this.tabContainer.getAttribute("overflow") != "true" &&
                          Services.prefs.getBoolPref("browser.tabs.animate");
            if (!animate) {
              t.setAttribute("fadein", "true");
              setTimeout(function (tabContainer) {
                tabContainer._handleNewTab(t);
              }, 0, this.tabContainer);
            }

            // invalidate caches
            this._browsers = null;
            this._visibleTabs = null;

            this.tabContainer.appendChild(t);

            // If this new tab is owned by another, assert that relationship
            if (aOwner)
              t.owner = aOwner;

            let b;
            let usingPreloadedContent = false;

            // If we open a new tab with the newtab URL in the default
            // userContext, check if there is a preloaded browser ready.
            // Private windows are not included because both the label and the
            // icon for the tab would be set incorrectly (see bug 1195981).
            if (aURI == BROWSER_NEW_TAB_URL && !aUserContextId &&
                !PrivateBrowsingUtils.isWindowPrivate(window)) {
              b = this._getPreloadedBrowser();
              usingPreloadedContent = !!b;
            }

            if (!b) {
              // No preloaded browser found, create one.
              b = this._createBrowser({remote: remote,
                                       uriIsAboutBlank: uriIsAboutBlank,
                                       userContextId: aUserContextId});
            }

            let notificationbox = this.getNotificationBox(b);
            var position = this.tabs.length - 1;
            var uniqueId = this._generateUniquePanelID();
            notificationbox.id = uniqueId;
            t.linkedPanel = uniqueId;
            t.linkedBrowser = b;
            this._tabForBrowser.set(b, t);
            t._tPos = position;
            t.lastAccessed = Date.now();
            this.tabContainer._setPositionalAttributes();

            // Inject the <browser> into the DOM if necessary.
            if (!notificationbox.parentNode) {
              // NB: this appendChild call causes us to run constructors for the
              // browser element, which fires off a bunch of notifications. Some
              // of those notifications can cause code to run that inspects our
              // state, so it is important that the tab element is fully
              // initialized by this point.
              this.mPanelContainer.appendChild(notificationbox);
            }

            // We've waited until the tab is in the DOM to set the label. This
            // allows the TabLabelModified event to be properly dispatched.
            if (!aURI || isBlankPageURL(aURI)) {
              t.label = this.mStringBundle.getString("tabs.emptyTabTitle");
            } else if (aURI.toLowerCase().startsWith("javascript:")) {
              // This can go away when bug 672618 or bug 55696 are fixed.
              t.label = aURI;
            }

            this.tabContainer.updateVisibility();

            // wire up a progress listener for the new browser object.
            var tabListener = this.mTabProgressListener(t, b, uriIsAboutBlank, usingPreloadedContent);
            const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"]
                                     .createInstance(Components.interfaces.nsIWebProgress);
            filter.addProgressListener(tabListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
            b.webProgress.addProgressListener(filter, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
            this.mTabListeners[position] = tabListener;
            this.mTabFilters[position] = filter;

            b.droppedLinkHandler = handleDroppedLink;

            // Swap in a preloaded customize tab, if available.
            if (aURI == "about:customizing") {
              usingPreloadedContent = gCustomizationTabPreloader.newTab(t);
            }

            // Dispatch a new tab notification.  We do this once we're
            // entirely done, so that things are in a consistent state
            // even if the event listener opens or closes tabs.
            var evt = document.createEvent("Events");
            evt.initEvent("TabOpen", true, false);
            t.dispatchEvent(evt);

            // If we didn't swap docShells with a preloaded browser
            // then let's just continue loading the page normally.
            if (!usingPreloadedContent && !uriIsAboutBlank) {
              // pretend the user typed this so it'll be available till
              // the document successfully loads
              if (aURI && gInitialPages.indexOf(aURI) == -1)
                b.userTypedValue = aURI;

              let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
              if (aAllowThirdPartyFixup) {
                flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
                flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
              }
              if (aFromExternal)
                flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
              if (aAllowMixedContent)
                flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
              try {
                b.loadURIWithFlags(aURI, {
                                   flags: flags,
                                   referrerURI: aNoReferrer ? null: aReferrerURI,
                                   referrerPolicy: aReferrerPolicy,
                                   charset: aCharset,
                                   postData: aPostData,
                                   });
              } catch (ex) {
                Cu.reportError(ex);
              }
            }

            // We start our browsers out as inactive, and then maintain
            // activeness in the tab switcher.
            b.docShellIsActive = false;

            // When addTab() is called with an URL that is not "about:blank" we
            // set the "nodefaultsrc" attribute that prevents a frameLoader
            // from being created as soon as the linked <browser> is inserted
            // into the DOM. We thus have to register the new outerWindowID
            // for non-remote browsers after we have called browser.loadURI().
            //
            // Note: Only do this of we still have a docShell. The TabOpen
            // event was dispatched above and a gBrowser.removeTab() call from
            // one of its listeners could cause us to fail here.
            if (!remote && b.docShell) {
              this._outerWindowIDBrowserMap.set(b.outerWindowID, b);
            }

            // Check if we're opening a tab related to the current tab and
            // move it to after the current tab.
            // aReferrerURI is null or undefined if the tab is opened from
            // an external application or bookmark, i.e. somewhere other
            // than the current tab.
            if ((aRelatedToCurrent == null ? aReferrerURI : aRelatedToCurrent) &&
                Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) {
              let newTabPos = (this._lastRelatedTab ||
                               this.selectedTab)._tPos + 1;
              if (this._lastRelatedTab)
                this._lastRelatedTab.owner = null;
              else
                t.owner = this.selectedTab;
              this.moveTabTo(t, newTabPos);
              this._lastRelatedTab = t;
            }

            if (animate) {
              requestAnimationFrame(function () {
                this.tabContainer._handleTabTelemetryStart(t, aURI);

                // kick the animation off
                t.setAttribute("fadein", "true");
              }.bind(this));
            }

            return t;
          
        
}

Most of it is boilerplate or leftover cruft from old features. I am a little bit surprised that the one line I keyed in on in the middle, this.tabContainer.appendChild(t);, worked out. I was just trying to see if I could at least manipulate the UI, and that seemed like a simple place to start. It turned out instead to be exactly what you wanted.

1 Like

Welcome DUzun! :slight_smile:

You guys are doing some excellent work here! Love how you and @kousu are sharing as you go through the process. Fantastic use of the forums!

1 Like

You solution looks good @kousu, thanks! I’ll play with it this evening and let you know how it works.

Yesterday I didn’t have the chance to do anything about it, as I had to deal with Safari Extension issues (same project, different set of issues).

My initial thought is to have a callback on setWindow(window, index, cb) and when .gBrowser is undefined, assume window not open yet and listen for open event before continuing. Basically by making it async we have more flexibility in the way we can handle different states.