new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/Panel.jsm
@@ -0,0 +1,252 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = [
+ "Panel",
+];
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+
+this.Panel = function (panelElt, iframeURL) {
+ this.p = panelElt;
+ this.iframeURL = iframeURL;
+ this._initPanel();
+ this.urlbar.addEventListener("keydown", this);
+ this.urlbar.addEventListener("input", this);
+ this._emitQueue = [];
+};
+
+this.Panel.prototype = {
+
+ get document() {
+ return this.p.ownerDocument;
+ },
+
+ get window() {
+ return this.document.defaultView;
+ },
+
+ get urlbar() {
+ return this.window.gURLBar;
+ },
+
+ iframe: null,
+
+ get iframeDocument() {
+ return this.iframe.contentDocument;
+ },
+
+ get iframeWindow() {
+ return this.iframe.contentWindow;
+ },
+
+ destroy() {
+ this.p.destroyAddonIframe(this);
+ this.urlbar.removeEventListener("keydown", this);
+ this.urlbar.removeEventListener("input", this);
+ },
+
+ _initPanel() {
+ this.iframe = this.p.initAddonIframe(this, {
+ _invalidate: this._invalidate.bind(this),
+ });
+ if (!this.iframe) {
+ // This will be the case when somebody else already owns the iframe.
+ // First consumer wins right now.
+ return;
+ }
+ let onLoad = event => {
+ this.iframe.removeEventListener("load", onLoad, true);
+ this._initIframeContent(event.target.defaultView);
+ };
+ this.iframe.addEventListener("load", onLoad, true);
+ this.iframe.setAttribute("src", this.iframeURL);
+ },
+
+ _initIframeContent(win) {
+ // Clone the urlbar API functions into the iframe window.
+ win = XPCNativeWrapper.unwrap(win);
+ let apiInstance = Cu.cloneInto(iframeAPIPrototype, win, {
+ cloneFunctions: true,
+ });
+ apiInstance._panel = this;
+ Object.defineProperty(win, "urlbar", {
+ get() {
+ return apiInstance;
+ },
+ });
+ },
+
+ // This is called by the popup directly. It overrides the popup's own
+ // _invalidate method.
+ _invalidate() {
+ this._emit("reset");
+ this._currentIndex = 0;
+ if (this._appendResultTimeout) {
+ this.window.clearTimeout(this._appendResultTimeout);
+ }
+ this._appendCurrentResult();
+ },
+
+ // This emulates the popup's own _appendCurrentResult method, except instead
+ // of appending results to the popup, it emits "result" events to the iframe.
+ _appendCurrentResult() {
+ let controller = this.p.mInput.controller;
+ for (let i = 0; i < this.p.maxResults; i++) {
+ let idx = this._currentIndex;
+ if (idx >= this.p._matchCount) {
+ break;
+ }
+ let url = controller.getValueAt(idx);
+ let action = this.urlbar._parseActionUrl(url);
+ this._emit("result", {
+ url: url,
+ action: action,
+ image: controller.getImageAt(idx),
+ title: controller.getCommentAt(idx),
+ type: controller.getStyleAt(idx),
+ text: controller.searchString.replace(/^\s+/, "").replace(/\s+$/, ""),
+ });
+ this._currentIndex++;
+ }
+ if (this._currentIndex < this.p.matchCount) {
+ this._appendResultTimeout = this.window.setTimeout(() => {
+ this._appendCurrentResult();
+ });
+ }
+ },
+
+ get height() {
+ return this.iframe.getBoundingClientRect().height;
+ },
+
+ set height(val) {
+ this.p.removeAttribute("height");
+ this.iframe.style.height = val + "px";
+ },
+
+ handleEvent(event) {
+ let methName = "_on" + event.type[0].toUpperCase() + event.type.substr(1);
+ this[methName](event);
+ },
+
+ _onKeydown(event) {
+ let emittedEvent = this._emitUrlbarEvent(event);
+ if (emittedEvent && emittedEvent.defaultPrevented) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ },
+
+ _onInput(event) {
+ this._emitUrlbarEvent(event);
+ },
+
+ _emitUrlbarEvent(event) {
+ let properties = [
+ "altKey",
+ "code",
+ "ctrlKey",
+ "key",
+ "metaKey",
+ "shiftKey",
+ ];
+ let detail = properties.reduce((memo, prop) => {
+ memo[prop] = event[prop];
+ return memo;
+ }, {});
+ return this._emit(event.type, detail);
+ },
+
+ _emit(eventName, detailObj=null) {
+ this._emitQueue.push({
+ name: eventName,
+ detail: detailObj,
+ });
+ return this._processEmitQueue();
+ },
+
+ _processEmitQueue() {
+ if (!this._emitQueue.length) {
+ return null;
+ }
+
+ // iframe.contentWindow can be undefined right after the iframe is created,
+ // even after a number of seconds have elapsed. Don't know why. But that's
+ // entirely the reason for having a queue instead of simply dispatching
+ // events as they're created, unfortunately.
+ if (!this.iframeWindow) {
+ if (!this._processEmitQueueTimer) {
+ this._processEmitQueueTimer = setInterval(() => {
+ this._processEmitQueue();
+ }, 100);
+ }
+ return null;
+ }
+
+ if (this._processEmitQueueTimer) {
+ clearInterval(this._processEmitQueueTimer);
+ delete this._processEmitQueueTimer;
+ }
+
+ let { name, detail } = this._emitQueue.shift();
+ let win = XPCNativeWrapper.unwrap(this.iframeWindow);
+ let event = new this.iframeWindow.CustomEvent(name, {
+ detail: Cu.cloneInto(detail, win),
+ cancelable: true,
+ });
+ this.iframeWindow.dispatchEvent(event);
+
+ // More events may be queued up, so recurse. Do it after a turn of the
+ // event loop to avoid growing the stack as big as the queue, and to let the
+ // caller handle the returned event first.
+ let recurseTimer = setTimeout(() => {
+ this._processEmitQueue();
+ }, 100);
+
+ return event;
+ },
+};
+
+
+// This is the consumer API that's cloned into the iframe window. Be careful of
+// defining static values on this, or even getters and setters (that aren't real
+// functions). The cloning process means that such values are copied by value,
+// at the time of cloning, which is probably not what you want. That's why some
+// of these are functions even though it'd be nicer if they were getters and
+// setters.
+let iframeAPIPrototype = {
+
+ getPanelHeight() {
+ return this._panel.height;
+ },
+
+ setPanelHeight(val) {
+ this._panel.height = val;
+ },
+
+ getValue() {
+ return this._panel.urlbar.value;
+ },
+
+ setValue(val) {
+ this._panel.urlbar.value = val;
+ },
+
+ getMaxResults() {
+ return this._panel.p.maxResults;
+ },
+
+ setMaxResults(val) {
+ this._panel.p.maxResults = val;
+ },
+
+ enter() {
+ this._panel.urlbar.handleCommand();
+ },
+};
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -43,16 +43,22 @@ subsuite = clipboard
[browser_removeUnsafeProtocolsFromURLBarPaste.js]
subsuite = clipboard
[browser_search_favicon.js]
[browser_tabMatchesInAwesomebar.js]
support-files =
moz.png
[browser_tabMatchesInAwesomebar_perwindowpb.js]
skip-if = os == 'linux' # Bug 1104755
+[browser_urlbarAddonIframe.js]
+support-files =
+ Panel.jsm
+ urlbarAddonIframe.html
+ urlbarAddonIframe.js
+ urlbarAddonIframeContentScript.js
[browser_urlbarAboutHomeLoading.js]
[browser_urlbarAutoFillTrimURLs.js]
[browser_urlbarCopying.js]
subsuite = clipboard
support-files =
authenticate.sjs
[browser_urlbarDecode.js]
[browser_urlbarDelete.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarAddonIframe.js
@@ -0,0 +1,220 @@
+"use strict";
+
+// The purpose of this test is to test the urlbar popup's add-on iframe. It has
+// a few parts:
+//
+// (1) This file, a normal browser mochitest.
+// (2) html/js files that are loaded in the urlbar popup's add-on iframe:
+// urlbarAddonIframe.{html,js}
+// (3) A content script that mediates between the first two parts:
+// urlbarAddonIframeContentScript.js
+//
+// The main test file (this file) sends messages to the content script, which
+// forwards them as events to the iframe. These messages tell the iframe js to
+// do various things like call functions on the urlbar API and expect events.
+// In response, the iframe js dispatches ack events to the content script, which
+// forwards them as messages to the main test file.
+//
+// The content script may not be necessary right now since the iframe is not
+// remote. But this structure ensures that if the iframe is made remote in the
+// future, then the test won't have to change very much, and ideally not at all.
+//
+// Actually there's one other part:
+//
+// (4) The Panel.jsm that's bundled with add-ons that use the iframe.
+//
+// Panel.jsm defines the API that's made available to add-on scripts running in
+// the iframe. This API is orthogonal to the add-on iframe itself. You could
+// load any html/js in the iframe, technically. But the purpose of the iframe
+// is to support this Panel.jsm API, so that's what this test tests.
+
+const PANEL_JSM_BASENAME = "Panel.jsm";
+const IFRAME_BASENAME = "urlbarAddonIframe.html";
+const CONTENT_SCRIPT_BASENAME = "urlbarAddonIframeContentScript.js";
+
+// The iframe's message manager.
+let gMsgMan;
+
+add_task(function* () {
+ let rootDirURL = getRootDirectory(gTestPath);
+ let jsmURL = rootDirURL + PANEL_JSM_BASENAME;
+ let iframeURL = rootDirURL + IFRAME_BASENAME;
+ let contentScriptURL = rootDirURL + CONTENT_SCRIPT_BASENAME;
+
+ let { Panel } = Cu.import(jsmURL, {});
+ let panel = new Panel(gURLBar.popup, iframeURL);
+ registerCleanupFunction(() => {
+ panel.destroy();
+ Assert.ok(gURLBar.popup._addonIframe === null, "iframe should be gone");
+ });
+
+ let iframe = gURLBar.popup._addonIframe;
+ Assert.ok(!!iframe, "iframe should not be null");
+
+ gMsgMan =
+ iframe.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
+ gMsgMan.loadFrameScript(contentScriptURL, false);
+
+ yield promiseIframeLoad();
+
+ // urlbar.getValue
+ let value = "this value set by the test";
+ gURLBar.value = value;
+ let readValue = yield promiseUrlbarFunctionCall("getValue");
+ Assert.equal(readValue, value, "value");
+
+ // urlbar.setValue
+ value = "this value set by the iframe";
+ yield promiseUrlbarFunctionCall("setValue", value);
+ Assert.equal(gURLBar.value, value, "setValue");
+
+ // urlbar.getMaxResults
+ let maxResults = gURLBar.popup.maxResults;
+ Assert.equal(typeof(maxResults), "number", "Sanity check");
+ let readMaxResults = yield promiseUrlbarFunctionCall("getMaxResults");
+ Assert.equal(readMaxResults, maxResults, "getMaxResults");
+
+ // urlbar.setMaxResults
+ let newMaxResults = maxResults + 10;
+ yield promiseUrlbarFunctionCall("setMaxResults", newMaxResults);
+ Assert.equal(gURLBar.popup.maxResults, newMaxResults, "setMaxResults");
+ gURLBar.popup.maxResults = maxResults;
+
+ // urlbar.enter
+ value = "http://mochi.test:8888/";
+ yield promiseUrlbarFunctionCall("setValue", value);
+ Assert.equal(gURLBar.value, value, "setValue");
+ yield promiseUrlbarFunctionCall("enter");
+ let browser = gBrowser.selectedBrowser;
+ yield BrowserTestUtils.browserLoaded(browser);
+ Assert.equal(browser.currentURI.spec, value,
+ "enter should have loaded the URL");
+
+ // input, reset, and result events. There should always be at least one
+ // result, the heuristic result.
+ value = "test";
+ let promiseValues = yield Promise.all([
+ promiseEvent("input")[1],
+ promiseEvent("reset")[1],
+ promiseEvent("result")[1],
+ promiseAutocompleteResultPopup(value, window, true),
+ ]);
+
+ // Check the heuristic result.
+ let result = promiseValues[2];
+ let engineName = Services.search.currentEngine.name;
+ Assert.equal(result.url,
+ `moz-action:searchengine,{"engineName":"${engineName}","input":"test","searchQuery":"test"}`,
+ "result.url");
+ Assert.ok("action" in result, "result.action");
+ Assert.equal(result.action.type, "searchengine", "result.action.type");
+ Assert.ok("params" in result.action, "result.action.params");
+ Assert.equal(result.action.params.engineName, engineName,
+ "result.action.params.engineName");
+ Assert.equal(typeof(result.image), "string", "result.image");
+ Assert.equal(result.title, engineName, "result.title");
+ Assert.equal(result.type, "action searchengine heuristic", "result.type");
+ Assert.equal(result.text, value, "result.text");
+
+ // keydown event. promiseEvent sends an async message to the iframe, but
+ // synthesizeKey is sync, so we need to wait until the content JS receives
+ // the message and adds its event listener before synthesizing the key.
+ let keydownPromises = promiseEvent("keydown");
+ yield keydownPromises[0];
+ EventUtils.synthesizeKey("KEY_ArrowDown", {
+ type: "keydown",
+ code: "ArrowDown",
+ });
+ yield keydownPromises[1];
+
+ // urlbar.getPanelHeight
+ let height = iframe.getBoundingClientRect().height;
+ let readHeight = yield promiseUrlbarFunctionCall("getPanelHeight");
+ Assert.equal(readHeight, height, "getPanelHeight");
+
+ // urlbar.setPanelHeight
+ let newHeight = height + 100;
+ yield promiseUrlbarFunctionCall("setPanelHeight", newHeight);
+ yield new Promise(resolve => {
+ // The height change is animated, so give it time to complete. Again, wait
+ // a sec to be safe.
+ setTimeout(resolve, 1000);
+ });
+ Assert.equal(iframe.getBoundingClientRect().height, newHeight,
+ "setPanelHeight");
+});
+
+function promiseIframeLoad() {
+ let msgName = "TestIframeLoadAck";
+ return new Promise(resolve => {
+ info("Waiting for iframe load ack");
+ gMsgMan.addMessageListener(msgName, function onMsg(msg) {
+ info("Received iframe load ack");
+ gMsgMan.removeMessageListener(msgName, onMsg);
+ resolve();
+ });
+ });
+}
+
+/**
+ * Returns a single promise that's resolved when the content JS has called the
+ * function.
+ */
+function promiseUrlbarFunctionCall(...args) {
+ return promiseMessage("function", args)[0];
+}
+
+/**
+ * Returns two promises in an array. The first is resolved when the content JS
+ * has added its event listener. The second is resolved when the content JS
+ * has received the event.
+ */
+function promiseEvent(type) {
+ return promiseMessage("event", type, 2);
+}
+
+let gNextMessageID = 1;
+
+/**
+ * Returns an array of promises, one per ack. Each is resolved when the content
+ * JS acks the message. numExpectedAcks is the number of acks you expect.
+ */
+function promiseMessage(type, data, numExpectedAcks=1) {
+ let testMsgName = "TestMessage";
+ let ackMsgName = "TestMessageAck";
+ let msgID = gNextMessageID++;
+ gMsgMan.sendAsyncMessage(testMsgName, {
+ type: type,
+ messageID: msgID,
+ data: data,
+ });
+ let ackPromises = [];
+ for (let i = 0; i < numExpectedAcks; i++) {
+ let ackIndex = i;
+ ackPromises.push(new Promise(resolve => {
+ info("Waiting for message ack: " + JSON.stringify({
+ type: type,
+ msgID: msgID,
+ ackIndex: ackIndex,
+ }));
+ gMsgMan.addMessageListener(ackMsgName, function onMsg(msg) {
+ // Messages have IDs so that an ack can be correctly paired with the
+ // initial message it's replying to. It's not an error if the ack's ID
+ // isn't equal to msgID here. That will happen when multiple messages
+ // have been sent in a single turn of the event loop so that they're all
+ // waiting on acks. Same goes for ackIndex.
+ if (msg.data.messageID != msgID || msg.data.ackIndex != ackIndex) {
+ return;
+ }
+ info("Received message ack: " + JSON.stringify({
+ type: type,
+ msgID: msg.data.messageID,
+ ackIndex: ackIndex,
+ }));
+ gMsgMan.removeMessageListener(ackMsgName, onMsg);
+ resolve(msg.data.data);
+ });
+ }));
+ }
+ return ackPromises;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/urlbarAddonIframe.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <script src="http://mochi.test:8888/browser/browser/base/content/test/urlbar/urlbarAddonIframe.js"></script>
+ </head>
+ <body>
+ Hello
+ </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/urlbarAddonIframe.js
@@ -0,0 +1,52 @@
+// Listen for messages from the test.
+addEventListener("TestEvent", event => {
+ let type = event.detail.type;
+ dump("urlbarAddonIframe.js got TestEvent, type=" + type +
+ " messageID=" + event.detail.messageID + "\n");
+ switch (type) {
+ case "function":
+ callUrlbarFunction(event.detail);
+ break;
+ case "event":
+ expectEvent(event.detail);
+ break;
+ }
+});
+
+// Calls a urlbar API function.
+function callUrlbarFunction(detail) {
+ let args = detail.data;
+ let methodName = args.shift();
+ dump("urlbarAddonIframe.js calling urlbar." + methodName + "\n");
+ let rv = urlbar[methodName](...args);
+ ack(detail, rv);
+}
+
+// Waits for an event of a specified type to happen.
+function expectEvent(detail) {
+ let type = detail.data;
+ dump("urlbarAddonIframe.js expecting event of type " + type + "\n");
+ // Ack that the message was received and an event listener was added.
+ ack(detail, null, 0);
+ addEventListener(type, function onEvent(event) {
+ dump("urlbarAddonIframe.js got event of type " + type + "\n");
+ if (event.type != type) {
+ return;
+ }
+ dump("urlbarAddonIframe.js got expected event\n");
+ removeEventListener(type, onEvent);
+ // Ack that the event was received.
+ ack(detail, event.detail, 1);
+ });
+}
+
+// Sends an ack to the test.
+function ack(originalEventDetail, ackData=null, ackIndex=0) {
+ dispatchEvent(new CustomEvent("TestEventAck", {
+ detail: {
+ messageID: originalEventDetail.messageID,
+ ackIndex: ackIndex,
+ data: ackData,
+ },
+ }));
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/urlbarAddonIframeContentScript.js
@@ -0,0 +1,23 @@
+// Forward messages from the test to the iframe as events.
+addMessageListener("TestMessage", msg => {
+ content.dispatchEvent(new content.CustomEvent("TestEvent", {
+ detail: Components.utils.cloneInto(msg.data, content),
+ }));
+});
+
+// Forward events from the iframe to the test as messages.
+addEventListener("TestEventAck", event => {
+ // The waiveXrays call is copied from the contentSearch.js part of
+ // browser_ContentSearch.js test. Not sure whether it's necessary here.
+ sendAsyncMessage("TestMessageAck", Components.utils.waiveXrays(event.detail));
+}, true, true);
+
+// Send a message to the test when the iframe is loaded.
+if (content.document.readyState == "complete") {
+ sendAsyncMessage("TestIframeLoadAck");
+} else {
+ addEventListener("load", function onLoad(event) {
+ removeEventListener("load", onLoad);
+ sendAsyncMessage("TestIframeLoadAck");
+ }, true, true);
+}
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1451,28 +1451,33 @@ file, You can obtain one at http://mozil
return this.input.mController.matchCount > 0 &&
this.input.mController
.getStyleAt(0)
.split(/\s+/).indexOf("heuristic") > 0;
]]>
</getter>
</property>
- <property name="maxResults" readonly="true">
+ <property name="maxResults">
<getter>
<![CDATA[
if (!this._maxResults) {
var prefService =
Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
this._maxResults = prefService.getIntPref("browser.urlbar.maxRichResults");
}
return this._maxResults;
]]>
</getter>
+ <setter>
+ <![CDATA[
+ return this._maxResults = parseInt(val);
+ ]]>
+ </setter>
</property>
<method name="openAutocompletePopup">
<parameter name="aInput"/>
<parameter name="aElement"/>
<body>
<![CDATA[
// initially the panel is hidden
@@ -1809,16 +1814,109 @@ file, You can obtain one at http://mozil
// disabled), this won't be called, and the selectedIndex will be
// the default -1 value. Then handleEnter will know it should not
// delay the action, cause a result wont't ever arrive.
this.input.controller.setInitiallySelectedIndex(0);
this.overrideValue = null;
]]></body>
</method>
+ <field name="_addonIframe">null</field>
+ <field name="_addonIframeOwner">null</field>
+ <field name="_addonIframeOverriddenFunctionsByName">{}</field>
+
+ <!-- These methods must be overridden and properly handled by the API
+ runtime so that it doesn't break the popup. If any of these methods
+ is not overridden, then initAddonIframe should throw. -->
+ <field name="_addonIframeOverrideFunctionNames">[
+ "_invalidate",
+ ]</field>
+
+ <field name="_addonIframeHiddenAnonids">[
+ "search-suggestions-notification",
+ "richlistbox",
+ "one-off-search-buttons",
+ ]</field>
+ <field name="_addonIframeHiddenDisplaysByAnonid">{}</field>
+
+ <method name="initAddonIframe">
+ <parameter name="owner"/>
+ <parameter name="overrides"/>
+ <body><![CDATA[
+ if (this._addonIframeOwner) {
+ // Another add-on has already requested the iframe. Return null to
+ // signal to the calling add-on that it should not take over the
+ // popup. First add-on wins for now.
+ return null;
+ }
+ // Make sure all overrides are provided before doing anything.
+ for (let name of this._addonIframeOverrideFunctionNames) {
+ if (typeof(overrides[name]) != "function") {
+ throw new Error(
+ "Override for method '" + name + "' must be given"
+ );
+ }
+ }
+ // OK, insert the iframe.
+ this._addonIframeOwner = owner;
+ this._addonIframe = this._makeAddonIframe();
+ this._addonIframeOverriddenFunctionsByName = {};
+ for (let name of this._addonIframeOverrideFunctionNames) {
+ this._addonIframeOverriddenFunctionsByName[name] = this[name];
+ this[name] = overrides[name];
+ }
+ return this._addonIframe;
+ ]]></body>
+ </method>
+
+ <method name="destroyAddonIframe">
+ <parameter name="owner"/>
+ <body><![CDATA[
+ if (this._addonIframeOwner != owner) {
+ throw new Error("You're not the iframe owner");
+ }
+ this._addonIframeOwner = null;
+ this._addonIframe.remove();
+ this._addonIframe = null;
+ for (let anonid of this._addonIframeHiddenAnonids) {
+ let child = document.getAnonymousElementByAttribute(
+ this, "anonid", anonid
+ );
+ child.style.display =
+ this._addonIframeHiddenDisplaysByAnonid[anonid];
+ }
+ for (let name in this._addonIframeOverriddenFunctionsByName) {
+ this[name] = this._addonIframeOverriddenFunctionsByName[name];
+ }
+ this._addonIframeOverriddenFunctionsByName = {};
+ ]]></body>
+ </method>
+
+ <method name="_makeAddonIframe">
+ <body><![CDATA[
+ this._addonIframeHiddenDisplaysByAnonid = {};
+ for (let anonid of this._addonIframeHiddenAnonids) {
+ let child = document.getAnonymousElementByAttribute(
+ this, "anonid", anonid
+ );
+ this._addonIframeHiddenDisplaysByAnonid[anonid] =
+ child.style.display;
+ child.style.display = "none";
+ }
+ let XUL_NS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let iframe = document.createElementNS(XUL_NS, "iframe");
+ iframe.setAttribute("type", "content");
+ iframe.setAttribute("flex", "1");
+ iframe.style.transition = "height 100ms";
+ this.appendChild(iframe);
+ return iframe;
+ ]]></body>
+ </method>
+
</implementation>
<handlers>
<handler event="SelectedOneOffButtonChanged"><![CDATA[
this._selectedOneOffChanged();
]]></handler>
<handler event="mousedown"><![CDATA[