Bug 575485 - Refactor commonDialog code into a sharable JSM (Part 3, jsm). r=gavin, a=me
☠☠ backed out by 827f362fc804 ☠ ☠
authorJustin Dolske <dolske@mozilla.com>
Mon, 25 Oct 2010 22:45:00 -0700
changeset 56490 fc2988ab64e5288d06f8dab6d42ceb4246b1482a
parent 56489 a2ef5d0f5052f28b01bac5c742d5aa36a381a368
child 56491 d253c44465ae0271c9ff5576a19ef68b39a8a4da
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersgavin, me
bugs575485
milestone2.0b8pre
Bug 575485 - Refactor commonDialog code into a sharable JSM (Part 3, jsm). r=gavin, a=me
toolkit/components/prompts/content/commonDialog.js
toolkit/components/prompts/content/commonDialog.xul
toolkit/components/prompts/src/CommonDialog.jsm
toolkit/components/prompts/src/Makefile.in
toolkit/components/prompts/test/test_modal_prompts.html
--- a/toolkit/components/prompts/content/commonDialog.js
+++ b/toolkit/components/prompts/content/commonDialog.js
@@ -40,302 +40,70 @@
  * ***** END LICENSE BLOCK ***** */
 
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cc = Components.classes;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/CommonDialog.jsm");
 
-let gArgs, promptType, numButtons, iconClass, soundID, hasInputField = true;
-let gDelayExpired = false, gBlurred = false;
+let propBag, args, Dialog;
 
