Bug 1428839 - Part 5 - Move more methods and properties to the PanelView class. r=Gijs draft
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Thu, 18 Jan 2018 16:13:24 +0000
changeset 722194 ec843c63d1a7bd4fffb0e9058dd535dc6bad37d0
parent 722193 c6fc658c774f3facc61ec4f4276a34f885cfbf8b
child 746552 05d75ae0395b46247816387c10add3c3cb4e9998
push id96081
push userpaolo.mozmail@amadzone.org
push dateThu, 18 Jan 2018 16:15:56 +0000
reviewersGijs
bugs1428839
milestone59.0a1
Bug 1428839 - Part 5 - Move more methods and properties to the PanelView class. r=Gijs MozReview-Commit-ID: 59fUuB35Ygy
browser/components/customizableui/PanelMultiView.jsm
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -105,16 +105,36 @@ this.AssociatedToNode = class {
    * nsIDOMWindowUtils for the window of this node.
    */
   get _dwu() {
     if (this.__dwu)
       return this.__dwu;
     return this.__dwu = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
                                    .getInterface(Ci.nsIDOMWindowUtils);
   }
+
+  /**
+   * Helper method to emit an event on an element, whilst also making sure that
+   * the correct method is called on CustomizableWidget instances.
+   *
+   * @param  {String}    eventName Name of the event to dispatch.
+   * @param  {Object}    [detail]  Event detail object. Optional.
+   * @param  {Boolean}   cancelable If the event can be canceled.
+   * @return {Boolean} `true` if the event was canceled by an event handler, `false`
+   *                   otherwise.
+   */
+  dispatchCustomEvent(eventName, detail, cancelable = false) {
+    let event = new this.window.CustomEvent(eventName, {
+      detail,
+      bubbles: true,
+      cancelable,
+    });
+    this.node.dispatchEvent(event);
+    return event.defaultPrevented;
+  }
 };
 
 /**
  * This is the implementation of the panelUI.xml XBL binding, moved to this
  * module, to make it easier to fork the logic for the newer photon structure.
  * Goals are:
  * 1. to make it easier to programmatically extend the list of panels,
  * 2. allow for navigation between panels multiple levels deep and
@@ -296,56 +316,16 @@ this.PanelMultiView = class extends this
     let subviews = Array.from(viewNodeContainer.childNodes);
     for (let subview of subviews) {
       // XBL lists the 'children' XBL element explicitly. :-(
       if (subview.nodeName != "children")
         this._panelViewCache.appendChild(subview);
     }
   }
 
-  _setHeader(viewNode, titleText) {
-    // If the header already exists, update or remove it as requested.
-    let header = viewNode.firstChild;
-    if (header && header.classList.contains("panel-header")) {
-      if (titleText) {
-        header.querySelector("label").setAttribute("value", titleText);
-      } else {
-        header.remove();
-      }
-      return;
-    }
-
-    // The header doesn't exist, only create it if needed.
-    if (!titleText) {
-      return;
-    }
-
-    header = this.document.createElement("box");
-    header.classList.add("panel-header");
-
-    let backButton = this.document.createElement("toolbarbutton");
-    backButton.className =
-      "subviewbutton subviewbutton-iconic subviewbutton-back";
-    backButton.setAttribute("closemenu", "none");
-    backButton.setAttribute("tabindex", "0");
-    backButton.setAttribute("tooltip",
-      this.node.getAttribute("data-subviewbutton-tooltip"));
-    backButton.addEventListener("command", () => {
-      // The panelmultiview element may change if the view is reused.
-      viewNode.panelMultiView.goBack();
-      backButton.blur();
-    });
-
-    let label = this.document.createElement("label");
-    label.setAttribute("value", titleText);
-
-    header.append(backButton, label);
-    viewNode.prepend(header);
-  }
-
   goBack() {
     let previous = this.openViews.pop();
     let current = this._currentSubView;
     return this.showSubView(current, null, previous);
   }
 
   /**
    * Checks whether it is possible to navigate backwards currently. Returns
@@ -372,33 +352,28 @@ this.PanelMultiView = class extends this
    * @param {panelview} [theOne] The panelview DOM node to ensure is visible.
    *                             Optional.
    */
   hideAllViewsExcept(theOne = null) {
     for (let panelview of this.knownViews) {
       // When the panelview was already reparented, don't interfere any more.
       if (panelview == theOne || !this.node || panelview.panelMultiView != this.node)
         continue;
-      if (panelview.hasAttribute("current"))
-        this._dispatchViewEvent(panelview, "ViewHiding");
-      panelview.removeAttribute("current");
+      PanelView.forNode(panelview).current = false;
     }
 
     this._viewShowing = null;
 
     if (!this.node || !theOne)
       return;
 
     if (!this.openViews.includes(theOne))
       this.openViews.push(theOne);
-    if (!theOne.hasAttribute("current")) {
-      theOne.setAttribute("current", true);
-      this.descriptionHeightWorkaround(theOne);
-      this._dispatchViewEvent(theOne, "ViewShown");
-    }
+
+    PanelView.forNode(theOne).current = true;
     this._showingSubView = theOne.id != this._mainViewId;
   }
 
   showSubView(aViewId, aAnchor, aPreviousView) {
     this._currentShowPromise = (async () => {
       // Support passing in the node directly.
       let viewNode = typeof aViewId == "string" ? this.node.querySelector("#" + aViewId) : aViewId;
       if (!viewNode) {
@@ -407,24 +382,25 @@ this.PanelMultiView = class extends this
           this._viewStack.appendChild(viewNode);
         } else {
           throw new Error(`Subview ${aViewId} doesn't exist!`);
         }
       } else if (viewNode.parentNode == this._panelViewCache) {
         this._viewStack.appendChild(viewNode);
       }
 
+      let newPanelView = PanelView.forNode(viewNode);
       this.knownViews.add(viewNode);
 
       viewNode.panelMultiView = this.node;
 
       let reverse = !!aPreviousView;
       if (!reverse) {
-        this._setHeader(viewNode, viewNode.getAttribute("title") ||
-                                  (aAnchor && aAnchor.getAttribute("label")));
+        newPanelView.setHeader(newPanelView.title ||
+                               (aAnchor && aAnchor.getAttribute("label")));
       }
 
       let previousViewNode = aPreviousView || this._currentSubView;
       // If the panelview to show is the same as the previous one, the 'ViewShowing'
       // event has already been dispatched. Don't do it twice.
       let showingSameView = viewNode == previousViewNode;
       let playTransition = (!!previousViewNode && !showingSameView && this._panel.state == "open");
       let isMainView = viewNode.id == this._mainViewId;
@@ -445,20 +421,17 @@ this.PanelMultiView = class extends this
         this._mainViewHeight = previousRect.height;
         this._viewContainer.style.minHeight = this._mainViewHeight + "px";
       }
 
       this._viewShowing = viewNode;
       // Because the 'mainview' attribute may be out-of-sync, due to view node
       // reparenting in combination with ephemeral PanelMultiView instances,
       // this is the best place to correct it (just before showing).
-      if (isMainView)
-        viewNode.setAttribute("mainview", true);
-      else
-        viewNode.removeAttribute("mainview");
+      newPanelView.mainview = isMainView;
 
       if (aAnchor) {
         viewNode.classList.add("PanelUI-subView");
       }
       if (!isMainView && this._mainViewWidth)
         viewNode.style.maxWidth = viewNode.style.minWidth = this._mainViewWidth + "px";
 
       if (!showingSameView || !viewNode.hasAttribute("current")) {
@@ -466,17 +439,17 @@ this.PanelMultiView = class extends this
         // to lazily populate the subview with things or perhaps even cancel this
         // whole operation.
         let detail = {
           blockers: new Set(),
           addBlocker(promise) {
             this.blockers.add(promise);
           }
         };
-        let cancel = this._dispatchViewEvent(viewNode, "ViewShowing", aAnchor, detail);
+        let cancel = newPanelView.dispatchCustomEvent("ViewShowing", detail, true);
         if (detail.blockers.size) {
           try {
             let results = await Promise.all(detail.blockers);
             cancel = cancel || results.some(val => val === false);
           } catch (e) {
             Cu.reportError(e);
             cancel = true;
           }
@@ -722,45 +695,16 @@ this.PanelMultiView = class extends this
       // We force 'display: none' on the previous view node to make sure that it
       // doesn't cause an annoying flicker whilst resetting the styles above.
       previousViewNode.style.display = "none";
       await BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {});
       previousViewNode.style.removeProperty("display");
     }
   }
 
