forked from mirrors/gecko-dev
259 lines
8 KiB
JavaScript
259 lines
8 KiB
JavaScript
/* 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";
|
|
|
|
module.metadata = {
|
|
"stability": "experimental"
|
|
};
|
|
|
|
const { Cu } = require("chrome");
|
|
const { Class } = require("../sdk/core/heritage");
|
|
const { curry } = require("../sdk/lang/functional");
|
|
const { EventTarget } = require("../sdk/event/target");
|
|
const { Disposable, setup, dispose } = require("../sdk/core/disposable");
|
|
const { emit, off, setListeners } = require("../sdk/event/core");
|
|
const { when } = require("../sdk/event/utils");
|
|
const { getFrameElement } = require("../sdk/window/utils");
|
|
const { contract, validate } = require("../sdk/util/contract");
|
|
const { data: { url: resolve }} = require("../sdk/self");
|
|
const { identify } = require("../sdk/ui/id");
|
|
const { isLocalURL, URL } = require("../sdk/url");
|
|
const { encode } = require("../sdk/base64");
|
|
const { marshal, demarshal } = require("./ports");
|
|
const { fromTarget } = require("./debuggee");
|
|
const { removed } = require("../sdk/dom/events");
|
|
const { id: addonID } = require("../sdk/self");
|
|
const { viewFor } = require("../sdk/view/core");
|
|
const { createView } = require("./panel/view");
|
|
|
|
const OUTER_FRAME_URI = module.uri.replace(/\.js$/, ".html");
|
|
const FRAME_SCRIPT = module.uri.replace("/panel.js", "/frame-script.js");
|
|
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
|
|
|
const makeID = name =>
|
|
("dev-panel-" + addonID + "-" + name).
|
|
split("/").join("-").
|
|
split(".").join("-").
|
|
split(" ").join("-").
|
|
replace(/[^A-Za-z0-9_\-]/g, "");
|
|
|
|
|
|
// Weak mapping between `Panel` instances and their frame's
|
|
// `nsIMessageManager`.
|
|
const managers = new WeakMap();
|
|
// Return `nsIMessageManager` for the given `Panel` instance.
|
|
const managerFor = x => managers.get(x);
|
|
|
|
// Weak mappinging between iframe's and their owner
|
|
// `Panel` instances.
|
|
const panels = new WeakMap();
|
|
const panelFor = frame => panels.get(frame);
|
|
|
|
// Weak mapping between panels and debugees they're targeting.
|
|
const debuggees = new WeakMap();
|
|
const debuggeeFor = panel => debuggees.get(panel);
|
|
|
|
const frames = new WeakMap();
|
|
const frameFor = panel => frames.get(panel);
|
|
|
|
const setAttributes = (node, attributes) => {
|
|
for (var key in attributes)
|
|
node.setAttribute(key, attributes[key]);
|
|
};
|
|
|
|
const onStateChange = ({target, data}) => {
|
|
const panel = panelFor(target);
|
|
panel.readyState = data.readyState;
|
|
emit(panel, data.type, { target: panel, type: data.type });
|
|
};
|
|
|
|
// port event listener on the message manager that demarshalls
|
|
// and forwards to the actual receiver. This is a workaround
|
|
// until Bug 914974 is fixed.
|
|
const onPortMessage = ({data, target}) => {
|
|
const port = demarshal(target, data.port);
|
|
if (port)
|
|
port.postMessage(data.message);
|
|
};
|
|
|
|
// When frame is removed from the toolbox destroy panel
|
|
// associated with it to release all the resources.
|
|
const onFrameRemove = frame => {
|
|
panelFor(frame).destroy();
|
|
};
|
|
|
|
const onFrameInited = frame => {
|
|
frame.style.visibility = "visible";
|
|
}
|
|
|
|
const inited = frame => new Promise(resolve => {
|
|
const { messageManager } = frame.frameLoader;
|
|
const listener = message => {
|
|
messageManager.removeMessageListener("sdk/event/ready", listener);
|
|
resolve(frame);
|
|
};
|
|
messageManager.addMessageListener("sdk/event/ready", listener);
|
|
});
|
|
|
|
const getTarget = ({target}) => target;
|
|
|
|
const Panel = Class({
|
|
extends: Disposable,
|
|
implements: [EventTarget],
|
|
get id() {
|
|
return makeID(this.name || this.label);
|
|
},
|
|
readyState: "uninitialized",
|
|
ready: function() {
|
|
const { readyState } = this;
|
|
const isReady = readyState === "complete" ||
|
|
readyState === "interactive";
|
|
return isReady ? Promise.resolve(this) :
|
|
when(this, "ready").then(getTarget);
|
|
},
|
|
loaded: function() {
|
|
const { readyState } = this;
|
|
const isLoaded = readyState === "complete";
|
|
return isLoaded ? Promise.resolve(this) :
|
|
when(this, "load").then(getTarget);
|
|
},
|
|
unloaded: function() {
|
|
const { readyState } = this;
|
|
const isUninitialized = readyState === "uninitialized";
|
|
return isUninitialized ? Promise.resolve(this) :
|
|
when(this, "unload").then(getTarget);
|
|
},
|
|
postMessage: function(data, ports=[]) {
|
|
const manager = managerFor(this);
|
|
manager.sendAsyncMessage("sdk/event/message", {
|
|
type: "message",
|
|
bubbles: false,
|
|
cancelable: false,
|
|
data: data,
|
|
origin: this.url,
|
|
ports: ports.map(marshal(manager))
|
|
});
|
|
}
|
|
});
|
|
exports.Panel = Panel;
|
|
|
|
validate.define(Panel, contract({
|
|
label: {
|
|
is: ["string"],
|
|
msg: "The `option.label` must be a provided"
|
|
},
|
|
tooltip: {
|
|
is: ["string", "undefined"],
|
|
msg: "The `option.tooltip` must be a string"
|
|
},
|
|
icon: {
|
|
is: ["string"],
|
|
map: x => x && resolve(x),
|
|
ok: x => isLocalURL(x),
|
|
msg: "The `options.icon` must be a valid local URI."
|
|
},
|
|
url: {
|
|
map: x => resolve(x.toString()),
|
|
is: ["string"],
|
|
ok: x => isLocalURL(x),
|
|
msg: "The `options.url` must be a valid local URI."
|
|
},
|
|
invertIconForLightTheme: {
|
|
is: ["boolean", "undefined"],
|
|
msg: "The `options.invertIconForLightTheme` must be a boolean."
|
|
},
|
|
invertIconForDarkTheme: {
|
|
is: ["boolean", "undefined"],
|
|
msg: "The `options.invertIconForDarkTheme` must be a boolean."
|
|
}
|
|
}));
|
|
|
|
setup.define(Panel, (panel, {window, toolbox, url}) => {
|
|
// Hack: Given that iframe created by devtools API is no good for us,
|
|
// we obtain original iframe and replace it with the one that has
|
|
// desired configuration.
|
|
const original = getFrameElement(window);
|
|
const container = original.parentNode;
|
|
original.remove();
|
|
const frame = createView(panel, container.ownerDocument);
|
|
|
|
// Following modifications are a temporary workaround until Bug 1049188
|
|
// is fixed.
|
|
// Enforce certain iframe customizations regardless of users request.
|
|
setAttributes(frame, {
|
|
"id": original.id,
|
|
"src": url,
|
|
"flex": 1,
|
|
"forceOwnRefreshDriver": "",
|
|
"tooltip": "aHTMLTooltip"
|
|
});
|
|
frame.style.visibility = "hidden";
|
|
frame.classList.add("toolbox-panel-iframe");
|
|
// Inject iframe into designated node until add-on author decides
|
|
// to inject it elsewhere instead.
|
|
if (!frame.parentNode)
|
|
container.appendChild(frame);
|
|
|
|
// associate view with a panel
|
|
frames.set(panel, frame);
|
|
|
|
// associate panel model with a frame view.
|
|
panels.set(frame, panel);
|
|
|
|
const debuggee = fromTarget(toolbox.target);
|
|
// associate debuggee with a panel.
|
|
debuggees.set(panel, debuggee);
|
|
|
|
|
|
// Setup listeners for the frame message manager.
|
|
const { messageManager } = frame.frameLoader;
|
|
messageManager.addMessageListener("sdk/event/ready", onStateChange);
|
|
messageManager.addMessageListener("sdk/event/load", onStateChange);
|
|
messageManager.addMessageListener("sdk/event/unload", onStateChange);
|
|
messageManager.addMessageListener("sdk/port/message", onPortMessage);
|
|
messageManager.loadFrameScript(FRAME_SCRIPT, false);
|
|
|
|
managers.set(panel, messageManager);
|
|
|
|
// destroy panel if frame is removed.
|
|
removed(frame).then(onFrameRemove);
|
|
// show frame when it is initialized.
|
|
inited(frame).then(onFrameInited);
|
|
|
|
|
|
// set listeners if there are ones defined on the prototype.
|
|
setListeners(panel, Object.getPrototypeOf(panel));
|
|
|
|
|
|
panel.setup({ debuggee: debuggee });
|
|
});
|
|
|
|
createView.define(Panel, (panel, document) => {
|
|
const frame = document.createElement("iframe");
|
|
setAttributes(frame, {
|
|
"sandbox": "allow-scripts",
|
|
// We end up using chrome iframe with forced message manager
|
|
// as fixing a swapFrameLoader seemed like a giant task (see
|
|
// Bug 1075490).
|
|
"type": "chrome",
|
|
"forcemessagemanager": true,
|
|
"transparent": true,
|
|
"seamless": "seamless",
|
|
});
|
|
return frame;
|
|
});
|
|
|
|
dispose.define(Panel, function(panel) {
|
|
debuggeeFor(panel).close();
|
|
|
|
debuggees.delete(panel);
|
|
managers.delete(panel);
|
|
frames.delete(panel);
|
|
panel.readyState = "destroyed";
|
|
panel.dispose();
|
|
});
|
|
|
|
viewFor.define(Panel, frameFor);
|