@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.
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
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 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 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…
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.
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.
Welcome DUzun!
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!
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 andopen
event is
emited onsdk/tabs
collection.
The new tab doesn’t catch any tab events afterwards! Neither does the
oldtab
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 ActionButton
s 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 exampletab.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!
I’m not 100% sure about “detaching”, but haven’t seen issues if used after everything is initialized.