|
@@ -1,2624 +0,0 @@
|
|
-# HG changeset patch
|
|
|
|
-# User Matt A. Tobin <email@mattatobin.com>
|
|
|
|
-# Date 1659988365 0
|
|
|
|
-No Bug - Create an isolated dianostic component for the suite.
|
|
|
|
-
|
|
|
|
-diff --git a/suite/buoy/ATTN-CC-SUITE-PATCHERS.txt b/suite/buoy/ATTN-CC-SUITE-PATCHERS.txt
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/ATTN-CC-SUITE-PATCHERS.txt
|
|
|
|
-@@ -0,0 +1,3 @@
|
|
|
|
-+Please exclude this component from any scripted or manual upgrades to the rest
|
|
|
|
-+of the suite (unless you are specifically updating this component).
|
|
|
|
-+It's handling is a special case and should be done seperately.
|
|
|
|
-\ No newline at end of file
|
|
|
|
-diff --git a/suite/buoy/ATTN-L10N-TRANSLATORS.txt b/suite/buoy/ATTN-L10N-TRANSLATORS.txt
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/ATTN-L10N-TRANSLATORS.txt
|
|
|
|
-@@ -0,0 +1,3 @@
|
|
|
|
-+This component can be ignored and does not need to be translated. Its only
|
|
|
|
-+purpose is to faciliate SeaMonkey Reconstruction as well as specific
|
|
|
|
-+testing cases.
|
|
|
|
-\ No newline at end of file
|
|
|
|
-diff --git a/suite/buoy/ZZ-buoy-prefs.js b/suite/buoy/ZZ-buoy-prefs.js
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/ZZ-buoy-prefs.js
|
|
|
|
-@@ -0,0 +1,2 @@
|
|
|
|
-+pref("toolkit.defaultChromeURI", "chrome://buoy/content/buoy.xhtml");
|
|
|
|
-+pref("prompts.contentPromptSubDialog", false);
|
|
|
|
-diff --git a/suite/buoy/content/buoy.css b/suite/buoy/content/buoy.css
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/content/buoy.css
|
|
|
|
-@@ -0,0 +1,9 @@
|
|
|
|
-+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
|
|
-+/* 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/. */
|
|
|
|
-+
|
|
|
|
-+html, body {
|
|
|
|
-+ height: 100%;
|
|
|
|
-+}
|
|
|
|
-+
|
|
|
|
-diff --git a/suite/buoy/content/buoy.js b/suite/buoy/content/buoy.js
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/content/buoy.js
|
|
|
|
-@@ -0,0 +1,85 @@
|
|
|
|
-+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
|
|
-+/* 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/. */
|
|
|
|
-+
|
|
|
|
-+// e10s
|
|
|
|
-+var { EzE10SUtils } = ChromeUtils.importESModule(
|
|
|
|
-+ "resource:///modules/EzE10SUtils.sys.mjs"
|
|
|
|
-+);
|
|
|
|
-+
|
|
|
|
-+// Devtools
|
|
|
|
-+ChromeUtils.defineESModuleGetters(this, {
|
|
|
|
-+ BrowserToolboxLauncher: "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs",
|
|
|
|
-+});
|
|
|
|
-+
|
|
|
|
-+Object.defineProperty(this, "BrowserConsoleManager", {
|
|
|
|
-+ get() {
|
|
|
|
-+ let { loader } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
|
|
|
|
-+ return loader.require("devtools/client/webconsole/browser-console-manager").BrowserConsoleManager;
|
|
|
|
-+ },
|
|
|
|
-+ configurable: true,
|
|
|
|
-+ enumerable: true,
|
|
|
|
-+});
|
|
|
|
-+
|
|
|
|
-+// Main functions
|
|
|
|
-+var gBuoy = {
|
|
|
|
-+ homepage: "about:version",
|
|
|
|
-+ toContent: function(aURL) {
|
|
|
|
-+ var browser = document.getElementById("main-browser");
|
|
|
|
-+ EzE10SUtils.loadURI(browser, aURL);
|
|
|
|
-+ },
|
|
|
|
-+ toChrome: function(inType, uri, features, args) {
|
|
|
|
-+ var topWindow = Services.wm.getMostRecentWindow(inType);
|
|
|
|
-+
|
|
|
|
-+ if (topWindow) {
|
|
|
|
-+ topWindow.focus();
|
|
|
|
-+ } else if (features) {
|
|
|
|
-+ Services.ww.openWindow(null, uri, "_blank", features, args);
|
|
|
|
-+ } else {
|
|
|
|
-+ Services.ww.openWindow(
|
|
|
|
-+ null,
|
|
|
|
-+ uri,
|
|
|
|
-+ "_blank",
|
|
|
|
-+ "chrome,all,dialog=no,extrachrome,menubar,resizable,scrollbars," +
|
|
|
|
-+ "status,location,toolbar,personalbar",
|
|
|
|
-+ args
|
|
|
|
-+ );
|
|
|
|
-+ }
|
|
|
|
-+ },
|
|
|
|
-+ navHome: function() {
|
|
|
|
-+ var browser = document.getElementById("main-browser");
|
|
|
|
-+ EzE10SUtils.loadURI(browser, this.homepage);
|
|
|
|
-+ },
|
|
|
|
-+ navigation: function(aNaviCmd) {
|
|
|
|
-+ var browser = document.getElementById("main-browser");
|
|
|
|
-+ switch (aNaviCmd) {
|
|
|
|
-+ case 'back':
|
|
|
|
-+ browser.goBack();
|
|
|
|
-+ break;
|
|
|
|
-+ case 'forward':
|
|
|
|
-+ browser.goForward();
|
|
|
|
-+ break;
|
|
|
|
-+ case 'reload':
|
|
|
|
-+ browser.reload();
|
|
|
|
-+ break;
|
|
|
|
-+ case 'stop':
|
|
|
|
-+ browser.stop();
|
|
|
|
-+ break;
|
|
|
|
-+ default:
|
|
|
|
-+ gBuoy.navHome();
|
|
|
|
-+ }
|
|
|
|
-+ },
|
|
|
|
-+ devtools: function() { BrowserToolboxLauncher.init(); },
|
|
|
|
-+ quitApp: function() { Services.startup.quit(Services.startup.eAttemptQuit); },
|
|
|
|
-+ startup: function() {
|
|
|
|
-+ var browser = document.getElementById("main-browser");
|
|
|
|
-+ EzE10SUtils.loadAboutBlank(browser);
|
|
|
|
-+ gBuoy.navHome();
|
|
|
|
-+ },
|
|
|
|
-+}
|
|
|
|
-+
|
|
|
|
-+// Devtools Compat
|
|
|
|
-+function openWebLinkIn(url, where, params) {
|
|
|
|
-+ gBuoy.toContent(url);
|
|
|
|
-+}
|
|
|
|
-diff --git a/suite/buoy/content/buoy.xhtml b/suite/buoy/content/buoy.xhtml
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/content/buoy.xhtml
|
|
|
|
-@@ -0,0 +1,155 @@
|
|
|
|
-+<?xml version="1.0"?>
|
|
|
|
-+<!-- 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/. -->
|
|
|
|
-+
|
|
|
|
-+<!-- Mozilla DocType Reference:
|
|
|
|
-+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
|
|
|
-+ xmlns:xbl="http://www.mozilla.org/xbl"
|
|
|
|
-+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
|
|
-+ xmlns:html="http://www.w3.org/1999/xhtml"
|
|
|
|
-+ xmlns:svg="http://www.w3.org/2000/svg"
|
|
|
|
-+ xmlns:em="http://www.mozilla.org/2004/em-rdf#"
|
|
|
|
-+-->
|
|
|
|
-+
|
|
|
|
-+<!DOCTYPE html>
|
|
|
|
-+
|
|
|
|
-+<html id="main-window"
|
|
|
|
-+ xmlns="http://www.w3.org/1999/xhtml"
|
|
|
|
-+ xmlns:html="http://www.w3.org/1999/xhtml"
|
|
|
|
-+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
|
|
|
-+ windowtype="buoy:main"
|
|
|
|
-+ screenX="10"
|
|
|
|
-+ screenY="10"
|
|
|
|
-+ width="640px"
|
|
|
|
-+ height="480px"
|
|
|
|
-+ scrolling="false"
|
|
|
|
-+ persist="screenX screenY width height sizemode">
|
|
|
|
-+ <head>
|
|
|
|
-+ <title>SeaMonkey Diagnostic & Testing Buoy</title>
|
|
|
|
-+ <link rel="stylesheet" href="chrome://global/skin/global.css" />
|
|
|
|
-+ <link rel="stylesheet" href="chrome://buoy/content/buoy.css" />
|
|
|
|
-+ <script defer="defer" src="chrome://global/content/customElements.js" />
|
|
|
|
-+ <script defer="defer" src="chrome://buoy/content/buoy.js" />
|
|
|
|
-+ <script>
|
|
|
|
-+ window.addEventListener("load", gBuoy.startup);
|
|
|
|
-+ </script>
|
|
|
|
-+ </head>
|
|
|
|
-+ <body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
|
|
|
-+
|
|
|
|
-+ <commandset id="mainCommandSet">
|
|
|
|
-+ <command id="cmd_Exit"
|
|
|
|
-+ oncommand="gBuoy.quitApp();"/>
|
|
|
|
-+ <command id="cmd_DevTools"
|
|
|
|
-+ oncommand="gBuoy.devtools();"/>
|
|
|
|
-+
|
|
|
|
-+ <command id="cmd_Navigator"
|
|
|
|
-+ oncommand="gBuoy.toChrome('navigator:browser',
|
|
|
|
-+ 'chrome://navigator/content/navigator.xhtml');"/>
|
|
|
|
-+ <command id="cmd_MailNews"
|
|
|
|
-+ oncommand="gBuoy.toChrome('mail:3pane',
|
|
|
|
-+ 'chrome://messenger/content/messenger.xhtml');"/>
|
|
|
|
-+ <command id="cmd_Composer"
|
|
|
|
-+ oncommand="gBuoy.toChrome('composer:html',
|
|
|
|
-+ 'chrome://editor/content/editor.xhtml');"/>
|
|
|
|
-+ <command id="cmd_Preferences"
|
|
|
|
-+ oncommand="gBuoy.toChrome('mozilla:preferences',
|
|
|
|
-+ 'chrome://communicator/content/pref/preferences.xhtml');"/>
|
|
|
|
-+
|
|
|
|
-+ <command id="cmd_AboutAbout"
|
|
|
|
-+ oncommand="gBuoy.toContent('about:about');"/>
|
|
|
|
-+ <command id="cmd_AboutConfig"
|
|
|
|
-+ oncommand="gBuoy.toContent('about:config');"/>
|
|
|
|
-+ <command id="cmd_AboutSupport"
|
|
|
|
-+ oncommand="gBuoy.toContent('about:support');"/>
|
|
|
|
-+ <command id="cmd_AboutVersion"
|
|
|
|
-+ oncommand="gBuoy.toContent('about:version');"/>
|
|
|
|
-+
|
|
|
|
-+ <command id="cmd_NavHome"
|
|
|
|
-+ oncommand="gBuoy.navHome();"/>
|
|
|
|
-+
|
|
|
|
-+ <command id="cmd_NavBack"
|
|
|
|
-+ oncommand="gBuoy.navigation('back');"/>
|
|
|
|
-+ <command id="cmd_NavForward"
|
|
|
|
-+ oncommand="gBuoy.navigation('forward');"/>
|
|
|
|
-+ <command id="cmd_NavReload"
|
|
|
|
-+ oncommand="gBuoy.navigation('reload');"/>
|
|
|
|
-+ <command id="cmd_NavStop"
|
|
|
|
-+ oncommand="gBuoy.navigation('stop');"/>
|
|
|
|
-+
|
|
|
|
-+ <command id="cmd_SeaMonkeyHomePage"
|
|
|
|
-+ oncommand="gBuoy.toContent('https://www.seamonkey-project.org/');"/>
|
|
|
|
-+ <command id="cmd_GetInvolved"
|
|
|
|
-+ oncommand="gBuoy.toContent('https://www.seamonkey-project.org/dev/get-involved');"/>
|
|
|
|
-+
|
|
|
|
-+ </commandset>
|
|
|
|
-+
|
|
|
|
-+ <vbox flex="1">
|
|
|
|
-+ <toolbox id="main-toolbox" style="border-bottom: 1px solid ThreeDShadow;">
|
|
|
|
-+ <menubar id="main-menubar">
|
|
|
|
-+ <menu id="file-menu" label="File">
|
|
|
|
-+ <menupopup id="file-popup">
|
|
|
|
-+ <menuitem label="Exit" command="cmd_Exit"/>
|
|
|
|
-+ </menupopup>
|
|
|
|
-+ </menu>
|
|
|
|
-+ <menu id="edit-menu" label="Edit">
|
|
|
|
-+ <menupopup id="edit-popup">
|
|
|
|
-+ <menuitem label="Configuration Editor" command="cmd_AboutConfig"/>
|
|
|
|
-+ <menuitem label="Preferences" command="cmd_Preferences"/>
|
|
|
|
-+ </menupopup>
|
|
|
|
-+ </menu>
|
|
|
|
-+ <menu id="go-menu" label="Go">
|
|
|
|
-+ <menupopup id="components-popup">
|
|
|
|
-+ <menuitem label="Back" command="cmd_NavBack"/>
|
|
|
|
-+ <menuitem label="Forward" command="cmd_NavForward"/>
|
|
|
|
-+ <menuitem label="Reload" command="cmd_NavReload"/>
|
|
|
|
-+ <menuitem label="Stop" command="cmd_NavStop"/>
|
|
|
|
-+ <menuitem label="Home" command="cmd_NavHome"/>
|
|
|
|
-+ </menupopup>
|
|
|
|
-+ </menu>
|
|
|
|
-+ <menu id="components-menu" label="Components">
|
|
|
|
-+ <menupopup id="components-popup">
|
|
|
|
-+ <menuitem label="Browser" command="cmd_Navigator"/>
|
|
|
|
-+ <menuitem label="Messenger" command="cmd_MailNews"/>
|
|
|
|
-+ <menuitem label="Composer" command="cmd_Composer"/>
|
|
|
|
-+ </menupopup>
|
|
|
|
-+ </menu>
|
|
|
|
-+ <menu id="tools-menu" label="Tools">
|
|
|
|
-+ <menupopup id="tools-popup">
|
|
|
|
-+ <menuitem label="About: Pages" command="cmd_AboutAbout"/>
|
|
|
|
-+ <menuitem label="Developer Tools" command="cmd_DevTools"/>
|
|
|
|
-+ </menupopup>
|
|
|
|
-+ </menu>
|
|
|
|
-+ <menu id="help-menu" label="Help">
|
|
|
|
-+ <menupopup id="help-popup">
|
|
|
|
-+ <menuitem label="Get Involved" command="cmd_GetInvolved"/>
|
|
|
|
-+ <menuitem label="Troubleshooting Information" command="cmd_AboutSupport"/>
|
|
|
|
-+ <menuitem label="About SeaMonkey" command="cmd_AboutVersion"/>
|
|
|
|
-+ </menupopup>
|
|
|
|
-+ </menu>
|
|
|
|
-+ </menubar>
|
|
|
|
-+ <toolbar id="navigation-toolbar">
|
|
|
|
-+ <toolbarbutton id="back-button" label="< Back" command="cmd_NavBack"/>
|
|
|
|
-+ <toolbarbutton id="forward-button" label="> Forward" command="cmd_NavForward"/>
|
|
|
|
-+ <toolbarbutton id="reload-button" label="O Reload" command="cmd_NavReload"/>
|
|
|
|
-+ <toolbarbutton id="stop-button" label="X Stop" command="cmd_NavStop"/>
|
|
|
|
-+ <html:input id="urlbar" style="flex: 1; margin: 2px; padding: 4px;" placeholder="Enter a URL..." />
|
|
|
|
-+ <toolbarbutton id="go-button" label="Go ->" oncommand="var urlbar = document.getElementById('urlbar');
|
|
|
|
-+ gBuoy.toContent(urlbar.value);
|
|
|
|
-+ urlbar.value = '';"/>
|
|
|
|
-+ </toolbar>
|
|
|
|
-+ <toolbar id="quick-links-toolbar">
|
|
|
|
-+ <button label="SeaMonkey Homepage" command="cmd_SeaMonkeyHomePage"/>
|
|
|
|
-+ <button label="Get Involved" command="cmd_GetInvolved"/>
|
|
|
|
-+ </toolbar>
|
|
|
|
-+ </toolbox>
|
|
|
|
-+ <browser id="main-browser"
|
|
|
|
-+ flex="1"
|
|
|
|
-+ type="content"
|
|
|
|
-+ primary="true"
|
|
|
|
-+ maychangeremoteness="true"
|
|
|
|
-+ nodefaultsrc="true" />
|
|
|
|
-+ </vbox>
|
|
|
|
-+ </body>
|
|
|
|
-+</html>
|
|
|
|
-+
|
|
|
|
-diff --git a/suite/buoy/jar.mn b/suite/buoy/jar.mn
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/jar.mn
|
|
|
|
-@@ -0,0 +1,16 @@
|
|
|
|
-+# 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/.
|
|
|
|
-+
|
|
|
|
-+#filter substitution
|
|
|
|
-+
|
|
|
|
-+buoy.jar:
|
|
|
|
-+% content buoy %content/buoy/ contentaccessible=yes
|
|
|
|
-+ content/buoy/buoy.css (content/buoy.css)
|
|
|
|
-+ content/buoy/buoy.js (content/buoy.js)
|
|
|
|
-+ content/buoy/buoy.xhtml (content/buoy.xhtml)
|
|
|
|
-+
|
|
|
|
-+[localization] @AB_CD@.jar:
|
|
|
|
-+ browser (moz-l10n/browser/**/*.ftl)
|
|
|
|
-+ buoy (locale/**/*.ftl)
|
|
|
|
-+
|
|
|
|
-diff --git a/suite/buoy/modules/BrowserWindowTracker.sys.mjs b/suite/buoy/modules/BrowserWindowTracker.sys.mjs
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/modules/BrowserWindowTracker.sys.mjs
|
|
|
|
-@@ -0,0 +1,6 @@
|
|
|
|
-+/* 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 module is deliberately not implemented. It only exists to keep
|
|
|
|
-+// the automated tests happy. See bug 1782621.
|
|
|
|
-diff --git a/suite/buoy/modules/CustomizableUI.sys.mjs b/suite/buoy/modules/CustomizableUI.sys.mjs
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/modules/CustomizableUI.sys.mjs
|
|
|
|
-@@ -0,0 +1,360 @@
|
|
|
|
-+/* 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 file is a copy of a file with the same name in Firefox. Only the
|
|
|
|
-+// pieces we're using, and a few pieces the devtools rely on such as the
|
|
|
|
-+// constants, remain.
|
|
|
|
-+
|
|
|
|
-+const lazy = {};
|
|
|
|
-+
|
|
|
|
-+ChromeUtils.defineESModuleGetters(lazy, {
|
|
|
|
-+ PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
|
|
|
|
-+});
|
|
|
|
-+
|
|
|
|
-+/**
|
|
|
|
-+ * gPanelsForWindow is a list of known panels in a window which we may need to close
|
|
|
|
-+ * should command events fire which target them.
|
|
|
|
-+ */
|
|
|
|
-+var gPanelsForWindow = new WeakMap();
|
|
|
|
-+
|
|
|
|
-+var CustomizableUIInternal = {
|
|
|
|
-+ addPanelCloseListeners(aPanel) {
|
|
|
|
-+ Services.els.addSystemEventListener(aPanel, "click", this, false);
|
|
|
|
-+ Services.els.addSystemEventListener(aPanel, "keypress", this, false);
|
|
|
|
-+ const win = aPanel.ownerGlobal;
|
|
|
|
-+ if (!gPanelsForWindow.has(win)) {
|
|
|
|
-+ gPanelsForWindow.set(win, new Set());
|
|
|
|
-+ }
|
|
|
|
-+ gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
|
|
|
|
-+ },
|
|
|
|
-+
|
|
|
|
-+ removePanelCloseListeners(aPanel) {
|
|
|
|
-+ Services.els.removeSystemEventListener(aPanel, "click", this, false);
|
|
|
|
-+ Services.els.removeSystemEventListener(aPanel, "keypress", this, false);
|
|
|
|
-+ const win = aPanel.ownerGlobal;
|
|
|
|
-+ const panels = gPanelsForWindow.get(win);
|
|
|
|
-+ if (panels) {
|
|
|
|
-+ panels.delete(this._getPanelForNode(aPanel));
|
|
|
|
-+ }
|
|
|
|
-+ },
|
|
|
|
-+
|
|
|
|
-+ handleEvent(aEvent) {
|
|
|
|
-+ switch (aEvent.type) {
|
|
|
|
-+ case "click":
|
|
|
|
-+ case "keypress":
|
|
|
|
-+ this.maybeAutoHidePanel(aEvent);
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ },
|
|
|
|
-+
|
|
|
|
-+ _getPanelForNode(aNode) {
|
|
|
|
-+ return aNode.closest("panel");
|
|
|
|
-+ },
|
|
|
|
-+
|
|
|
|
-+ /*
|
|
|
|
-+ * If people put things in the panel which need more than single-click interaction,
|
|
|
|
-+ * we don't want to close it. Right now we check for text inputs and menu buttons.
|
|
|
|
-+ * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank
|
|
|
|
-+ * part of the menu.
|
|
|
|
-+ */
|
|
|
|
-+ _isOnInteractiveElement(aEvent) {
|
|
|
|
-+ function getMenuPopupForDescendant(aNode) {
|
|
|
|
-+ let lastPopup = null;
|
|
|
|
-+ while (
|
|
|
|
-+ aNode &&
|
|
|
|
-+ aNode.parentNode &&
|
|
|
|
-+ aNode.parentNode.localName.startsWith("menu")
|
|
|
|
-+ ) {
|
|
|
|
-+ lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup;
|
|
|
|
-+ aNode = aNode.parentNode;
|
|
|
|
-+ }
|
|
|
|
-+ return lastPopup;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ let target = aEvent.target;
|
|
|
|
-+ const panel = this._getPanelForNode(aEvent.currentTarget);
|
|
|
|
-+ // This can happen in e.g. customize mode. If there's no panel,
|
|
|
|
-+ // there's clearly nothing for us to close; pretend we're interactive.
|
|
|
|
-+ if (!panel) {
|
|
|
|
-+ return true;
|
|
|
|
-+ }
|
|
|
|
-+ // We keep track of:
|
|
|
|
-+ // whether we're in an input container (text field)
|
|
|
|
-+ let inInput = false;
|
|
|
|
-+ // whether we're in a popup/context menu
|
|
|
|
-+ let inMenu = false;
|
|
|
|
-+ // whether we're in a toolbarbutton/toolbaritem
|
|
|
|
-+ let inItem = false;
|
|
|
|
-+ // whether the current menuitem has a valid closemenu attribute
|
|
|
|
-+ let menuitemCloseMenu = "auto";
|
|
|
|
-+
|
|
|
|
-+ // While keeping track of that, we go from the original target back up,
|
|
|
|
-+ // to the panel if we have to. We bail as soon as we find an input,
|
|
|
|
-+ // a toolbarbutton/item, or the panel:
|
|
|
|
-+ while (target) {
|
|
|
|
-+ // Skip out of iframes etc:
|
|
|
|
-+ if (target.nodeType == target.DOCUMENT_NODE) {
|
|
|
|
-+ if (!target.defaultView) {
|
|
|
|
-+ // Err, we're done.
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ // Find containing browser or iframe element in the parent doc.
|
|
|
|
-+ target = target.defaultView.docShell.chromeEventHandler;
|
|
|
|
-+ if (!target) {
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+ const tagName = target.localName;
|
|
|
|
-+ inInput = tagName == "input";
|
|
|
|
-+ inItem = tagName == "toolbaritem" || tagName == "toolbarbutton";
|
|
|
|
-+ const isMenuItem = tagName == "menuitem";
|
|
|
|
-+ inMenu = inMenu || isMenuItem;
|
|
|
|
-+
|
|
|
|
-+ if (isMenuItem && target.hasAttribute("closemenu")) {
|
|
|
|
-+ const closemenuVal = target.getAttribute("closemenu");
|
|
|
|
-+ menuitemCloseMenu =
|
|
|
|
-+ closemenuVal == "single" || closemenuVal == "none"
|
|
|
|
-+ ? closemenuVal
|
|
|
|
-+ : "auto";
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // Keep the menu open and break out of the loop if the click happened on
|
|
|
|
-+ // the ShadowRoot or a disabled menu item.
|
|
|
|
-+ if (
|
|
|
|
-+ target.nodeType == target.DOCUMENT_FRAGMENT_NODE ||
|
|
|
|
-+ target.getAttribute("disabled") == "true"
|
|
|
|
-+ ) {
|
|
|
|
-+ return true;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // This isn't in the loop condition because we want to break before
|
|
|
|
-+ // changing |target| if any of these conditions are true
|
|
|
|
-+ if (inInput || inItem || target == panel) {
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ // We need specific code for popups: the item on which they were invoked
|
|
|
|
-+ // isn't necessarily in their parentNode chain:
|
|
|
|
-+ if (isMenuItem) {
|
|
|
|
-+ const topmostMenuPopup = getMenuPopupForDescendant(target);
|
|
|
|
-+ target =
|
|
|
|
-+ (topmostMenuPopup && topmostMenuPopup.triggerNode) ||
|
|
|
|
-+ target.parentNode;
|
|
|
|
-+ } else {
|
|
|
|
-+ target = target.parentNode;
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // If the user clicked a menu item...
|
|
|
|
-+ if (inMenu) {
|
|
|
|
-+ // We care if we're in an input also,
|
|
|
|
-+ // or if the user specified closemenu!="auto":
|
|
|
|
-+ if (inInput || menuitemCloseMenu != "auto") {
|
|
|
|
-+ return true;
|
|
|
|
-+ }
|
|
|
|
-+ // Otherwise, we're probably fine to close the panel
|
|
|
|
-+ return false;
|
|
|
|
-+ }
|
|
|
|
-+ // If we're not in a menu, and we *are* in a type="menu" toolbarbutton,
|
|
|
|
-+ // we'll now interact with the menu
|
|
|
|
-+ if (inItem && target.getAttribute("type") == "menu") {
|
|
|
|
-+ return true;
|
|
|
|
-+ }
|
|
|
|
-+ return inInput || !inItem;
|
|
|
|
-+ },
|
|
|
|
-+
|
|
|
|
-+ hidePanelForNode(aNode) {
|
|
|
|
-+ const panel = this._getPanelForNode(aNode);
|
|
|
|
-+ if (panel) {
|
|
|
|
-+ lazy.PanelMultiView.hidePopup(panel);
|
|
|
|
-+ }
|
|
|
|
-+ },
|
|
|
|
-+
|
|
|
|
-+ maybeAutoHidePanel(aEvent) {
|
|
|
|
-+ const eventType = aEvent.type;
|
|
|
|
-+ if (eventType == "keypress" && aEvent.keyCode != aEvent.DOM_VK_RETURN) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ if (eventType == "click" && aEvent.button != 0) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // We don't check preventDefault - it makes sense that this was prevented,
|
|
|
|
-+ // but we probably still want to close the panel. If consumers don't want
|
|
|
|
-+ // this to happen, they should specify the closemenu attribute.
|
|
|
|
-+ if (eventType != "command" && this._isOnInteractiveElement(aEvent)) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // We can't use event.target because we might have passed an anonymous
|
|
|
|
-+ // content boundary as well, and so target points to the outer element in
|
|
|
|
-+ // that case. Unfortunately, this means we get anonymous child nodes instead
|
|
|
|
-+ // of the real ones, so looking for the 'stoooop, don't close me' attributes
|
|
|
|
-+ // is more involved.
|
|
|
|
-+ let target = aEvent.originalTarget;
|
|
|
|
-+ while (target.parentNode && target.localName != "panel") {
|
|
|
|
-+ if (
|
|
|
|
-+ target.getAttribute("closemenu") == "none" ||
|
|
|
|
-+ target.getAttribute("widget-type") == "view" ||
|
|
|
|
-+ target.getAttribute("widget-type") == "button-and-view"
|
|
|
|
-+ ) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+ target = target.parentNode;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // If we get here, we can actually hide the popup:
|
|
|
|
-+ this.hidePanelForNode(aEvent.target);
|
|
|
|
-+ },
|
|
|
|
-+};
|
|
|
|
-+Object.freeze(CustomizableUIInternal);
|
|
|
|
-+
|
|
|
|
-+export var CustomizableUI = {
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant reference to the ID of the navigation toolbar.
|
|
|
|
-+ */
|
|
|
|
-+ AREA_NAVBAR: "nav-bar",
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant reference to the ID of the menubar's toolbar.
|
|
|
|
-+ */
|
|
|
|
-+ AREA_MENUBAR: "toolbar-menubar",
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant reference to the ID of the tabstrip toolbar.
|
|
|
|
-+ */
|
|
|
|
-+ AREA_TABSTRIP: "TabsToolbar",
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant reference to the ID of the bookmarks toolbar.
|
|
|
|
-+ */
|
|
|
|
-+ AREA_BOOKMARKS: "PersonalToolbar",
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant reference to the ID of the non-dymanic (fixed) list in the overflow panel.
|
|
|
|
-+ */
|
|
|
|
-+ AREA_FIXED_OVERFLOW_PANEL: "widget-overflow-fixed-list",
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant indicating the area is a menu panel.
|
|
|
|
-+ */
|
|
|
|
-+ TYPE_MENU_PANEL: "menu-panel",
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant indicating the area is a toolbar.
|
|
|
|
-+ */
|
|
|
|
-+ TYPE_TOOLBAR: "toolbar",
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant indicating a XUL-type provider.
|
|
|
|
-+ */
|
|
|
|
-+ PROVIDER_XUL: "xul",
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant indicating an API-type provider.
|
|
|
|
-+ */
|
|
|
|
-+ PROVIDER_API: "api",
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant indicating dynamic (special) widgets: spring, spacer, and separator.
|
|
|
|
-+ */
|
|
|
|
-+ PROVIDER_SPECIAL: "special",
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant indicating the widget is built-in
|
|
|
|
-+ */
|
|
|
|
-+ SOURCE_BUILTIN: "builtin",
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant indicating the widget is externally provided
|
|
|
|
-+ * (e.g. by add-ons or other items not part of the builtin widget set).
|
|
|
|
-+ */
|
|
|
|
-+ SOURCE_EXTERNAL: "external",
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant indicating the reason the event was fired was a window closing
|
|
|
|
-+ */
|
|
|
|
-+ REASON_WINDOW_CLOSED: "window-closed",
|
|
|
|
-+ /**
|
|
|
|
-+ * Constant indicating the reason the event was fired was an area being
|
|
|
|
-+ * unregistered separately from window closing mechanics.
|
|
|
|
-+ */
|
|
|
|
-+ REASON_AREA_UNREGISTERED: "area-unregistered",
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Add a widget to an area.
|
|
|
|
-+ * If the area to which you try to add is not known to CustomizableUI,
|
|
|
|
-+ * this will throw.
|
|
|
|
-+ * If the area to which you try to add is the same as the area in which
|
|
|
|
-+ * the widget is currently placed, this will do the same as
|
|
|
|
-+ * moveWidgetWithinArea.
|
|
|
|
-+ * If the widget cannot be removed from its original location, this will
|
|
|
|
-+ * no-op.
|
|
|
|
-+ *
|
|
|
|
-+ * This will fire an onWidgetAdded notification,
|
|
|
|
-+ * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
|
|
|
|
-+ * for each window CustomizableUI knows about.
|
|
|
|
-+ *
|
|
|
|
-+ * @param aWidgetId the ID of the widget to add
|
|
|
|
-+ * @param aArea the ID of the area to add the widget to
|
|
|
|
-+ * @param aPosition the position at which to add the widget. If you do not
|
|
|
|
-+ * pass a position, the widget will be added to the end
|
|
|
|
-+ * of the area.
|
|
|
|
-+ */
|
|
|
|
-+ addWidgetToArea(aWidgetId, aArea, aPosition) {},
|
|
|
|
-+ /**
|
|
|
|
-+ * Remove a widget from its area. If the widget cannot be removed from its
|
|
|
|
-+ * area, or is not in any area, this will no-op. Otherwise, this will fire an
|
|
|
|
-+ * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and
|
|
|
|
-+ * onWidgetAfterDOMChange notification for each window CustomizableUI knows
|
|
|
|
-+ * about.
|
|
|
|
-+ *
|
|
|
|
-+ * @param aWidgetId the ID of the widget to remove
|
|
|
|
-+ */
|
|
|
|
-+ removeWidgetFromArea(aWidgetId) {},
|
|
|
|
-+ /**
|
|
|
|
-+ * Get the placement of a widget. This is by far the best way to obtain
|
|
|
|
-+ * information about what the state of your widget is. The internals of
|
|
|
|
-+ * this call are cheap (no DOM necessary) and you will know where the user
|
|
|
|
-+ * has put your widget.
|
|
|
|
-+ *
|
|
|
|
-+ * @param aWidgetId the ID of the widget whose placement you want to know
|
|
|
|
-+ * @returns
|
|
|
|
-+ * {
|
|
|
|
-+ * area: "somearea", // The ID of the area where the widget is placed
|
|
|
|
-+ * position: 42 // the index in the placements array corresponding to
|
|
|
|
-+ * // your widget.
|
|
|
|
-+ * }
|
|
|
|
-+ *
|
|
|
|
-+ * OR
|
|
|
|
-+ *
|
|
|
|
-+ * null // if the widget is not placed anywhere (ie in the palette)
|
|
|
|
-+ */
|
|
|
|
-+ getPlacementOfWidget(aWidgetId, aOnlyRegistered = true, aDeadAreas = false) {
|
|
|
|
-+ return null;
|
|
|
|
-+ },
|
|
|
|
-+ /**
|
|
|
|
-+ * Add listeners to a panel that will close it. For use from the menu panel
|
|
|
|
-+ * and overflowable toolbar implementations, unlikely to be useful for
|
|
|
|
-+ * consumers.
|
|
|
|
-+ *
|
|
|
|
-+ * @param aPanel the panel to which listeners should be attached.
|
|
|
|
-+ */
|
|
|
|
-+ addPanelCloseListeners(aPanel) {
|
|
|
|
-+ CustomizableUIInternal.addPanelCloseListeners(aPanel);
|
|
|
|
-+ },
|
|
|
|
-+ /**
|
|
|
|
-+ * Remove close listeners that have been added to a panel with
|
|
|
|
-+ * addPanelCloseListeners. For use from the menu panel and overflowable
|
|
|
|
-+ * toolbar implementations, unlikely to be useful for consumers.
|
|
|
|
-+ *
|
|
|
|
-+ * @param aPanel the panel from which listeners should be removed.
|
|
|
|
-+ */
|
|
|
|
-+ removePanelCloseListeners(aPanel) {
|
|
|
|
-+ CustomizableUIInternal.removePanelCloseListeners(aPanel);
|
|
|
|
-+ },
|
|
|
|
-+ /**
|
|
|
|
-+ * Notify toolbox(es) of a particular event. If you don't pass aWindow,
|
|
|
|
-+ * all toolboxes will be notified. For use from Customize Mode only,
|
|
|
|
-+ * do not use otherwise.
|
|
|
|
-+ *
|
|
|
|
-+ * @param aEvent the name of the event to send.
|
|
|
|
-+ * @param aDetails optional, the details of the event.
|
|
|
|
-+ * @param aWindow optional, the window in which to send the event.
|
|
|
|
-+ */
|
|
|
|
-+ dispatchToolboxEvent(aEvent, aDetails = {}, aWindow = null) {},
|
|
|
|
-+};
|
|
|
|
-+Object.freeze(CustomizableUI);
|
|
|
|
-diff --git a/suite/buoy/modules/EzE10SUtils.sys.mjs b/suite/buoy/modules/EzE10SUtils.sys.mjs
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/modules/EzE10SUtils.sys.mjs
|
|
|
|
-@@ -0,0 +1,93 @@
|
|
|
|
-+/* 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/. */
|
|
|
|
-+
|
|
|
|
-+import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs";
|
|
|
|
-+
|
|
|
|
-+import { ExtensionParent } from "resource://gre/modules/ExtensionParent.sys.mjs";
|
|
|
|
-+
|
|
|
|
-+export var EzE10SUtils = {
|
|
|
|
-+ /**
|
|
|
|
-+ * Loads about:blank in `browser` without switching remoteness. about:blank
|
|
|
|
-+ * can load in a local browser or a remote browser, and `loadURI` will make
|
|
|
|
-+ * it load in a remote browser even if you don't want it to.
|
|
|
|
-+ *
|
|
|
|
-+ * @param {nsIBrowser} browser
|
|
|
|
-+ */
|
|
|
|
-+ loadAboutBlank(browser) {
|
|
|
|
-+ if (!browser.currentURI || browser.currentURI.spec == "about:blank") {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+ browser.loadURI(Services.io.newURI("about:blank"), {
|
|
|
|
-+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
|
|
-+ remoteTypeOverride: browser.remoteType,
|
|
|
|
-+ });
|
|
|
|
-+ },
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Loads `uri` in `browser`, changing to a remote/local browser if necessary.
|
|
|
|
-+ *
|
|
|
|
-+ * @see `nsIWebNavigation.loadURI`
|
|
|
|
-+ *
|
|
|
|
-+ * @param {nsIBrowser} browser
|
|
|
|
-+ * @param {string} uri
|
|
|
|
-+ * @param {object} params
|
|
|
|
-+ */
|
|
|
|
-+ loadURI(browser, uri, params = {}) {
|
|
|
|
-+ const multiProcess = browser.ownerGlobal.docShell.QueryInterface(
|
|
|
|
-+ Ci.nsILoadContext
|
|
|
|
-+ ).useRemoteTabs;
|
|
|
|
-+ const remoteSubframes = browser.ownerGlobal.docShell.QueryInterface(
|
|
|
|
-+ Ci.nsILoadContext
|
|
|
|
-+ ).useRemoteSubframes;
|
|
|
|
-+
|
|
|
|
-+ const isRemote = browser.getAttribute("remote") == "true";
|
|
|
|
-+ const remoteType = E10SUtils.getRemoteTypeForURI(
|
|
|
|
-+ uri,
|
|
|
|
-+ multiProcess,
|
|
|
|
-+ remoteSubframes
|
|
|
|
-+ );
|
|
|
|
-+ const shouldBeRemote = remoteType !== E10SUtils.NOT_REMOTE;
|
|
|
|
-+
|
|
|
|
-+ if (shouldBeRemote != isRemote) {
|
|
|
|
-+ this.changeRemoteness(browser, remoteType);
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ params.triggeringPrincipal =
|
|
|
|
-+ params.triggeringPrincipal ||
|
|
|
|
-+ Services.scriptSecurityManager.getSystemPrincipal();
|
|
|
|
-+ browser.fixupAndLoadURIString(uri, params);
|
|
|
|
-+ },
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Force `browser` to be a remote/local browser.
|
|
|
|
-+ *
|
|
|
|
-+ * @see E10SUtils.sys.mjs for remote types.
|
|
|
|
-+ *
|
|
|
|
-+ * @param {nsIBrowser} browser - the browser to enforce the remoteness of.
|
|
|
|
-+ * @param {string} remoteType - the remoteness to enforce.
|
|
|
|
-+ * @returns {boolean} true if any change happened on the browser (which would
|
|
|
|
-+ * not be the case if its remoteness is already in the correct state).
|
|
|
|
-+ */
|
|
|
|
-+ changeRemoteness(browser, remoteType) {
|
|
|
|
-+ if (browser.remoteType == remoteType) {
|
|
|
|
-+ return false;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ browser.destroy();
|
|
|
|
-+
|
|
|
|
-+ if (remoteType) {
|
|
|
|
-+ browser.setAttribute("remote", "true");
|
|
|
|
-+ browser.setAttribute("remoteType", remoteType);
|
|
|
|
-+ } else {
|
|
|
|
-+ browser.setAttribute("remote", "false");
|
|
|
|
-+ browser.removeAttribute("remoteType");
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ browser.changeRemoteness({ remoteType });
|
|
|
|
-+ browser.construct();
|
|
|
|
-+ ExtensionParent.apiManager.emit("extension-browser-inserted", browser);
|
|
|
|
-+
|
|
|
|
-+ return true;
|
|
|
|
-+ },
|
|
|
|
-+};
|
|
|
|
-diff --git a/suite/buoy/modules/PanelMultiView.sys.mjs b/suite/buoy/modules/PanelMultiView.sys.mjs
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/modules/PanelMultiView.sys.mjs
|
|
|
|
-@@ -0,0 +1,1700 @@
|
|
|
|
-+/* 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/. */
|
|
|
|
-+
|
|
|
|
-+/**
|
|
|
|
-+ * Allows a popup panel to host multiple subviews. The main view shown when the
|
|
|
|
-+ * panel is opened may slide out to display a subview, which in turn may lead to
|
|
|
|
-+ * other subviews in a cascade menu pattern.
|
|
|
|
-+ *
|
|
|
|
-+ * The <panel> element should contain a <panelmultiview> element. Views are
|
|
|
|
-+ * declared using <panelview> elements that are usually children of the main
|
|
|
|
-+ * <panelmultiview> element, although they don't need to be, as views can also
|
|
|
|
-+ * be imported into the panel from other panels or popup sets.
|
|
|
|
-+ *
|
|
|
|
-+ * The panel should be opened asynchronously using the openPopup static method
|
|
|
|
-+ * on the PanelMultiView object. This will display the view specified using the
|
|
|
|
-+ * mainViewId attribute on the contained <panelmultiview> element.
|
|
|
|
-+ *
|
|
|
|
-+ * Specific subviews can slide in using the showSubView method, and backwards
|
|
|
|
-+ * navigation can be done using the goBack method or through a button in the
|
|
|
|
-+ * subview headers.
|
|
|
|
-+ *
|
|
|
|
-+ * The process of displaying the main view or a new subview requires multiple
|
|
|
|
-+ * steps to be completed, hence at any given time the <panelview> element may
|
|
|
|
-+ * be in different states:
|
|
|
|
-+ *
|
|
|
|
-+ * -- Open or closed
|
|
|
|
-+ *
|
|
|
|
-+ * All the <panelview> elements start "closed", meaning that they are not
|
|
|
|
-+ * associated to a <panelmultiview> element and can be located anywhere in
|
|
|
|
-+ * the document. When the openPopup or showSubView methods are called, the
|
|
|
|
-+ * relevant view becomes "open" and the <panelview> element may be moved to
|
|
|
|
-+ * ensure it is a descendant of the <panelmultiview> element.
|
|
|
|
-+ *
|
|
|
|
-+ * The "ViewShowing" event is fired at this point, when the view is not
|
|
|
|
-+ * visible yet. The event is allowed to cancel the operation, in which case
|
|
|
|
-+ * the view is closed immediately.
|
|
|
|
-+ *
|
|
|
|
-+ * Closing the view does not move the node back to its original position.
|
|
|
|
-+ *
|
|
|
|
-+ * -- Visible or invisible
|
|
|
|
-+ *
|
|
|
|
-+ * This indicates whether the view is visible in the document from a layout
|
|
|
|
-+ * perspective, regardless of whether it is currently scrolled into view. In
|
|
|
|
-+ * fact, all subviews are already visible before they start sliding in.
|
|
|
|
-+ *
|
|
|
|
-+ * Before scrolling into view, a view may become visible but be placed in a
|
|
|
|
-+ * special off-screen area of the document where layout and measurements can
|
|
|
|
-+ * take place asynchronously.
|
|
|
|
-+ *
|
|
|
|
-+ * When navigating forward, an open view may become invisible but stay open
|
|
|
|
-+ * after sliding out of view. The last known size of these views is still
|
|
|
|
-+ * taken into account for determining the overall panel size.
|
|
|
|
-+ *
|
|
|
|
-+ * When navigating backwards, an open subview will first become invisible and
|
|
|
|
-+ * then will be closed.
|
|
|
|
-+ *
|
|
|
|
-+ * -- Active or inactive
|
|
|
|
-+ *
|
|
|
|
-+ * This indicates whether the view is fully scrolled into the visible area
|
|
|
|
-+ * and ready to receive mouse and keyboard events. An active view is always
|
|
|
|
-+ * visible, but a visible view may be inactive. For example, during a scroll
|
|
|
|
-+ * transition, both views will be inactive.
|
|
|
|
-+ *
|
|
|
|
-+ * When a view becomes active, the ViewShown event is fired synchronously,
|
|
|
|
-+ * and the showSubView and goBack methods can be called for navigation.
|
|
|
|
-+ *
|
|
|
|
-+ * For the main view of the panel, the ViewShown event is dispatched during
|
|
|
|
-+ * the "popupshown" event, which means that other "popupshown" handlers may
|
|
|
|
-+ * be called before the view is active. Thus, code that needs to perform
|
|
|
|
-+ * further navigation automatically should either use the ViewShown event or
|
|
|
|
-+ * wait for an event loop tick, like BrowserTestUtils.waitForEvent does.
|
|
|
|
-+ *
|
|
|
|
-+ * -- Navigating with the keyboard
|
|
|
|
-+ *
|
|
|
|
-+ * An open view may keep state related to keyboard navigation, even if it is
|
|
|
|
-+ * invisible. When a view is closed, keyboard navigation state is cleared.
|
|
|
|
-+ *
|
|
|
|
-+ * This diagram shows how <panelview> nodes move during navigation:
|
|
|
|
-+ *
|
|
|
|
-+ * In this <panelmultiview> In other panels Action
|
|
|
|
-+ * ┌───┬───┬───┐ ┌───┬───┐
|
|
|
|
-+ * │(A)│ B │ C │ │ D │ E │ Open panel
|
|
|
|
-+ * └───┴───┴───┘ └───┴───┘
|
|
|
|
-+ * ┌───┬───┬───┐ ┌───┬───┐
|
|
|
|
-+ * │{A}│(C)│ B │ │ D │ E │ Show subview C
|
|
|
|
-+ * └───┴───┴───┘ └───┴───┘
|
|
|
|
-+ * ┌───┬───┬───┬───┐ ┌───┐
|
|
|
|
-+ * │{A}│{C}│(D)│ B │ │ E │ Show subview D
|
|
|
|
-+ * └───┴───┴───┴───┘ └───┘
|
|
|
|
-+ * │ ┌───┬───┬───┬───┐ ┌───┐
|
|
|
|
-+ * │ │{A}│(C)│ D │ B │ │ E │ Go back
|
|
|
|
-+ * │ └───┴───┴───┴───┘ └───┘
|
|
|
|
-+ * │ │ │
|
|
|
|
-+ * │ │ └── Currently visible view
|
|
|
|
-+ * │ │ │
|
|
|
|
-+ * └───┴───┴── Open views
|
|
|
|
-+ */
|
|
|
|
-+
|
|
|
|
-+const lazy = {};
|
|
|
|
-+
|
|
|
|
-+ChromeUtils.defineLazyGetter(lazy, "gBundle", function () {
|
|
|
|
-+ return Services.strings.createBundle(
|
|
|
|
-+ "chrome://messenger/locale/messenger.properties"
|
|
|
|
-+ );
|
|
|
|
-+});
|
|
|
|
-+
|
|
|
|
-+/**
|
|
|
|
-+ * Safety timeout after which asynchronous events will be canceled if any of the
|
|
|
|
-+ * registered blockers does not return.
|
|
|
|
-+ */
|
|
|
|
-+const BLOCKERS_TIMEOUT_MS = 10000;
|
|
|
|
-+
|
|
|
|
-+const TRANSITION_PHASES = Object.freeze({
|
|
|
|
-+ START: 1,
|
|
|
|
-+ PREPARE: 2,
|
|
|
|
-+ TRANSITION: 3,
|
|
|
|
-+});
|
|
|
|
-+
|
|
|
|
-+const gNodeToObjectMap = new WeakMap();
|
|
|
|
-+const gWindowsWithUnloadHandler = new WeakSet();
|
|
|
|
-+
|
|
|
|
-+/**
|
|
|
|
-+ * Allows associating an object to a node lazily using a weak map.
|
|
|
|
-+ *
|
|
|
|
-+ * Classes deriving from this one may be easily converted to Custom Elements,
|
|
|
|
-+ * although they would lose the ability of being associated lazily.
|
|
|
|
-+ */
|
|
|
|
-+var AssociatedToNode = class {
|
|
|
|
-+ constructor(node) {
|
|
|
|
-+ /**
|
|
|
|
-+ * Node associated to this object.
|
|
|
|
-+ */
|
|
|
|
-+ this.node = node;
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * This promise is resolved when the current set of blockers set by event
|
|
|
|
-+ * handlers have all been processed.
|
|
|
|
-+ */
|
|
|
|
-+ this._blockersPromise = Promise.resolve();
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Retrieves the instance associated with the given node, constructing a new
|
|
|
|
-+ * one if necessary. When the last reference to the node is released, the
|
|
|
|
-+ * object instance will be garbage collected as well.
|
|
|
|
-+ */
|
|
|
|
-+ static forNode(node) {
|
|
|
|
-+ let associatedToNode = gNodeToObjectMap.get(node);
|
|
|
|
-+ if (!associatedToNode) {
|
|
|
|
-+ associatedToNode = new this(node);
|
|
|
|
-+ gNodeToObjectMap.set(node, associatedToNode);
|
|
|
|
-+ }
|
|
|
|
-+ return associatedToNode;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ get document() {
|
|
|
|
-+ return this.node.ownerDocument;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ get window() {
|
|
|
|
-+ return this.node.ownerGlobal;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ _getBoundsWithoutFlushing(element) {
|
|
|
|
-+ return this.window.windowUtils.getBoundsWithoutFlushing(element);
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Dispatches a custom event on this element.
|
|
|
|
-+ *
|
|
|
|
-+ * @param {string} eventName Name of the event to dispatch.
|
|
|
|
-+ * @param {object} [detail] Event detail object. Optional.
|
|
|
|
-+ * @param {boolean} cancelable If the event can be canceled.
|
|
|
|
-+ * @returns {boolean} `true` if the event was canceled by an event handler, `false`
|
|
|
|
-+ * otherwise.
|
|
|
|
-+ */
|
|
|
|
-+ dispatchCustomEvent(eventName, detail, cancelable = false) {
|
|
|
|
-+ const event = new this.window.CustomEvent(eventName, {
|
|
|
|
-+ detail,
|
|
|
|
-+ bubbles: true,
|
|
|
|
-+ cancelable,
|
|
|
|
-+ });
|
|
|
|
-+ this.node.dispatchEvent(event);
|
|
|
|
-+ return event.defaultPrevented;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Dispatches a custom event on this element and waits for any blocking
|
|
|
|
-+ * promises registered using the "addBlocker" function on the details object.
|
|
|
|
-+ * If this function is called again, the event is only dispatched after all
|
|
|
|
-+ * the previously registered blockers have returned.
|
|
|
|
-+ *
|
|
|
|
-+ * The event can be canceled either by resolving any blocking promise to the
|
|
|
|
-+ * boolean value "false" or by calling preventDefault on the event. Rejections
|
|
|
|
-+ * and exceptions will be reported and will cancel the event.
|
|
|
|
-+ *
|
|
|
|
-+ * Blocking should be used sporadically because it slows down the interface.
|
|
|
|
-+ * Also, non-reentrancy is not strictly guaranteed because a safety timeout of
|
|
|
|
-+ * BLOCKERS_TIMEOUT_MS is implemented, after which the event will be canceled.
|
|
|
|
-+ * This helps to prevent deadlocks if any of the event handlers does not
|
|
|
|
-+ * resolve a blocker promise.
|
|
|
|
-+ *
|
|
|
|
-+ * @note Since there is no use case for dispatching different asynchronous
|
|
|
|
-+ * events in parallel for the same element, this function will also wait
|
|
|
|
-+ * for previous blockers when the event name is different.
|
|
|
|
-+ *
|
|
|
|
-+ * @param eventName
|
|
|
|
-+ * Name of the custom event to dispatch.
|
|
|
|
-+ *
|
|
|
|
-+ * @resolves True if the event was canceled by a handler, false otherwise.
|
|
|
|
-+ */
|
|
|
|
-+ async dispatchAsyncEvent(eventName) {
|
|
|
|
-+ // Wait for all the previous blockers before dispatching the event.
|
|
|
|
-+ const blockersPromise = this._blockersPromise.catch(() => {});
|
|
|
|
-+ return (this._blockersPromise = blockersPromise.then(async () => {
|
|
|
|
-+ const blockers = new Set();
|
|
|
|
-+ let cancel = this.dispatchCustomEvent(
|
|
|
|
-+ eventName,
|
|
|
|
-+ {
|
|
|
|
-+ addBlocker(promise) {
|
|
|
|
-+ // Any exception in the blocker will cancel the operation.
|
|
|
|
-+ blockers.add(
|
|
|
|
-+ promise.catch(ex => {
|
|
|
|
-+ console.error(ex);
|
|
|
|
-+ return true;
|
|
|
|
-+ })
|
|
|
|
-+ );
|
|
|
|
-+ },
|
|
|
|
-+ },
|
|
|
|
-+ true
|
|
|
|
-+ );
|
|
|
|
-+ if (blockers.size) {
|
|
|
|
-+ const timeoutPromise = new Promise((resolve, reject) => {
|
|
|
|
-+ this.window.setTimeout(reject, BLOCKERS_TIMEOUT_MS);
|
|
|
|
-+ });
|
|
|
|
-+ try {
|
|
|
|
-+ const results = await Promise.race([
|
|
|
|
-+ Promise.all(blockers),
|
|
|
|
-+ timeoutPromise,
|
|
|
|
-+ ]);
|
|
|
|
-+ cancel = cancel || results.some(result => result === false);
|
|
|
|
-+ } catch (ex) {
|
|
|
|
-+ console.error(
|
|
|
|
-+ new Error(`One of the blockers for ${eventName} timed out.`)
|
|
|
|
-+ );
|
|
|
|
-+ return true;
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+ return cancel;
|
|
|
|
-+ }));
|
|
|
|
-+ }
|
|
|
|
-+};
|
|
|
|
-+
|
|
|
|
-+/**
|
|
|
|
-+ * This is associated to <panelmultiview> elements.
|
|
|
|
-+ */
|
|
|
|
-+export class PanelMultiView extends AssociatedToNode {
|
|
|
|
-+ /**
|
|
|
|
-+ * Tries to open the specified <panel> and displays the main view specified
|
|
|
|
-+ * with the "mainViewId" attribute on the <panelmultiview> node it contains.
|
|
|
|
-+ *
|
|
|
|
-+ * If the panel does not contain a <panelmultiview>, it is opened directly.
|
|
|
|
-+ * This allows consumers like page actions to accept different panel types.
|
|
|
|
-+ *
|
|
|
|
-+ * @see The non-static openPopup method for details.
|
|
|
|
-+ */
|
|
|
|
-+ static async openPopup(panelNode, ...args) {
|
|
|
|
-+ const panelMultiViewNode = panelNode.querySelector("panelmultiview");
|
|
|
|
-+ if (panelMultiViewNode) {
|
|
|
|
-+ return this.forNode(panelMultiViewNode).openPopup(...args);
|
|
|
|
-+ }
|
|
|
|
-+ panelNode.openPopup(...args);
|
|
|
|
-+ return true;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Closes the specified <panel> which contains a <panelmultiview> node.
|
|
|
|
-+ *
|
|
|
|
-+ * If the panel does not contain a <panelmultiview>, it is closed directly.
|
|
|
|
-+ * This allows consumers like page actions to accept different panel types.
|
|
|
|
-+ *
|
|
|
|
-+ * @see The non-static hidePopup method for details.
|
|
|
|
-+ */
|
|
|
|
-+ static hidePopup(panelNode) {
|
|
|
|
-+ const panelMultiViewNode = panelNode.querySelector("panelmultiview");
|
|
|
|
-+ if (panelMultiViewNode) {
|
|
|
|
-+ this.forNode(panelMultiViewNode).hidePopup();
|
|
|
|
-+ } else {
|
|
|
|
-+ panelNode.hidePopup();
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Removes the specified <panel> from the document, ensuring that any
|
|
|
|
-+ * <panelmultiview> node it contains is destroyed properly.
|
|
|
|
-+ *
|
|
|
|
-+ * If the viewCacheId attribute is present on the <panelmultiview> element,
|
|
|
|
-+ * imported subviews will be moved out again to the element it specifies, so
|
|
|
|
-+ * that the panel element can be removed safely.
|
|
|
|
-+ *
|
|
|
|
-+ * If the panel does not contain a <panelmultiview>, it is removed directly.
|
|
|
|
-+ * This allows consumers like page actions to accept different panel types.
|
|
|
|
-+ */
|
|
|
|
-+ static removePopup(panelNode) {
|
|
|
|
-+ try {
|
|
|
|
-+ const panelMultiViewNode = panelNode.querySelector("panelmultiview");
|
|
|
|
-+ if (panelMultiViewNode) {
|
|
|
|
-+ const panelMultiView = this.forNode(panelMultiViewNode);
|
|
|
|
-+ panelMultiView._moveOutKids();
|
|
|
|
-+ panelMultiView.disconnect();
|
|
|
|
-+ }
|
|
|
|
-+ } finally {
|
|
|
|
-+ // Make sure to remove the panel element even if disconnecting fails.
|
|
|
|
-+ panelNode.remove();
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Ensures that when the specified window is closed all the <panelmultiview>
|
|
|
|
-+ * node it contains are destroyed properly.
|
|
|
|
-+ */
|
|
|
|
-+ static ensureUnloadHandlerRegistered(window) {
|
|
|
|
-+ if (gWindowsWithUnloadHandler.has(window)) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ window.addEventListener(
|
|
|
|
-+ "unload",
|
|
|
|
-+ () => {
|
|
|
|
-+ for (const panelMultiViewNode of window.document.querySelectorAll(
|
|
|
|
-+ "panelmultiview"
|
|
|
|
-+ )) {
|
|
|
|
-+ this.forNode(panelMultiViewNode).disconnect();
|
|
|
|
-+ }
|
|
|
|
-+ },
|
|
|
|
-+ { once: true }
|
|
|
|
-+ );
|
|
|
|
-+
|
|
|
|
-+ gWindowsWithUnloadHandler.add(window);
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ get _panel() {
|
|
|
|
-+ return this.node.parentNode;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ set _transitioning(val) {
|
|
|
|
-+ if (val) {
|
|
|
|
-+ this.node.setAttribute("transitioning", "true");
|
|
|
|
-+ } else {
|
|
|
|
-+ this.node.removeAttribute("transitioning");
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ get _screenManager() {
|
|
|
|
-+ if (this.__screenManager) {
|
|
|
|
-+ return this.__screenManager;
|
|
|
|
-+ }
|
|
|
|
-+ return (this.__screenManager = Cc[
|
|
|
|
-+ "@mozilla.org/gfx/screenmanager;1"
|
|
|
|
-+ ].getService(Ci.nsIScreenManager));
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ constructor(node) {
|
|
|
|
-+ super(node);
|
|
|
|
-+ this._openPopupPromise = Promise.resolve(false);
|
|
|
|
-+ this._openPopupCancelCallback = () => {};
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ connect() {
|
|
|
|
-+ this.connected = true;
|
|
|
|
-+
|
|
|
|
-+ PanelMultiView.ensureUnloadHandlerRegistered(this.window);
|
|
|
|
-+
|
|
|
|
-+ const viewContainer = (this._viewContainer =
|
|
|
|
-+ this.document.createXULElement("box"));
|
|
|
|
-+ viewContainer.classList.add("panel-viewcontainer");
|
|
|
|
-+
|
|
|
|
-+ const viewStack = (this._viewStack = this.document.createXULElement("box"));
|
|
|
|
-+ viewStack.classList.add("panel-viewstack");
|
|
|
|
-+ viewContainer.append(viewStack);
|
|
|
|
-+
|
|
|
|
-+ const offscreenViewContainer = this.document.createXULElement("box");
|
|
|
|
-+ offscreenViewContainer.classList.add("panel-viewcontainer", "offscreen");
|
|
|
|
-+
|
|
|
|
-+ const offscreenViewStack = (this._offscreenViewStack =
|
|
|
|
-+ this.document.createXULElement("box"));
|
|
|
|
-+ offscreenViewStack.classList.add("panel-viewstack");
|
|
|
|
-+ offscreenViewContainer.append(offscreenViewStack);
|
|
|
|
-+
|
|
|
|
-+ this.node.prepend(offscreenViewContainer);
|
|
|
|
-+ this.node.prepend(viewContainer);
|
|
|
|
-+
|
|
|
|
-+ this.openViews = [];
|
|
|
|
-+
|
|
|
|
-+ this._panel.addEventListener("popupshowing", this);
|
|
|
|
-+ this._panel.addEventListener("popuppositioned", this);
|
|
|
|
-+ this._panel.addEventListener("popuphidden", this);
|
|
|
|
-+ this._panel.addEventListener("popupshown", this);
|
|
|
|
-+
|
|
|
|
-+ // Proxy these public properties and methods, as used elsewhere by various
|
|
|
|
-+ // parts of the browser, to this instance.
|
|
|
|
-+ ["goBack", "showSubView"].forEach(method => {
|
|
|
|
-+ Object.defineProperty(this.node, method, {
|
|
|
|
-+ enumerable: true,
|
|
|
|
-+ value: (...args) => this[method](...args),
|
|
|
|
-+ });
|
|
|
|
-+ });
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ disconnect() {
|
|
|
|
-+ // Guard against re-entrancy.
|
|
|
|
-+ if (!this.node || !this.connected) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ this._panel.removeEventListener("mousemove", this);
|
|
|
|
-+ this._panel.removeEventListener("popupshowing", this);
|
|
|
|
-+ this._panel.removeEventListener("popuppositioned", this);
|
|
|
|
-+ this._panel.removeEventListener("popupshown", this);
|
|
|
|
-+ this._panel.removeEventListener("popuphidden", this);
|
|
|
|
-+ this.window.removeEventListener("keydown", this, true);
|
|
|
|
-+ this.node =
|
|
|
|
-+ this._openPopupPromise =
|
|
|
|
-+ this._openPopupCancelCallback =
|
|
|
|
-+ this._viewContainer =
|
|
|
|
-+ this._viewStack =
|
|
|
|
-+ this._transitionDetails =
|
|
|
|
-+ null;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Tries to open the panel associated with this PanelMultiView, and displays
|
|
|
|
-+ * the main view specified with the "mainViewId" attribute.
|
|
|
|
-+ *
|
|
|
|
-+ * The hidePopup method can be called while the operation is in progress to
|
|
|
|
-+ * prevent the panel from being displayed. View events may also cancel the
|
|
|
|
-+ * operation, so there is no guarantee that the panel will become visible.
|
|
|
|
-+ *
|
|
|
|
-+ * The "popuphidden" event will be fired either when the operation is canceled
|
|
|
|
-+ * or when the popup is closed later. This event can be used for example to
|
|
|
|
-+ * reset the "open" state of the anchor or tear down temporary panels.
|
|
|
|
-+ *
|
|
|
|
-+ * If this method is called again before the panel is shown, the result
|
|
|
|
-+ * depends on the operation currently in progress. If the operation was not
|
|
|
|
-+ * canceled, the panel is opened using the arguments from the previous call,
|
|
|
|
-+ * and this call is ignored. If the operation was canceled, it will be
|
|
|
|
-+ * retried again using the arguments from this call.
|
|
|
|
-+ *
|
|
|
|
-+ * It's not necessary for the <panelmultiview> binding to be connected when
|
|
|
|
-+ * this method is called, but the containing panel must have its display
|
|
|
|
-+ * turned on, for example it shouldn't have the "hidden" attribute.
|
|
|
|
-+ *
|
|
|
|
-+ * @param anchor
|
|
|
|
-+ * The node to anchor the popup to.
|
|
|
|
-+ * @param options
|
|
|
|
-+ * Either options to use or a string position. This is forwarded to
|
|
|
|
-+ * the openPopup method of the panel.
|
|
|
|
-+ * @param args
|
|
|
|
-+ * Additional arguments to be forwarded to the openPopup method of the
|
|
|
|
-+ * panel.
|
|
|
|
-+ *
|
|
|
|
-+ * @resolves With true as soon as the request to display the panel has been
|
|
|
|
-+ * sent, or with false if the operation was canceled. The state of
|
|
|
|
-+ * the panel at this point is not guaranteed. It may be still
|
|
|
|
-+ * showing, completely shown, or completely hidden.
|
|
|
|
-+ * @rejects If an exception is thrown at any point in the process before the
|
|
|
|
-+ * request to display the panel is sent.
|
|
|
|
-+ */
|
|
|
|
-+ async openPopup(anchor, options, ...args) {
|
|
|
|
-+ // Set up the function that allows hidePopup or a second call to showPopup
|
|
|
|
-+ // to cancel the specific panel opening operation that we're starting below.
|
|
|
|
-+ // This function must be synchronous, meaning we can't use Promise.race,
|
|
|
|
-+ // because hidePopup wants to dispatch the "popuphidden" event synchronously
|
|
|
|
-+ // even if the panel has not been opened yet.
|
|
|
|
-+ let canCancel = true;
|
|
|
|
-+ const cancelCallback = (this._openPopupCancelCallback = () => {
|
|
|
|
-+ // If the cancel callback is called and the panel hasn't been prepared
|
|
|
|
-+ // yet, cancel showing it. Setting canCancel to false will prevent the
|
|
|
|
-+ // popup from opening. If the panel has opened by the time the cancel
|
|
|
|
-+ // callback is called, canCancel will be false already, and we will not
|
|
|
|
-+ // fire the "popuphidden" event.
|
|
|
|
-+ if (canCancel && this.node) {
|
|
|
|
-+ canCancel = false;
|
|
|
|
-+ this.dispatchCustomEvent("popuphidden");
|
|
|
|
-+ }
|
|
|
|
-+ });
|
|
|
|
-+
|
|
|
|
-+ // Create a promise that is resolved with the result of the last call to
|
|
|
|
-+ // this method, where errors indicate that the panel was not opened.
|
|
|
|
-+ const openPopupPromise = this._openPopupPromise.catch(() => {
|
|
|
|
-+ return false;
|
|
|
|
-+ });
|
|
|
|
-+
|
|
|
|
-+ // Make the preparation done before showing the panel non-reentrant. The
|
|
|
|
-+ // promise created here will be resolved only after the panel preparation is
|
|
|
|
-+ // completed, even if a cancellation request is received in the meantime.
|
|
|
|
-+ return (this._openPopupPromise = openPopupPromise.then(async wasShown => {
|
|
|
|
-+ // The panel may have been destroyed in the meantime.
|
|
|
|
-+ if (!this.node) {
|
|
|
|
-+ return false;
|
|
|
|
-+ }
|
|
|
|
-+ // If the panel has been already opened there is nothing more to do. We
|
|
|
|
-+ // check the actual state of the panel rather than setting some state in
|
|
|
|
-+ // our handler of the "popuphidden" event because this has a lower chance
|
|
|
|
-+ // of locking indefinitely if events aren't raised in the expected order.
|
|
|
|
-+ if (wasShown && ["open", "showing"].includes(this._panel.state)) {
|
|
|
|
-+ return true;
|
|
|
|
-+ }
|
|
|
|
-+ try {
|
|
|
|
-+ if (!this.connected) {
|
|
|
|
-+ this.connect();
|
|
|
|
-+ }
|
|
|
|
-+ // Allow any of the ViewShowing handlers to prevent showing the main view.
|
|
|
|
-+ if (!(await this._showMainView())) {
|
|
|
|
-+ cancelCallback();
|
|
|
|
-+ }
|
|
|
|
-+ } catch (ex) {
|
|
|
|
-+ cancelCallback();
|
|
|
|
-+ throw ex;
|
|
|
|
-+ }
|
|
|
|
-+ // If a cancellation request was received there is nothing more to do.
|
|
|
|
-+ if (!canCancel || !this.node) {
|
|
|
|
-+ return false;
|
|
|
|
-+ }
|
|
|
|
-+ // We have to set canCancel to false before opening the popup because the
|
|
|
|
-+ // hidePopup method of PanelMultiView can be re-entered by event handlers.
|
|
|
|
-+ // If the openPopup call fails, however, we still have to dispatch the
|
|
|
|
-+ // "popuphidden" event even if canCancel was set to false.
|
|
|
|
-+ try {
|
|
|
|
-+ canCancel = false;
|
|
|
|
-+ this._panel.openPopup(anchor, options, ...args);
|
|
|
|
-+
|
|
|
|
-+ // On Windows, if another popup is hiding while we call openPopup, the
|
|
|
|
-+ // call won't fail but the popup won't open. In this case, we have to
|
|
|
|
-+ // dispatch an artificial "popuphidden" event to reset our state.
|
|
|
|
-+ if (this._panel.state == "closed" && this.openViews.length) {
|
|
|
|
-+ this.dispatchCustomEvent("popuphidden");
|
|
|
|
-+ return false;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ if (
|
|
|
|
-+ options &&
|
|
|
|
-+ typeof options == "object" &&
|
|
|
|
-+ options.triggerEvent &&
|
|
|
|
-+ options.triggerEvent.type == "keypress" &&
|
|
|
|
-+ this.openViews.length
|
|
|
|
-+ ) {
|
|
|
|
-+ // This was opened via the keyboard, so focus the first item.
|
|
|
|
-+ this.openViews[0].focusWhenActive = true;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ return true;
|
|
|
|
-+ } catch (ex) {
|
|
|
|
-+ this.dispatchCustomEvent("popuphidden");
|
|
|
|
-+ throw ex;
|
|
|
|
-+ }
|
|
|
|
-+ }));
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Closes the panel associated with this PanelMultiView.
|
|
|
|
-+ *
|
|
|
|
-+ * If the openPopup method was called but the panel has not been displayed
|
|
|
|
-+ * yet, the operation is canceled and the panel will not be displayed, but the
|
|
|
|
-+ * "popuphidden" event is fired synchronously anyways.
|
|
|
|
-+ *
|
|
|
|
-+ * This means that by the time this method returns all the operations handled
|
|
|
|
-+ * by the "popuphidden" event are completed, for example resetting the "open"
|
|
|
|
-+ * state of the anchor, and the panel is already invisible.
|
|
|
|
-+ */
|
|
|
|
-+ hidePopup() {
|
|
|
|
-+ if (!this.node || !this.connected) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // If we have already reached the _panel.openPopup call in the openPopup
|
|
|
|
-+ // method, we can call hidePopup. Otherwise, we have to cancel the latest
|
|
|
|
-+ // request to open the panel, which will have no effect if the request has
|
|
|
|
-+ // been canceled already.
|
|
|
|
-+ if (["open", "showing"].includes(this._panel.state)) {
|
|
|
|
-+ this._panel.hidePopup();
|
|
|
|
-+ } else {
|
|
|
|
-+ this._openPopupCancelCallback();
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // We close all the views synchronously, so that they are ready to be opened
|
|
|
|
-+ // in other PanelMultiView instances. The "popuphidden" handler may also
|
|
|
|
-+ // call this function, but the second time openViews will be empty.
|
|
|
|
-+ this.closeAllViews();
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Move any child subviews into the element defined by "viewCacheId" to make
|
|
|
|
-+ * sure they will not be removed together with the <panelmultiview> element.
|
|
|
|
-+ */
|
|
|
|
-+ _moveOutKids() {
|
|
|
|
-+ const viewCacheId = this.node.getAttribute("viewCacheId");
|
|
|
|
-+ if (!viewCacheId) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // Node.children and Node.children is live to DOM changes like the
|
|
|
|
-+ // ones we're about to do, so iterate over a static copy:
|
|
|
|
-+ const subviews = Array.from(this._viewStack.children);
|
|
|
|
-+ const viewCache = this.document.getElementById(viewCacheId);
|
|
|
|
-+ for (const subview of subviews) {
|
|
|
|
-+ viewCache.appendChild(subview);
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Slides in the specified view as a subview.
|
|
|
|
-+ *
|
|
|
|
-+ * @param viewIdOrNode
|
|
|
|
-+ * DOM element or string ID of the <panelview> to display.
|
|
|
|
-+ * @param anchor
|
|
|
|
-+ * DOM element that triggered the subview, which will be highlighted
|
|
|
|
-+ * and whose "label" attribute will be used for the title of the
|
|
|
|
-+ * subview when a "title" attribute is not specified.
|
|
|
|
-+ */
|
|
|
|
-+ showSubView(viewIdOrNode, anchor) {
|
|
|
|
-+ this._showSubView(viewIdOrNode, anchor).catch(console.error);
|
|
|
|
-+ }
|
|
|
|
-+ async _showSubView(viewIdOrNode, anchor) {
|
|
|
|
-+ const viewNode =
|
|
|
|
-+ typeof viewIdOrNode == "string"
|
|
|
|
-+ ? this.document.getElementById(viewIdOrNode)
|
|
|
|
-+ : viewIdOrNode;
|
|
|
|
-+ if (!viewNode) {
|
|
|
|
-+ console.error(new Error(`Subview ${viewIdOrNode} doesn't exist.`));
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ if (!this.openViews.length) {
|
|
|
|
-+ console.error(new Error(`Cannot show a subview in a closed panel.`));
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ const prevPanelView = this.openViews[this.openViews.length - 1];
|
|
|
|
-+ const nextPanelView = PanelView.forNode(viewNode);
|
|
|
|
-+ if (this.openViews.includes(nextPanelView)) {
|
|
|
|
-+ console.error(new Error(`Subview ${viewNode.id} is already open.`));
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // Do not re-enter the process if navigation is already in progress. Since
|
|
|
|
-+ // there is only one active view at any given time, we can do this check
|
|
|
|
-+ // safely, even considering that during the navigation process the actual
|
|
|
|
-+ // view to which prevPanelView refers will change.
|
|
|
|
-+ if (!prevPanelView.active) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+ // If prevPanelView._doingKeyboardActivation is true, it will be reset to
|
|
|
|
-+ // false synchronously. Therefore, we must capture it before we use any
|
|
|
|
-+ // "await" statements.
|
|
|
|
-+ const doingKeyboardActivation = prevPanelView._doingKeyboardActivation;
|
|
|
|
-+ // Marking the view that is about to scrolled out of the visible area as
|
|
|
|
-+ // inactive will prevent re-entrancy and also disable keyboard navigation.
|
|
|
|
-+ // From this point onwards, "await" statements can be used safely.
|
|
|
|
-+ prevPanelView.active = false;
|
|
|
|
-+
|
|
|
|
-+ // Provide visual feedback while navigation is in progress, starting before
|
|
|
|
-+ // the transition starts and ending when the previous view is invisible.
|
|
|
|
-+ if (anchor) {
|
|
|
|
-+ anchor.setAttribute("open", "true");
|
|
|
|
-+ }
|
|
|
|
-+ try {
|
|
|
|
-+ // If the ViewShowing event cancels the operation we have to re-enable
|
|
|
|
-+ // keyboard navigation, but this must be avoided if the panel was closed.
|
|
|
|
-+ if (!(await this._openView(nextPanelView))) {
|
|
|
|
-+ if (prevPanelView.isOpenIn(this)) {
|
|
|
|
-+ // We don't raise a ViewShown event because nothing actually changed.
|
|
|
|
-+ // Technically we should use a different state flag just because there
|
|
|
|
-+ // is code that could check the "active" property to determine whether
|
|
|
|
-+ // to wait for a ViewShown event later, but this only happens in
|
|
|
|
-+ // regression tests and is less likely to be a technique used in
|
|
|
|
-+ // production code, where use of ViewShown is less common.
|
|
|
|
-+ prevPanelView.active = true;
|
|
|
|
-+ }
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ prevPanelView.captureKnownSize();
|
|
|
|
-+
|
|
|
|
-+ // The main view of a panel can be a subview in another one. Make sure to
|
|
|
|
-+ // reset all the properties that may be set on a subview.
|
|
|
|
-+ nextPanelView.mainview = false;
|
|
|
|
-+ // The header may change based on how the subview was opened.
|
|
|
|
-+ nextPanelView.headerText =
|
|
|
|
-+ viewNode.getAttribute("title") ||
|
|
|
|
-+ (anchor && anchor.getAttribute("label"));
|
|
|
|
-+ // The constrained width of subviews may also vary between panels.
|
|
|
|
-+ nextPanelView.minMaxWidth = prevPanelView.knownWidth;
|
|
|
|
-+
|
|
|
|
-+ if (anchor) {
|
|
|
|
-+ viewNode.classList.add("PanelUI-subView");
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ await this._transitionViews(prevPanelView.node, viewNode, false, anchor);
|
|
|
|
-+ } finally {
|
|
|
|
-+ if (anchor) {
|
|
|
|
-+ anchor.removeAttribute("open");
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ nextPanelView.focusWhenActive = doingKeyboardActivation;
|
|
|
|
-+ this._activateView(nextPanelView);
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Navigates backwards by sliding out the most recent subview.
|
|
|
|
-+ */
|
|
|
|
-+ goBack() {
|
|
|
|
-+ this._goBack().catch(console.error);
|
|
|
|
-+ }
|
|
|
|
-+ async _goBack() {
|
|
|
|
-+ if (this.openViews.length < 2) {
|
|
|
|
-+ // This may be called by keyboard navigation or external code when only
|
|
|
|
-+ // the main view is open.
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ const prevPanelView = this.openViews[this.openViews.length - 1];
|
|
|
|
-+ const nextPanelView = this.openViews[this.openViews.length - 2];
|
|
|
|
-+
|
|
|
|
-+ // Like in the showSubView method, do not re-enter navigation while it is
|
|
|
|
-+ // in progress, and make the view inactive immediately. From this point
|
|
|
|
-+ // onwards, "await" statements can be used safely.
|
|
|
|
-+ if (!prevPanelView.active) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ prevPanelView.active = false;
|
|
|
|
-+
|
|
|
|
-+ prevPanelView.captureKnownSize();
|
|
|
|
-+
|
|
|
|
-+ await this._transitionViews(prevPanelView.node, nextPanelView.node, true);
|
|
|
|
-+
|
|
|
|
-+ this._closeLatestView();
|
|
|
|
-+
|
|
|
|
-+ this._activateView(nextPanelView);
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Prepares the main view before showing the panel.
|
|
|
|
-+ */
|
|
|
|
-+ async _showMainView() {
|
|
|
|
-+ const nextPanelView = PanelView.forNode(
|
|
|
|
-+ this.document.getElementById(this.node.getAttribute("mainViewId"))
|
|
|
|
-+ );
|
|
|
|
-+
|
|
|
|
-+ // If the view is already open in another panel, close the panel first.
|
|
|
|
-+ const oldPanelMultiViewNode = nextPanelView.node.panelMultiView;
|
|
|
|
-+ if (oldPanelMultiViewNode) {
|
|
|
|
-+ PanelMultiView.forNode(oldPanelMultiViewNode).hidePopup();
|
|
|
|
-+ // Wait for a layout flush after hiding the popup, otherwise the view may
|
|
|
|
-+ // not be displayed correctly for some time after the new panel is opened.
|
|
|
|
-+ // This is filed as bug 1441015.
|
|
|
|
-+ await this.window.promiseDocumentFlushed(() => {});
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ if (!(await this._openView(nextPanelView))) {
|
|
|
|
-+ return false;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // The main view of a panel can be a subview in another one. Make sure to
|
|
|
|
-+ // reset all the properties that may be set on a subview.
|
|
|
|
-+ nextPanelView.mainview = true;
|
|
|
|
-+ nextPanelView.headerText = "";
|
|
|
|
-+ nextPanelView.minMaxWidth = 0;
|
|
|
|
-+
|
|
|
|
-+ // Ensure the view will be visible once the panel is opened.
|
|
|
|
-+ nextPanelView.visible = true;
|
|
|
|
-+
|
|
|
|
-+ return true;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Opens the specified PanelView and dispatches the ViewShowing event, which
|
|
|
|
-+ * can be used to populate the subview or cancel the operation.
|
|
|
|
-+ *
|
|
|
|
-+ * This also clears all the attributes and styles that may be left by a
|
|
|
|
-+ * transition that was interrupted.
|
|
|
|
-+ *
|
|
|
|
-+ * @resolves With true if the view was opened, false otherwise.
|
|
|
|
-+ */
|
|
|
|
-+ async _openView(panelView) {
|
|
|
|
-+ if (panelView.node.parentNode != this._viewStack) {
|
|
|
|
-+ this._viewStack.appendChild(panelView.node);
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ panelView.node.panelMultiView = this.node;
|
|
|
|
-+ this.openViews.push(panelView);
|
|
|
|
-+
|
|
|
|
-+ const canceled = await panelView.dispatchAsyncEvent("ViewShowing");
|
|
|
|
-+
|
|
|
|
-+ // The panel can be hidden while we are processing the ViewShowing event.
|
|
|
|
-+ // This results in all the views being closed synchronously, and at this
|
|
|
|
-+ // point the ViewHiding event has already been dispatched for all of them.
|
|
|
|
-+ if (!this.openViews.length) {
|
|
|
|
-+ return false;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // Check if the event requested cancellation but the panel is still open.
|
|
|
|
-+ if (canceled) {
|
|
|
|
-+ // Handlers for ViewShowing can't know if a different handler requested
|
|
|
|
-+ // cancellation, so this will dispatch a ViewHiding event to give a chance
|
|
|
|
-+ // to clean up.
|
|
|
|
-+ this._closeLatestView();
|
|
|
|
-+ return false;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // Clean up all the attributes and styles related to transitions. We do this
|
|
|
|
-+ // here rather than when the view is closed because we are likely to make
|
|
|
|
-+ // other DOM modifications soon, which isn't the case when closing.
|
|
|
|
-+ const { style } = panelView.node;
|
|
|
|
-+ style.removeProperty("outline");
|
|
|
|
-+ style.removeProperty("width");
|
|
|
|
-+
|
|
|
|
-+ return true;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Activates the specified view and raises the ViewShown event, unless the
|
|
|
|
-+ * view was closed in the meantime.
|
|
|
|
-+ */
|
|
|
|
-+ _activateView(panelView) {
|
|
|
|
-+ if (panelView.isOpenIn(this)) {
|
|
|
|
-+ panelView.active = true;
|
|
|
|
-+ if (panelView.focusWhenActive) {
|
|
|
|
-+ panelView.focusFirstNavigableElement(false, true);
|
|
|
|
-+ panelView.focusWhenActive = false;
|
|
|
|
-+ }
|
|
|
|
-+ panelView.dispatchCustomEvent("ViewShown");
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Closes the most recent PanelView and raises the ViewHiding event.
|
|
|
|
-+ *
|
|
|
|
-+ * @note The ViewHiding event is not cancelable and should probably be renamed
|
|
|
|
-+ * to ViewHidden or ViewClosed instead, see bug 1438507.
|
|
|
|
-+ */
|
|
|
|
-+ _closeLatestView() {
|
|
|
|
-+ const panelView = this.openViews.pop();
|
|
|
|
-+ panelView.clearNavigation();
|
|
|
|
-+ panelView.dispatchCustomEvent("ViewHiding");
|
|
|
|
-+ panelView.node.panelMultiView = null;
|
|
|
|
-+ // Views become invisible synchronously when they are closed, and they won't
|
|
|
|
-+ // become visible again until they are opened. When this is called at the
|
|
|
|
-+ // end of backwards navigation, the view is already invisible.
|
|
|
|
-+ panelView.visible = false;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Closes all the views that are currently open.
|
|
|
|
-+ */
|
|
|
|
-+ closeAllViews() {
|
|
|
|
-+ // Raise ViewHiding events for open views in reverse order.
|
|
|
|
-+ while (this.openViews.length) {
|
|
|
|
-+ this._closeLatestView();
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Apply a transition to 'slide' from the currently active view to the next
|
|
|
|
-+ * one.
|
|
|
|
-+ * Sliding the next subview in means that the previous panelview stays where it
|
|
|
|
-+ * is and the active panelview slides in from the left in LTR mode, right in
|
|
|
|
-+ * RTL mode.
|
|
|
|
-+ *
|
|
|
|
-+ * @param {panelview} previousViewNode Node that is currently displayed, but
|
|
|
|
-+ * is about to be transitioned away. This
|
|
|
|
-+ * must be already inactive at this point.
|
|
|
|
-+ * @param {panelview} viewNode - Node that will becode the active view,
|
|
|
|
-+ * after the transition has finished.
|
|
|
|
-+ * @param {boolean} reverse Whether we're navigation back to a
|
|
|
|
-+ * previous view or forward to a next view.
|
|
|
|
-+ */
|
|
|
|
-+ async _transitionViews(previousViewNode, viewNode, reverse) {
|
|
|
|
-+ const { window } = this;
|
|
|
|
-+
|
|
|
|
-+ const nextPanelView = PanelView.forNode(viewNode);
|
|
|
|
-+ const prevPanelView = PanelView.forNode(previousViewNode);
|
|
|
|
-+
|
|
|
|
-+ const details = (this._transitionDetails = {
|
|
|
|
-+ phase: TRANSITION_PHASES.START,
|
|
|
|
-+ });
|
|
|
|
-+
|
|
|
|
-+ // Set the viewContainer dimensions to make sure only the current view is
|
|
|
|
-+ // visible.
|
|
|
|
-+ const olderView = reverse ? nextPanelView : prevPanelView;
|
|
|
|
-+ this._viewContainer.style.minHeight = olderView.knownHeight + "px";
|
|
|
|
-+ this._viewContainer.style.height = prevPanelView.knownHeight + "px";
|
|
|
|
-+ this._viewContainer.style.width = prevPanelView.knownWidth + "px";
|
|
|
|
-+ // Lock the dimensions of the window that hosts the popup panel.
|
|
|
|
-+ const rect = this._getBoundsWithoutFlushing(this._panel);
|
|
|
|
-+ this._panel.style.width = rect.width + "px";
|
|
|
|
-+ this._panel.style.height = rect.height + "px";
|
|
|
|
-+
|
|
|
|
-+ let viewRect;
|
|
|
|
-+ if (reverse) {
|
|
|
|
-+ // Use the cached size when going back to a previous view, but not when
|
|
|
|
-+ // reopening a subview, because its contents may have changed.
|
|
|
|
-+ viewRect = {
|
|
|
|
-+ width: nextPanelView.knownWidth,
|
|
|
|
-+ height: nextPanelView.knownHeight,
|
|
|
|
-+ };
|
|
|
|
-+ nextPanelView.visible = true;
|
|
|
|
-+ } else if (viewNode.customRectGetter) {
|
|
|
|
-+ // We use a customRectGetter for WebExtensions panels, because they need
|
|
|
|
-+ // to query the size from an embedded browser. The presence of this
|
|
|
|
-+ // getter also provides an indication that the view node shouldn't be
|
|
|
|
-+ // moved around, otherwise the state of the browser would get disrupted.
|
|
|
|
-+ const width = prevPanelView.knownWidth;
|
|
|
|
-+ const height = prevPanelView.knownHeight;
|
|
|
|
-+ viewRect = Object.assign({ height, width }, viewNode.customRectGetter());
|
|
|
|
-+ nextPanelView.visible = true;
|
|
|
|
-+ // Until the header is visible, it has 0 height.
|
|
|
|
-+ // Wait for layout before measuring it
|
|
|
|
-+ const header = viewNode.firstElementChild;
|
|
|
|
-+ if (header && header.classList.contains("panel-header")) {
|
|
|
|
-+ viewRect.height += await window.promiseDocumentFlushed(() => {
|
|
|
|
-+ return this._getBoundsWithoutFlushing(header).height;
|
|
|
|
-+ });
|
|
|
|
-+ }
|
|
|
|
-+ } else {
|
|
|
|
-+ this._offscreenViewStack.style.minHeight = olderView.knownHeight + "px";
|
|
|
|
-+ this._offscreenViewStack.appendChild(viewNode);
|
|
|
|
-+ nextPanelView.visible = true;
|
|
|
|
-+
|
|
|
|
-+ viewRect = await window.promiseDocumentFlushed(() => {
|
|
|
|
-+ return this._getBoundsWithoutFlushing(viewNode);
|
|
|
|
-+ });
|
|
|
|
-+ // Bail out if the panel was closed in the meantime.
|
|
|
|
-+ if (!nextPanelView.isOpenIn(this)) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // Place back the view after all the other views that are already open in
|
|
|
|
-+ // order for the transition to work as expected.
|
|
|
|
-+ this._viewStack.appendChild(viewNode);
|
|
|
|
-+
|
|
|
|
-+ this._offscreenViewStack.style.removeProperty("min-height");
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ this._transitioning = true;
|
|
|
|
-+ details.phase = TRANSITION_PHASES.PREPARE;
|
|
|
|
-+
|
|
|
|
-+ // The 'magic' part: build up the amount of pixels to move right or left.
|
|
|
|
-+ const moveToLeft =
|
|
|
|
-+ (this.window.RTL_UI && !reverse) || (!this.window.RTL_UI && reverse);
|
|
|
|
-+ const deltaX = prevPanelView.knownWidth;
|
|
|
|
-+ const deepestNode = reverse ? previousViewNode : viewNode;
|
|
|
|
-+
|
|
|
|
-+ // With a transition when navigating backwards - user hits the 'back'
|
|
|
|
-+ // button - we need to make sure that the views are positioned in a way
|
|
|
|
-+ // that a translateX() unveils the previous view from the right direction.
|
|
|
|
-+ if (reverse) {
|
|
|
|
-+ this._viewStack.style.marginInlineStart = "-" + deltaX + "px";
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // Set the transition style and listen for its end to clean up and make sure
|
|
|
|
-+ // the box sizing becomes dynamic again.
|
|
|
|
-+ // Somehow, putting these properties in PanelUI.css doesn't work for newly
|
|
|
|
-+ // shown nodes in a XUL parent node.
|
|
|
|
-+ this._viewStack.style.transition =
|
|
|
|
-+ "transform var(--animation-easing-function)" +
|
|
|
|
-+ " var(--panelui-subview-transition-duration)";
|
|
|
|
-+ this._viewStack.style.willChange = "transform";
|
|
|
|
-+ // Use an outline instead of a border so that the size is not affected.
|
|
|
|
-+ deepestNode.style.outline = "1px solid var(--panel-separator-color)";
|
|
|
|
-+
|
|
|
|
-+ // Now that all the elements are in place for the start of the transition,
|
|
|
|
-+ // give the layout code a chance to set the initial values.
|
|
|
|
-+ await window.promiseDocumentFlushed(() => {});
|
|
|
|
-+ // Bail out if the panel was closed in the meantime.
|
|
|
|
-+ if (!nextPanelView.isOpenIn(this)) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // Now set the viewContainer dimensions to that of the new view, which
|
|
|
|
-+ // kicks of the height animation.
|
|
|
|
-+ this._viewContainer.style.height = viewRect.height + "px";
|
|
|
|
-+ this._viewContainer.style.width = viewRect.width + "px";
|
|
|
|
-+ this._panel.style.removeProperty("width");
|
|
|
|
-+ this._panel.style.removeProperty("height");
|
|
|
|
-+
|
|
|
|
-+ // We're setting the width property to prevent flickering during the
|
|
|
|
-+ // sliding animation with smaller views.
|
|
|
|
-+ viewNode.style.width = viewRect.width + "px";
|
|
|
|
-+
|
|
|
|
-+ // Kick off the transition!
|
|
|
|
-+ details.phase = TRANSITION_PHASES.TRANSITION;
|
|
|
|
-+
|
|
|
|
-+ // If we're going to show the main view, we can remove the
|
|
|
|
-+ // min-height property on the view container.
|
|
|
|
-+ if (viewNode.getAttribute("mainview")) {
|
|
|
|
-+ this._viewContainer.style.removeProperty("min-height");
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ this._viewStack.style.transform =
|
|
|
|
-+ "translateX(" + (moveToLeft ? "" : "-") + deltaX + "px)";
|
|
|
|
-+
|
|
|
|
-+ await new Promise(resolve => {
|
|
|
|
-+ details.resolve = resolve;
|
|
|
|
-+ this._viewContainer.addEventListener(
|
|
|
|
-+ "transitionend",
|
|
|
|
-+ (details.listener = ev => {
|
|
|
|
-+ // It's quite common that `height` on the view container doesn't need
|
|
|
|
-+ // to transition, so we make sure to do all the work on the transform
|
|
|
|
-+ // transition-end, because that is guaranteed to happen.
|
|
|
|
-+ if (ev.target != this._viewStack || ev.propertyName != "transform") {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+ this._viewContainer.removeEventListener(
|
|
|
|
-+ "transitionend",
|
|
|
|
-+ details.listener
|
|
|
|
-+ );
|
|
|
|
-+ delete details.listener;
|
|
|
|
-+ resolve();
|
|
|
|
-+ })
|
|
|
|
-+ );
|
|
|
|
-+ this._viewContainer.addEventListener(
|
|
|
|
-+ "transitioncancel",
|
|
|
|
-+ (details.cancelListener = ev => {
|
|
|
|
-+ if (ev.target != this._viewStack) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+ this._viewContainer.removeEventListener(
|
|
|
|
-+ "transitioncancel",
|
|
|
|
-+ details.cancelListener
|
|
|
|
-+ );
|
|
|
|
-+ delete details.cancelListener;
|
|
|
|
-+ resolve();
|
|
|
|
-+ })
|
|
|
|
-+ );
|
|
|
|
-+ });
|
|
|
|
-+
|
|
|
|
-+ // Bail out if the panel was closed during the transition.
|
|
|
|
-+ if (!nextPanelView.isOpenIn(this)) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+ prevPanelView.visible = false;
|
|
|
|
-+
|
|
|
|
-+ // This will complete the operation by removing any transition properties.
|
|
|
|
-+ nextPanelView.node.style.removeProperty("width");
|
|
|
|
-+ deepestNode.style.removeProperty("outline");
|
|
|
|
-+ this._cleanupTransitionPhase();
|
|
|
|
-+
|
|
|
|
-+ nextPanelView.focusSelectedElement();
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Attempt to clean up the attributes and properties set by `_transitionViews`
|
|
|
|
-+ * above. Which attributes and properties depends on the phase the transition
|
|
|
|
-+ * was left from.
|
|
|
|
-+ */
|
|
|
|
-+ _cleanupTransitionPhase() {
|
|
|
|
-+ if (!this._transitionDetails) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ const { phase, resolve, listener, cancelListener } =
|
|
|
|
-+ this._transitionDetails;
|
|
|
|
-+ this._transitionDetails = null;
|
|
|
|
-+
|
|
|
|
-+ if (phase >= TRANSITION_PHASES.START) {
|
|
|
|
-+ this._panel.style.removeProperty("width");
|
|
|
|
-+ this._panel.style.removeProperty("height");
|
|
|
|
-+ this._viewContainer.style.removeProperty("height");
|
|
|
|
-+ this._viewContainer.style.removeProperty("width");
|
|
|
|
-+ }
|
|
|
|
-+ if (phase >= TRANSITION_PHASES.PREPARE) {
|
|
|
|
-+ this._transitioning = false;
|
|
|
|
-+ this._viewStack.style.removeProperty("margin-inline-start");
|
|
|
|
-+ this._viewStack.style.removeProperty("transition");
|
|
|
|
-+ }
|
|
|
|
-+ if (phase >= TRANSITION_PHASES.TRANSITION) {
|
|
|
|
-+ this._viewStack.style.removeProperty("transform");
|
|
|
|
-+ if (listener) {
|
|
|
|
-+ this._viewContainer.removeEventListener("transitionend", listener);
|
|
|
|
-+ }
|
|
|
|
-+ if (cancelListener) {
|
|
|
|
-+ this._viewContainer.removeEventListener(
|
|
|
|
-+ "transitioncancel",
|
|
|
|
-+ cancelListener
|
|
|
|
-+ );
|
|
|
|
-+ }
|
|
|
|
-+ if (resolve) {
|
|
|
|
-+ resolve();
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ _calculateMaxHeight(aEvent) {
|
|
|
|
-+ // While opening the panel, we have to limit the maximum height of any
|
|
|
|
-+ // view based on the space that will be available. We cannot just use
|
|
|
|
-+ // window.screen.availTop and availHeight because these may return an
|
|
|
|
-+ // incorrect value when the window spans multiple screens.
|
|
|
|
-+ const anchor = this._panel.anchorNode;
|
|
|
|
-+ const anchorRect = anchor.getBoundingClientRect();
|
|
|
|
-+
|
|
|
|
-+ const screen = this._screenManager.screenForRect(
|
|
|
|
-+ anchor.screenX,
|
|
|
|
-+ anchor.screenY,
|
|
|
|
-+ anchorRect.width,
|
|
|
|
-+ anchorRect.height
|
|
|
|
-+ );
|
|
|
|
-+ const availTop = {},
|
|
|
|
-+ availHeight = {};
|
|
|
|
-+ screen.GetAvailRect({}, availTop, {}, availHeight);
|
|
|
|
-+ const cssAvailTop = availTop.value / screen.defaultCSSScaleFactor;
|
|
|
|
-+
|
|
|
|
-+ // The distance from the anchor to the available margin of the screen is
|
|
|
|
-+ // based on whether the panel will open towards the top or the bottom.
|
|
|
|
-+ let maxHeight;
|
|
|
|
-+ if (aEvent.alignmentPosition.startsWith("before_")) {
|
|
|
|
-+ maxHeight = anchor.screenY - cssAvailTop;
|
|
|
|
-+ } else {
|
|
|
|
-+ const anchorScreenBottom = anchor.screenY + anchorRect.height;
|
|
|
|
-+ const cssAvailHeight = availHeight.value / screen.defaultCSSScaleFactor;
|
|
|
|
-+ maxHeight = cssAvailTop + cssAvailHeight - anchorScreenBottom;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // To go from the maximum height of the panel to the maximum height of
|
|
|
|
-+ // the view stack, we need to subtract the height of the arrow and the
|
|
|
|
-+ // height of the opposite margin, but we cannot get their actual values
|
|
|
|
-+ // because the panel is not visible yet. However, we know that this is
|
|
|
|
-+ // currently 11px on Mac, 13px on Windows, and 13px on Linux. We also
|
|
|
|
-+ // want an extra margin, both for visual reasons and to prevent glitches
|
|
|
|
-+ // due to small rounding errors. So, we just use a value that makes
|
|
|
|
-+ // sense for all platforms. If the arrow visuals change significantly,
|
|
|
|
-+ // this value will be easy to adjust.
|
|
|
|
-+ const EXTRA_MARGIN_PX = 20;
|
|
|
|
-+ maxHeight -= EXTRA_MARGIN_PX;
|
|
|
|
-+ return maxHeight;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ handleEvent(aEvent) {
|
|
|
|
-+ // Only process actual popup events from the panel or events we generate
|
|
|
|
-+ // ourselves, but not from menus being shown from within the panel.
|
|
|
|
-+ if (
|
|
|
|
-+ aEvent.type.startsWith("popup") &&
|
|
|
|
-+ aEvent.target != this._panel &&
|
|
|
|
-+ aEvent.target != this.node
|
|
|
|
-+ ) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+ switch (aEvent.type) {
|
|
|
|
-+ case "keydown":
|
|
|
|
-+ // Since we start listening for the "keydown" event when the popup is
|
|
|
|
-+ // already showing and stop listening when the panel is hidden, we
|
|
|
|
-+ // always have at least one view open.
|
|
|
|
-+ const currentView = this.openViews[this.openViews.length - 1];
|
|
|
|
-+ currentView.keyNavigation(aEvent);
|
|
|
|
-+ break;
|
|
|
|
-+ case "mousemove":
|
|
|
|
-+ this.openViews.forEach(panelView => panelView.clearNavigation());
|
|
|
|
-+ break;
|
|
|
|
-+ case "popupshowing": {
|
|
|
|
-+ this._viewContainer.setAttribute("panelopen", "true");
|
|
|
|
-+ if (!this.node.hasAttribute("disablekeynav")) {
|
|
|
|
-+ // We add the keydown handler on the window so that it handles key
|
|
|
|
-+ // presses when a panel appears but doesn't get focus, as happens
|
|
|
|
-+ // when a button to open a panel is clicked with the mouse.
|
|
|
|
-+ // However, this means the listener is on an ancestor of the panel,
|
|
|
|
-+ // which means that handlers such as ToolbarKeyboardNavigator are
|
|
|
|
-+ // deeper in the tree. Therefore, this must be a capturing listener
|
|
|
|
-+ // so we get the event first.
|
|
|
|
-+ this.window.addEventListener("keydown", this, true);
|
|
|
|
-+ this._panel.addEventListener("mousemove", this);
|
|
|
|
-+ }
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ case "popuppositioned": {
|
|
|
|
-+ if (this._panel.state == "showing") {
|
|
|
|
-+ const maxHeight = this._calculateMaxHeight(aEvent);
|
|
|
|
-+ this._viewStack.style.maxHeight = maxHeight + "px";
|
|
|
|
-+ this._offscreenViewStack.style.maxHeight = maxHeight + "px";
|
|
|
|
-+ }
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ case "popupshown":
|
|
|
|
-+ // The main view is always open and visible when the panel is first
|
|
|
|
-+ // shown, so we can check the height of the description elements it
|
|
|
|
-+ // contains and notify consumers using the ViewShown event. In order to
|
|
|
|
-+ // minimize flicker we need to allow synchronous reflows, and we still
|
|
|
|
-+ // make sure the ViewShown event is dispatched synchronously.
|
|
|
|
-+ const mainPanelView = this.openViews[0];
|
|
|
|
-+ this._activateView(mainPanelView);
|
|
|
|
-+ break;
|
|
|
|
-+ case "popuphidden": {
|
|
|
|
-+ // WebExtensions consumers can hide the popup from viewshowing, or
|
|
|
|
-+ // mid-transition, which disrupts our state:
|
|
|
|
-+ this._transitioning = false;
|
|
|
|
-+ this._viewContainer.removeAttribute("panelopen");
|
|
|
|
-+ this._cleanupTransitionPhase();
|
|
|
|
-+ this.window.removeEventListener("keydown", this, true);
|
|
|
|
-+ this._panel.removeEventListener("mousemove", this);
|
|
|
|
-+ this.closeAllViews();
|
|
|
|
-+
|
|
|
|
-+ // Clear the main view size caches. The dimensions could be different
|
|
|
|
-+ // when the popup is opened again, e.g. through touch mode sizing.
|
|
|
|
-+ this._viewContainer.style.removeProperty("min-height");
|
|
|
|
-+ this._viewStack.style.removeProperty("max-height");
|
|
|
|
-+ this._viewContainer.style.removeProperty("width");
|
|
|
|
-+ this._viewContainer.style.removeProperty("height");
|
|
|
|
-+
|
|
|
|
-+ this.dispatchCustomEvent("PanelMultiViewHidden");
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+}
|
|
|
|
-+
|
|
|
|
-+/**
|
|
|
|
-+ * This is associated to <panelview> elements.
|
|
|
|
-+ */
|
|
|
|
-+export class PanelView extends AssociatedToNode {
|
|
|
|
-+ constructor(node) {
|
|
|
|
-+ super(node);
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Indicates whether the view is active. When this is false, consumers can
|
|
|
|
-+ * wait for the ViewShown event to know when the view becomes active.
|
|
|
|
-+ */
|
|
|
|
-+ this.active = false;
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Specifies whether the view should be focused when active. When this
|
|
|
|
-+ * is true, the first navigable element in the view will be focused
|
|
|
|
-+ * when the view becomes active. This should be set to true when the view
|
|
|
|
-+ * is activated from the keyboard. It will be set to false once the view
|
|
|
|
-+ * is active.
|
|
|
|
-+ */
|
|
|
|
-+ this.focusWhenActive = false;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Indicates whether the view is open in the specified PanelMultiView object.
|
|
|
|
-+ */
|
|
|
|
-+ isOpenIn(panelMultiView) {
|
|
|
|
-+ return this.node.panelMultiView == panelMultiView.node;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * The "mainview" attribute is set before the panel is opened when this view
|
|
|
|
-+ * is displayed as the main view, and is removed before the <panelview> is
|
|
|
|
-+ * displayed as a subview. The same view element can be displayed as a main
|
|
|
|
-+ * view and as a subview at different times.
|
|
|
|
-+ */
|
|
|
|
-+ set mainview(value) {
|
|
|
|
-+ if (value) {
|
|
|
|
-+ this.node.setAttribute("mainview", true);
|
|
|
|
-+ } else {
|
|
|
|
-+ this.node.removeAttribute("mainview");
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Determines whether the view is visible. Setting this to false also resets
|
|
|
|
-+ * the "active" property.
|
|
|
|
-+ */
|
|
|
|
-+ set visible(value) {
|
|
|
|
-+ if (value) {
|
|
|
|
-+ this.node.setAttribute("visible", true);
|
|
|
|
-+ } else {
|
|
|
|
-+ this.node.removeAttribute("visible");
|
|
|
|
-+ this.active = false;
|
|
|
|
-+ this.focusWhenActive = false;
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Constrains the width of this view using the "min-width" and "max-width"
|
|
|
|
-+ * styles. Setting this to zero removes the constraints.
|
|
|
|
-+ */
|
|
|
|
-+ set minMaxWidth(value) {
|
|
|
|
-+ const style = this.node.style;
|
|
|
|
-+ if (value) {
|
|
|
|
-+ style.minWidth = style.maxWidth = value + "px";
|
|
|
|
-+ } else {
|
|
|
|
-+ style.removeProperty("min-width");
|
|
|
|
-+ style.removeProperty("max-width");
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Adds a header with the given title, or removes it if the title is empty.
|
|
|
|
-+ */
|
|
|
|
-+ set headerText(value) {
|
|
|
|
-+ // If the header already exists, update or remove it as requested.
|
|
|
|
-+ let header = this.node.firstElementChild;
|
|
|
|
-+ if (header && header.classList.contains("panel-header")) {
|
|
|
|
-+ if (value) {
|
|
|
|
-+ header.querySelector(".panel-header > h1 > span").textContent = value;
|
|
|
|
-+ } else {
|
|
|
|
-+ header.remove();
|
|
|
|
-+ }
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // The header doesn't exist, only create it if needed.
|
|
|
|
-+ if (!value) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ header = this.document.createXULElement("box");
|
|
|
|
-+ header.classList.add("panel-header");
|
|
|
|
-+
|
|
|
|
-+ const backButton = this.document.createXULElement("toolbarbutton");
|
|
|
|
-+ backButton.className =
|
|
|
|
-+ "subviewbutton subviewbutton-iconic subviewbutton-back";
|
|
|
|
-+ backButton.setAttribute("closemenu", "none");
|
|
|
|
-+ backButton.setAttribute("tabindex", "0");
|
|
|
|
-+
|
|
|
|
-+ backButton.setAttribute(
|
|
|
|
-+ "aria-label",
|
|
|
|
-+ lazy.gBundle.GetStringFromName("panel.back")
|
|
|
|
-+ );
|
|
|
|
-+
|
|
|
|
-+ backButton.addEventListener("command", () => {
|
|
|
|
-+ // The panelmultiview element may change if the view is reused.
|
|
|
|
-+ this.node.panelMultiView.goBack();
|
|
|
|
-+ backButton.blur();
|
|
|
|
-+ });
|
|
|
|
-+
|
|
|
|
-+ const h1 = this.document.createElement("h1");
|
|
|
|
-+ const span = this.document.createElement("span");
|
|
|
|
-+ span.textContent = value;
|
|
|
|
-+ h1.appendChild(span);
|
|
|
|
-+
|
|
|
|
-+ header.append(backButton, h1);
|
|
|
|
-+ this.node.prepend(header);
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Populates the "knownWidth" and "knownHeight" properties with the current
|
|
|
|
-+ * dimensions of the view. These may be zero if the view is invisible.
|
|
|
|
-+ *
|
|
|
|
-+ * These values are relevant during transitions and are retained for backwards
|
|
|
|
-+ * navigation if the view is still open but is invisible.
|
|
|
|
-+ */
|
|
|
|
-+ captureKnownSize() {
|
|
|
|
-+ const rect = this._getBoundsWithoutFlushing(this.node);
|
|
|
|
-+ this.knownWidth = rect.width;
|
|
|
|
-+ this.knownHeight = rect.height;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Determine whether an element can only be navigated to with tab/shift+tab,
|
|
|
|
-+ * not the arrow keys.
|
|
|
|
-+ */
|
|
|
|
-+ _isNavigableWithTabOnly(element) {
|
|
|
|
-+ const tag = element.localName;
|
|
|
|
-+ return (
|
|
|
|
-+ tag == "menulist" ||
|
|
|
|
-+ tag == "input" ||
|
|
|
|
-+ tag == "textarea" ||
|
|
|
|
-+ // Allow tab to reach embedded documents in extension panels.
|
|
|
|
-+ tag == "browser"
|
|
|
|
-+ );
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Make a TreeWalker for keyboard navigation.
|
|
|
|
-+ *
|
|
|
|
-+ * @param {boolean} arrowKey If `true`, elements only navigable with tab are
|
|
|
|
-+ * excluded.
|
|
|
|
-+ */
|
|
|
|
-+ _makeNavigableTreeWalker(arrowKey) {
|
|
|
|
-+ const filter = node => {
|
|
|
|
-+ if (node.disabled) {
|
|
|
|
-+ return NodeFilter.FILTER_REJECT;
|
|
|
|
-+ }
|
|
|
|
-+ const bounds = this._getBoundsWithoutFlushing(node);
|
|
|
|
-+ if (bounds.width == 0 || bounds.height == 0) {
|
|
|
|
-+ return NodeFilter.FILTER_REJECT;
|
|
|
|
-+ }
|
|
|
|
-+ if (
|
|
|
|
-+ node.tagName == "button" ||
|
|
|
|
-+ node.tagName == "toolbarbutton" ||
|
|
|
|
-+ node.classList.contains("text-link") ||
|
|
|
|
-+ (!arrowKey && this._isNavigableWithTabOnly(node))
|
|
|
|
-+ ) {
|
|
|
|
-+ // Set the tabindex attribute to make sure the node is focusable.
|
|
|
|
-+ if (!node.hasAttribute("tabindex")) {
|
|
|
|
-+ node.setAttribute("tabindex", "-1");
|
|
|
|
-+ }
|
|
|
|
-+ return NodeFilter.FILTER_ACCEPT;
|
|
|
|
-+ }
|
|
|
|
-+ return NodeFilter.FILTER_SKIP;
|
|
|
|
-+ };
|
|
|
|
-+ return this.document.createTreeWalker(
|
|
|
|
-+ this.node,
|
|
|
|
-+ NodeFilter.SHOW_ELEMENT,
|
|
|
|
-+ filter
|
|
|
|
-+ );
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Get a TreeWalker which finds elements navigable with tab/shift+tab.
|
|
|
|
-+ */
|
|
|
|
-+ get _tabNavigableWalker() {
|
|
|
|
-+ if (!this.__tabNavigableWalker) {
|
|
|
|
-+ this.__tabNavigableWalker = this._makeNavigableTreeWalker(false);
|
|
|
|
-+ }
|
|
|
|
-+ return this.__tabNavigableWalker;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Get a TreeWalker which finds elements navigable with up/down arrow keys.
|
|
|
|
-+ */
|
|
|
|
-+ get _arrowNavigableWalker() {
|
|
|
|
-+ if (!this.__arrowNavigableWalker) {
|
|
|
|
-+ this.__arrowNavigableWalker = this._makeNavigableTreeWalker(true);
|
|
|
|
-+ }
|
|
|
|
-+ return this.__arrowNavigableWalker;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Element that is currently selected with the keyboard, or null if no element
|
|
|
|
-+ * is selected. Since the reference is held weakly, it can become null or
|
|
|
|
-+ * undefined at any time.
|
|
|
|
-+ */
|
|
|
|
-+ get selectedElement() {
|
|
|
|
-+ return this._selectedElement && this._selectedElement.get();
|
|
|
|
-+ }
|
|
|
|
-+ set selectedElement(value) {
|
|
|
|
-+ if (!value) {
|
|
|
|
-+ delete this._selectedElement;
|
|
|
|
-+ } else {
|
|
|
|
-+ this._selectedElement = Cu.getWeakReference(value);
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Focuses and moves keyboard selection to the first navigable element.
|
|
|
|
-+ * This is a no-op if there are no navigable elements.
|
|
|
|
-+ *
|
|
|
|
-+ * @param {boolean} homeKey - `true` if this is for the home key.
|
|
|
|
-+ * @param {boolean} skipBack - `true` if the Back button should be skipped.
|
|
|
|
-+ */
|
|
|
|
-+ focusFirstNavigableElement(homeKey = false, skipBack = false) {
|
|
|
|
-+ // The home key is conceptually similar to the up/down arrow keys.
|
|
|
|
-+ const walker = homeKey
|
|
|
|
-+ ? this._arrowNavigableWalker
|
|
|
|
-+ : this._tabNavigableWalker;
|
|
|
|
-+ walker.currentNode = walker.root;
|
|
|
|
-+ this.selectedElement = walker.firstChild();
|
|
|
|
-+ if (
|
|
|
|
-+ skipBack &&
|
|
|
|
-+ walker.currentNode &&
|
|
|
|
-+ walker.currentNode.classList.contains("subviewbutton-back") &&
|
|
|
|
-+ walker.nextNode()
|
|
|
|
-+ ) {
|
|
|
|
-+ this.selectedElement = walker.currentNode;
|
|
|
|
-+ }
|
|
|
|
-+ this.focusSelectedElement(/* byKey */ true);
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Focuses and moves keyboard selection to the last navigable element.
|
|
|
|
-+ * This is a no-op if there are no navigable elements.
|
|
|
|
-+ *
|
|
|
|
-+ * @param {boolean} endKey - `true` if this is for the end key.
|
|
|
|
-+ */
|
|
|
|
-+ focusLastNavigableElement(endKey = false) {
|
|
|
|
-+ // The end key is conceptually similar to the up/down arrow keys.
|
|
|
|
-+ const walker = endKey
|
|
|
|
-+ ? this._arrowNavigableWalker
|
|
|
|
-+ : this._tabNavigableWalker;
|
|
|
|
-+ walker.currentNode = walker.root;
|
|
|
|
-+ this.selectedElement = walker.lastChild();
|
|
|
|
-+ this.focusSelectedElement(/* byKey */ true);
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Based on going up or down, select the previous or next focusable element.
|
|
|
|
-+ *
|
|
|
|
-+ * @param {boolean} isDown - whether we're going down (true) or up (false).
|
|
|
|
-+ * @param {boolean} arrowKey - `true` if this is for the up/down arrow keys.
|
|
|
|
-+ *
|
|
|
|
-+ * @returns {DOMNode} the element we selected.
|
|
|
|
-+ */
|
|
|
|
-+ moveSelection(isDown, arrowKey = false) {
|
|
|
|
-+ const walker = arrowKey
|
|
|
|
-+ ? this._arrowNavigableWalker
|
|
|
|
-+ : this._tabNavigableWalker;
|
|
|
|
-+ const oldSel = this.selectedElement;
|
|
|
|
-+ let newSel;
|
|
|
|
-+ if (oldSel) {
|
|
|
|
-+ walker.currentNode = oldSel;
|
|
|
|
-+ newSel = isDown ? walker.nextNode() : walker.previousNode();
|
|
|
|
-+ }
|
|
|
|
-+ // If we couldn't find something, select the first or last item:
|
|
|
|
-+ if (!newSel) {
|
|
|
|
-+ walker.currentNode = walker.root;
|
|
|
|
-+ newSel = isDown ? walker.firstChild() : walker.lastChild();
|
|
|
|
-+ }
|
|
|
|
-+ this.selectedElement = newSel;
|
|
|
|
-+ return newSel;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Allow for navigating subview buttons using the arrow keys and the Enter key.
|
|
|
|
-+ * The Up and Down keys can be used to navigate the list up and down and the
|
|
|
|
-+ * Enter, Right or Left - depending on the text direction - key can be used to
|
|
|
|
-+ * simulate a click on the currently selected button.
|
|
|
|
-+ * The Right or Left key - depending on the text direction - can be used to
|
|
|
|
-+ * navigate to the previous view, functioning as a shortcut for the view's
|
|
|
|
-+ * back button.
|
|
|
|
-+ * Thus, in LTR mode:
|
|
|
|
-+ * - The Right key functions the same as the Enter key, simulating a click
|
|
|
|
-+ * - The Left key triggers a navigation back to the previous view.
|
|
|
|
-+ *
|
|
|
|
-+ * Key navigation is only enabled while the view is active, meaning that this
|
|
|
|
-+ * method will return early if it is invoked during a sliding transition.
|
|
|
|
-+ *
|
|
|
|
-+ * @param {KeyEvent} event
|
|
|
|
-+ */
|
|
|
|
-+ /* eslint-disable-next-line complexity */
|
|
|
|
-+ keyNavigation(event) {
|
|
|
|
-+ if (!this.active) {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ let focus = this.document.activeElement;
|
|
|
|
-+ // Make sure the focus is actually inside the panel. (It might not be if
|
|
|
|
-+ // the panel was opened with the mouse.) If it isn't, we don't care
|
|
|
|
-+ // about it for our purposes.
|
|
|
|
-+ // We use Node.compareDocumentPosition because Node.contains doesn't
|
|
|
|
-+ // behave as expected for anonymous content; e.g. the input inside a
|
|
|
|
-+ // textbox.
|
|
|
|
-+ if (
|
|
|
|
-+ focus &&
|
|
|
|
-+ !(
|
|
|
|
-+ this.node.compareDocumentPosition(focus) &
|
|
|
|
-+ Node.DOCUMENT_POSITION_CONTAINED_BY
|
|
|
|
-+ )
|
|
|
|
-+ ) {
|
|
|
|
-+ focus = null;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ // Extension panels contain embedded documents. We can't manage
|
|
|
|
-+ // keyboard navigation within those.
|
|
|
|
-+ if (focus && focus.tagName == "browser") {
|
|
|
|
-+ return;
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ const stop = () => {
|
|
|
|
-+ event.stopPropagation();
|
|
|
|
-+ event.preventDefault();
|
|
|
|
-+ };
|
|
|
|
-+
|
|
|
|
-+ // If the focused element is only navigable with tab, it wants the arrow
|
|
|
|
-+ // keys, etc. We shouldn't handle any keys except tab and shift+tab.
|
|
|
|
-+ // We make a function for this for performance reasons: we only want to
|
|
|
|
-+ // check this for keys we potentially care about, not *all* keys.
|
|
|
|
-+ const tabOnly = () => {
|
|
|
|
-+ // We use the real focus rather than this.selectedElement because focus
|
|
|
|
-+ // might have been moved without keyboard navigation (e.g. mouse click)
|
|
|
|
-+ // and this.selectedElement is only updated for keyboard navigation.
|
|
|
|
-+ return focus && this._isNavigableWithTabOnly(focus);
|
|
|
|
-+ };
|
|
|
|
-+
|
|
|
|
-+ // If a context menu is open, we must let it handle all keys.
|
|
|
|
-+ // Normally, this just happens, but because we have a capturing window
|
|
|
|
-+ // keydown listener, our listener takes precedence.
|
|
|
|
-+ // Again, we only want to do this check on demand for performance.
|
|
|
|
-+ const isContextMenuOpen = () => {
|
|
|
|
-+ if (!focus) {
|
|
|
|
-+ return false;
|
|
|
|
-+ }
|
|
|
|
-+ const contextNode = focus.closest("[context]");
|
|
|
|
-+ if (!contextNode) {
|
|
|
|
-+ return false;
|
|
|
|
-+ }
|
|
|
|
-+ const context = contextNode.getAttribute("context");
|
|
|
|
-+ const popup = this.document.getElementById(context);
|
|
|
|
-+ return popup && popup.state == "open";
|
|
|
|
-+ };
|
|
|
|
-+
|
|
|
|
-+ const keyCode = event.code;
|
|
|
|
-+ switch (keyCode) {
|
|
|
|
-+ case "ArrowDown":
|
|
|
|
-+ case "ArrowUp":
|
|
|
|
-+ if (tabOnly()) {
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ // Fall-through...
|
|
|
|
-+ case "Tab": {
|
|
|
|
-+ if (isContextMenuOpen()) {
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ stop();
|
|
|
|
-+ const isDown =
|
|
|
|
-+ keyCode == "ArrowDown" || (keyCode == "Tab" && !event.shiftKey);
|
|
|
|
-+ const button = this.moveSelection(isDown, keyCode != "Tab");
|
|
|
|
-+ Services.focus.setFocus(button, Services.focus.FLAG_BYKEY);
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ case "Home":
|
|
|
|
-+ if (tabOnly() || isContextMenuOpen()) {
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ stop();
|
|
|
|
-+ this.focusFirstNavigableElement(true);
|
|
|
|
-+ break;
|
|
|
|
-+ case "End":
|
|
|
|
-+ if (tabOnly() || isContextMenuOpen()) {
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ stop();
|
|
|
|
-+ this.focusLastNavigableElement(true);
|
|
|
|
-+ break;
|
|
|
|
-+ case "ArrowLeft":
|
|
|
|
-+ case "ArrowRight": {
|
|
|
|
-+ if (tabOnly() || isContextMenuOpen()) {
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ stop();
|
|
|
|
-+ if (
|
|
|
|
-+ (!this.window.RTL_UI && keyCode == "ArrowLeft") ||
|
|
|
|
-+ (this.window.RTL_UI && keyCode == "ArrowRight")
|
|
|
|
-+ ) {
|
|
|
|
-+ this.node.panelMultiView.goBack();
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ // If the current button is _not_ one that points to a subview, pressing
|
|
|
|
-+ // the arrow key shouldn't do anything.
|
|
|
|
-+ const button = this.selectedElement;
|
|
|
|
-+ if (!button || !button.classList.contains("subviewbutton-nav")) {
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+ // Fall-through...
|
|
|
|
-+ case "Space":
|
|
|
|
-+ case "NumpadEnter":
|
|
|
|
-+ case "Enter": {
|
|
|
|
-+ if (tabOnly() || isContextMenuOpen()) {
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ const button = this.selectedElement;
|
|
|
|
-+ if (!button) {
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ stop();
|
|
|
|
-+
|
|
|
|
-+ this._doingKeyboardActivation = true;
|
|
|
|
-+ // Unfortunately, 'tabindex' doesn't execute the default action, so
|
|
|
|
-+ // we explicitly do this here.
|
|
|
|
-+ // We are sending a command event, a mousedown event and then a click
|
|
|
|
-+ // event. This is done in order to mimic a "real" mouse click event.
|
|
|
|
-+ // Normally, the command event executes the action, then the click event
|
|
|
|
-+ // closes the menu. However, in some cases (e.g. the Library button),
|
|
|
|
-+ // there is no command event handler and the mousedown event executes the
|
|
|
|
-+ // action instead.
|
|
|
|
-+ button.doCommand();
|
|
|
|
-+ let dispEvent = new event.target.ownerGlobal.MouseEvent("mousedown", {
|
|
|
|
-+ bubbles: true,
|
|
|
|
-+ });
|
|
|
|
-+ button.dispatchEvent(dispEvent);
|
|
|
|
-+ dispEvent = new event.target.ownerGlobal.MouseEvent("click", {
|
|
|
|
-+ bubbles: true,
|
|
|
|
-+ });
|
|
|
|
-+ button.dispatchEvent(dispEvent);
|
|
|
|
-+ this._doingKeyboardActivation = false;
|
|
|
|
-+ break;
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Focus the last selected element in the view, if any.
|
|
|
|
-+ *
|
|
|
|
-+ * @param byKey {Boolean} whether focus was moved by the user pressing a key.
|
|
|
|
-+ * Needed to ensure we show focus styles in the right cases.
|
|
|
|
-+ */
|
|
|
|
-+ focusSelectedElement(byKey = false) {
|
|
|
|
-+ const selected = this.selectedElement;
|
|
|
|
-+ if (selected) {
|
|
|
|
-+ const flag = byKey ? "FLAG_BYKEY" : "FLAG_BYELEMENTFOCUS";
|
|
|
|
-+ Services.focus.setFocus(selected, Services.focus[flag]);
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+
|
|
|
|
-+ /**
|
|
|
|
-+ * Clear all traces of keyboard navigation happening right now.
|
|
|
|
-+ */
|
|
|
|
-+ clearNavigation() {
|
|
|
|
-+ const selected = this.selectedElement;
|
|
|
|
-+ if (selected) {
|
|
|
|
-+ selected.blur();
|
|
|
|
-+ this.selectedElement = null;
|
|
|
|
-+ }
|
|
|
|
-+ }
|
|
|
|
-+}
|
|
|
|
-diff --git a/suite/buoy/modules/SessionStore.sys.mjs b/suite/buoy/modules/SessionStore.sys.mjs
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/modules/SessionStore.sys.mjs
|
|
|
|
-@@ -0,0 +1,12 @@
|
|
|
|
-+/* 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 is a shim for SessionStore in moz-central to prevent bug 1713801. Only
|
|
|
|
-+ * the methods that appear to be hit by comm-central are implemented.
|
|
|
|
-+ */
|
|
|
|
-+export var SessionStore = {
|
|
|
|
-+ updateSessionStoreFromTablistener(aBrowser, aBrowsingContext, aData) {},
|
|
|
|
-+ maybeExitCrashedState() {},
|
|
|
|
-+};
|
|
|
|
-diff --git a/suite/buoy/moz-l10n/browser/appExtensionFields.ftl b/suite/buoy/moz-l10n/browser/appExtensionFields.ftl
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/moz-l10n/browser/appExtensionFields.ftl
|
|
|
|
-@@ -0,0 +1,10 @@
|
|
|
|
-+# 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/.
|
|
|
|
-+
|
|
|
|
-+## Theme names and descriptions used in the Themes panel in about:addons
|
|
|
|
-+
|
|
|
|
-+# "Auto" is short for automatic. It can be localized without limitations.
|
|
|
|
-+extension-default-theme-name-auto=System theme — auto
|
|
|
|
-+extension-default-theme-description=Follow the operating system setting for buttons, menus, and windows.
|
|
|
|
-+
|
|
|
|
-diff --git a/suite/buoy/moz-l10n/browser/branding/brandings.ftl b/suite/buoy/moz-l10n/browser/branding/brandings.ftl
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/moz-l10n/browser/branding/brandings.ftl
|
|
|
|
-@@ -0,0 +1,17 @@
|
|
|
|
-+# 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/.
|
|
|
|
-+
|
|
|
|
-+## The following feature names must be treated as a brand.
|
|
|
|
-+##
|
|
|
|
-+## They cannot be:
|
|
|
|
-+## - Transliterated.
|
|
|
|
-+## - Translated.
|
|
|
|
-+##
|
|
|
|
-+## Declension should be avoided where possible, leaving the original
|
|
|
|
-+## brand unaltered in prominent UI positions.
|
|
|
|
-+##
|
|
|
|
-+## For further details, consult:
|
|
|
|
-+## https://mozilla-l10n.github.io/styleguides/mozilla_general/#brands-copyright-and-trademark
|
|
|
|
-+
|
|
|
|
-+-profiler-brand-name = Firefox Profiler
|
|
|
|
-diff --git a/suite/buoy/moz.build b/suite/buoy/moz.build
|
|
|
|
-new file mode 100644
|
|
|
|
---- /dev/null
|
|
|
|
-+++ b/suite/buoy/moz.build
|
|
|
|
-@@ -0,0 +1,15 @@
|
|
|
|
-+EXTRA_JS_MODULES += [
|
|
|
|
-+ "modules/BrowserWindowTracker.sys.mjs",
|
|
|
|
-+ "modules/CustomizableUI.sys.mjs",
|
|
|
|
-+ "modules/EzE10SUtils.sys.mjs",
|
|
|
|
-+ "modules/PanelMultiView.sys.mjs",
|
|
|
|
-+]
|
|
|
|
-+
|
|
|
|
-+EXTRA_JS_MODULES.sessionstore += [
|
|
|
|
-+ "modules/SessionStore.sys.mjs",
|
|
|
|
-+]
|
|
|
|
-+
|
|
|
|
-+
|
|
|
|
-+JS_PREFERENCE_FILES += ["ZZ-buoy-prefs.js"]
|
|
|
|
-+
|
|
|
|
-+JAR_MANIFESTS += ["jar.mn"]
|
|
|
|
-diff --git a/suite/moz.build b/suite/moz.build
|
|
|
|
---- a/suite/moz.build
|
|
|
|
-+++ b/suite/moz.build
|
|
|
|
-@@ -24,16 +24,19 @@ DIRS += [
|
|
|
|
- if CONFIG["MOZ_THUNDERBIRD_RUST"]:
|
|
|
|
- DEFINES["MOZ_THUNDERBIRD_RUST"] = 1
|
|
|
|
-
|
|
|
|
- if CONFIG["MOZ_OVERRIDE_GKRUST"]:
|
|
|
|
- DIRS += [
|
|
|
|
- "../rust",
|
|
|
|
- ]
|
|
|
|
-
|
|
|
|
-+if CONFIG['MOZ_SUITE_BUOY']:
|
|
|
|
-+ DIRS += ['buoy']
|
|
|
|
-+
|
|
|
|
- if CONFIG['MOZ_IRC']:
|
|
|
|
- DIRS += ['chatzilla']
|
|
|
|
-
|
|
|
|
- if CONFIG["MAKENSISU"]:
|
|
|
|
- DIRS += ["installer/windows"]
|
|
|
|
-
|
|
|
|
- if CONFIG["MOZ_BUNDLED_FONTS"]:
|
|
|
|
- DIRS += ["/browser/fonts"]
|
|
|
|
-diff --git a/suite/moz.configure b/suite/moz.configure
|
|
|
|
---- a/suite/moz.configure
|
|
|
|
-+++ b/suite/moz.configure
|
|
|
|
-@@ -99,16 +99,29 @@ def moz_override_cargo_config(enable_rus
|
|
|
|
-
|
|
|
|
-
|
|
|
|
- set_config(
|
|
|
|
- "MOZ_OVERRIDE_CARGO_CONFIG",
|
|
|
|
- moz_override_cargo_config,
|
|
|
|
- when="--enable-thunderbird-rust",
|
|
|
|
- )
|
|
|
|
-
|
|
|
|
-+# =========================================================
|
|
|
|
-+# = Diagnostic "Buoy" Component
|
|
|
|
-+# =========================================================
|
|
|
|
-+option(
|
|
|
|
-+ "--enable-buoy", default=False, help="Enable building of the SeaMonkey Diagnostic Component"
|
|
|
|
-+)
|
|
|
|
-+
|
|
|
|
-+@depends_if("--enable-buoy")
|
|
|
|
-+def buoy(arg):
|
|
|
|
-+ return True
|
|
|
|
-+
|
|
|
|
-+set_config("MOZ_SUITE_BUOY", buoy)
|
|
|
|
-+
|
|
|
|
- # Building extensions is disabled by default.
|
|
|
|
-
|
|
|
|
- # =========================================================
|
|
|
|
- # = ChatZilla extension
|
|
|
|
- # =========================================================
|
|
|
|
- option(
|
|
|
|
- "--enable-irc", default=False, help="Enable building of the ChatZilla IRC extension"
|
|
|
|
- )
|
|
|