-function earlyInit() {
-    // This is called before onload fires, so we can't be certain that any elements
-    // in the document have their bindings ready, so don't call any methods/properties
-    // here on xul elements that come from xbl bindings.
+function commonDialogOnLoad() {
+    propBag = window.arguments[0].QueryInterface(Ci.nsIWritablePropertyBag2)
+                                 .QueryInterface(Ci.nsIWritablePropertyBag);
+    // Convert to a JS object
+    args = {};
+    let propEnum = propBag.enumerator;
+    while (propEnum.hasMoreElements()) {
+        let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+        args[prop.name] = prop.value;
+    }
 
-    gArgs = window.arguments[0].QueryInterface(Ci.nsIWritablePropertyBag2)
-                               .QueryInterface(Ci.nsIWritablePropertyBag);
-
-    promptType = gArgs.getProperty("promptType");
+    let dialog = document.documentElement;
 
-    switch (promptType) {
-      case "alert":
-      case "alertCheck":
-        hasInputField = false;
-        numButtons    = 1;
-        iconClass     = "alert-icon";
-        soundID       = Ci.nsISound.EVENT_ALERT_DIALOG_OPEN;
-        break;
-      case "confirmCheck":
-      case "confirm":
-        hasInputField = false;
-        numButtons    = 2;
-        iconClass     = "question-icon";
-        soundID       = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN;
-        break;
-      case "confirmEx":
-        numButtons = 0;
-        if (gArgs.hasKey("button0Label"))
-            numButtons++;
-        if (gArgs.hasKey("button1Label"))
-            numButtons++;
-        if (gArgs.hasKey("button2Label"))
-            numButtons++;
-        if (gArgs.hasKey("button3Label"))
-            numButtons++;
-        if (numButtons == 0)
-            throw "A dialog with no buttons? Can not haz.";
-        hasInputField = false;
-        iconClass     = "question-icon";
-        soundID       = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN;
-        break;
-      case "prompt":
-        numButtons = 2;
-        iconClass  = "question-icon";
-        soundID    = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
-        initTextbox("login", gArgs.getProperty("value"));
-        // Clear the label, since this isn't really a username prompt.
-        document.getElementById("loginLabel").setAttribute("value", "");
-        break;
-      case "promptUserAndPass":
-        numButtons = 2;
-        iconClass  = "authentication-icon question-icon";
-        soundID    = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
-        initTextbox("login",     gArgs.getProperty("user"));
-        initTextbox("password1", gArgs.getProperty("pass"));
-        break;
-      case "promptPassword":
-        numButtons = 2;
-        iconClass  = "authentication-icon question-icon";
-        soundID    = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
-        initTextbox("password1", gArgs.getProperty("pass"));
-        // Clear the label, since the message presumably indicates its purpose.
-        document.getElementById("password1Label").setAttribute("value", "");
-        break;
-      default:
-        Cu.reportError("commonDialog opened for unknown type: " + promptType);
-        window.close();
-    }
+    let ui = {
+        loginContainer     : document.getElementById("loginContainer"),
+        loginTextbox       : document.getElementById("loginTextbox"),
+        loginLabel         : document.getElementById("loginLabel"),
+        password1Container : document.getElementById("password1Container"),
+        password1Textbox   : document.getElementById("password1Textbox"),
+        password1Label     : document.getElementById("password1Label"),
+        infoBody           : document.getElementById("info.body"),
+        infoTitle          : document.getElementById("info.title"),
+        infoIcon           : document.getElementById("info.icon"),
+        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,
+    };
+
+    // limit the dialog to the screen width
+    document.getElementById("filler").maxWidth = screen.availWidth;
+    Services.obs.addObserver(softkbObserver, "softkb-change", false);
+
+    Dialog = new CommonDialog(args, ui);
+    Dialog.onLoad(dialog);
+    window.getAttention();
 }
 
-function initTextbox(aName, aValue) {
-    document.getElementById(aName + "Container").hidden = false;
-    document.getElementById(aName + "Textbox").setAttribute("value", aValue);
-}
-
-function setLabelForNode(aNode, aLabel) {
-    // This is for labels which may contain embedded access keys.
-    // If we end in (&X) where X represents the access key, optionally preceded
-    // by spaces and/or followed by the ':' character, store the access key and
-    // remove the access key placeholder + leading spaces from the label.
-    // Otherwise a character preceded by one but not two &s is the access key.
-    // Store it and remove the &.
-
-    // Note that if you change the following code, see the comment of
-    // nsTextBoxFrame::UpdateAccessTitle.
-    var accessKey = null;
-    if (/ *\(\&([^&])\)(:)?$/.test(aLabel)) {
-        aLabel = RegExp.leftContext + RegExp.$2;
-        accessKey = RegExp.$1;
-    } else if (/^(.*[^&])?\&(([^&]).*$)/.test(aLabel)) {
-        aLabel = RegExp.$1 + RegExp.$2;
-        accessKey = RegExp.$3;
-    }
-
-    // && is the magic sequence to embed an & in your label.
-    aLabel = aLabel.replace(/\&\&/g, "&");
-    aNode.label = aLabel;
-
-    // XXXjag bug 325251
-    // Need to set this after aNode.setAttribute("value", aLabel);
-    if (accessKey)
-        aNode.accessKey = accessKey;
+function commonDialogOnUnload() {
+    Services.obs.removeObserver(softkbObserver, "softkb-change");
+    // Convert args back into property bag
+    for (let propName in args)
+        propBag.setProperty(propName, args[propName]);
 }
 
 function softkbObserver(subject, topic, data) {
     let rect = JSON.parse(data);
     if (rect) {
         let height = rect.bottom - rect.top;
         let width  = rect.right - rect.left;
         let top    = (rect.top + (height - window.innerHeight) / 2);
         let left   = (rect.left + (width - window.innerWidth) / 2);
         window.moveTo(left, top);
     }
 }
-
-function commonDialogOnLoad() {
-    // limit the dialog to the screen width
-    document.getElementById("filler").maxWidth = screen.availWidth;
-
-    // set the document title
-    let title = gArgs.getProperty("title");
-    document.title = title;
-    // OS X doesn't have a title on modal dialogs, this is hidden on other platforms.
-    document.getElementById("info.title").appendChild(document.createTextNode(title));
-
-    Services.obs.addObserver(softkbObserver, "softkb-change", false);
-
-    // Set button labels and visibility
-    let dialog = document.documentElement;
-    switch (numButtons) {
-      case 4:
-        setLabelForNode(dialog.getButton("extra2"), gArgs.getProperty("button3Label"));
-        dialog.getButton("extra2").hidden = false;
-        // fall through
-      case 3:
-        setLabelForNode(dialog.getButton("extra1"), gArgs.getProperty("button2Label"));
-        dialog.getButton("extra1").hidden = false;
-        // fall through
-      case 2:
-        if (gArgs.hasKey("button1Label"))
-            setLabelForNode(dialog.getButton("cancel"), gArgs.getProperty("button1Label"));
-        break;
-
-      case 1:
-        dialog.getButton("cancel").hidden = true;
-        break;
-    }
-    if (gArgs.hasKey("button0Label"))
-        setLabelForNode(dialog.getButton("accept"), gArgs.getProperty("button0Label"));
-
-    // display the main text
-    // Bug 317334 - crop string length as a workaround.
-    let croppedMessage = gArgs.getProperty("text").substr(0, 10000);
-    document.getElementById("info.body").appendChild(document.createTextNode(croppedMessage));
-
-    if (gArgs.hasKey("checkLabel")) {
-        let label = gArgs.getProperty("checkLabel")
-        // Only show the checkbox if label has a value.
-        if (label) {
-            document.getElementById("checkboxContainer").hidden = false;
-            let checkboxElement = document.getElementById("checkbox");
-            setLabelForNode(checkboxElement, label);
-            checkboxElement.checked = gArgs.getProperty("checked");
-        }
-    }
-
-    // set the icon
-    document.getElementById("info.icon").className += " " + iconClass;
-
-    // set default result to cancelled
-    gArgs.setProperty("ok", false);
-    gArgs.setProperty("buttonNumClicked", 1);
-
-
-    // If there are no input fields on the dialog, select the default button.
-    // Otherwise, select the appropriate input field.
-    if (!hasInputField) {
-        let dlgButtons = ['accept', 'cancel', 'extra1', 'extra2'];
-
-        // Set the default button and focus it on non-OS X systems
-        let b = 0;
-        if (gArgs.hasKey("defaultButtonNum"))
-            b = gArgs.getProperty("defaultButtonNum");
-        let dButton = dlgButtons[b];
-        // XXX shouldn't we set the default even when a textbox is focused?
-        dialog.defaultButton = dButton;
-#ifndef XP_MACOSX
-        dialog.getButton(dButton).focus();
-#endif
-    } else {
-        if (promptType == "promptPassword")
-            document.getElementById("password1Textbox").select();
-        else
-            document.getElementById("loginTextbox").select();
-    }
-
-    if (gArgs.hasKey("enableDelay") && gArgs.getProperty("enableDelay")) {
-        let delayInterval = Services.prefs.getIntPref("security.dialog_enable_delay");
-        setButtonsEnabledState(dialog, false);
-        setTimeout(function () {
-                        // Don't automatically enable the buttons if we're not in the foreground
-                        if (!gBlurred)
-                            setButtonsEnabledState(dialog, true);
-                        gDelayExpired = true;
-                    }, delayInterval);
-
-        addEventListener("blur", commonDialogBlur, false);
-        addEventListener("focus", commonDialogFocus, false);
-    }
-
-    window.getAttention();
-
-    // play sound
-    try {
-        if (soundID) {
-            Cc["@mozilla.org/sound;1"].
-            createInstance(Ci.nsISound).
-            playEventSound(soundID);
-        }
-    } catch (e) { }
-
-    Services.obs.notifyObservers(window, "common-dialog-loaded", null);
-}
-
-function setButtonsEnabledState(dialog, enabled) {
-    dialog.getButton("accept").disabled = !enabled;
-    dialog.getButton("extra1").disabled = !enabled;
-    dialog.getButton("extra2").disabled = !enabled;
-}
-
-function commonDialogOnUnload() {
-    Services.obs.removeObserver(softkbObserver, "softkb-change");
-}
-
-function commonDialogBlur(aEvent) {
-    if (aEvent.target != document)
-        return;
-    gBlurred = true;
-    let dialog = document.documentElement;
-    setButtonsEnabledState(dialog, false);
-}
-
-function commonDialogFocus(aEvent) {
-    if (aEvent.target != document)
-        return;
-    gBlurred = false;
-    let dialog = document.documentElement;
-    // When refocusing the window, don't enable the buttons unless the countdown
-    // delay has expired.
-    if (gDelayExpired)
-        setTimeout(setButtonsEnabledState, 250, dialog, true);
-}
-
-function onCheckboxClick(aCheckboxElement) {
-    gArgs.setProperty("checked", aCheckboxElement.checked);
-}
-
-function commonDialogOnAccept() {
-    gArgs.setProperty("ok", true);
-    gArgs.setProperty("buttonNumClicked", 0);
-
-    let username = document.getElementById("loginTextbox").value;
-    let password = document.getElementById("password1Textbox").value;
-
-    // Return textfield values
-    switch (promptType) {
-      case "prompt":
-        gArgs.setProperty("value", username);
-        break;
-      case "promptUserAndPass":
-        gArgs.setProperty("user", username);
-        gArgs.setProperty("pass", password);
-        break;
-      case "promptPassword":
-        gArgs.setProperty("pass", password);
-        break;
-    }
-}
-
-function commonDialogOnExtra1() {
-    // .setProperty("ok", true)?
-    gArgs.setProperty("buttonNumClicked", 2);
-    window.close();
-}
-
-function commonDialogOnExtra2() {
-    // .setProperty("ok", true)?
-    gArgs.setProperty("buttonNumClicked", 3);
-    window.close();
-}
--- a/toolkit/components/prompts/content/commonDialog.xul
+++ b/toolkit/components/prompts/content/commonDialog.xul
@@ -6,19 +6,20 @@
 
 <!DOCTYPE dialog SYSTEM "chrome://global/locale/commonDialog.dtd">
 
 <dialog id="commonDialog"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         aria-describedby="info.body"
         onload="commonDialogOnLoad();"
         onunload="commonDialogOnUnload();"
-        ondialogaccept="return commonDialogOnAccept();"
-        ondialogextra1="return commonDialogOnExtra1();"
-        ondialogextra2="return commonDialogOnExtra2();"
+        ondialogaccept="Dialog.onButton0(); return true;"
+        ondialogcancel="Dialog.onButton1(); return true;"
+        ondialogextra1="Dialog.onButton2(); window.close();"
+        ondialogextra2="Dialog.onButton3(); window.close();"
         buttonpack="center">
 
   <script type="application/javascript" src="chrome://global/content/commonDialog.js"/>
   <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
 
   <commandset id="selectEditMenuItems">
     <command id="cmd_copy" oncommand="goDoCommand('cmd_copy')" disabled="true"/>
     <command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')"/>
@@ -70,21 +71,14 @@
         <textbox id="loginTextbox"/>
       </row>
       <row id ="password1Container" hidden="true" align="center">
         <label id="password1Label" value="&editfield1.label;" control="password1Textbox"/>
         <textbox type="password" id="password1Textbox"/>
       </row>
       <row id="checkboxContainer" hidden="true">
         <spacer/>
-        <checkbox id="checkbox" oncommand="onCheckboxClick(this);"/>
+        <checkbox id="checkbox" oncommand="Dialog.onCheckbox()"/>
       </row>
     </rows>
   </grid>
 
-
-  <!-- This method is called inline because it may unset hidden="true" on the
-       above boxes, causing their frames to be build and bindings to load.
-       So, by calling this inline, we guarantee the textboxes and checkboxes
-       above will have their bindings before initButtons is called, and the
-       dialog will be intrinsically sized correctly. -->
-  <script type="application/javascript">earlyInit();</script>
 </dialog>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/prompts/src/CommonDialog.jsm
@@ -0,0 +1,350 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is CommonDialog.jsm code.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Justin Dolske <dolske@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var EXPORTED_SYMBOLS = ["CommonDialog"];
+
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+
+function CommonDialog(args, ui) {
+    this.args = args;
+    this.ui   = ui;
+}
+
+CommonDialog.prototype = {
+    args : null,
+    ui   : null,
+
+    hasInputField : true,
+    numButtons    : undefined,
+    iconClass     : undefined,
+    soundID       : undefined,
+    focusTimer    : null,
+
+    onLoad : function(xulDialog) {
+        switch (this.args.promptType) {
+          case "alert":
+          case "alertCheck":
+            this.hasInputField = false;
+            this.numButtons    = 1;
+            this.iconClass     = ["alert-icon"];
+            this.soundID       = Ci.nsISound.EVENT_ALERT_DIALOG_OPEN;
+            break;
+          case "confirmCheck":
+          case "confirm":
+            this.hasInputField = false;
+            this.numButtons    = 2;
+            this.iconClass     = ["question-icon"];
+            this.soundID       = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN;
+            break;
+          case "confirmEx":
+            var numButtons = 0;
+            if (this.args.button0Label)
+                numButtons++;
+            if (this.args.button1Label)
+                numButtons++;
+            if (this.args.button2Label)
+                numButtons++;
+            if (this.args.button3Label)
+                numButtons++;
+            if (numButtons == 0)
+                throw "A dialog with no buttons? Can not haz.";
+            this.numButtons    = numButtons;
+            this.hasInputField = false;
+            this.iconClass     = ["question-icon"];
+            this.soundID       = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN;
+            break;
+          case "prompt":
+            this.numButtons = 2;
+            this.iconClass  = ["question-icon"];
+            this.soundID    = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
+            this.initTextbox("login", this.args.value);
+            // Clear the label, since this isn't really a username prompt.
+            this.ui.loginLabel.setAttribute("value", "");
+            break;
+          case "promptUserAndPass":
+            this.numButtons = 2;
+            this.iconClass  = ["authentication-icon", "question-icon"];
+            this.soundID    = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
+            this.initTextbox("login",     this.args.user);
+            this.initTextbox("password1", this.args.pass);
+            break;
+          case "promptPassword":
+            this.numButtons = 2;
+            this.iconClass  = ["authentication-icon", "question-icon"];
+            this.soundID    = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN;
+            this.initTextbox("password1", this.args.pass);
+            // Clear the label, since the message presumably indicates its purpose.
+            this.ui.password1Label.setAttribute("value", "");
+            break;
+          default:
+            Cu.reportError("commonDialog opened for unknown type: " + this.args.promptType);
+            throw "unknown dialog type";
+        }
+
+        // set the document title
+        let title = this.args.title;
+        // OS X doesn't have a title on modal dialogs, this is hidden on other platforms.
+        let infoTitle = this.ui.infoTitle;
+        infoTitle.appendChild(infoTitle.ownerDocument.createTextNode(title));
+        if (xulDialog)
+            xulDialog.ownerDocument.title = title;
+
+        // Set button labels and visibility
+        //
+        // This assumes that button0 defaults to a visible "ok" button, and
+        // button1 defaults to a visible "cancel" button. The other 2 buttons
+        // have no default labels (and are hidden).
+        switch (this.numButtons) {
+          case 4:
+            this.setLabelForNode(this.ui.button3, this.args.button3Label);
+            this.ui.button3.hidden = false;
+            // fall through
+          case 3:
+            this.setLabelForNode(this.ui.button2, this.args.button2Label);
+            this.ui.button2.hidden = false;
+            // fall through
+          case 2:
+            // Defaults to a visible "cancel" button
+            if (this.args.button1Label)
+                this.setLabelForNode(this.ui.button1, this.args.button1Label);
+            break;
+
+          case 1:
+            this.ui.button1.hidden = true;
+            break;
+        }
+        // Defaults to a visible "ok" button
+        if (this.args.button0Label)
+            this.setLabelForNode(this.ui.button0, this.args.button0Label);
+
+        // display the main text
+        // Bug 317334 - crop string length as a workaround.
+        let croppedMessage = this.args.text.substr(0, 10000);
+        let infoBody = this.ui.infoBody;
+        infoBody.appendChild(infoBody.ownerDocument.createTextNode(croppedMessage));
+
+        let label = this.args.checkLabel;
+        if (label) {
+            // Only show the checkbox if label has a value.
+            this.ui.checkboxContainer.hidden = false;
+            this.setLabelForNode(this.ui.checkbox, label);
+            this.ui.checkbox.checked = this.args.checked;
+        }
+
+        // set the icon
+        let icon = this.ui.infoIcon;
+        this.iconClass.forEach(function(el,idx,arr) icon.classList.add(el));
+
+        // set default result to cancelled
+        this.args.ok = false;
+        this.args.buttonNumClicked = 1;
+
+
+        // If there are no input fields on the dialog, select the default button.
+        // Otherwise, select the appropriate input field.
+        // XXX shouldn't we set an unfocused default even when a textbox is focused?
+        if (!this.hasInputField) {
+            // Set the default button (and focus it on non-OS X systems)
+            let b = 0;
+            if (this.args.defaultButtonNum)
+                b = this.args.defaultButtonNum;
+            let button = this.ui["button" + b];
+            if (xulDialog) {
+                xulDialog.defaultButton = ['accept', 'cancel', 'extra1', 'extra2'][b];
+                let isOSX = ("nsILocalFileMac" in Components.interfaces);
+                if (!isOSX)
+                    button.focus();
+            }
+            // TODO:
+            // else
+            //     (tabmodal prompts need to set a default button for Enter to act upon)
+        } else {
+            if (this.args.promptType == "promptPassword")
+                this.ui.password1Textbox.select();
+            else
+                this.ui.loginTextbox.select();
+        }
+
+        if (this.args.enableDelay) {
+            this.setButtonsEnabledState(false);
+            // Use a longer, pref-controlled delay when the dialog is first opened.
+            let delayTime = Services.prefs.getIntPref("security.dialog_enable_delay");
+            this.startOnFocusDelay(delayTime);
+            let self = this;
+            this.ui.focusTarget.addEventListener("blur",  function(e) { self.onBlur(e);  }, false);
+            this.ui.focusTarget.addEventListener("focus", function(e) { self.onFocus(e); }, false);
+        }
+
+        // play sound
+        try {
+            if (this.soundID) {
+                Cc["@mozilla.org/sound;1"].
+                createInstance(Ci.nsISound).
+                playEventSound(soundID);
+            }
+        } catch (e) { }
+
+        if (xulDialog)
+            Services.obs.notifyObservers(xulDialog.ownerDocument.defaultView, "common-dialog-loaded", null);
+        // TODO:
+        // else
+        //    (notify using what as the subject?)
+    },
+
+    setLabelForNode: function(aNode, aLabel) {
+        // This is for labels which may contain embedded access keys.
+        // If we end in (&X) where X represents the access key, optionally preceded
+        // by spaces and/or followed by the ':' character, store the access key and
+        // remove the access key placeholder + leading spaces from the label.
+        // Otherwise a character preceded by one but not two &s is the access key.
+        // Store it and remove the &.
+
+        // Note that if you change the following code, see the comment of
+        // nsTextBoxFrame::UpdateAccessTitle.
+        var accessKey = null;
+        if (/ *\(\&([^&])\)(:)?$/.test(aLabel)) {
+            aLabel = RegExp.leftContext + RegExp.$2;
+            accessKey = RegExp.$1;
+        } else if (/^(.*[^&])?\&(([^&]).*$)/.test(aLabel)) {
+            aLabel = RegExp.$1 + RegExp.$2;
+            accessKey = RegExp.$3;
+        }
+
+        // && is the magic sequence to embed an & in your label.
+        aLabel = aLabel.replace(/\&\&/g, "&");
+        aNode.label = aLabel;
+
+        // XXXjag bug 325251
+        // Need to set this after aNode.setAttribute("value", aLabel);
+        if (accessKey)
+            aNode.accessKey = accessKey;
+    },
+
+
+    initTextbox : function (aName, aValue) {
+        this.ui[aName + "Container"].hidden = false;
+        this.ui[aName + "Textbox"].setAttribute("value", aValue);
+    },
+
+    setButtonsEnabledState : function(enabled) {
+        this.ui.button0.disabled = !enabled;
+        // button1 (cancel) remains enabled.
+        this.ui.button2.disabled = !enabled;
+        this.ui.button3.disabled = !enabled;
+    },
+
+    onBlur : function (aEvent) {
+        if (aEvent.target != this.ui.focusTarget)
+            return;
+        this.setButtonsEnabledState(false);
+
+        // If we blur while waiting to enable the buttons, just cancel the
+        // timer to ensure the delay doesn't fire while not focused.
+        if (this.focusTimer) {
+            this.focusTimer.cancel();
+            this.focusTimer = null;
+        }
+    },
+
+    onFocus : function (aEvent) {
+        if (aEvent.target != this.ui.focusTarget)
+            return;
+        this.startOnFocusDelay();
+    },
+
+    startOnFocusDelay : function(delayTime) {
+        // Shouldn't already have a timer, but just in case...
+        if (this.focusTimer)
+            return;
+        // If no delay specified, use 250ms. (This is the normal case for when
+        // after the dialog has been opened and focus shifts.)
+        if (!delayTime)
+            delayTime = 250;
+        let self = this;
+        this.focusTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+        this.focusTimer.initWithCallback(function() { self.onFocusTimeout(); },
+                                         delayTime, Ci.nsITimer.TYPE_ONE_SHOT);
+    },
+
+    onFocusTimeout : function() {
+        this.focusTimer = null;
+        this.setButtonsEnabledState(true);
+    },
+
+    onCheckbox : function() {
+        this.args.checked = this.ui.checkbox.checked;
+    },
+
+    onButton0 : function() {
+        this.args.ok = true;
+        this.args.buttonNumClicked = 0;
+
+        let username = this.ui.loginTextbox.value;
+        let password = this.ui.password1Textbox.value;
+
+        // Return textfield values
+        switch (this.args.promptType) {
+          case "prompt":
+            this.args.value = username;
+            break;
+          case "promptUserAndPass":
+            this.args.user = username;
+            this.args.pass = password;
+            break;
+          case "promptPassword":
+            this.args.pass = password;
+            break;
+        }
+    },
+
+    onButton1 : function() {
+        this.args.buttonNumClicked = 1;
+    },
+
+    onButton2 : function() {
+        this.args.buttonNumClicked = 2;
+    },
+
+    onButton3 : function() {
+        this.args.buttonNumClicked = 3;
+    },
+};
--- a/toolkit/components/prompts/src/Makefile.in
+++ b/toolkit/components/prompts/src/Makefile.in
@@ -43,9 +43,13 @@ include $(DEPTH)/config/autoconf.mk
 
 MODULE = prompter
 
 EXTRA_COMPONENTS = \
     nsPrompter.js \
     nsPrompter.manifest \
     $(NULL)
 
+EXTRA_JS_MODULES = \
+    CommonDialog.jsm \
+    $(NULL)
+
 include $(topsrcdir)/config/rules.mk
--- a/toolkit/components/prompts/test/test_modal_prompts.html
+++ b/toolkit/components/prompts/test/test_modal_prompts.html
@@ -19,16 +19,30 @@ Prompter tests: modal prompts
 <script class="testbody" type="text/javascript;version=1.8">
 
 netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
 
 let prompter = Cc["@mozilla.org/embedcomp/prompt-service;1"].
                getService(Ci.nsIPromptService2);
 let ioService = Cc["@mozilla.org/network/io-service;1"].
                 getService(Ci.nsIIOService);
+let pollTimer;
+
+function pollDialog(dialog) {
+    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
+
+    if (dialog.getButton("accept").disabled)
+        return;
+
+    ok(true, "dialog button is enabled now");
+    pollTimer.cancel();
+    pollTimer = null;
+    dialog.acceptDialog();
+    didDialog = true;
+}
 
 function checkExpectedState(doc, state) {
     let msg        = doc.getElementById("info.body").textContent;
     let icon       = doc.getElementById("info.icon");
     let textOuter  = doc.getElementById("loginContainer");
     let passOuter  = doc.getElementById("password1Container");
     let checkOuter = doc.getElementById("checkboxContainer");
     let textField  = doc.getElementById("loginTextbox");
@@ -649,16 +663,37 @@ function handleDialog(doc, testNum) {
             textValue   : "",
             passValue   : "",
             checkMsg    : "",
             checked     : false,
         };
         checkExpectedState(doc, state);
         break;
 
+      case 30:
+        // ConfirmEx (with delay, ok)
+        state = {
+            msg   : "This is the confirmEx delay text.",
+            title : "TestTitle",
+            iconClass   : "question-icon",
+            textHidden  : true,
+            passHidden  : true,
+            checkHidden : true,
+            textValue   : "",
+            passValue   : "",
+            checkMsg    : "",
+            checked     : false,
+        };
+        is(dialog.getButton("accept").label, "OK",     "Checking accept-button label");
+        is(dialog.getButton("cancel").label, "Cancel", "Checking cancel-button label");
+        is(dialog.getButton("accept").disabled, true,  "Checking accept-button is disabled");
+        is(dialog.getButton("cancel").disabled, false, "Checking cancel-button isn't disabled ");
+        checkExpectedState(doc, state);
+        break;
+
 
       case 100:
         // PromptAuth (no realm, ok, with checkbox)
         state = {
             msg : 'Enter username and password for http://example.com',
             title : "TestTitle",
             iconClass   : "authentication-icon question-icon",
             textHidden  : false,
@@ -706,16 +741,24 @@ function handleDialog(doc, testNum) {
 
       default:
         ok(false, "Uhh, unhandled switch for testNum #" + testNum);
         break;
     }
 
     if (testNum == 28) {
         dialog._doButtonCommand("extra1");
+    } else if (testNum == 30) {
+        // Buttons are disabled at the moment, poll until they're reenabled.
+        // Can't use setInterval here, because the window's in a modal state
+        // and thus DOM events are suppressed.
+        pollTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+        pollTimer.initWithCallback(function() { pollDialog(dialog); },
+                                   100, Ci.nsITimer.TYPE_REPEATING_SLACK);
+        return;
     } else {
         if (clickOK)
             dialog.acceptDialog();
         else
             dialog.cancelDialog();
     }
 
     ok(true, "handleDialog done");
@@ -1044,16 +1087,25 @@ ok(didDialog, "handleDialog was invoked"
 // ===== test 29  =====
 // Alert, no window
 testNum++;
 startCallbackTimer();
 prompter.alert(null, "TestTitle", "This is the alert text.");
 ok(didDialog, "handleDialog was invoked");
 
 
+// ===== test 30 =====
+// ConfirmEx (delay, ok)
+testNum++;
+startCallbackTimer();
+flags = (Ci.nsIPromptService.STD_OK_CANCEL_BUTTONS | Ci.nsIPromptService.BUTTON_DELAY_ENABLE);
+clickedButton = prompter.confirmEx(window, "TestTitle", "This is the confirmEx delay text.", flags, null, null, null, null, {});
+is(clickedButton, 0, "checked expected button num click");
+ok(didDialog, "handleDialog was invoked");
+
 // promptAuth already tested via password manager but do a few specific things here.
 
 
 var channel = ioService.newChannel("http://example.com", null, null);
 var level = Ci.nsIAuthPrompt2.LEVEL_NONE;
 var authinfo = {
     username : "",
     password : "",