summaryrefslogtreecommitdiffstats
path: root/devtools/client/responsive.html/browser/swap.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/responsive.html/browser/swap.js')
-rw-r--r--devtools/client/responsive.html/browser/swap.js309
1 files changed, 309 insertions, 0 deletions
diff --git a/devtools/client/responsive.html/browser/swap.js b/devtools/client/responsive.html/browser/swap.js
new file mode 100644
index 000000000..7ab028065
--- /dev/null
+++ b/devtools/client/responsive.html/browser/swap.js
@@ -0,0 +1,309 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Ci } = require("chrome");
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+const { tunnelToInnerBrowser } = require("./tunnel");
+
+/**
+ * Swap page content from an existing tab into a new browser within a container
+ * page. Page state is preserved by using `swapFrameLoaders`, just like when
+ * you move a tab to a new window. This provides a seamless transition for the
+ * user since the page is not reloaded.
+ *
+ * See /devtools/docs/responsive-design-mode.md for a high level overview of how
+ * this is used in RDM. The steps described there are copied into the code
+ * below.
+ *
+ * For additional low level details about swapping browser content,
+ * see /devtools/client/responsive.html/docs/browser-swap.md.
+ *
+ * @param tab
+ * A browser tab with content to be swapped.
+ * @param containerURL
+ * URL to a page that holds an inner browser.
+ * @param getInnerBrowser
+ * Function that returns a Promise to the inner browser within the
+ * container page. It is called with the outer browser that loaded the
+ * container page.
+ */
+function swapToInnerBrowser({ tab, containerURL, getInnerBrowser }) {
+ let gBrowser = tab.ownerDocument.defaultView.gBrowser;
+ let innerBrowser;
+ let tunnel;
+
+ // Dispatch a custom event each time the _viewport content_ is swapped from one browser
+ // to another. DevTools server code uses this to follow the content if there is an
+ // active DevTools connection. While browser.xml does dispatch it's own SwapDocShells
+ // event, this one is easier for DevTools to follow because it's only emitted once per
+ // transition, instead of twice like SwapDocShells.
+ let dispatchDevToolsBrowserSwap = (from, to) => {
+ let CustomEvent = tab.ownerDocument.defaultView.CustomEvent;
+ let event = new CustomEvent("DevTools:BrowserSwap", {
+ detail: to,
+ bubbles: true,
+ });
+ from.dispatchEvent(event);
+ };
+
+ return {
+
+ start: Task.async(function* () {
+ tab.isResponsiveDesignMode = true;
+
+ // Freeze navigation temporarily to avoid "blinking" in the location bar.
+ freezeNavigationState(tab);
+
+ // 1. Create a temporary, hidden tab to load the tool UI.
+ let containerTab = gBrowser.addTab("about:blank", {
+ skipAnimation: true,
+ forceNotRemote: true,
+ });
+ gBrowser.hideTab(containerTab);
+ let containerBrowser = containerTab.linkedBrowser;
+ // Prevent the `containerURL` from ending up in the tab's history.
+ containerBrowser.loadURIWithFlags(containerURL, {
+ flags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
+ });
+
+ // Copy tab listener state flags to container tab. Each tab gets its own tab
+ // listener and state flags which cache document loading progress. The state flags
+ // are checked when switching tabs to update the browser UI. The later step of
+ // `swapBrowsersAndCloseOther` will fold the state back into the main tab.
+ let stateFlags = gBrowser._tabListeners.get(tab).mStateFlags;
+ gBrowser._tabListeners.get(containerTab).mStateFlags = stateFlags;
+
+ // 2. Mark the tool tab browser's docshell as active so the viewport frame
+ // is created eagerly and will be ready to swap.
+ // This line is crucial when the tool UI is loaded into a background tab.
+ // Without it, the viewport browser's frame is created lazily, leading to
+ // a multi-second delay before it would be possible to `swapFrameLoaders`.
+ // Even worse than the delay, there appears to be no obvious event fired
+ // after the frame is set lazily, so it's unclear how to know that work
+ // has finished.
+ containerBrowser.docShellIsActive = true;
+
+ // 3. Create the initial viewport inside the tool UI.
+ // The calling application will use container page loaded into the tab to
+ // do whatever it needs to create the inner browser.
+ yield tabLoaded(containerTab);
+ innerBrowser = yield getInnerBrowser(containerBrowser);
+ addXULBrowserDecorations(innerBrowser);
+ if (innerBrowser.isRemoteBrowser != tab.linkedBrowser.isRemoteBrowser) {
+ throw new Error("The inner browser's remoteness must match the " +
+ "original tab.");
+ }
+
+ // 4. Swap tab content from the regular browser tab to the browser within
+ // the viewport in the tool UI, preserving all state via
+ // `gBrowser._swapBrowserDocShells`.
+ dispatchDevToolsBrowserSwap(tab.linkedBrowser, innerBrowser);
+ gBrowser._swapBrowserDocShells(tab, innerBrowser);
+
+ // 5. Force the original browser tab to be non-remote since the tool UI
+ // must be loaded in the parent process, and we're about to swap the
+ // tool UI into this tab.
+ gBrowser.updateBrowserRemoteness(tab.linkedBrowser, false);
+
+ // 6. Swap the tool UI (with viewport showing the content) into the
+ // original browser tab and close the temporary tab used to load the
+ // tool via `swapBrowsersAndCloseOther`.
+ gBrowser.swapBrowsersAndCloseOther(tab, containerTab);
+
+ // 7. Start a tunnel from the tool tab's browser to the viewport browser
+ // so that some browser UI functions, like navigation, are connected to
+ // the content in the viewport, instead of the tool page.
+ tunnel = tunnelToInnerBrowser(tab.linkedBrowser, innerBrowser);
+ yield tunnel.start();
+
+ // Swapping browsers disconnects the find bar UI from the browser.
+ // If the find bar has been initialized, reconnect it.
+ if (gBrowser.isFindBarInitialized(tab)) {
+ let findBar = gBrowser.getFindBar(tab);
+ findBar.browser = tab.linkedBrowser;
+ if (!findBar.hidden) {
+ // Force the find bar to activate again, restoring the search string.
+ findBar.onFindCommand();
+ }
+ }
+
+ // Force the browser UI to match the new state of the tab and browser.
+ thawNavigationState(tab);
+ gBrowser.setTabTitle(tab);
+ gBrowser.updateCurrentBrowser(true);
+ }),
+
+ stop() {
+ // 1. Stop the tunnel between outer and inner browsers.
+ tunnel.stop();
+ tunnel = null;
+
+ // 2. Create a temporary, hidden tab to hold the content.
+ let contentTab = gBrowser.addTab("about:blank", {
+ skipAnimation: true,
+ });
+ gBrowser.hideTab(contentTab);
+ let contentBrowser = contentTab.linkedBrowser;
+
+ // 3. Mark the content tab browser's docshell as active so the frame
+ // is created eagerly and will be ready to swap.
+ contentBrowser.docShellIsActive = true;
+
+ // 4. Swap tab content from the browser within the viewport in the tool UI
+ // to the regular browser tab, preserving all state via
+ // `gBrowser._swapBrowserDocShells`.
+ dispatchDevToolsBrowserSwap(innerBrowser, contentBrowser);
+ gBrowser._swapBrowserDocShells(contentTab, innerBrowser);
+ innerBrowser = null;
+
+ // Copy tab listener state flags to content tab. See similar comment in `start`
+ // above for more details.
+ let stateFlags = gBrowser._tabListeners.get(tab).mStateFlags;
+ gBrowser._tabListeners.get(contentTab).mStateFlags = stateFlags;
+
+ // 5. Force the original browser tab to be remote since web content is
+ // loaded in the child process, and we're about to swap the content
+ // into this tab.
+ gBrowser.updateBrowserRemoteness(tab.linkedBrowser, true);
+
+ // 6. Swap the content into the original browser tab and close the
+ // temporary tab used to hold the content via
+ // `swapBrowsersAndCloseOther`.
+ dispatchDevToolsBrowserSwap(contentBrowser, tab.linkedBrowser);
+ gBrowser.swapBrowsersAndCloseOther(tab, contentTab);
+
+ // Swapping browsers disconnects the find bar UI from the browser.
+ // If the find bar has been initialized, reconnect it.
+ if (gBrowser.isFindBarInitialized(tab)) {
+ let findBar = gBrowser.getFindBar(tab);
+ findBar.browser = tab.linkedBrowser;
+ if (!findBar.hidden) {
+ // Force the find bar to activate again, restoring the search string.
+ findBar.onFindCommand();
+ }
+ }
+
+ gBrowser = null;
+
+ // The focus manager seems to get a little dizzy after all this swapping. If a
+ // content element had been focused inside the viewport before stopping, it will
+ // have lost focus. Activate the frame to restore expected focus.
+ tab.linkedBrowser.frameLoader.activateRemoteFrame();
+
+ delete tab.isResponsiveDesignMode;
+ },
+
+ };
+}
+
+/**
+ * Browser navigation properties we'll freeze temporarily to avoid "blinking" in the
+ * location bar, etc. caused by the containerURL peeking through before the swap is
+ * complete.
+ */
+const NAVIGATION_PROPERTIES = [
+ "currentURI",
+ "contentTitle",
+ "securityUI",
+];
+
+function freezeNavigationState(tab) {
+ // Browser navigation properties we'll freeze temporarily to avoid "blinking" in the
+ // location bar, etc. caused by the containerURL peeking through before the swap is
+ // complete.
+ for (let property of NAVIGATION_PROPERTIES) {
+ let value = tab.linkedBrowser[property];
+ Object.defineProperty(tab.linkedBrowser, property, {
+ get() {
+ return value;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+}
+
+function thawNavigationState(tab) {
+ // Thaw out the properties we froze at the beginning now that the swap is complete.
+ for (let property of NAVIGATION_PROPERTIES) {
+ delete tab.linkedBrowser[property];
+ }
+}
+
+/**
+ * Browser elements that are passed to `gBrowser._swapBrowserDocShells` are
+ * expected to have certain properties that currently exist only on
+ * <xul:browser> elements. In particular, <iframe mozbrowser> elements don't
+ * have them.
+ *
+ * Rather than duplicate the swapping code used by the browser to work around
+ * this, we stub out the missing properties needed for the swap to complete.
+ */
+function addXULBrowserDecorations(browser) {
+ if (browser.isRemoteBrowser == undefined) {
+ Object.defineProperty(browser, "isRemoteBrowser", {
+ get() {
+ return this.getAttribute("remote") == "true";
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+ if (browser.messageManager == undefined) {
+ Object.defineProperty(browser, "messageManager", {
+ get() {
+ return this.frameLoader.messageManager;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+ if (browser.outerWindowID == undefined) {
+ Object.defineProperty(browser, "outerWindowID", {
+ get() {
+ return browser._outerWindowID;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+
+ // It's not necessary for these to actually do anything. These properties are
+ // swapped between browsers in browser.xml's `swapDocShells`, and then their
+ // `swapBrowser` methods are called, so we define them here for that to work
+ // without errors. During the swap process above, these will move from the
+ // the new inner browser to the original tab's browser (step 4) and then to
+ // the temporary container tab's browser (step 7), which is then closed.
+ if (browser._remoteWebNavigationImpl == undefined) {
+ browser._remoteWebNavigationImpl = {
+ swapBrowser() {},
+ };
+ }
+ if (browser._remoteWebProgressManager == undefined) {
+ browser._remoteWebProgressManager = {
+ swapBrowser() {},
+ };
+ }
+}
+
+function tabLoaded(tab) {
+ let deferred = promise.defer();
+
+ function handle(event) {
+ if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+ event.target.location.href == "about:blank") {
+ return;
+ }
+ tab.linkedBrowser.removeEventListener("load", handle, true);
+ deferred.resolve(event);
+ }
+
+ tab.linkedBrowser.addEventListener("load", handle, true);
+ return deferred.promise;
+}
+
+exports.swapToInnerBrowser = swapToInnerBrowser;