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
--- 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;