Bug 1259093: Part 1 - Allow async initialization from ViewShowing events. r?Gijs draft
authorKris Maglione <maglione.k@gmail.com>
Sun, 14 Aug 2016 18:33:10 -0700
changeset 400540 7964660fb2234a9352d2682bcea84171f879a907
parent 399411 e472c47e7912625a1a9cadf6dcfb7bcec2afc686
child 400541 3b8a9351f6487b82bb8601c5d2e6f81407eb124e
child 401495 c07402726353cb395c827c1f66257ebcb817a1a7
push id26179
push usermaglione.k@gmail.com
push dateMon, 15 Aug 2016 03:48:47 +0000
reviewersGijs
bugs1259093
milestone51.0a1
Bug 1259093: Part 1 - Allow async initialization from ViewShowing events. r?Gijs This changes the `detail` of the ViewShowing events in an incompatible way. There were only a few places it was used in mozilla-central, which I fixed. I searched add-ons in DXR and didn't find any that used the `detail` member of this event, but I had to resort to a random sampling, so it's possible that some do exist. MozReview-Commit-ID: CYzGw6KH7uI
browser/components/customizableui/CustomizableUI.jsm
browser/components/customizableui/CustomizableWidgets.jsm
browser/components/customizableui/content/panelUI.js
browser/components/customizableui/content/panelUI.xml
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -3228,17 +3228,27 @@ this.CustomizableUI = {
    *                  passing the document from which it was removed. This is
    *                  useful especially for 'view' type widgets that need to
    *                  cleanup after views that were constructed on the fly.
    * - onCommand(aEvt): Only useful for button widgets; a function that will be
    *                    invoked when the user activates the button.
    * - onClick(aEvt): Attached to all widgets; a function that will be invoked
    *                  when the user clicks the widget.
    * - onViewShowing(aEvt): Only useful for views; a function that will be
-   *                  invoked when a user shows your view.
+   *                  invoked when a user shows your view. If any event
+   *                  handler calls aEvt.preventDefault(), the view will
+   *                  not be shown.
+   *
+   *                  The event's `detail` property is an object with an
+   *                  `addBlocker` method. Handlers which need to
+   *                  perform asynchronous operations before the view is
+   *                  shown may pass this method a Promise, which will
+   *                  prevent the view from showing until it resolves.
+   *                  Additionally, if the promise resolves to the exact
+   *                  value `false`, the view will not be shown.
    * - onViewHiding(aEvt): Only useful for views; a function that will be
    *                  invoked when a user hides your view.
    * - tooltiptext:   string to use for the tooltip of the widget
    * - label:         string to use for the label of the widget
    * - removable:     whether the widget is removable (optional, default: true)
    *                  NB: if you specify false here, you must provide a
    *                  defaultArea, too.
    * - overflows:     whether widget can overflow when in an overflowable
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -176,17 +176,17 @@ const CustomizableWidgets = [
     type: "view",
     viewId: "PanelUI-history",
     shortcutId: "key_gotoHistory",
     tooltiptext: "history-panelmenu.tooltiptext2",
     defaultArea: CustomizableUI.AREA_PANEL,
     onViewShowing: function(aEvent) {
       // Populate our list of history
       const kMaxResults = 15;
-      let doc = aEvent.detail.ownerDocument;
+      let doc = aEvent.target.ownerDocument;
       let win = doc.defaultView;
 
       let options = PlacesUtils.history.getNewQueryOptions();
       options.excludeQueries = true;
       options.queryType = options.QUERY_TYPE_HISTORY;
       options.sortingMode = options.SORT_BY_DATE_DESCENDING;
       options.maxResults = kMaxResults;
       let query = PlacesUtils.history.getNewQuery();
@@ -904,17 +904,17 @@ const CustomizableWidgets = [
       if (feeds && feeds.length == 1 && isClick) {
         aEvent.preventDefault();
         aEvent.stopPropagation();
         win.FeedHandler.subscribeToFeed(feeds[0].href, aEvent);
         CustomizableUI.hidePanelForNode(aEvent.target);
       }
     },
     onViewShowing: function(aEvent) {
-      let doc = aEvent.detail.ownerDocument;
+      let doc = aEvent.target.ownerDocument;
       let container = doc.getElementById("PanelUI-feeds");
       let gotView = doc.defaultView.FeedHandler.buildFeedList(container, true);
 
       // For no feeds or only a single one, don't show the panel.
       if (!gotView) {
         aEvent.preventDefault();
         aEvent.stopPropagation();
         return;
@@ -1123,17 +1123,17 @@ const CustomizableWidgets = [
       this.updateVisibility(aNode);
 
       if (!this.hasObserver) {
         Services.prefs.addObserver("privacy.userContext.enabled", this, true);
         this.hasObserver = true;
       }
     },
     onViewShowing: function(aEvent) {
-      let doc = aEvent.detail.ownerDocument;
+      let doc = aEvent.target.ownerDocument;
 
       let items = doc.getElementById("PanelUI-containersItems");
 
       while (items.firstChild) {
         items.firstChild.remove();
       }
 
       let fragment = doc.createDocumentFragment();
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -298,17 +298,17 @@ const PanelUI = {
 
   /**
    * Shows a subview in the panel with a given ID.
    *
    * @param aViewId the ID of the subview to show.
    * @param aAnchor the element that spawned the subview.
    * @param aPlacementArea the CustomizableUI area that aAnchor is in.
    */
-  showSubView: function(aViewId, aAnchor, aPlacementArea) {
+  showSubView: Task.async(function*(aViewId, aAnchor, aPlacementArea) {
     this._ensureEventListenersAdded();
     let viewNode = document.getElementById(aViewId);
     if (!viewNode) {
       Cu.reportError("Could not show panel subview with id: " + aViewId);
       return;
     }
 
     if (!aAnchor) {
@@ -317,20 +317,33 @@ const PanelUI = {
     }
 
     if (aPlacementArea == CustomizableUI.AREA_PANEL) {
       this.multiView.showSubView(aViewId, aAnchor);
     } else if (!aAnchor.open) {
       aAnchor.open = true;
       // Emit the ViewShowing event so that the widget definition has a chance
       // to lazily populate the subview with things.
-      let evt = document.createEvent("CustomEvent");
-      evt.initCustomEvent("ViewShowing", true, true, viewNode);
+      let detail = {
+        blockers: new Set(),
+        addBlocker(aPromise) {
+          this.blockers.add(aPromise);
+        },
+      };
+
+      let evt = new CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
       viewNode.dispatchEvent(evt);
-      if (evt.defaultPrevented) {
+
+      let cancel = evt.defaultPrevented;
+      if (detail.blockers.size) {
+        let results = yield Promise.all(detail.blockers);
+        cancel = cancel || results.some(val => val === false);
+      }
+
+      if (cancel) {
         aAnchor.open = false;
         return;
       }
 
       let tempPanel = document.createElement("panel");
       tempPanel.setAttribute("type", "arrow");
       tempPanel.setAttribute("id", "customizationui-widget-panel");
       tempPanel.setAttribute("class", "cui-widget-panel");
@@ -370,17 +383,17 @@ const PanelUI = {
         document.getAnonymousElementByAttribute(aAnchor, "class",
                                                 "toolbarbutton-icon");
 
       if (iconAnchor && aAnchor.id) {
         iconAnchor.setAttribute("consumeanchor", aAnchor.id);
       }
       tempPanel.openPopup(iconAnchor || aAnchor, "bottomcenter topright");
     }
-  },
+  }),
 
   /**
    * NB: The enable- and disableSingleSubviewPanelAnimations methods only
    * affect the hiding/showing animations of single-subview panels (tempPanel
    * in the showSubView method).
    */
   disableSingleSubviewPanelAnimations: function() {
     this._disableAnimations = true;
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -171,54 +171,69 @@
           this._shiftMainView();
         ]]></body>
       </method>
 
       <method name="showSubView">
         <parameter name="aViewId"/>
         <parameter name="aAnchor"/>
         <body><![CDATA[
-          let viewNode = this.querySelector("#" + aViewId);
-          viewNode.setAttribute("current", true);
-          // Emit the ViewShowing event so that the widget definition has a chance
-          // to lazily populate the subview with things.
-          let evt = document.createEvent("CustomEvent");
-          evt.initCustomEvent("ViewShowing", true, true, viewNode);
-          viewNode.dispatchEvent(evt);
-          if (evt.defaultPrevented) {
-            return;
-          }
+          Task.spawn(function*() {
+            let viewNode = this.querySelector("#" + aViewId);
+            viewNode.setAttribute("current", true);
+            // Emit the ViewShowing event so that the widget definition has a chance
+            // to lazily populate the subview with things.
+            let detail = {
+              blockers: new Set(),
+              addBlocker(aPromise) {
+                this.blockers.add(aPromise);
+              },
+            };
 
-          this._currentSubView = viewNode;
+            let evt = new CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
+            viewNode.dispatchEvent(evt);
+
+            let cancel = evt.defaultPrevented;
+            if (detail.blockers.size) {
+              let results = yield Promise.all(detail.blockers);
+              cancel = cancel || results.some(val => val === false);
+            }
+
+            if (cancel) {
+              return;
+            }
+
+            this._currentSubView = viewNode;
 
-          // Now we have to transition the panel. There are a few parts to this:
-          //
-          // 1) The main view content gets shifted so that the center of the anchor
-          //    node is at the left-most edge of the panel.
-          // 2) The subview deck slides in so that it takes up almost all of the
-          //    panel.
-          // 3) If the subview is taller then the main panel contents, then the panel
-          //    must grow to meet that new height. Otherwise, it must shrink.
-          //
-          // All three of these actions make use of CSS transformations, so they
-          // should all occur simultaneously.
-          this.setAttribute("viewtype", "subview");
-          this._shiftMainView(aAnchor);
+            // Now we have to transition the panel. There are a few parts to this:
+            //
+            // 1) The main view content gets shifted so that the center of the anchor
+            //    node is at the left-most edge of the panel.
+            // 2) The subview deck slides in so that it takes up almost all of the
+            //    panel.
+            // 3) If the subview is taller then the main panel contents, then the panel
+            //    must grow to meet that new height. Otherwise, it must shrink.
+            //
+            // All three of these actions make use of CSS transformations, so they
+            // should all occur simultaneously.
+            this.setAttribute("viewtype", "subview");
+            this._shiftMainView(aAnchor);
 
-          this._mainViewHeight = this._viewStack.clientHeight;
+            this._mainViewHeight = this._viewStack.clientHeight;
 
-          let newHeight = this._heightOfSubview(viewNode, this._subViews);
-          this._setViewContainerHeight(newHeight);
+            let newHeight = this._heightOfSubview(viewNode, this._subViews);
+            this._setViewContainerHeight(newHeight);
 
-          this._subViewObserver.observe(viewNode, {
-            attributes: true,
-            characterData: true,
-            childList: true,
-            subtree: true
-          });
+            this._subViewObserver.observe(viewNode, {
+              attributes: true,
+              characterData: true,
+              childList: true,
+              subtree: true
+            });
+          }.bind(this));
         ]]></body>
       </method>
 
       <method name="_setViewContainerHeight">
         <parameter name="aHeight"/>
         <body><![CDATA[
           let container = this._viewContainer;
           this._transitioning = true;