diff options
Diffstat (limited to 'toolkit/components/prompts/content')
-rw-r--r-- | toolkit/components/prompts/content/adjustableTitle.js | 193 | ||||
-rw-r--r-- | toolkit/components/prompts/content/commonDialog.css | 133 | ||||
-rw-r--r-- | toolkit/components/prompts/content/commonDialog.js | 199 | ||||
-rw-r--r-- | toolkit/components/prompts/content/commonDialog.xhtml | 119 | ||||
-rw-r--r-- | toolkit/components/prompts/content/selectDialog.js | 83 | ||||
-rw-r--r-- | toolkit/components/prompts/content/selectDialog.xhtml | 25 | ||||
-rw-r--r-- | toolkit/components/prompts/content/tabprompts.css | 119 | ||||
-rw-r--r-- | toolkit/components/prompts/content/tabprompts.sys.mjs | 298 |
8 files changed, 1169 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..ac01353aae --- /dev/null +++ b/toolkit/components/prompts/content/commonDialog.css @@ -0,0 +1,133 @@ +/* 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; +} + +#spinnerContainer { + align-items: center; +} + +#spinnerContainer > img { + width: 16px; + height: 16px; +} + +#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..d9b39f696a --- /dev/null +++ b/toolkit/components/prompts/content/commonDialog.js @@ -0,0 +1,199 @@ +/* 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" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gContentAnalysis", + "@mozilla.org/contentanalysis;1", + Ci.nsIContentAnalysis +); + +// imported by adjustableTitle.js loaded in the same context: +/* globals PromptUtils */ + +var propBag, args, Dialog; + +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"), + spinnerContainer: document.getElementById("spinnerContainer"), + 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()); + if (lazy.gContentAnalysis.isActive && args.owningBrowsingContext?.isContent) { + ui.loginTextbox?.addEventListener("paste", async event => { + let data = event.clipboardData.getData("text/plain"); + if (data?.length > 0) { + // Prevent the paste from happening until content analysis returns a response + event.preventDefault(); + // Selections can be forward or backward, so use min/max + const startIndex = Math.min( + ui.loginTextbox.selectionStart, + ui.loginTextbox.selectionEnd + ); + const endIndex = Math.max( + ui.loginTextbox.selectionStart, + ui.loginTextbox.selectionEnd + ); + const selectionDirection = + endIndex < startIndex ? "backward" : "forward"; + try { + const response = await lazy.gContentAnalysis.analyzeContentRequest( + { + requestToken: Services.uuid.generateUUID().toString(), + resources: [], + analysisType: Ci.nsIContentAnalysisRequest.eBulkDataEntry, + operationTypeForDisplay: Ci.nsIContentAnalysisRequest.eClipboard, + url: args.owningBrowsingContext.currentURI, + textContent: data, + windowGlobalParent: + args.owningBrowsingContext.currentWindowContext, + }, + true + ); + if (response.shouldAllowContent) { + ui.loginTextbox.value = + ui.loginTextbox.value.slice(0, startIndex) + + data + + ui.loginTextbox.value.slice(endIndex); + ui.loginTextbox.focus(); + if (startIndex !== endIndex) { + // Select the pasted text + ui.loginTextbox.setSelectionRange( + startIndex, + startIndex + data.length, + selectionDirection + ); + } + } + } catch (error) { + console.error("Content analysis request returned error: ", error); + } + } + }); + } + + 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..def3b93956 --- /dev/null +++ b/toolkit/components/prompts/content/commonDialog.xhtml @@ -0,0 +1,119 @@ +<?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/. --> + +<!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="stylesheet" href="chrome://global/skin/global.css" /> + <html:link + rel="stylesheet" + href="chrome://global/content/commonDialog.css" + /> + <html:link + rel="stylesheet" + href="chrome://global/skin/commonDialog.css" + /> + + <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="spinnerContainer" class="dialogRow" hidden="hidden"> + <img + src="chrome://global/skin/icons/loading.png" + data-l10n-id="common-dialog-spinner" + srcset=" + chrome://global/skin/icons/loading.png, + chrome://global/skin/icons/loading@2x.png 1.25x + " + /> + </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..59918cdce0 --- /dev/null +++ b/toolkit/components/prompts/content/selectDialog.xhtml @@ -0,0 +1,25 @@ +<?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/. --> + +<!DOCTYPE window> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" +> + <html:link rel="stylesheet" href="chrome://global/skin/global.css" /> + + <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 + } + } +}; |