-  /**
-   * Helper method to emit an event on a panelview, whilst also making sure that
-   * the correct method is called on CustomizableWidget instances.
-   *
-   * @param  {panelview} viewNode  Target of the event to dispatch.
-   * @param  {String}    eventName Name of the event to dispatch.
-   * @param  {DOMNode}   [anchor]  Node where the panel is anchored to. Optional.
-   * @param  {Object}    [detail]  Event detail object. Optional.
-   * @return {Boolean} `true` if the event was canceled by an event handler, `false`
-   *                   otherwise.
-   */
-  _dispatchViewEvent(viewNode, eventName, anchor, detail) {
-    let cancel = false;
-    if (eventName != "PanelMultiViewHidden") {
-      // Don't need to do this for PanelMultiViewHidden event
-      CustomizableUI.ensureSubviewListeners(viewNode);
-    }
-
-    let evt = new this.window.CustomEvent(eventName, {
-      detail,
-      bubbles: true,
-      cancelable: eventName == "ViewShowing"
-    });
-    viewNode.dispatchEvent(evt);
-    if (!cancel)
-      cancel = evt.defaultPrevented;
-    return cancel;
-  }
-
   _calculateMaxHeight() {
     // While opening the panel, we have to limit the maximum height of any
     // view based on the space that will be available. We cannot just use
     // window.screen.availTop and availHeight because these may return an
     // incorrect value when the window spans multiple screens.
     let anchorBox = this._panel.anchorNode.boxObject;
     let screen = this._screenManager.screenForRect(anchorBox.screenX,
                                                    anchorBox.screenY,
@@ -859,17 +803,17 @@ this.PanelMultiView = class extends this
         // when the popup is opened again, e.g. through touch mode sizing.
         this._mainViewHeight = 0;
         this._mainViewWidth = 0;
         this._viewContainer.style.removeProperty("min-height");
         this._viewStack.style.removeProperty("max-height");
         this._viewContainer.style.removeProperty("min-width");
         this._viewContainer.style.removeProperty("max-width");
 
-        this._dispatchViewEvent(this.node, "PanelMultiViewHidden");
+        this.dispatchCustomEvent("PanelMultiViewHidden");
         break;
       }
     }
   }
 
   /**
    * Based on going up or down, select the previous or next focusable button
    * in the current view.
@@ -1068,17 +1012,121 @@ this.PanelMultiView = class extends this
    *
    * This may trigger a synchronous layout.
    *
    * @param viewNode
    *        Indicates the node to scan for descendant elements. This is the main
    *        view if omitted.
    */
   descriptionHeightWorkaround(viewNode = this._mainView) {
-    if (!viewNode || !viewNode.hasAttribute("descriptionheightworkaround")) {
+    if (!viewNode) {
+      return;
+    }
+
+    PanelView.forNode(viewNode).descriptionHeightWorkaround();
+  }
+};
+
+/**
+ * This is associated to <panelview> elements.
+ */
+this.PanelView = class extends this.AssociatedToNode {
+  get title() {
+    return this.node.getAttribute("title");
+  }
+
+  /**
+   * The "mainview" attribute is set before the panel is opened when this view
+   * is displayed as the main view, and is reset before the <panelview> is
+   * displayed as a subview.
+   */
+  set mainview(value) {
+    if (value) {
+      this.node.setAttribute("mainview", true);
+    } else {
+      this.node.removeAttribute("mainview");
+    }
+  }
+
+  set current(value) {
+    if (value) {
+      if (!this.node.hasAttribute("current")) {
+        this.node.setAttribute("current", true);
+        this.descriptionHeightWorkaround();
+        this.dispatchCustomEvent("ViewShown");
+      }
+    } else if (this.node.hasAttribute("current")) {
+      this.dispatchCustomEvent("ViewHiding");
+      this.node.removeAttribute("current");
+    }
+  }
+
+  /**
+   * Also make sure that the correct method is called on CustomizableWidget.
+   */
+  dispatchCustomEvent(...args) {
+    CustomizableUI.ensureSubviewListeners(this.node);
+    super.dispatchCustomEvent(...args);
+  }
+
+  /**
+   * Adds a header with the given title, or removes it if the title is empty.
+   */
+  setHeader(titleText) {
+    // If the header already exists, update or remove it as requested.
+    let header = this.node.firstChild;
+    if (header && header.classList.contains("panel-header")) {
+      if (titleText) {
+        header.querySelector("label").setAttribute("value", titleText);
+      } else {
+        header.remove();
+      }
+      return;
+    }
+
+    // The header doesn't exist, only create it if needed.
+    if (!titleText) {
+      return;
+    }
+
+    header = this.document.createElement("box");
+    header.classList.add("panel-header");
+
+    let backButton = this.document.createElement("toolbarbutton");
+    backButton.className =
+      "subviewbutton subviewbutton-iconic subviewbutton-back";
+    backButton.setAttribute("closemenu", "none");
+    backButton.setAttribute("tabindex", "0");
+    backButton.setAttribute("tooltip",
+      this.node.getAttribute("data-subviewbutton-tooltip"));
+    backButton.addEventListener("command", () => {
+      // The panelmultiview element may change if the view is reused.
+      this.node.panelMultiView.goBack();
+      backButton.blur();
+    });
+
+    let label = this.document.createElement("label");
+    label.setAttribute("value", titleText);
+
+    header.append(backButton, label);
+    this.node.prepend(header);
+  }
+
+  /**
+   * If the main view or a subview contains wrapping elements, the attribute
+   * "descriptionheightworkaround" should be set on the view to force all the
+   * wrapping "description", "label" or "toolbarbutton" elements to a fixed
+   * height. If the attribute is set and the visibility, contents, or width
+   * of any of these elements changes, this function should be called to
+   * refresh the calculated heights.
+   *
+   * This may trigger a synchronous layout.
+   */
+  descriptionHeightWorkaround() {
+    if (!this.node.hasAttribute("descriptionheightworkaround")) {
       // This view does not require the workaround.
       return;
     }
 
     // We batch DOM changes together in order to reduce synchronous layouts.
     // First we reset any change we may have made previously. The first time
     // this is called, and in the best case scenario, this has no effect.
     let items = [];
@@ -1086,17 +1134,17 @@ this.PanelMultiView = class extends this
     // and also don't have a value attribute can be multiline (if their
     // text content is long enough).
     let isMultiline = ":not(:-moz-any([hidden],[value],:empty))";
     let selector = [
       "description" + isMultiline,
       "label" + isMultiline,
       "toolbarbutton[wrap]:not([hidden])",
     ].join(",");
-    for (let element of viewNode.querySelectorAll(selector)) {
+    for (let element of this.node.querySelectorAll(selector)) {
       // Ignore items in hidden containers.
       if (element.closest("[hidden]")) {
         continue;
       }
       // Take the label for toolbarbuttons; it only exists on those elements.
       element = element.labelElement || element;
 
       let bounds = element.getBoundingClientRect();
@@ -1125,22 +1173,17 @@ this.PanelMultiView = class extends this
     }
 
     // Now we can make all the necessary DOM changes at once.
     for (let { element, bounds } of items) {
       this._multiLineElementsMap.set(element, { bounds, textContent: element.textContent });
       element.style.height = bounds.height + "px";
     }
   }
-};
 
-/**
- * This is associated to <panelview> elements.
- */
-this.PanelView = class extends this.AssociatedToNode {
   /**
    * Retrieves the button elements that can be used for navigation using the
    * keyboard, that is all enabled buttons including the back button if visible.
    *
    * @return {Array}
    */
   getNavigableElements() {
     let buttons = Array.from(this.node.querySelectorAll(".subviewbutton:not([disabled])"));