diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/components/prompts/content/tabprompts.jsm | 307 |
1 files changed, 307 insertions, 0 deletions
diff --git a/toolkit/components/prompts/content/tabprompts.jsm b/toolkit/components/prompts/content/tabprompts.jsm new file mode 100644 index 0000000000..13a75f01a9 --- /dev/null +++ b/toolkit/components/prompts/content/tabprompts.jsm @@ -0,0 +1,307 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["TabModalPrompt"]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +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.import( + "resource://gre/modules/CommonDialog.jsm" + ); + 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 + } + } +}; |