/* 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/. */
/* eslint-env browser */
"use strict";
const Services = require("Services");
const flags = require("devtools/shared/flags");
const { PureComponent } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const e10s = require("../utils/e10s");
const message = require("../utils/message");
const { getToplevelWindow } = require("../utils/window");
const FRAME_SCRIPT = "resource://devtools/client/responsive.html/browser/content.js";
class Browser extends PureComponent {
/**
* This component is not allowed to depend directly on frequently changing data (width,
* height). Any changes in props would cause the <iframe> to be removed and added again,
* throwing away the current state of the page.
*/
static get propTypes() {
return {
swapAfterMount: PropTypes.bool.isRequired,
onBrowserMounted: PropTypes.func.isRequired,
onContentResize: PropTypes.func.isRequired,
};
}
constructor(props) {
super(props);
this.onContentResize = this.onContentResize.bind(this);
}
/**
* Before the browser is mounted, listen for `remote-browser-shown` so that we know when
* the browser is fully ready. Without waiting for an event such as this, we don't know
* whether all frame state for the browser is fully initialized (since some happens
* async after the element is added), and swapping browsers can fail if this state is
* not ready.
*/
componentWillMount() {
this.browserShown = new Promise(resolve => {
let handler = frameLoader => {
if (frameLoader.ownerElement != this.browser) {
return;
}
Services.obs.removeObserver(handler, "remote-browser-shown");
resolve();
};
Services.obs.addObserver(handler, "remote-browser-shown");
});
}
/**
* Once the browser element has mounted, load the frame script and enable
* various features, like floating scrollbars.
*/
async componentDidMount() {
// If we are not swapping browsers after mount, it's safe to start the frame
// script now.
if (!this.props.swapAfterMount) {
await this.startFrameScript();
}
// Notify manager.js that this browser has mounted, so that it can trigger
// a swap if needed and continue with the rest of its startup.
await this.browserShown;
this.props.onBrowserMounted();
// If we are swapping browsers after mount, wait for the swap to complete
// and start the frame script after that.
if (this.props.swapAfterMount) {
await message.wait(window, "start-frame-script");
await this.startFrameScript();
message.post(window, "start-frame-script:done");
}
// Stop the frame script when requested in the future.
message.wait(window, "stop-frame-script").then(() => {
this.stopFrameScript();
});
}
onContentResize(msg) {
let { onContentResize } = this.props;
let { width, height } = msg.data;
onContentResize({
width,
height,
});
}
async startFrameScript() {
let {
browser,
onContentResize,
} = this;
let mm = browser.frameLoader.messageManager;
// Notify tests when the content has received a resize event. This is not
// quite the same timing as when we _set_ a new size around the browser,
// since it still needs to do async work before the content is actually
// resized to match.
e10s.on(mm, "OnContentResize", onContentResize);
let ready = e10s.once(mm, "ChildScriptReady");
mm.loadFrameScript(FRAME_SCRIPT, true);
await ready;
let browserWindow = getToplevelWindow(window);
let requiresFloatingScrollbars =
!browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
await e10s.request(mm, "Start", {
requiresFloatingScrollbars,
// Tests expect events on resize to wait for various size changes
notifyOnResize: flags.testing,
});
}
async stopFrameScript() {
let {
browser,
onContentResize,
} = this;
let mm = browser.frameLoader.messageManager;
e10s.off(mm, "OnContentResize", onContentResize);
await e10s.request(mm, "Stop");
message.post(window, "stop-frame-script:done");
}
render() {
// In the case of @remote and @remoteType, the attribute must be set before the
// element is added to the DOM to have any effect, which we are able to do with this
// approach.
//
// @noisolation and @allowfullscreen are needed so that these frames have the same
// access to browser features as regular browser tabs. The `swapFrameLoaders` platform
// API we use compares such features before allowing the swap to proceed.
return dom.iframe(
{
allowFullScreen: "true",
className: "browser",
height: "100%",
mozbrowser: "true",
noisolation: "true",
remote: "true",
remotetype: "web",
src: "about:blank",
width: "100%",
ref: browser => {
this.browser = browser;
},
}
);
}
}
module.exports = Browser;