Bug 1324278 - Port FX changes for subdialogs.js to TB. r=mkmelin
authorRichard Marti <richard.marti@gmail.com>
Sun, 23 Oct 2016 20:47:27 +0200
changeset 20942 72bc94bebbabfed990212cb8959c11fe5c974e05
parent 20941 e0909fce521e07ee3efbe5b5e741ffae1994a116
child 20943 f7bc43a2fcccb6c5c4e8cd74221ee79c82c71664
push id12694
push userrichard.marti@gmail.com
push dateFri, 30 Dec 2016 06:24:45 +0000
treeherdercomm-central@72bc94bebbab [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1324278, 1130411, 1264968, 1286854, 1191778, 1036595
Bug 1324278 - Port FX changes for subdialogs.js to TB. r=mkmelin Bug 1130411 - deal with opening dialogs when the previous dialog is not unloaded yet Bug 1264968 - Use replace uses of removed String.contains with the new .includes method Bug 1286854 - Replace ownerDocument.defaultView with ownerGlobal in browser/ Bug 1191778 - remove references to mozapps content preferences.css stylesheet Bug 1036595 - Convert the 'Clear recent history' dialog to be in-content
mail/components/preferences/subdialogs.js
--- a/mail/components/preferences/subdialogs.js
+++ b/mail/components/preferences/subdialogs.js
@@ -6,21 +6,21 @@
 
 var gSubDialog = {
   _closingCallback: null,
   _closingEvent: null,
   _isClosing: false,
   _frame: null,
   _overlay: null,
   _box: null,
-  _injectedStyleSheets: ["chrome://mozapps/content/preferences/preferences.css",
-                         "chrome://messenger/skin/preferences/preferences.css",
-                         "chrome://global/skin/in-content/common.css",
-                         "chrome://messenger/skin/preferences/aboutPreferences.css",
-                         "chrome://messenger/skin/preferences/dialog.css"],
+  _injectedStyleSheets: [
+    "chrome://messenger/skin/preferences/preferences.css",
+    "chrome://global/skin/in-content/common.css",
+    "chrome://messenger/skin/preferences/aboutPreferences.css",
+    "chrome://messenger/skin/preferences/dialog.css"],
 
   // Store the original instantApply pref for restoring after closing the dialog
   _instantApplyOrig: Services.prefs.getBoolPref("browser.preferences.instantApply"),
   _resizeObserver: null,
 
   init: function() {
     this._frame = document.getElementById("dialogFrame");
     this._overlay = document.getElementById("dialogOverlay");
@@ -39,41 +39,59 @@ var gSubDialog = {
       'xml-stylesheet',
       'href="' + aStylesheetURL + '" type="text/css"'
     );
     this._frame.contentDocument.insertBefore(contentStylesheet,
                                              this._frame.contentDocument.documentElement);
   },
 
   open: function(aURL, aFeatures = null, aParams = null, aClosingCallback = null) {
+    // If we're already open/opening on this URL, do nothing.
+    if (this._openedURL == aURL && !this._isClosing) {
+      return;
+    }
+    // If we're open on some (other) URL or we're closing, open when closing has finished.
+    if (this._openedURL || this._isClosing) {
+      if (!this._isClosing) {
+        this.close();
+      }
+      let args = Array.from(arguments);
+      this._closingPromise.then(() => {
+        this.open.apply(this, args);
+      });
+      return;
+    }
     this._addDialogEventListeners();
 
     Services.prefs.setBoolPref("browser.preferences.instantApply", true);
     let features = (!!aFeatures ? aFeatures + "," : "") + "resizable,dialog=no,centerscreen";
     let dialog = window.openDialog(aURL, "dialogFrame", features, aParams);
     if (aClosingCallback) {
       this._closingCallback = aClosingCallback.bind(dialog);
     }
 
     this._closingEvent = null;
     this._isClosing = false;
+    this._openedURL = aURL;
 
     features = features.replace(/,/g, "&");
     let featureParams = new URLSearchParams(features.toLowerCase());
     this._box.setAttribute("resizable", featureParams.has("resizable") &&
                                         featureParams.get("resizable") != "no" &&
                                         featureParams.get("resizable") != "0");
-    return dialog;
   },
 
   close: function(aEvent = null) {
     if (this._isClosing) {
       return;
     }
     this._isClosing = true;
+    this._closingPromise = new Promise(resolve => {
+      this._resolveClosePromise = resolve;
+    });
 
     if (this._closingCallback) {
       try {
         this._closingCallback.call(null, aEvent);
       } catch (ex) {
         Cu.reportError(ex);
       }
       this._closingCallback = null;
@@ -89,24 +107,34 @@ var gSubDialog = {
     this._box.removeAttribute("width");
     this._box.removeAttribute("height");
     this._box.style.removeProperty("min-height");
     this._box.style.removeProperty("min-width");
 
     setTimeout(() => {
       // Unload the dialog after the event listeners run so that the load of about:blank isn't
       // cancelled by the ESC <key>.
+      let onBlankLoad = e => {
+        if (this._frame.contentWindow.location.href == "about:blank") {
+          this._frame.removeEventListener("load", onBlankLoad);
+          // We're now officially done closing, so update the state to reflect that.
+          delete this._openedURL;
+          this._isClosing = false;
+          this._resolveClosePromise();
+        }
+      };
+      this._frame.addEventListener("load", onBlankLoad);
       this._frame.loadURI("about:blank");
     }, 0);
   },
 
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
       case "command":
-        this.close(aEvent);
+        this._frame.contentWindow.close();
         break;
       case "dialogclosing":
         this._onDialogClosing(aEvent);
         break;
       case "DOMTitleChanged":
         this.updateTitle(aEvent);
         break;
       case "DOMFrameContentLoaded":
@@ -125,30 +153,46 @@ var gSubDialog = {
         this._onParentWinFocus(aEvent);
         break;
     }
   },
 
   /* Private methods */
 
   _onUnload: function(aEvent) {
-    if (aEvent.target.location.href != "about:blank") {
-      this.close(this._closingEvent);
+    if (aEvent.target.location.href == this._openedURL) {
+      this._frame.contentWindow.close();
     }
   },
 
   _onContentLoaded: function(aEvent) {
-    if (aEvent.target != this._frame || aEvent.target.contentWindow.location == "about:blank")
+    if (aEvent.target != this._frame || aEvent.target.contentWindow.location == "about:blank") {
       return;
+    }
 
     for (let styleSheetURL of this._injectedStyleSheets) {
       this.injectXMLStylesheet(styleSheetURL);
     }
 
-    this._frame.contentWindow.addEventListener("dialogclosing", this, {once: true});
+    // Provide the ability for the dialog to know that it is being loaded "in-content".
+    this._frame.contentDocument.documentElement.setAttribute("subdialog", "true");
+
+    this._frame.contentWindow.addEventListener("dialogclosing", this);
+
+    let oldResizeBy = this._frame.contentWindow.resizeBy;
+    this._frame.contentWindow.resizeBy = function(resizeByWidth, resizeByHeight) {
+      // Only handle resizeByHeight currently.
+      let frameHeight = gSubDialog._frame.clientHeight;
+      let boxMinHeight = parseFloat(getComputedStyle(gSubDialog._box).minHeight, 10);
+
+      gSubDialog._frame.style.height = (frameHeight + resizeByHeight) + "px";
+      gSubDialog._box.style.minHeight = (boxMinHeight + resizeByHeight) + "px";
+
+      oldResizeBy.call(gSubDialog._frame.contentWindow, resizeByWidth, resizeByHeight);
+    };
 
     // Make window.close calls work like dialog closing.
     let oldClose = this._frame.contentWindow.close;
     this._frame.contentWindow.close = function() {
       var closingEvent = gSubDialog._closingEvent;
       if (!closingEvent) {
         closingEvent = new CustomEvent("dialogclosing", {
           bubbles: true,
@@ -165,18 +209,19 @@ var gSubDialog = {
     // XXX: Hack to make focus during the dialog's load functions work. Make the element visible
     // sooner in DOMContentLoaded but mostly invisible instead of changing visibility just before
     // the dialog's load event.
     this._overlay.style.visibility = "visible";
     this._overlay.style.opacity = "0.01";
   },
 
   _onLoad: function(aEvent) {
-    if (aEvent.target.contentWindow.location == "about:blank")
+    if (aEvent.target.contentWindow.location == "about:blank") {
       return;
+    }
 
     // Do this on load to wait for the CSS to load and apply before calculating the size.
     let docEl = this._frame.contentDocument.documentElement;
 
     let groupBoxTitle = document.getAnonymousElementByAttribute(this._box, "class", "groupbox-title");
     let groupBoxTitleHeight = groupBoxTitle.clientHeight +
                               parseFloat(getComputedStyle(groupBoxTitle).borderBottomWidth);
 
@@ -239,48 +284,52 @@ var gSubDialog = {
     this._frame.style.height = frameHeight;
     this._box.style.minHeight = "calc(" +
                                 (boxVerticalBorder + groupBoxTitleHeight + boxVerticalPadding) +
                                 "px + " + frameMinHeight + ")";
 
     this._overlay.style.visibility = "visible";
     this._overlay.style.opacity = ""; // XXX: focus hack continued from _onContentLoaded
 
-    this._resizeObserver = new MutationObserver(this._onResize);
-    this._resizeObserver.observe(this._box, {attributes: true});
+    if (this._box.getAttribute("resizable") == "true") {
+      this._resizeObserver = new MutationObserver(this._onResize);
+      this._resizeObserver.observe(this._box, {attributes: true});
+    }
 
     this._trapFocus();
   },
 
   _onResize: function(mutations) {
     let frame = gSubDialog._frame;
     // The width and height styles are needed for the initial
     // layout of the frame, but afterward they need to be removed
     // or their presence will restrict the contents of the <browser>
     // from resizing to a smaller size.
     frame.style.removeProperty("width");
     frame.style.removeProperty("height");
 
     let docEl = frame.contentDocument.documentElement;
     let persistedAttributes = docEl.getAttribute("persist");
-    if (!persistedAttributes.contains("width") &&
-        !persistedAttributes.contains("height")) {
+    if (!persistedAttributes ||
+        (!persistedAttributes.includes("width") &&
+         !persistedAttributes.includes("height"))) {
       return;
     }
 
     for (let mutation of mutations) {
       if (mutation.attributeName == "width") {
         docEl.setAttribute("width", docEl.scrollWidth);
       } else if (mutation.attributeName == "height") {
         docEl.setAttribute("height", docEl.scrollHeight);
       }
     }
   },
 
   _onDialogClosing: function(aEvent) {
+    this._frame.contentWindow.removeEventListener("dialogclosing", this);
     this._closingEvent = aEvent;
   },
 
   _onKeyDown: function(aEvent) {
     if (aEvent.currentTarget == window && aEvent.keyCode == aEvent.DOM_VK_ESCAPE &&
         !aEvent.defaultPrevented) {
       this.close(aEvent);
       return;
@@ -301,17 +350,17 @@ var gSubDialog = {
     }
 
     let forward = !aEvent.shiftKey;
     // check if focus is leaving the frame (incl. the close button):
     if ((aEvent.target == this._closeButton && !forward) ||
         (isLastFocusableElement(aEvent.originalTarget) && forward)) {
       aEvent.preventDefault();
       aEvent.stopImmediatePropagation();
-      let parentWin = this._getBrowser().ownerDocument.defaultView;
+      let parentWin = this._getBrowser().ownerGlobal;
       if (forward) {
         fm.moveFocus(parentWin, null, fm.MOVEFOCUS_FIRST, fm.FLAG_BYKEY);
       } else {
         // Somehow, moving back 'past' the opening doc is not trivial. Cheat by doing it in 2 steps:
         fm.moveFocus(window, null, fm.MOVEFOCUS_ROOT, fm.FLAG_BYKEY);
         fm.moveFocus(parentWin, null, fm.MOVEFOCUS_BACKWARD, fm.FLAG_BYKEY);
       }
     }