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