1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7var EXPORTED_SYMBOLS = ["TabModalPrompt"]; 8 9const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 10const { AppConstants } = ChromeUtils.import( 11 "resource://gre/modules/AppConstants.jsm" 12); 13 14var TabModalPrompt = class { 15 constructor(win) { 16 this.win = win; 17 let newPrompt = (this.element = win.document.createElement( 18 "tabmodalprompt" 19 )); 20 newPrompt.setAttribute("role", "dialog"); 21 let randomIdSuffix = Math.random() 22 .toString(32) 23 .substr(2); 24 newPrompt.setAttribute("aria-describedby", `infoBody-${randomIdSuffix}`); 25 newPrompt.appendChild( 26 win.MozXULElement.parseXULToFragment( 27 ` 28 <div class="tabmodalprompt-mainContainer" xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> 29 <div class="tabmodalprompt-topContainer"> 30 <div class="tabmodalprompt-infoContainer"> 31 <div class="tabmodalprompt-infoTitle infoTitle" hidden="hidden"/> 32 <div class="tabmodalprompt-infoBody infoBody" id="infoBody-${randomIdSuffix}" tabindex="-1"/> 33 </div> 34 35 <div class="tabmodalprompt-loginContainer" hidden="hidden"> 36 <xul:label class="tabmodalprompt-loginLabel" value="&editfield0.label;" control="loginTextbox-${randomIdSuffix}"/> 37 <input class="tabmodalprompt-loginTextbox" id="loginTextbox-${randomIdSuffix}"/> 38 </div> 39 40 <div class="tabmodalprompt-password1Container" hidden="hidden"> 41 <xul:label class="tabmodalprompt-password1Label" value="&editfield1.label;" control="password1Textbox-${randomIdSuffix}"/> 42 <input class="tabmodalprompt-password1Textbox" type="password" id="password1Textbox-${randomIdSuffix}"/> 43 </div> 44 45 <div class="tabmodalprompt-checkboxContainer" hidden="hidden"> 46 <div/> 47 <xul:checkbox class="tabmodalprompt-checkbox"/> 48 </div> 49 50 <!-- content goes here --> 51 </div> 52 <div class="tabmodalprompt-buttonContainer"> 53 <xul:button class="tabmodalprompt-button3" hidden="true"/> 54 <div class="tabmodalprompt-buttonSpacer"/> 55 <xul:button class="tabmodalprompt-button0" label="&okButton.label;"/> 56 <xul:button class="tabmodalprompt-button2" hidden="true"/> 57 <xul:button class="tabmodalprompt-button1" label="&cancelButton.label;"/> 58 </div> 59 </div>`, 60 [ 61 "chrome://global/locale/commonDialog.dtd", 62 "chrome://global/locale/dialogOverlay.dtd", 63 ] 64 ) 65 ); 66 67 this.ui = { 68 prompt: this, 69 promptContainer: this.element, 70 mainContainer: newPrompt.querySelector(".tabmodalprompt-mainContainer"), 71 loginContainer: newPrompt.querySelector(".tabmodalprompt-loginContainer"), 72 loginTextbox: newPrompt.querySelector(".tabmodalprompt-loginTextbox"), 73 loginLabel: newPrompt.querySelector(".tabmodalprompt-loginLabel"), 74 password1Container: newPrompt.querySelector( 75 ".tabmodalprompt-password1Container" 76 ), 77 password1Textbox: newPrompt.querySelector( 78 ".tabmodalprompt-password1Textbox" 79 ), 80 password1Label: newPrompt.querySelector(".tabmodalprompt-password1Label"), 81 infoContainer: newPrompt.querySelector(".tabmodalprompt-infoContainer"), 82 infoBody: newPrompt.querySelector(".tabmodalprompt-infoBody"), 83 infoTitle: newPrompt.querySelector(".tabmodalprompt-infoTitle"), 84 infoIcon: null, 85 rows: newPrompt.querySelector(".tabmodalprompt-topContainer"), 86 checkbox: newPrompt.querySelector(".tabmodalprompt-checkbox"), 87 checkboxContainer: newPrompt.querySelector( 88 ".tabmodalprompt-checkboxContainer" 89 ), 90 button3: newPrompt.querySelector(".tabmodalprompt-button3"), 91 button2: newPrompt.querySelector(".tabmodalprompt-button2"), 92 button1: newPrompt.querySelector(".tabmodalprompt-button1"), 93 button0: newPrompt.querySelector(".tabmodalprompt-button0"), 94 // focusTarget (for BUTTON_DELAY_ENABLE) not yet supported 95 }; 96 97 if (AppConstants.XP_UNIX) { 98 // Reorder buttons on Linux 99 let buttonContainer = newPrompt.querySelector( 100 ".tabmodalprompt-buttonContainer" 101 ); 102 buttonContainer.appendChild(this.ui.button3); 103 buttonContainer.appendChild(this.ui.button2); 104 buttonContainer.appendChild( 105 newPrompt.querySelector(".tabmodalprompt-buttonSpacer") 106 ); 107 buttonContainer.appendChild(this.ui.button1); 108 buttonContainer.appendChild(this.ui.button0); 109 } 110 111 this.ui.button0.addEventListener( 112 "command", 113 this.onButtonClick.bind(this, 0) 114 ); 115 this.ui.button1.addEventListener( 116 "command", 117 this.onButtonClick.bind(this, 1) 118 ); 119 this.ui.button2.addEventListener( 120 "command", 121 this.onButtonClick.bind(this, 2) 122 ); 123 this.ui.button3.addEventListener( 124 "command", 125 this.onButtonClick.bind(this, 3) 126 ); 127 // Anonymous wrapper used here because |Dialog| doesn't exist until init() is called! 128 this.ui.checkbox.addEventListener("command", () => { 129 this.Dialog.onCheckbox(); 130 }); 131 132 /** 133 * Based on dialog.xml handlers 134 */ 135 this.element.addEventListener( 136 "keypress", 137 event => { 138 switch (event.keyCode) { 139 case KeyEvent.DOM_VK_RETURN: 140 this.onKeyAction("default", event); 141 break; 142 143 case KeyEvent.DOM_VK_ESCAPE: 144 this.onKeyAction("cancel", event); 145 break; 146 147 default: 148 if ( 149 AppConstants.platform == "macosx" && 150 event.key == "." && 151 event.metaKey 152 ) { 153 this.onKeyAction("cancel", event); 154 } 155 break; 156 } 157 }, 158 { mozSystemGroup: true } 159 ); 160 161 this.element.addEventListener( 162 "focus", 163 event => { 164 let bnum = this.args.defaultButtonNum || 0; 165 let defaultButton = this.ui["button" + bnum]; 166 167 if (AppConstants.platform == "macosx") { 168 // On OS X, the default button always stays marked as such (until 169 // the entire prompt blurs). 170 defaultButton.setAttribute("default", "true"); 171 } else { 172 // On other platforms, the default button is only marked as such 173 // when no other button has focus. XUL buttons on not-OSX will 174 // react to pressing enter as a command, so you can't trigger the 175 // default without tabbing to it or something that isn't a button. 176 let focusedDefault = event.originalTarget == defaultButton; 177 let someButtonFocused = 178 event.originalTarget.localName == "button" || 179 event.originalTarget.localName == "toolbarbutton"; 180 if (focusedDefault || !someButtonFocused) { 181 defaultButton.setAttribute("default", "true"); 182 } 183 } 184 }, 185 true 186 ); 187 188 this.element.addEventListener("blur", () => { 189 // If focus shifted to somewhere else in the browser, don't make 190 // the default button look active. 191 let bnum = this.args.defaultButtonNum || 0; 192 let button = this.ui["button" + bnum]; 193 button.removeAttribute("default"); 194 }); 195 } 196 197 init(args, linkedTab, onCloseCallback) { 198 this.args = args; 199 this.linkedTab = linkedTab; 200 this.onCloseCallback = onCloseCallback; 201 202 if (args.enableDelay) { 203 throw new Error( 204 "BUTTON_DELAY_ENABLE not yet supported for tab-modal prompts" 205 ); 206 } 207 208 // Apply styling depending on modalType (content or tab prompt) 209 if (args.modalType === Ci.nsIPrompt.MODAL_TYPE_TAB) { 210 this.element.classList.add("tab-prompt"); 211 } else { 212 this.element.classList.add("content-prompt"); 213 } 214 215 // We need to remove the prompt when the tab or browser window is closed or 216 // the page navigates, else we never unwind the event loop and that's sad times. 217 // Remember to cleanup in shutdownPrompt()! 218 this.win.addEventListener("resize", this); 219 this.win.addEventListener("unload", this); 220 if (linkedTab) { 221 linkedTab.addEventListener("TabClose", this); 222 } 223 // Note: 224 // nsPrompter.js or in e10s mode browser-parent.js call abortPrompt, 225 // when the domWindow, for which the prompt was created, generates 226 // a "pagehide" event. 227 228 let tmp = {}; 229 ChromeUtils.import("resource://gre/modules/CommonDialog.jsm", tmp); 230 this.Dialog = new tmp.CommonDialog(args, this.ui); 231 this.Dialog.onLoad(null); 232 233 // For content prompts display the tabprompt title that shows the prompt origin when 234 // the prompt origin is not the same as that of the top window. 235 if ( 236 args.modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT && 237 args.showCallerOrigin 238 ) { 239 this.ui.infoTitle.removeAttribute("hidden"); 240 } 241 242 // TODO: should unhide buttonSpacer on Windows when there are 4 buttons. 243 // Better yet, just drop support for 4-button dialogs. (bug 609510) 244 } 245 246 shutdownPrompt() { 247 // remove our event listeners 248 try { 249 this.win.removeEventListener("resize", this); 250 this.win.removeEventListener("unload", this); 251 if (this.linkedTab) { 252 this.linkedTab.removeEventListener("TabClose", this); 253 } 254 } catch (e) {} 255 // invoke callback 256 this.onCloseCallback(); 257 this.win = null; 258 this.ui = null; 259 // Intentionally not cleaning up |this.element| here -- 260 // TabModalPromptBox.removePrompt() would need it and it might not 261 // be called yet -- see browser_double_close_tabs.js. 262 } 263 264 abortPrompt() { 265 // Called from other code when the page changes. 266 this.Dialog.abortPrompt(); 267 this.shutdownPrompt(); 268 } 269 270 handleEvent(aEvent) { 271 switch (aEvent.type) { 272 case "unload": 273 case "TabClose": 274 this.abortPrompt(); 275 break; 276 } 277 } 278 279 onButtonClick(buttonNum) { 280 // We want to do all the work her asynchronously off a Gecko 281 // runnable, because of situations like the one described in 282 // https://bugzilla.mozilla.org/show_bug.cgi?id=1167575#c35 : we 283 // get here off processing of an OS event and will also process 284 // one more Gecko runnable before we break out of the event loop 285 // spin whoever posted the prompt is doing. If we do all our 286 // work sync, we will exit modal state _before_ processing that 287 // runnable, and if exiting moral state posts a runnable we will 288 // incorrectly process that runnable before leaving our event 289 // loop spin. 290 Services.tm.dispatchToMainThread(() => { 291 this.Dialog["onButton" + buttonNum](); 292 this.shutdownPrompt(); 293 }); 294 } 295 296 onKeyAction(action, event) { 297 if (event.defaultPrevented) { 298 return; 299 } 300 301 event.stopPropagation(); 302 if (action == "default") { 303 let bnum = this.args.defaultButtonNum || 0; 304 this.onButtonClick(bnum); 305 } else { 306 // action == "cancel" 307 this.onButtonClick(1); // Cancel button 308 } 309 } 310}; 311