Tear off tab with SDK

@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.

I’ve done some testing on you methods, @kousu.

There is a serious source of issues!

After calling tab.detach(), a new window with a new tab is created, even though the view of the old tab is transferred to the new window and state is preserved. The tab gets a new id and open event is emited on sdk/tabs collection.
The new tab doesn’t catch any tab events afterwards! Neither does the old tab model.
If you access tab properties from old tab model, it still works, but after the tab is moved to the new window, before fully initialized, trying to access for example tab.url throws an error, because viewFor(tab) == undefined at some point.
I’m not sure, but there is a chance that the events are not emited for the new tab across other extensions as well.
This is unacceptable!

gBrowser.tabContainer.appendChild(tabView) is not working.
gBrowser.tabContainer is simply a DOM element, so is tabView = viewFor(sdkTab).

By moving tabView to a different window, the associated panel (tab’s browser) is not moved automatically.

In gBrowser.addTab, after appending the tab to new window, a browser is created:

this.tabContainer.appendChild(t);
// ...
b = this._createBrowser(...); 
/// ...
t.linkedBrowser = b;

Really? What Firefox version? I’m 44.0.2 on Arch Linux. It works for me.

FF 44.0.2 and 47.0a1 (Nightly) on Windows.

I’ve done some testing on you methods, @kousu.

There is a serious source of issues!

After calling tab.detach(), a new window with a new tab is created,
even though the view of the old tab is transferred to the new window
and state is preserved. The tab gets a new id and open event is
emited on sdk/tabs collection.
The new tab doesn’t catch any tab events afterwards! Neither does the
old tab model.

I don’t see that tabs.on('open'). In fact, I never see one for any first tab in a window, which seems like a bug to me but you never know.

It would be odd for moving a tab to trigger an open if its still the same tab.

Can you share your experiments as test code? Its easiest to interrogate this if each bug you’re seeing comes in a separate extension that I can jpm run, though ActionButtons embedded within a single extension would be okay.

If you access tab properties from old tab model, it still works, but
after the tab is moved to the new window, before fully initialized,
trying to access for example tab.url throws an error, because
viewFor(tab) == undefined at some point.

This is the same error I had above. The SDK docs explicitly say you can’t trust tab.url until tab.on('ready'), and while they don’t explicitly say when you can or can’t trust having viewFor(.) or view for(tab).gBrowser, I found that you need to wait as well. Just do your code in a tab.on('ready') handler and deal with it.

I’m not sure, but there is a chance that the events are not emited for
the new tab across other extensions as well.
This is unacceptable!

It’s not a new tab though so it shouldn’t be emitting these events anyway. But can you show this in a test case?

Here is the index.js file:

    // -------------------------------------------------------------
    /// Testing and playing
    // -------------------------------------------------------------
    var sw = require('./setWindow');
    // -------------------------------------------------------------
    const {
        setTimeout,
        setImmediate,
    } = require("sdk/timers");

    var TABS = require("sdk/tabs");
    var browserWindows = require("sdk/windows").browserWindows;
    // -------------------------------------------------------------

    // Marck start time
    var tmr = Date.now();

    // -------------------------------------------------------------
    /// Listeners:
    browserWindows.on('open', function (win) {
        log('~win', getWinId(win), 'open', win.tabs.length, win.tabs[0].id);
    });

    // on open tabs might be uninitialized and it is dangerous to alter it in any way
    TABS.on('open', function (tab, event) {
        log('~tab', tab.id, 'open', tab.readyState, tab.url, event?true:event);
    });

    // This one is not documented, but interesting
    TABS.on('create', function (tab, ...args) {
        log('~tab', tab.id, 'create', tab.readyState, tab.url, ...args);
    });
    TABS.on('ready', function (tab) {
        log('~tab', tab.id, 'ready', tab.readyState, tab.url);
    });
    TABS.on('load', function (tab) {
        log('~tab', tab.id, 'load', tab.readyState, tab.url);
    });
    TABS.on('move', function (tab, ...args) {
        log('~tab', tab.id, 'move', tab.readyState, tab.url, ...args);
    });

    /// Some URLs
    var urls = [
        'https://duzun.me/?window1_tab0_open_before_window',
        'https://www.google.com/?window1_tab1_detatched_to_window3',
        'https://nodejs.org/en/?window1_tab3_next_to_detatched_tab',
        'https://discourse.mozilla-community.org/t/tear-off-tab-with-sdk/7085/19/',
        'http://npmjs.org/?window2_tab1_moved_to_window1',
    ];

    /// Experiments
    var w0 = browserWindows[0];
    w0.tabs[0].url = urls[3];

    var w1 = browserWindows.open({
        url: urls[0],
        onOpen: function (w) {
            // Can't open any tab on this window until t0.readyState != 'uninitialized', so use what SDK offers - onReady,
            // even though it depends on document.readyState == 'interactive' inside tab, which could take a lot of time to happen :-(
            var t0 = w.tabs[0];
            t0.once('ready', function (t0) {
                w.tabs.open({
                    url: urls[1],
                    index: 1,
                    onOpen: function (t1) {
                        log('~t1', t1.id,'detaching', getWinId(t1.window));
                        t1.detach();

                        // This one would try to access .url multiple times, thus throws an error
                        wait4url(t1, log);
                    },
                });
                w.tabs.open({
                    url: urls[2],
                    index: 3,
                });
            });

            var t4 = w0.tabs[0];
            t4.once('ready', function (t4) {
                w0.tabs.open({
                    url: urls[4],
                    onReady: (tab) => {
                        log('~move tab', tab.id, tab.url, 'from', getWinId(w0), 'to', getWinId(w1));
                        tab.setWindow(w1);
                    },
                });
            });
        }
    });

    /// Helpers:

    function passed() {
        return Date.now() - tmr;
    }

    function log(...args) {
        console.log(passed(), '~', ...args);
    }

    function getWinId(win) {
        if ( !win ) return undefined;
        var id = win.id;
        if ( id ) return id;
        win.id = getWinId._id = id = (getWinId._id||0) + 1;
        return id;
    }

    function wait4url(tab, cb) {
        var readyState;
        return (function _wait() {
            var url = tab.url;
            // new tab's url is 'about:newtab'
            if ( (url == 'about:blank' || !url) && (readyState = tab.readyState) != 'complete' && readyState != 'interactive' ) {
                return setTimeout(_wait, 4);
            }
            cb(tab, url);
            return url;
        }())
    }

Also save your patches of Tab to setWindow.js file.

With this test the detached window emits events, but earlier today while playing with code I’ve got in a situation where it didn’t. I guess becouse of an error right after .detatch call.

setWindow() behaves as described earlier, and there is an error in the console after detatch().

You can filter console output by “~” and follow events…

Finally I’ve come up with a complete solution! :mask:

I’m not 100% sure about “detaching”, but haven’t seen issues if used after everything is initialized.

1 Like