summaryrefslogtreecommitdiffstats
path: root/toolkit/components/prompts/content
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/prompts/content')
-rw-r--r--toolkit/components/prompts/content/adjustableTitle.js193
-rw-r--r--toolkit/components/prompts/content/commonDialog.css124
-rw-r--r--toolkit/components/prompts/content/commonDialog.js148
-rw-r--r--toolkit/components/prompts/content/commonDialog.xhtml103
-rw-r--r--toolkit/components/prompts/content/selectDialog.js83
-rw-r--r--toolkit/components/prompts/content/selectDialog.xhtml21
-rw-r--r--toolkit/components/prompts/content/tabprompts.css119
-rw-r--r--toolkit/components/prompts/content/tabprompts.sys.mjs298
8 files changed, 1089 insertions, 0 deletions
diff --git a/toolkit/components/prompts/content/adjustableTitle.js b/toolkit/components/prompts/content/adjustableTitle.js
new file mode 100644
index 0000000000..bd9afd909a
--- /dev/null
+++ b/toolkit/components/prompts/content/adjustableTitle.js
@@ -0,0 +1,193 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let { PromptUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromptUtils.sys.mjs"
+);
+
+const AdjustableTitle = {
+ _cssSnippet: `
+ #titleContainer {
+ /* This gets display: flex by virtue of being a row in a subdialog, from
+ * commonDialog.css . */
+ flex-shrink: 0;
+
+ flex-direction: row;
+ align-items: baseline;
+
+ margin-inline: 4px;
+ /* Ensure we don't exceed the bounds of the dialog: */
+ max-width: calc(100vw - 32px);
+
+ --icon-size: 16px;
+ }
+
+ #titleContainer[noicon] > .titleIcon {
+ display: none;
+ }
+
+ .titleIcon {
+ width: var(--icon-size);
+ height: var(--icon-size);
+ padding-inline-end: 4px;
+ flex-shrink: 0;
+
+ background-image: var(--icon-url, url("chrome://global/skin/icons/defaultFavicon.svg"));
+ background-size: 16px 16px;
+ background-origin: content-box;
+ background-repeat: no-repeat;
+ background-color: var(--in-content-page-background);
+ -moz-context-properties: fill;
+ fill: currentColor;
+ }
+
+ #titleCropper:not([nomaskfade]) {
+ display: inline-flex;
+ }
+
+ #titleCropper {
+ overflow: hidden;
+
+ justify-content: right;
+ mask-repeat: no-repeat;
+ /* go from left to right with the mask: */
+ --mask-dir: right;
+ }
+
+ #titleContainer:not([noicon]) > #titleCropper {
+ /* Align the icon and text: */
+ translate: 0 calc(-1px - max(.6 * var(--icon-size) - .6em, 0px));
+ }
+
+ #titleCropper[rtlorigin] {
+ justify-content: left;
+ /* go from right to left with the mask: */
+ --mask-dir: left;
+ }
+
+
+ #titleCropper:not([nomaskfade]) #titleText {
+ display: inline-flex;
+ white-space: nowrap;
+ }
+
+ #titleText {
+ font-weight: 600;
+ flex: 1 0 auto; /* Grow but do not shrink. */
+ unicode-bidi: plaintext; /* Ensure we align RTL text correctly. */
+ text-align: match-parent;
+ }
+
+ #titleCropper[overflown] {
+ mask-image: linear-gradient(to var(--mask-dir), transparent, black 100px);
+ }
+
+ /* hide the old title */
+ #infoTitle {
+ display: none;
+ }
+ `,
+
+ _insertMarkup() {
+ let iconEl = document.createElement("span");
+ iconEl.className = "titleIcon";
+ this._titleCropEl = document.createElement("span");
+ this._titleCropEl.id = "titleCropper";
+ this._titleEl = document.createElement("span");
+ this._titleEl.id = "titleText";
+ this._containerEl = document.createElement("div");
+ this._containerEl.id = "titleContainer";
+ this._containerEl.className = "dialogRow titleContainer";
+ this._titleCropEl.append(this._titleEl);
+ this._containerEl.append(iconEl, this._titleCropEl);
+ let targetID = document.documentElement.getAttribute("headerparent");
+ document.getElementById(targetID).prepend(this._containerEl);
+ let styleEl = document.createElement("style");
+ styleEl.textContent = this._cssSnippet;
+ document.documentElement.prepend(styleEl);
+ },
+
+ _overflowHandler() {
+ requestAnimationFrame(async () => {
+ let isOverflown;
+ try {
+ isOverflown = await window.promiseDocumentFlushed(() => {
+ return (
+ this._titleCropEl.getBoundingClientRect().width <
+ this._titleEl.getBoundingClientRect().width
+ );
+ });
+ } catch (ex) {
+ // In automated tests, this can fail with a DOM exception if
+ // the window has closed by the time layout tries to call us.
+ // In this case, just bail, and only log any other errors:
+ if (
+ !DOMException.isInstance(ex) ||
+ ex.name != "NoModificationAllowedError"
+ ) {
+ console.error(ex);
+ }
+ return;
+ }
+ this._titleCropEl.toggleAttribute("overflown", isOverflown);
+ if (isOverflown) {
+ this._titleEl.setAttribute("title", this._titleEl.textContent);
+ } else {
+ this._titleEl.removeAttribute("title");
+ }
+ });
+ },
+
+ _updateTitle(title) {
+ title = JSON.parse(title);
+ if (title.raw) {
+ this._titleEl.textContent = title.raw;
+ let { DIRECTION_RTL } = window.windowUtils;
+ this._titleCropEl.toggleAttribute(
+ "rtlorigin",
+ window.windowUtils.getDirectionFromText(title.raw) == DIRECTION_RTL
+ );
+ } else {
+ document.l10n.setAttributes(this._titleEl, title.l10nId);
+ }
+
+ if (!document.documentElement.hasAttribute("neediconheader")) {
+ this._containerEl.setAttribute("noicon", "true");
+ } else if (title.shouldUseMaskFade) {
+ this._overflowHandler();
+ } else {
+ this._titleCropEl.toggleAttribute("nomaskfade", true);
+ }
+ },
+
+ init() {
+ // Only run this if we're embedded and proton modals are enabled.
+ if (!window.docShell.chromeEventHandler) {
+ return;
+ }
+
+ this._insertMarkup();
+ let title = document.documentElement.getAttribute("headertitle");
+ if (title) {
+ this._updateTitle(title);
+ }
+ this._mutObs = new MutationObserver(() => {
+ this._updateTitle(document.documentElement.getAttribute("headertitle"));
+ });
+ this._mutObs.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["headertitle"],
+ });
+ },
+};
+
+document.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ AdjustableTitle.init();
+ },
+ { once: true }
+);
diff --git a/toolkit/components/prompts/content/commonDialog.css b/toolkit/components/prompts/content/commonDialog.css
new file mode 100644
index 0000000000..3521af13c6
--- /dev/null
+++ b/toolkit/components/prompts/content/commonDialog.css
@@ -0,0 +1,124 @@
+/* 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/. */
+
+:root {
+ min-width: 29em;
+}
+
+dialog[insecureauth] {
+ --icon-url: url("chrome://global/skin/icons/security-broken.svg");
+}
+
+#dialogGrid {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ align-items: center;
+ max-height: 100%;
+}
+
+.dialogRow:not([hidden]) {
+ display: contents;
+}
+
+#iconContainer {
+ align-self: start;
+}
+
+#infoContainer {
+ max-width: 45em;
+}
+
+#infoTitle {
+ margin-bottom: 1em;
+}
+
+#infoBody {
+ -moz-user-focus: normal;
+ user-select: text;
+ cursor: text !important;
+ white-space: pre-wrap;
+ word-break: break-word;
+ unicode-bidi: plaintext;
+ overflow-y: auto;
+}
+
+.sizeDetermined,
+.sizeDetermined::part(content-box),
+.sizeDetermined #infoBody,
+.sizeDetermined #infoRow,
+.sizeDetermined #infoContainer {
+ /* Allow stuff to shrink */
+ min-height: 0;
+}
+
+.sizeDetermined #infoRow {
+ /* Make the info row take all the available space, potentially shrinking,
+ * since it's what contains the infoBody, which is scrollable */
+ flex: 1;
+}
+
+#loginLabel, #password1Label {
+ text-align: start;
+}
+
+#loginTextbox,
+#password1Textbox {
+ text-align: match-parent;
+}
+
+/* use flexbox instead of grid: */
+dialog[subdialog],
+dialog[subdialog] #dialogGrid,
+dialog[subdialog] #infoContainer,
+dialog[subdialog] .dialogRow:not([hidden]) {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+}
+
+dialog[subdialog] #iconContainer {
+ display: none;
+}
+
+/* Fix padding/spacing */
+dialog[subdialog] {
+ --grid-padding: 16px;
+ /* All the inner items should have 4px inline margin, leading to 1.16em spacing
+ * between the dialog and its contents, and 8px horizontal spacing between items. */
+ padding: var(--grid-padding) calc(var(--grid-padding) - 4px);
+}
+
+/* Use an ID selector for the dialog to win on specificity... */
+#commonDialog[subdialog] description,
+#commonDialog[subdialog] checkbox {
+ margin: 0 4px;
+}
+
+#commonDialog[subdialog] label {
+ margin: 4px; /* Labels for input boxes should get block as well as the regular inline margin. */
+}
+
+#commonDialog[subdialog] .checkbox-label {
+ /* The checkbox already has the horizontal margin, so override the rule
+ * above. */
+ margin: 0;
+}
+
+#commonDialog[subdialog] input {
+ margin: 0 4px 4px;
+}
+
+/* Add vertical spaces between rows: */
+dialog[subdialog] .dialogRow {
+ margin-block: 0 var(--grid-padding);
+}
+
+/* Fix spacing in the `prompt()` case: */
+dialog[subdialog] #loginLabel[value=""] {
+ display: none;
+}
+
+dialog[subdialog][windowtype="prompt:prompt"] #infoRow {
+ margin-bottom: 4px;
+}
diff --git a/toolkit/components/prompts/content/commonDialog.js b/toolkit/components/prompts/content/commonDialog.js
new file mode 100644
index 0000000000..a18b7cbd6c
--- /dev/null
+++ b/toolkit/components/prompts/content/commonDialog.js
@@ -0,0 +1,148 @@
+/* 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/. */
+
+const { CommonDialog } = ChromeUtils.importESModule(
+ "resource://gre/modules/CommonDialog.sys.mjs"
+);
+
+// imported by adjustableTitle.js loaded in the same context:
+/* globals PromptUtils */
+
+var propBag, args, Dialog;
+
+// Inherit color scheme overrides from parent window. This is to inherit the
+// color scheme of dark themed PBM windows.
+{
+ let openerColorSchemeOverride =
+ window.opener?.browsingContext?.top.prefersColorSchemeOverride;
+ if (
+ openerColorSchemeOverride &&
+ window.browsingContext == window.browsingContext.top
+ ) {
+ window.browsingContext.prefersColorSchemeOverride =
+ openerColorSchemeOverride;
+ }
+}
+
+function commonDialogOnLoad() {
+ propBag = window.arguments[0]
+ .QueryInterface(Ci.nsIWritablePropertyBag2)
+ .QueryInterface(Ci.nsIWritablePropertyBag);
+ // Convert to a JS object
+ args = {};
+ for (let prop of propBag.enumerator) {
+ args[prop.name] = prop.value;
+ }
+
+ let dialog = document.getElementById("commonDialog");
+
+ let needIconifiedHeader =
+ args.modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT ||
+ ["promptUserAndPass", "promptPassword"].includes(args.promptType) ||
+ args.headerIconURL;
+ let root = document.documentElement;
+ if (needIconifiedHeader) {
+ root.setAttribute("neediconheader", "true");
+ }
+ let title = { raw: args.title };
+ let { promptPrincipal } = args;
+ if (promptPrincipal) {
+ if (promptPrincipal.isNullPrincipal) {
+ title = { l10nId: "common-dialog-title-null" };
+ } else if (promptPrincipal.isSystemPrincipal) {
+ title = { l10nId: "common-dialog-title-system" };
+ root.style.setProperty(
+ "--icon-url",
+ "url('chrome://branding/content/icon32.png')"
+ );
+ } else if (promptPrincipal.addonPolicy) {
+ title.raw = promptPrincipal.addonPolicy.name;
+ } else if (promptPrincipal.isContentPrincipal) {
+ try {
+ title.raw = promptPrincipal.URI.displayHostPort;
+ } catch (ex) {
+ // hostPort getter can throw, e.g. for about URIs.
+ title.raw = promptPrincipal.originNoSuffix;
+ }
+ // hostPort can be empty for file URIs.
+ if (!title.raw) {
+ title.raw = promptPrincipal.prePath;
+ }
+ } else {
+ title = { l10nId: "common-dialog-title-unknown" };
+ }
+ } else if (args.authOrigin) {
+ title = { raw: args.authOrigin };
+ }
+ if (args.headerIconURL) {
+ root.style.setProperty("--icon-url", `url('${args.headerIconURL}')`);
+ }
+ // Fade and crop potentially long raw titles, e.g., origins and hostnames.
+ title.shouldUseMaskFade = title.raw && (args.authOrigin || promptPrincipal);
+ root.setAttribute("headertitle", JSON.stringify(title));
+ if (args.isInsecureAuth) {
+ dialog.setAttribute("insecureauth", "true");
+ }
+
+ let ui = {
+ prompt: window,
+ loginContainer: document.getElementById("loginContainer"),
+ loginTextbox: document.getElementById("loginTextbox"),
+ loginLabel: document.getElementById("loginLabel"),
+ password1Container: document.getElementById("password1Container"),
+ password1Textbox: document.getElementById("password1Textbox"),
+ password1Label: document.getElementById("password1Label"),
+ infoRow: document.getElementById("infoRow"),
+ infoBody: document.getElementById("infoBody"),
+ infoTitle: document.getElementById("infoTitle"),
+ infoIcon: document.getElementById("infoIcon"),
+ checkbox: document.getElementById("checkbox"),
+ checkboxContainer: document.getElementById("checkboxContainer"),
+ button3: dialog.getButton("extra2"),
+ button2: dialog.getButton("extra1"),
+ button1: dialog.getButton("cancel"),
+ button0: dialog.getButton("accept"),
+ focusTarget: window,
+ };
+
+ Dialog = new CommonDialog(args, ui);
+ window.addEventListener("dialogclosing", function (aEvent) {
+ if (aEvent.detail?.abort) {
+ Dialog.abortPrompt();
+ }
+ });
+ document.addEventListener("dialogaccept", function () {
+ Dialog.onButton0();
+ });
+ document.addEventListener("dialogcancel", function () {
+ Dialog.onButton1();
+ });
+ document.addEventListener("dialogextra1", function () {
+ Dialog.onButton2();
+ window.close();
+ });
+ document.addEventListener("dialogextra2", function () {
+ Dialog.onButton3();
+ window.close();
+ });
+ document.subDialogSetDefaultFocus = isInitialFocus =>
+ Dialog.setDefaultFocus(isInitialFocus);
+ Dialog.onLoad(dialog);
+
+ // resize the window to the content
+ window.sizeToContent();
+
+ // If the icon hasn't loaded yet, size the window to the content again when
+ // it does, as its layout can change.
+ ui.infoIcon.addEventListener("load", () => window.sizeToContent());
+
+ window.getAttention();
+}
+
+function commonDialogOnUnload() {
+ // Convert args back into property bag
+ for (let propName in args) {
+ propBag.setProperty(propName, args[propName]);
+ }
+}
diff --git a/toolkit/components/prompts/content/commonDialog.xhtml b/toolkit/components/prompts/content/commonDialog.xhtml
new file mode 100644
index 0000000000..02e0749a9e
--- /dev/null
+++ b/toolkit/components/prompts/content/commonDialog.xhtml
@@ -0,0 +1,103 @@
+<?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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/content/commonDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/commonDialog.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window
+ id="commonDialogWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ aria-describedby="infoBody"
+ headerparent="dialogGrid"
+ onunload="commonDialogOnUnload();"
+>
+ <dialog id="commonDialog" buttonpack="end">
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link rel="localization" href="toolkit/global/commonDialog.ftl" />
+ </linkset>
+ <script src="chrome://global/content/adjustableTitle.js" />
+ <script src="chrome://global/content/commonDialog.js" />
+ <script src="chrome://global/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://global/content/customElements.js" />
+ <script>
+ /* eslint-disable no-undef */
+ document.addEventListener("DOMContentLoaded", function () {
+ commonDialogOnLoad();
+ });
+ </script>
+
+ <commandset id="selectEditMenuItems">
+ <command
+ id="cmd_copy"
+ oncommand="goDoCommand('cmd_copy')"
+ disabled="true"
+ />
+ <command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')" />
+ </commandset>
+
+ <popupset id="contentAreaContextSet">
+ <menupopup
+ id="contentAreaContextMenu"
+ onpopupshowing="goUpdateCommand('cmd_copy')"
+ >
+ <menuitem
+ id="context-copy"
+ data-l10n-id="common-dialog-copy-cmd"
+ command="cmd_copy"
+ disabled="true"
+ />
+ <menuitem
+ id="context-selectall"
+ data-l10n-id="common-dialog-select-all-cmd"
+ command="cmd_selectAll"
+ />
+ </menupopup>
+ </popupset>
+
+ <div xmlns="http://www.w3.org/1999/xhtml" id="dialogGrid">
+ <div class="dialogRow" id="infoRow" hidden="hidden">
+ <div id="iconContainer">
+ <xul:image id="infoIcon" />
+ </div>
+ <div id="infoContainer">
+ <xul:description id="infoTitle" />
+ <xul:description
+ id="infoBody"
+ context="contentAreaContextMenu"
+ noinitialfocus="true"
+ />
+ </div>
+ </div>
+ <div id="loginContainer" class="dialogRow" hidden="hidden">
+ <xul:label
+ id="loginLabel"
+ data-l10n-id="common-dialog-username"
+ control="loginTextbox"
+ />
+ <input type="text" id="loginTextbox" dir="ltr" />
+ </div>
+ <div id="password1Container" class="dialogRow" hidden="hidden">
+ <xul:label
+ id="password1Label"
+ data-l10n-id="common-dialog-password"
+ control="password1Textbox"
+ />
+ <input type="password" id="password1Textbox" dir="ltr" />
+ </div>
+ <div id="checkboxContainer" class="dialogRow" hidden="hidden">
+ <div />
+ <!-- spacer -->
+ <xul:checkbox id="checkbox" oncommand="Dialog.onCheckbox()" />
+ </div>
+ </div>
+ </dialog>
+</window>
diff --git a/toolkit/components/prompts/content/selectDialog.js b/toolkit/components/prompts/content/selectDialog.js
new file mode 100644
index 0000000000..86809bc879
--- /dev/null
+++ b/toolkit/components/prompts/content/selectDialog.js
@@ -0,0 +1,83 @@
+/* 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/. */
+
+// Defined in dialog.xml.
+/* globals centerWindowOnScreen:false, moveToAlertPosition:false */
+
+var propBag, listBox, args;
+
+function onDCL() {
+ propBag = window.arguments[0]
+ .QueryInterface(Ci.nsIWritablePropertyBag2)
+ .QueryInterface(Ci.nsIWritablePropertyBag);
+
+ // Convert to a JS object
+ let args = {};
+ for (let prop of propBag.enumerator) {
+ args[prop.name] = prop.value;
+ }
+
+ let promptType = propBag.getProperty("promptType");
+ if (promptType != "select") {
+ console.error("selectDialog opened for unknown type: ", promptType);
+ window.close();
+ }
+
+ // Default to canceled.
+ propBag.setProperty("ok", false);
+
+ document.title = propBag.getProperty("title");
+
+ let text = propBag.getProperty("text");
+ document.getElementById("info.txt").setAttribute("value", text);
+
+ let items = propBag.getProperty("list");
+ listBox = document.getElementById("list");
+
+ for (let i = 0; i < items.length; i++) {
+ let str = items[i];
+ if (str == "") {
+ str = "<>";
+ }
+ listBox.appendItem(str);
+ listBox.getItemAtIndex(i).addEventListener("dblclick", dialogDoubleClick);
+ }
+ listBox.selectedIndex = 0;
+}
+
+function onLoad() {
+ listBox.focus();
+
+ document.addEventListener("dialogaccept", dialogOK);
+ // resize the window to the content
+ window.sizeToContent();
+
+ // Move to the right location
+ moveToAlertPosition();
+ centerWindowOnScreen();
+
+ // play sound
+ try {
+ if (!args.openedWithTabDialog) {
+ Cc["@mozilla.org/sound;1"]
+ .getService(Ci.nsISound)
+ .playEventSound(Ci.nsISound.EVENT_SELECT_DIALOG_OPEN);
+ }
+ } catch (e) {}
+
+ Services.obs.notifyObservers(window, "select-dialog-loaded");
+}
+
+function dialogOK() {
+ propBag.setProperty("selected", listBox.selectedIndex);
+ propBag.setProperty("ok", true);
+}
+
+function dialogDoubleClick() {
+ dialogOK();
+ window.close();
+}
+
+document.addEventListener("DOMContentLoaded", onDCL);
+window.addEventListener("load", onLoad, { once: true });
diff --git a/toolkit/components/prompts/content/selectDialog.xhtml b/toolkit/components/prompts/content/selectDialog.xhtml
new file mode 100644
index 0000000000..27963ebd57
--- /dev/null
+++ b/toolkit/components/prompts/content/selectDialog.xhtml
@@ -0,0 +1,21 @@
+<?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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<!DOCTYPE window>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <dialog>
+ <script src="chrome://global/content/selectDialog.js" />
+ <keyset id="dialogKeys" />
+ <vbox style="width: 24em; margin: 5px">
+ <label id="info.txt" />
+ <vbox>
+ <richlistbox id="list" class="theme-listbox" style="height: 8em" />
+ </vbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/toolkit/components/prompts/content/tabprompts.css b/toolkit/components/prompts/content/tabprompts.css
new file mode 100644
index 0000000000..539e8792cc
--- /dev/null
+++ b/toolkit/components/prompts/content/tabprompts.css
@@ -0,0 +1,119 @@
+/* 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/. */
+
+/* Tab Modal Prompt boxes */
+
+.tabModalBackground {
+ justify-content: center;
+ flex-direction: column;
+}
+
+.tabModalBackground,
+tabmodalprompt {
+ width: 100%;
+ height: 100%;
+}
+
+tabmodalprompt {
+ --tabmodalprompt-padding: 20px;
+ overflow: hidden;
+ text-shadow: none; /* remove lightweight theme text-shadow */
+}
+
+tabmodalprompt:not([hidden]) {
+ display: grid;
+ grid-template-rows: 1fr [dialog-start] auto [dialog-end] 2fr;
+ justify-items: center;
+}
+
+/*
+ Adjustments for chrome level tab-prompts to make them
+ overlap with the upper chrome UI and move them in
+ front of content prompts.
+*/
+tabmodalprompt.tab-prompt {
+ overflow: visible;
+ z-index: 1;
+ grid-template-rows: [dialog-start] auto [dialog-end] 1fr;
+}
+
+tabmodalprompt.tab-prompt .tabmodalprompt-mainContainer {
+ margin-top: -5px;
+}
+
+.tabmodalprompt-mainContainer {
+ min-width: 20em;
+ min-height: 12em;
+ max-width: calc(60% + calc(var(--tabmodalprompt-padding) * 2));
+ -moz-user-focus: normal;
+ grid-row: dialog;
+
+ display: flex;
+ flex-direction: column;
+}
+
+.tabmodalprompt-topContainer {
+ flex-grow: 1;
+ padding: var(--tabmodalprompt-padding);
+ display: grid;
+ grid-template-columns: auto 1fr;
+ align-items: baseline;
+ align-content: center; /* center content vertically */
+ max-width: 100%;
+ min-height: 0;
+ max-height: 60vh;
+ box-sizing: border-box;
+}
+
+.tabmodalprompt-topContainer > div:not(.tabmodalprompt-infoContainer, [hidden]) {
+ display: contents;
+}
+
+.tabmodalprompt-infoContainer {
+ grid-column: span 2;
+
+ display: block;
+ margin-block: auto;
+ max-width: 100%;
+ height: 100%;
+ min-height: 0;
+ justify-self: center; /* center text, but only when it fits in one line */
+}
+
+/* When all elements in the first column are hidden, prevent the second column
+ from becoming the first one because it won't have the right fraction */
+.tabmodalprompt-topContainer > div > *:nth-child(2) {
+ grid-column: 2;
+}
+
+.infoTitle {
+ margin-bottom: 1em !important;
+ font-weight: bold;
+}
+
+.infoBody {
+ margin: 0 !important;
+ -moz-user-focus: normal;
+ user-select: text;
+ cursor: text !important;
+ white-space: pre-wrap;
+ unicode-bidi: plaintext;
+ outline: none; /* remove focus outline */
+ overflow: auto;
+ max-width: 100%;
+ max-height: 100%;
+}
+
+tabmodalprompt label[value=""] {
+ display: none;
+}
+
+.tabmodalprompt-buttonContainer {
+ display: flex;
+ padding: 12px var(--tabmodalprompt-padding) 15px;
+}
+
+.tabmodalprompt-buttonSpacer {
+ flex-grow: 1;
+}
diff --git a/toolkit/components/prompts/content/tabprompts.sys.mjs b/toolkit/components/prompts/content/tabprompts.sys.mjs
new file mode 100644
index 0000000000..7edb024fa8
--- /dev/null
+++ b/toolkit/components/prompts/content/tabprompts.sys.mjs
@@ -0,0 +1,298 @@
+/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+export var TabModalPrompt = class {
+ constructor(win) {
+ this.win = win;
+ let newPrompt = (this.element =
+ win.document.createElement("tabmodalprompt"));
+ win.MozXULElement.insertFTLIfNeeded("toolkit/global/tabprompts.ftl");
+ newPrompt.setAttribute("role", "dialog");
+ let randomIdSuffix = Math.random().toString(32).substring(2);
+ newPrompt.setAttribute("aria-describedby", `infoBody-${randomIdSuffix}`);
+ newPrompt.appendChild(
+ win.MozXULElement.parseXULToFragment(
+ `
+ <div class="tabmodalprompt-mainContainer" xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <div class="tabmodalprompt-topContainer">
+ <div class="tabmodalprompt-infoContainer">
+ <div class="tabmodalprompt-infoTitle infoTitle" hidden="hidden"/>
+ <div class="tabmodalprompt-infoBody infoBody" id="infoBody-${randomIdSuffix}" tabindex="-1"/>
+ </div>
+
+ <div class="tabmodalprompt-loginContainer" hidden="hidden">
+ <xul:label class="tabmodalprompt-loginLabel" data-l10n-id="tabmodalprompt-username" control="loginTextbox-${randomIdSuffix}"/>
+ <input class="tabmodalprompt-loginTextbox" id="loginTextbox-${randomIdSuffix}"/>
+ </div>
+
+ <div class="tabmodalprompt-password1Container" hidden="hidden">
+ <xul:label class="tabmodalprompt-password1Label" data-l10n-id="tabmodalprompt-password" control="password1Textbox-${randomIdSuffix}"/>
+ <input class="tabmodalprompt-password1Textbox" type="password" id="password1Textbox-${randomIdSuffix}"/>
+ </div>
+
+ <div class="tabmodalprompt-checkboxContainer" hidden="hidden">
+ <div/>
+ <xul:checkbox class="tabmodalprompt-checkbox"/>
+ </div>
+
+ <!-- content goes here -->
+ </div>
+ <div class="tabmodalprompt-buttonContainer">
+ <xul:button class="tabmodalprompt-button3" hidden="true"/>
+ <div class="tabmodalprompt-buttonSpacer"/>
+ <xul:button class="tabmodalprompt-button0" data-l10n-id="tabmodalprompt-ok-button"/>
+ <xul:button class="tabmodalprompt-button2" hidden="true"/>
+ <xul:button class="tabmodalprompt-button1" data-l10n-id="tabmodalprompt-cancel-button"/>
+ </div>
+ </div>`
+ )
+ );
+
+ this.ui = {
+ prompt: this,
+ promptContainer: this.element,
+ mainContainer: newPrompt.querySelector(".tabmodalprompt-mainContainer"),
+ loginContainer: newPrompt.querySelector(".tabmodalprompt-loginContainer"),
+ loginTextbox: newPrompt.querySelector(".tabmodalprompt-loginTextbox"),
+ loginLabel: newPrompt.querySelector(".tabmodalprompt-loginLabel"),
+ password1Container: newPrompt.querySelector(
+ ".tabmodalprompt-password1Container"
+ ),
+ password1Textbox: newPrompt.querySelector(
+ ".tabmodalprompt-password1Textbox"
+ ),
+ password1Label: newPrompt.querySelector(".tabmodalprompt-password1Label"),
+ infoContainer: newPrompt.querySelector(".tabmodalprompt-infoContainer"),
+ infoBody: newPrompt.querySelector(".tabmodalprompt-infoBody"),
+ infoTitle: newPrompt.querySelector(".tabmodalprompt-infoTitle"),
+ infoIcon: null,
+ rows: newPrompt.querySelector(".tabmodalprompt-topContainer"),
+ checkbox: newPrompt.querySelector(".tabmodalprompt-checkbox"),
+ checkboxContainer: newPrompt.querySelector(
+ ".tabmodalprompt-checkboxContainer"
+ ),
+ button3: newPrompt.querySelector(".tabmodalprompt-button3"),
+ button2: newPrompt.querySelector(".tabmodalprompt-button2"),
+ button1: newPrompt.querySelector(".tabmodalprompt-button1"),
+ button0: newPrompt.querySelector(".tabmodalprompt-button0"),
+ // focusTarget (for BUTTON_DELAY_ENABLE) not yet supported
+ };
+
+ if (AppConstants.XP_UNIX) {
+ // Reorder buttons on Linux
+ let buttonContainer = newPrompt.querySelector(
+ ".tabmodalprompt-buttonContainer"
+ );
+ buttonContainer.appendChild(this.ui.button3);
+ buttonContainer.appendChild(this.ui.button2);
+ buttonContainer.appendChild(
+ newPrompt.querySelector(".tabmodalprompt-buttonSpacer")
+ );
+ buttonContainer.appendChild(this.ui.button1);
+ buttonContainer.appendChild(this.ui.button0);
+ }
+
+ this.ui.button0.addEventListener(
+ "command",
+ this.onButtonClick.bind(this, 0)
+ );
+ this.ui.button1.addEventListener(
+ "command",
+ this.onButtonClick.bind(this, 1)
+ );
+ this.ui.button2.addEventListener(
+ "command",
+ this.onButtonClick.bind(this, 2)
+ );
+ this.ui.button3.addEventListener(
+ "command",
+ this.onButtonClick.bind(this, 3)
+ );
+ // Anonymous wrapper used here because |Dialog| doesn't exist until init() is called!
+ this.ui.checkbox.addEventListener("command", () => {
+ this.Dialog.onCheckbox();
+ });
+
+ /**
+ * Based on dialog.xml handlers
+ */
+ this.element.addEventListener(
+ "keypress",
+ event => {
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_RETURN:
+ this.onKeyAction("default", event);
+ break;
+
+ case KeyEvent.DOM_VK_ESCAPE:
+ this.onKeyAction("cancel", event);
+ break;
+
+ default:
+ if (
+ AppConstants.platform == "macosx" &&
+ event.key == "." &&
+ event.metaKey
+ ) {
+ this.onKeyAction("cancel", event);
+ }
+ break;
+ }
+ },
+ { mozSystemGroup: true }
+ );
+
+ this.element.addEventListener(
+ "focus",
+ event => {
+ let bnum = this.args.defaultButtonNum || 0;
+ let defaultButton = this.ui["button" + bnum];
+
+ if (AppConstants.platform == "macosx") {
+ // On OS X, the default button always stays marked as such (until
+ // the entire prompt blurs).
+ defaultButton.setAttribute("default", "true");
+ } else {
+ // On other platforms, the default button is only marked as such
+ // when no other button has focus. XUL buttons on not-OSX will
+ // react to pressing enter as a command, so you can't trigger the
+ // default without tabbing to it or something that isn't a button.
+ let focusedDefault = event.originalTarget == defaultButton;
+ let someButtonFocused =
+ event.originalTarget.localName == "button" ||
+ event.originalTarget.localName == "toolbarbutton";
+ if (focusedDefault || !someButtonFocused) {
+ defaultButton.setAttribute("default", "true");
+ }
+ }
+ },
+ true
+ );
+
+ this.element.addEventListener("blur", () => {
+ // If focus shifted to somewhere else in the browser, don't make
+ // the default button look active.
+ let bnum = this.args.defaultButtonNum || 0;
+ let button = this.ui["button" + bnum];
+ button.removeAttribute("default");
+ });
+ }
+
+ init(args, linkedTab, onCloseCallback) {
+ this.args = args;
+ this.linkedTab = linkedTab;
+ this.onCloseCallback = onCloseCallback;
+
+ if (args.enableDelay) {
+ throw new Error(
+ "BUTTON_DELAY_ENABLE not yet supported for tab-modal prompts"
+ );
+ }
+
+ // Apply styling depending on modalType (content or tab prompt)
+ if (args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) {
+ this.element.classList.add("tab-prompt");
+ } else {
+ this.element.classList.add("content-prompt");
+ }
+
+ // We need to remove the prompt when the tab or browser window is closed or
+ // the page navigates, else we never unwind the event loop and that's sad times.
+ // Remember to cleanup in shutdownPrompt()!
+ this.win.addEventListener("resize", this);
+ this.win.addEventListener("unload", this);
+ if (linkedTab) {
+ linkedTab.addEventListener("TabClose", this);
+ }
+ // Note:
+ // nsPrompter.js or in e10s mode browser-parent.js call abortPrompt,
+ // when the domWindow, for which the prompt was created, generates
+ // a "pagehide" event.
+
+ let { CommonDialog } = ChromeUtils.importESModule(
+ "resource://gre/modules/CommonDialog.sys.mjs"
+ );
+ this.Dialog = new CommonDialog(args, this.ui);
+ this.Dialog.onLoad(null);
+
+ // For content prompts display the tabprompt title that shows the prompt origin when
+ // the prompt origin is not the same as that of the top window.
+ if (
+ args.modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT &&
+ args.showCallerOrigin
+ ) {
+ this.ui.infoTitle.removeAttribute("hidden");
+ }
+
+ // TODO: should unhide buttonSpacer on Windows when there are 4 buttons.
+ // Better yet, just drop support for 4-button dialogs. (bug 609510)
+ }
+
+ shutdownPrompt() {
+ // remove our event listeners
+ try {
+ this.win.removeEventListener("resize", this);
+ this.win.removeEventListener("unload", this);
+ if (this.linkedTab) {
+ this.linkedTab.removeEventListener("TabClose", this);
+ }
+ } catch (e) {}
+ // invoke callback
+ this.onCloseCallback();
+ this.win = null;
+ this.ui = null;
+ // Intentionally not cleaning up |this.element| here --
+ // TabModalPromptBox.removePrompt() would need it and it might not
+ // be called yet -- see browser_double_close_tabs.js.
+ }
+
+ abortPrompt() {
+ // Called from other code when the page changes.
+ this.Dialog.abortPrompt();
+ this.shutdownPrompt();
+ }
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "unload":
+ case "TabClose":
+ this.abortPrompt();
+ break;
+ }
+ }
+
+ onButtonClick(buttonNum) {
+ // We want to do all the work her asynchronously off a Gecko
+ // runnable, because of situations like the one described in
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1167575#c35 : we
+ // get here off processing of an OS event and will also process
+ // one more Gecko runnable before we break out of the event loop
+ // spin whoever posted the prompt is doing. If we do all our
+ // work sync, we will exit modal state _before_ processing that
+ // runnable, and if exiting moral state posts a runnable we will
+ // incorrectly process that runnable before leaving our event
+ // loop spin.
+ Services.tm.dispatchToMainThread(() => {
+ this.Dialog["onButton" + buttonNum]();
+ this.shutdownPrompt();
+ });
+ }
+
+ onKeyAction(action, event) {
+ if (event.defaultPrevented) {
+ return;
+ }
+
+ event.stopPropagation();
+ if (action == "default") {
+ let bnum = this.args.defaultButtonNum || 0;
+ this.onButtonClick(bnum);
+ } else {
+ // action == "cancel"
+ this.onButtonClick(1); // Cancel button
+ }
+ }
+};