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