diff options
Diffstat (limited to 'addon-sdk/source/lib/dev/panel.js')
-rw-r--r-- | addon-sdk/source/lib/dev/panel.js | 259 |
1 files changed, 259 insertions, 0 deletions
diff --git a/addon-sdk/source/lib/dev/panel.js b/addon-sdk/source/lib/dev/panel.js new file mode 100644 index 000000000..1ef6a303a --- /dev/null +++ b/addon-sdk/source/lib/dev/panel.js @@ -0,0 +1,259 @@ +/* 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); |