Bug 1363178 - Move the panelmultiview JS implementation to a separate module. r?Gijs draft
authorMike de Boer <mdeboer@mozilla.com>
Mon, 08 May 2017 16:42:16 -0400
changeset 574414 602786220871f7d0454db9b1102ef97cb53c51ba
parent 574259 38f862749aed4e9a874182a9019e0671289088b0
child 574438 fbd66409d1ad3dde5cfa244d48857eb75d304d9b
push id57700
push usermdeboer@mozilla.com
push dateMon, 08 May 2017 20:44:57 +0000
reviewersGijs
bugs1363178
milestone55.0a1
Bug 1363178 - Move the panelmultiview JS implementation to a separate module. r?Gijs MozReview-Commit-ID: 92GCeTOp2cP
browser/components/customizableui/PanelMultiView.jsm
browser/components/customizableui/content/panelUI.xml
browser/components/customizableui/moz.build
copy from browser/components/customizableui/content/panelUI.xml
copy to browser/components/customizableui/PanelMultiView.jsm
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -1,517 +1,477 @@
-<?xml version="1.0"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-<bindings id="browserPanelUIBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl">
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["PanelMultiView"];
 
-  <binding id="panelmultiview">
-    <resources>
-      <stylesheet src="chrome://browser/content/customizableui/panelUI.css"/>
-    </resources>
-    <content>
-      <xul:box anonid="viewContainer" class="panel-viewcontainer" xbl:inherits="panelopen,viewtype,transitioning">
-        <xul:stack anonid="viewStack" xbl:inherits="viewtype,transitioning" viewtype="main" class="panel-viewstack">
-          <xul:vbox anonid="mainViewContainer" class="panel-mainview" xbl:inherits="viewtype"/>
+/**
+ * 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
+ * 3. maintain the pre-photon structure with as little effort possible.
+ *
+ * @type {PanelMultiView}
+ */
+this.PanelMultiView = class {
+  get document() {
+    return this.node.ownerDocument;
+  }
 
-          <!-- Used to capture click events over the PanelUI-mainView if we're in
-               subview mode. That way, any click on the PanelUI-mainView causes us
-               to revert to the mainView mode, whereupon PanelUI-click-capture then
-               allows click events to go through it. -->
-          <xul:vbox anonid="clickCapturer" class="panel-clickcapturer"/>
+  get window() {
+    return this.node.ownerGlobal;
+  }
+
+  get _panel() {
+    return this.node.parentNode;
+  }
 
-          <!-- We manually set display: none (via a CSS attribute selector) on the
-               subviews that are not being displayed. We're using this over a deck
-               because a deck assumes the size of its largest child, regardless of
-               whether or not it is shown. That's not good for our case, since we
-               want to allow each subview to be uniquely sized. -->
-          <xul:vbox anonid="subViews" class="panel-subviews" xbl:inherits="panelopen">
-            <children includes="panelview"/>
-          </xul:vbox>
-        </xul:stack>
-      </xul:box>
-    </content>
-    <implementation implements="nsIDOMEventListener">
-      <field name="_clickCapturer" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "clickCapturer");
-      </field>
-      <field name="_viewContainer" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "viewContainer");
-      </field>
-      <field name="_mainViewContainer" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "mainViewContainer");
-      </field>
-      <field name="_subViews" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "subViews");
-      </field>
-      <field name="_viewStack" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "viewStack");
-      </field>
-      <field name="_panel" readonly="true">
-        this.parentNode;
-      </field>
+  get showingSubView() {
+    return this._viewStack.getAttribute("viewtype") == "subview";
+  }
+  get _mainViewId() {
+    return this.node.getAttribute("mainViewId");
+  }
+  set _mainViewId(val) {
+    this.node.setAttribute("mainViewId", val);
+    return val;
+  }
+  get _mainView() {
+    return this._mainViewId ? this.document.getElementById(this._mainViewId) : null;
+  }
+  get showingSubViewAsMainView() {
+    return this.node.getAttribute("mainViewIsSubView") == "true";
+  }
+
+  get ignoreMutations() {
+    return this._ignoreMutations;
+  }
+  set ignoreMutations(val) {
+    this._ignoreMutations = val;
+    if (!val && this._panel.state == "open") {
+      if (this.showingSubView) {
+        this._syncContainerWithSubView();
+      } else {
+        this._syncContainerWithMainView();
+      }
+    }
+  }
 
-      <field name="_currentSubView">null</field>
-      <field name="_anchorElement">null</field>
-      <field name="_mainViewHeight">0</field>
-      <field name="_subViewObserver">null</field>
-      <field name="__transitioning">false</field>
-      <field name="_ignoreMutations">false</field>
+  get _transitioning() {
+    return this.__transitioning;
+  }
+  set _transitioning(val) {
+    this.__transitioning = val;
+    if (val) {
+      this.node.setAttribute("transitioning", "true");
+    } else {
+      this.node.removeAttribute("transitioning");
+    }
+  }
+
+  constructor(xulNode) {
+    this.node = xulNode;
+
+    this._currentSubView = this._anchorElement = this._subViewObserver = null;
+    this._mainViewHeight = 0;
+    this.__transitioning = this._ignoreMutations = false;
+
+    const {document, window} = this;
+
+    this._clickCapturer =
+      document.getAnonymousElementByAttribute(this.node, "anonid", "clickCapturer");
+    this._viewContainer =
+      document.getAnonymousElementByAttribute(this.node, "anonid", "viewContainer");
+    this._mainViewContainer =
+      document.getAnonymousElementByAttribute(this.node, "anonid", "mainViewContainer");
+    this._subViews =
+      document.getAnonymousElementByAttribute(this.node, "anonid", "subViews");
+    this._viewStack =
+      document.getAnonymousElementByAttribute(this.node, "anonid", "viewStack");
 
-      <property name="showingSubView" readonly="true"
-                onget="return this._viewStack.getAttribute('viewtype') == 'subview'"/>
-      <property name="_mainViewId" onget="return this.getAttribute('mainViewId');" onset="this.setAttribute('mainViewId', val); return val;"/>
-      <property name="_mainView" readonly="true"
-                onget="return this._mainViewId ? document.getElementById(this._mainViewId) : null;"/>
-      <property name="showingSubViewAsMainView" readonly="true"
-                onget="return this.getAttribute('mainViewIsSubView') == 'true'"/>
+    this._clickCapturer.addEventListener("click", this);
+    this._panel.addEventListener("popupshowing", this);
+    this._panel.addEventListener("popupshown", this);
+    this._panel.addEventListener("popuphidden", this);
+    this._subViews.addEventListener("overflow", this);
+    this._mainViewContainer.addEventListener("overflow", this);
+
+    // Get a MutationObserver ready to react to subview size changes. We
+    // only attach this MutationObserver when a subview is being displayed.
+    this._subViewObserver = new window.MutationObserver(this._syncContainerWithSubView.bind(this));
+    this._mainViewObserver = new window.MutationObserver(this._syncContainerWithMainView.bind(this));
+
+    this._mainViewContainer.setAttribute("panelid", this._panel.id);
 
-      <property name="ignoreMutations">
-        <getter>
-          return this._ignoreMutations;
-        </getter>
-        <setter><![CDATA[
-          this._ignoreMutations = val;
-          if (!val && this._panel.state == "open") {
-            if (this.showingSubView) {
-              this._syncContainerWithSubView();
-            } else {
-              this._syncContainerWithMainView();
-            }
-          }
-        ]]></setter>
-      </property>
+    if (this._mainView) {
+      this.setMainView(this._mainView);
+    }
+    this.node.setAttribute("viewtype", "main");
+
+    // Proxy these public properties and methods, as used elsewhere by various
+    // parts of the browser, to this instance.
+    ["_mainView", "ignoreMutations", "showingSubView"].forEach(property => {
+      Object.defineProperty(this.node, property, {
+        enumerable: true,
+        get: () => this[property],
+        set: (val) => this[property] = val
+      });
+    });
+    ["setHeightToFit", "setMainView", "showMainView", "showSubView"].forEach(method => {
+      Object.defineProperty(this.node, method, {
+        enumerable: true,
+        value: (...args) => this[method](...args)
+      });
+    });
+  }
 
-      <property name="_transitioning">
-        <getter>
-          return this.__transitioning;
-        </getter>
-        <setter><![CDATA[
-          this.__transitioning = val;
-          if (val) {
-            this.setAttribute("transitioning", "true");
-          } else {
-            this.removeAttribute("transitioning");
-          }
-        ]]></setter>
-      </property>
-      <constructor><![CDATA[
-        this._clickCapturer.addEventListener("click", this);
-        this._panel.addEventListener("popupshowing", this);
-        this._panel.addEventListener("popupshown", this);
-        this._panel.addEventListener("popuphidden", this);
-        this._subViews.addEventListener("overflow", this);
-        this._mainViewContainer.addEventListener("overflow", this);
+  destructor() {
+    if (this._mainView) {
+      this._mainView.removeAttribute("mainview");
+    }
+    this._mainViewObserver.disconnect();
+    this._subViewObserver.disconnect();
+    this._panel.removeEventListener("popupshowing", this);
+    this._panel.removeEventListener("popupshown", this);
+    this._panel.removeEventListener("popuphidden", this);
+    this._subViews.removeEventListener("overflow", this);
+    this._mainViewContainer.removeEventListener("overflow", this);
+    this._clickCapturer.removeEventListener("click", this);
+
+    this.node = this.__clickCapturer = this.__viewContainer = this.__mainViewContainer =
+      this.__subViews = this.__viewStack = null;
+  }
+
+  setMainView(aNewMainView) {
+    if (this._mainView) {
+      this._mainViewObserver.disconnect();
+      this._subViews.appendChild(this._mainView);
+      this._mainView.removeAttribute("mainview");
+    }
+    this._mainViewId = aNewMainView.id;
+    aNewMainView.setAttribute("mainview", "true");
+    this._mainViewContainer.appendChild(aNewMainView);
+  }
 
-        // Get a MutationObserver ready to react to subview size changes. We
-        // only attach this MutationObserver when a subview is being displayed.
-        this._subViewObserver =
-          new MutationObserver(this._syncContainerWithSubView.bind(this));
-        this._mainViewObserver =
-          new MutationObserver(this._syncContainerWithMainView.bind(this));
+  showMainView() {
+    if (this.showingSubView) {
+      let viewNode = this._currentSubView;
+      let evt = new this.window.CustomEvent("ViewHiding", { bubbles: true, cancelable: true });
+      viewNode.dispatchEvent(evt);
 
-        this._mainViewContainer.setAttribute("panelid",
-                                             this._panel.id);
+      viewNode.removeAttribute("current");
+      this._currentSubView = null;
+
+      this._subViewObserver.disconnect();
+
+      this._setViewContainerHeight(this._mainViewHeight);
 
-        if (this._mainView) {
-          this.setMainView(this._mainView);
-        }
-        this.setAttribute("viewtype", "main");
-      ]]></constructor>
+      this.node.setAttribute("viewtype", "main");
+    }
+
+    this._shiftMainView();
+  }
 
-      <destructor><![CDATA[
-        if (this._mainView) {
-          this._mainView.removeAttribute("mainview");
+  showSubView(aViewId, aAnchor) {
+    const {document, window} = this;
+    window.Task.spawn(function*() {
+      let viewNode = this.node.querySelector("#" + aViewId);
+      if (!viewNode) {
+        viewNode = document.getElementById(aViewId);
+        if (viewNode) {
+          this._subViews.appendChild(viewNode);
+        } else {
+          throw new Error(`Subview ${aViewId} doesn't exist!`);
         }
-        this._mainViewObserver.disconnect();
-        this._subViewObserver.disconnect();
-        this._panel.removeEventListener("popupshowing", this);
-        this._panel.removeEventListener("popupshown", this);
-        this._panel.removeEventListener("popuphidden", this);
-        this._subViews.removeEventListener("overflow", this);
-        this._mainViewContainer.removeEventListener("overflow", this);
-        this._clickCapturer.removeEventListener("click", this);
-      ]]></destructor>
+      }
+      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);
+        },
+      };
 
-      <method name="setMainView">
-        <parameter name="aNewMainView"/>
-        <body><![CDATA[
-        if (this._mainView) {
-          this._mainViewObserver.disconnect();
-          this._subViews.appendChild(this._mainView);
-          this._mainView.removeAttribute("mainview");
+      let evt = new window.CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
+      viewNode.dispatchEvent(evt);
+
+      let cancel = evt.defaultPrevented;
+      if (detail.blockers.size) {
+        try {
+          let results = yield window.Promise.all(detail.blockers);
+          cancel = cancel || results.some(val => val === false);
+        } catch (e) {
+          Components.utils.reportError(e);
+          cancel = true;
         }
-        this._mainViewId = aNewMainView.id;
-        aNewMainView.setAttribute("mainview", "true");
-        this._mainViewContainer.appendChild(aNewMainView);
-        ]]></body>
-      </method>
-
-      <method name="showMainView">
-        <body><![CDATA[
-          if (this.showingSubView) {
-            let viewNode = this._currentSubView;
-            let evt = document.createEvent("CustomEvent");
-            evt.initCustomEvent("ViewHiding", true, true, viewNode);
-            viewNode.dispatchEvent(evt);
-
-            viewNode.removeAttribute("current");
-            this._currentSubView = null;
-
-            this._subViewObserver.disconnect();
-
-            this._setViewContainerHeight(this._mainViewHeight);
-
-            this.setAttribute("viewtype", "main");
-          }
-
-          this._shiftMainView();
-        ]]></body>
-      </method>
+      }
 
-      <method name="showSubView">
-        <parameter name="aViewId"/>
-        <parameter name="aAnchor"/>
-        <body><![CDATA[
-          Task.spawn(function*() {
-            let viewNode = this.querySelector("#" + aViewId);
-            if (!viewNode) {
-              viewNode = document.getElementById(aViewId);
-              if (viewNode) {
-                this._subViews.appendChild(viewNode);
-              } else {
-                throw new Error(`Subview ${aViewId} doesn't exist!`);
-              }
-            }
-            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);
-              },
-            };
+      if (cancel) {
+        return;
+      }
 
-            let evt = new CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
-            viewNode.dispatchEvent(evt);
-
-            let cancel = evt.defaultPrevented;
-            if (detail.blockers.size) {
-              try {
-                let results = yield Promise.all(detail.blockers);
-                cancel = cancel || results.some(val => val === false);
-              } catch (e) {
-                Components.utils.reportError(e);
-                cancel = true;
-              }
-            }
-
-            if (cancel) {
-              return;
-            }
+      this._currentSubView = viewNode;
 
-            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);
-
-            this._mainViewHeight = this._viewStack.clientHeight;
-
-            let newHeight = this._heightOfSubview(viewNode, this._subViews);
-            this._setViewContainerHeight(newHeight);
+      // 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.node.setAttribute("viewtype", "subview");
+      this._shiftMainView(aAnchor);
 
-            this._subViewObserver.observe(viewNode, {
-              attributes: true,
-              characterData: true,
-              childList: true,
-              subtree: true
-            });
-          }.bind(this));
-        ]]></body>
-      </method>
+      this._mainViewHeight = this._viewStack.clientHeight;
 
-      <method name="_setViewContainerHeight">
-        <parameter name="aHeight"/>
-        <body><![CDATA[
-          let container = this._viewContainer;
-          this._transitioning = true;
-
-          let onTransitionEnd = () => {
-            container.removeEventListener("transitionend", onTransitionEnd);
-            this._transitioning = false;
-          };
+      let newHeight = this._heightOfSubview(viewNode, this._subViews);
+      this._setViewContainerHeight(newHeight);
 
-          container.addEventListener("transitionend", onTransitionEnd);
-          container.style.height = `${aHeight}px`;
-        ]]></body>
-      </method>
+      this._subViewObserver.observe(viewNode, {
+        attributes: true,
+        characterData: true,
+        childList: true,
+        subtree: true
+      });
+    }.bind(this));
+  }
+
+  _setViewContainerHeight(aHeight) {
+    let container = this._viewContainer;
+    this._transitioning = true;
+
+    let onTransitionEnd = () => {
+      container.removeEventListener("transitionend", onTransitionEnd);
+      this._transitioning = false;
+    };
 
-      <method name="_shiftMainView">
-        <parameter name="aAnchor"/>
-        <body><![CDATA[
-          if (aAnchor) {
-            // We need to find the edge of the anchor, relative to the main panel.
-            // Then we need to add half the width of the anchor. This is the target
-            // that we need to transition to.
-            let anchorRect = aAnchor.getBoundingClientRect();
-            let mainViewRect = this._mainViewContainer.getBoundingClientRect();
-            let center = aAnchor.clientWidth / 2;
-            let direction = aAnchor.ownerGlobal.getComputedStyle(aAnchor).direction;
-            let edge;
-            if (direction == "ltr") {
-              edge = anchorRect.left - mainViewRect.left;
-            } else {
-              edge = mainViewRect.right - anchorRect.right;
-            }
+    container.addEventListener("transitionend", onTransitionEnd);
+    container.style.height = `${aHeight}px`;
+  }
+
+  _shiftMainView(aAnchor) {
+    if (aAnchor) {
+      // We need to find the edge of the anchor, relative to the main panel.
+      // Then we need to add half the width of the anchor. This is the target
+      // that we need to transition to.
+      let anchorRect = aAnchor.getBoundingClientRect();
+      let mainViewRect = this._mainViewContainer.getBoundingClientRect();
+      let center = aAnchor.clientWidth / 2;
+      let direction = aAnchor.ownerGlobal.getComputedStyle(aAnchor).direction;
+      let edge;
+      if (direction == "ltr") {
+        edge = anchorRect.left - mainViewRect.left;
+      } else {
+        edge = mainViewRect.right - anchorRect.right;
+      }
 
-            // If the anchor is an element on the far end of the mainView we
-            // don't want to shift the mainView too far, we would reveal empty
-            // space otherwise.
-            let cstyle = window.getComputedStyle(document.documentElement);
-            let exitSubViewGutterWidth =
-              cstyle.getPropertyValue("--panel-ui-exit-subview-gutter-width");
-            let maxShift = mainViewRect.width - parseInt(exitSubViewGutterWidth);
-            let target = Math.min(maxShift, edge + center);
+      // If the anchor is an element on the far end of the mainView we
+      // don't want to shift the mainView too far, we would reveal empty
+      // space otherwise.
+      let cstyle = this.window.getComputedStyle(this.document.documentElement);
+      let exitSubViewGutterWidth =
+        cstyle.getPropertyValue("--panel-ui-exit-subview-gutter-width");
+      let maxShift = mainViewRect.width - parseInt(exitSubViewGutterWidth);
+      let target = Math.min(maxShift, edge + center);
 
-            let neg = direction == "ltr" ? "-" : "";
-            this._mainViewContainer.style.transform = `translateX(${neg}${target}px)`;
-            aAnchor.setAttribute("panel-multiview-anchor", true);
-          } else {
-            this._mainViewContainer.style.transform = "";
-            if (this.anchorElement)
-              this.anchorElement.removeAttribute("panel-multiview-anchor");
-          }
-          this.anchorElement = aAnchor;
-        ]]></body>
-      </method>
+      let neg = direction == "ltr" ? "-" : "";
+      this._mainViewContainer.style.transform = `translateX(${neg}${target}px)`;
+      aAnchor.setAttribute("panel-multiview-anchor", true);
+    } else {
+      this._mainViewContainer.style.transform = "";
+      if (this.anchorElement)
+        this.anchorElement.removeAttribute("panel-multiview-anchor");
+    }
+    this.anchorElement = aAnchor;
+  }
 
-      <method name="handleEvent">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          if (aEvent.type.startsWith("popup") && aEvent.target != this._panel) {
-            // Shouldn't act on e.g. context menus being shown from within the panel.
-            return;
+  handleEvent(aEvent) {
+    if (aEvent.type.startsWith("popup") && aEvent.target != this._panel) {
+      // Shouldn't act on e.g. context menus being shown from within the panel.
+      return;
+    }
+    switch (aEvent.type) {
+      case "click":
+        if (aEvent.originalTarget == this._clickCapturer) {
+          this.showMainView();
+        }
+        break;
+      case "overflow":
+        if (aEvent.target.localName == "vbox") {
+          // Resize the right view on the next tick.
+          if (this.showingSubView) {
+            this.window.setTimeout(this._syncContainerWithSubView.bind(this), 0);
+          } else if (!this.transitioning) {
+            this.window.setTimeout(this._syncContainerWithMainView.bind(this), 0);
           }
-          switch (aEvent.type) {
-            case "click":
-              if (aEvent.originalTarget == this._clickCapturer) {
-                this.showMainView();
-              }
-              break;
-            case "overflow":
-              if (aEvent.target.localName == "vbox") {
-                // Resize the right view on the next tick.
-                if (this.showingSubView) {
-                  setTimeout(this._syncContainerWithSubView.bind(this), 0);
-                } else if (!this.transitioning) {
-                  setTimeout(this._syncContainerWithMainView.bind(this), 0);
-                }
-              }
-              break;
-            case "popupshowing":
-              this.setAttribute("panelopen", "true");
-              // Bug 941196 - The panel can get taller when opening a subview. Disabling
-              // autoPositioning means that the panel won't jump around if an opened
-              // subview causes the panel to exceed the dimensions of the screen in the
-              // direction that the panel originally opened in. This property resets
-              // every time the popup closes, which is why we have to set it each time.
-              this._panel.autoPosition = false;
-              this._syncContainerWithMainView();
+        }
+        break;
+      case "popupshowing":
+        this.node.setAttribute("panelopen", "true");
+        // Bug 941196 - The panel can get taller when opening a subview. Disabling
+        // autoPositioning means that the panel won't jump around if an opened
+        // subview causes the panel to exceed the dimensions of the screen in the
+        // direction that the panel originally opened in. This property resets
+        // every time the popup closes, which is why we have to set it each time.
+        this._panel.autoPosition = false;
+        this._syncContainerWithMainView();
+
+        this._mainViewObserver.observe(this._mainView, {
+          attributes: true,
+          characterData: true,
+          childList: true,
+          subtree: true
+        });
 
-              this._mainViewObserver.observe(this._mainView, {
-                attributes: true,
-                characterData: true,
-                childList: true,
-                subtree: true
-              });
+        break;
+      case "popupshown":
+        this._setMaxHeight();
+        break;
+      case "popuphidden":
+        this.node.removeAttribute("panelopen");
+        this._mainView.style.removeProperty("height");
+        this.showMainView();
+        this._mainViewObserver.disconnect();
+        break;
+    }
+  }
 
-              break;
-            case "popupshown":
-              this._setMaxHeight();
-              break;
-            case "popuphidden":
-              this.removeAttribute("panelopen");
-              this._mainView.style.removeProperty("height");
-              this.showMainView();
-              this._mainViewObserver.disconnect();
-              break;
-          }
-        ]]></body>
-      </method>
+  _shouldSetPosition() {
+    return this.node.getAttribute("nosubviews") == "true";
+  }
 
-      <method name="_shouldSetPosition">
-        <body><![CDATA[
-          return this.getAttribute("nosubviews") == "true";
-        ]]></body>
-      </method>
-
-      <method name="_shouldSetHeight">
-        <body><![CDATA[
-          return this.getAttribute("nosubviews") != "true";
-        ]]></body>
-      </method>
-
-      <method name="_setMaxHeight">
-        <body><![CDATA[
-          if (!this._shouldSetHeight())
-            return;
+  _shouldSetHeight() {
+    return this.node.getAttribute("nosubviews") != "true";
+  }
 
-          // Ignore the mutation that'll fire when we set the height of
-          // the main view.
-          this.ignoreMutations = true;
-          this._mainView.style.height =
-            this.getBoundingClientRect().height + "px";
-          this.ignoreMutations = false;
-        ]]></body>
-      </method>
-      <method name="_adjustContainerHeight">
-        <body><![CDATA[
-          if (!this.ignoreMutations && !this.showingSubView && !this._transitioning) {
-            let height;
-            if (this.showingSubViewAsMainView) {
-              height = this._heightOfSubview(this._mainView);
-            } else {
-              height = this._mainView.scrollHeight;
-            }
-            this._viewContainer.style.height = height + "px";
-          }
-        ]]></body>
-      </method>
-      <method name="_syncContainerWithSubView">
-        <body><![CDATA[
-          // Check that this panel is still alive:
-          if (!this._panel || !this._panel.parentNode) {
-            return;
-          }
+  _setMaxHeight() {
+    if (!this._shouldSetHeight())
+      return;
+
+    // Ignore the mutation that'll fire when we set the height of
+    // the main view.
+    this.ignoreMutations = true;
+    this._mainView.style.height = this.node.getBoundingClientRect().height + "px";
+    this.ignoreMutations = false;
+  }
+
+  _adjustContainerHeight() {
+    if (!this.ignoreMutations && !this.showingSubView && !this._transitioning) {
+      let height;
+      if (this.showingSubViewAsMainView) {
+        height = this._heightOfSubview(this._mainView);
+      } else {
+        height = this._mainView.scrollHeight;
+      }
+      this._viewContainer.style.height = height + "px";
+    }
+  }
 
-          if (!this.ignoreMutations && this.showingSubView) {
-            let newHeight = this._heightOfSubview(this._currentSubView, this._subViews);
-            this._viewContainer.style.height = newHeight + "px";
-          }
-        ]]></body>
-      </method>
-      <method name="_syncContainerWithMainView">
-        <body><![CDATA[
-          // Check that this panel is still alive:
-          if (!this._panel || !this._panel.parentNode) {
-            return;
-          }
+  _syncContainerWithSubView() {
+    // Check that this panel is still alive:
+    if (!this._panel || !this._panel.parentNode) {
+      return;
+    }
+
+    if (!this.ignoreMutations && this.showingSubView) {
+      let newHeight = this._heightOfSubview(this._currentSubView, this._subViews);
+      this._viewContainer.style.height = newHeight + "px";
+    }
+  }
 
-          if (this._shouldSetPosition()) {
-            this._panel.adjustArrowPosition();
-          }
+  _syncContainerWithMainView() {
+    // Check that this panel is still alive:
+    if (!this._panel || !this._panel.parentNode) {
+      return;
+    }
 
-          if (this._shouldSetHeight()) {
-            this._adjustContainerHeight();
-          }
-        ]]></body>
-      </method>
+    if (this._shouldSetPosition()) {
+      this._panel.adjustArrowPosition();
+    }
+
+    if (this._shouldSetHeight()) {
+      this._adjustContainerHeight();
+    }
+  }
 
-      <!-- Call this when the height of one of your views (the main view or a
-           subview) changes and you want the heights of the multiview and panel
-           to be the same as the view's height.
-           If the caller can give a hint of the expected height change with the
-           optional aExpectedChange parameter, it prevents flicker. -->
-      <method name="setHeightToFit">
-        <parameter name="aExpectedChange"/>
-        <body><![CDATA[
-          // Set the max-height to zero, wait until the height is actually
-          // updated, and then remove it.  If it's not removed, weird things can
-          // happen, like widgets in the panel won't respond to clicks even
-          // though they're visible.
-          let count = 5;
-          let height = getComputedStyle(this).height;
-          if (aExpectedChange)
-            this.style.maxHeight = (parseInt(height) + aExpectedChange) + "px";
-          else
-            this.style.maxHeight = "0";
-          let interval = setInterval(() => {
-            if (height != getComputedStyle(this).height || --count == 0) {
-              clearInterval(interval);
-              this.style.removeProperty("max-height");
-            }
-          }, 0);
-        ]]></body>
-      </method>
+  /**
+   * Call this when the height of one of your views (the main view or a
+   * subview) changes and you want the heights of the multiview and panel
+   * to be the same as the view's height.
+   * If the caller can give a hint of the expected height change with the
+   * optional aExpectedChange parameter, it prevents flicker.
+   */
+  setHeightToFit(aExpectedChange) {
+    // Set the max-height to zero, wait until the height is actually
+    // updated, and then remove it.  If it's not removed, weird things can
+    // happen, like widgets in the panel won't respond to clicks even
+    // though they're visible.
+    const {window} = this;
+    let count = 5;
+    let height = window.getComputedStyle(this.node).height;
+    if (aExpectedChange)
+      this.node.style.maxHeight = (parseInt(height, 10) + aExpectedChange) + "px";
+    else
+      this.node.style.maxHeight = "0";
+    let interval = window.setInterval(() => {
+      if (height != window.getComputedStyle(this.node).height || --count == 0) {
+        window.clearInterval(interval);
+        this.node.style.removeProperty("max-height");
+      }
+    }, 0);
+  }
 
-      <method name="_heightOfSubview">
-        <parameter name="aSubview"/>
-        <parameter name="aContainerToCheck"/>
-        <body><![CDATA[
-          function getFullHeight(element) {
-            // XXXgijs: unfortunately, scrollHeight rounds values, and there's no alternative
-            // that works with overflow: auto elements. Fortunately for us,
-            // we have exactly 1 (potentially) scrolling element in here (the subview body),
-            // and rounding 1 value is OK - rounding more than 1 and adding them means we get
-            // off-by-1 errors. Now we might be off by a subpixel, but we care less about that.
-            // So, use scrollHeight *only* if the element is vertically scrollable.
-            let height;
-            let elementCS;
-            if (element.scrollTopMax) {
-              height = element.scrollHeight;
-              // Bounding client rects include borders, scrollHeight doesn't:
-              elementCS = win.getComputedStyle(element);
-              height += parseFloat(elementCS.borderTopWidth) +
-                        parseFloat(elementCS.borderBottomWidth);
-            } else {
-              height = element.getBoundingClientRect().height;
-              if (height > 0) {
-                elementCS = win.getComputedStyle(element);
-              }
-            }
-            if (elementCS) {
-              // Include margins - but not borders or paddings because they
-              // were dealt with above.
-              height += parseFloat(elementCS.marginTop) + parseFloat(elementCS.marginBottom);
-            }
-            return height;
-          }
-          let win = aSubview.ownerGlobal;
-          let body = aSubview.querySelector(".panel-subview-body");
-          let height = getFullHeight(body || aSubview);
-          if (body) {
-            let header = aSubview.querySelector(".panel-subview-header");
-            let footer = aSubview.querySelector(".panel-subview-footer");
-            height += (header ? getFullHeight(header) : 0) +
-                      (footer ? getFullHeight(footer) : 0);
-          }
-          if (aContainerToCheck) {
-            let containerCS = win.getComputedStyle(aContainerToCheck);
-            height += parseFloat(containerCS.paddingTop) + parseFloat(containerCS.paddingBottom);
-          }
-          return Math.ceil(height);
-        ]]></body>
-      </method>
-
-    </implementation>
-  </binding>
-
-  <binding id="panelview">
-    <implementation>
-      <property name="panelMultiView" readonly="true">
-        <getter><![CDATA[
-          if (this.parentNode.localName != "panelmultiview") {
-            return document.getBindingParent(this.parentNode);
-          }
-
-          return this.parentNode;
-        ]]></getter>
-      </property>
-    </implementation>
-  </binding>
-</bindings>
+  _heightOfSubview(aSubview, aContainerToCheck) {
+    function getFullHeight(element) {
+      // XXXgijs: unfortunately, scrollHeight rounds values, and there's no alternative
+      // that works with overflow: auto elements. Fortunately for us,
+      // we have exactly 1 (potentially) scrolling element in here (the subview body),
+      // and rounding 1 value is OK - rounding more than 1 and adding them means we get
+      // off-by-1 errors. Now we might be off by a subpixel, but we care less about that.
+      // So, use scrollHeight *only* if the element is vertically scrollable.
+      let height;
+      let elementCS;
+      if (element.scrollTopMax) {
+        height = element.scrollHeight;
+        // Bounding client rects include borders, scrollHeight doesn't:
+        elementCS = win.getComputedStyle(element);
+        height += parseFloat(elementCS.borderTopWidth) +
+                  parseFloat(elementCS.borderBottomWidth);
+      } else {
+        height = element.getBoundingClientRect().height;
+        if (height > 0) {
+          elementCS = win.getComputedStyle(element);
+        }
+      }
+      if (elementCS) {
+        // Include margins - but not borders or paddings because they
+        // were dealt with above.
+        height += parseFloat(elementCS.marginTop) + parseFloat(elementCS.marginBottom);
+      }
+      return height;
+    }
+    let win = aSubview.ownerGlobal;
+    let body = aSubview.querySelector(".panel-subview-body");
+    let height = getFullHeight(body || aSubview);
+    if (body) {
+      let header = aSubview.querySelector(".panel-subview-header");
+      let footer = aSubview.querySelector(".panel-subview-footer");
+      height += (header ? getFullHeight(header) : 0) +
+                (footer ? getFullHeight(footer) : 0);
+    }
+    if (aContainerToCheck) {
+      let containerCS = win.getComputedStyle(aContainerToCheck);
+      height += parseFloat(containerCS.paddingTop) + parseFloat(containerCS.paddingBottom);
+    }
+    return Math.ceil(height);
+  }
+}
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -29,481 +29,26 @@
                whether or not it is shown. That's not good for our case, since we
                want to allow each subview to be uniquely sized. -->
           <xul:vbox anonid="subViews" class="panel-subviews" xbl:inherits="panelopen">
             <children includes="panelview"/>
           </xul:vbox>
         </xul:stack>
       </xul:box>
     </content>
-    <implementation implements="nsIDOMEventListener">
-      <field name="_clickCapturer" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "clickCapturer");
-      </field>
-      <field name="_viewContainer" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "viewContainer");
-      </field>
-      <field name="_mainViewContainer" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "mainViewContainer");
-      </field>
-      <field name="_subViews" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "subViews");
-      </field>
-      <field name="_viewStack" readonly="true">
-        document.getAnonymousElementByAttribute(this, "anonid", "viewStack");
-      </field>
-      <field name="_panel" readonly="true">
-        this.parentNode;
-      </field>
-
-      <field name="_currentSubView">null</field>
-      <field name="_anchorElement">null</field>
-      <field name="_mainViewHeight">0</field>
-      <field name="_subViewObserver">null</field>
-      <field name="__transitioning">false</field>
-      <field name="_ignoreMutations">false</field>
-
-      <property name="showingSubView" readonly="true"
-                onget="return this._viewStack.getAttribute('viewtype') == 'subview'"/>
-      <property name="_mainViewId" onget="return this.getAttribute('mainViewId');" onset="this.setAttribute('mainViewId', val); return val;"/>
-      <property name="_mainView" readonly="true"
-                onget="return this._mainViewId ? document.getElementById(this._mainViewId) : null;"/>
-      <property name="showingSubViewAsMainView" readonly="true"
-                onget="return this.getAttribute('mainViewIsSubView') == 'true'"/>
-
-      <property name="ignoreMutations">
-        <getter>
-          return this._ignoreMutations;
-        </getter>
-        <setter><![CDATA[
-          this._ignoreMutations = val;
-          if (!val && this._panel.state == "open") {
-            if (this.showingSubView) {
-              this._syncContainerWithSubView();
-            } else {
-              this._syncContainerWithMainView();
-            }
-          }
-        ]]></setter>
-      </property>
-
-      <property name="_transitioning">
-        <getter>
-          return this.__transitioning;
-        </getter>
-        <setter><![CDATA[
-          this.__transitioning = val;
-          if (val) {
-            this.setAttribute("transitioning", "true");
-          } else {
-            this.removeAttribute("transitioning");
-          }
-        ]]></setter>
-      </property>
+    <implementation>
       <constructor><![CDATA[
-        this._clickCapturer.addEventListener("click", this);
-        this._panel.addEventListener("popupshowing", this);
-        this._panel.addEventListener("popupshown", this);
-        this._panel.addEventListener("popuphidden", this);
-        this._subViews.addEventListener("overflow", this);
-        this._mainViewContainer.addEventListener("overflow", this);
-
-        // Get a MutationObserver ready to react to subview size changes. We
-        // only attach this MutationObserver when a subview is being displayed.
-        this._subViewObserver =
-          new MutationObserver(this._syncContainerWithSubView.bind(this));
-        this._mainViewObserver =
-          new MutationObserver(this._syncContainerWithMainView.bind(this));
-
-        this._mainViewContainer.setAttribute("panelid",
-                                             this._panel.id);
-
-        if (this._mainView) {
-          this.setMainView(this._mainView);
-        }
-        this.setAttribute("viewtype", "main");
-      ]]></constructor>
-
-      <destructor><![CDATA[
-        if (this._mainView) {
-          this._mainView.removeAttribute("mainview");
-        }
-        this._mainViewObserver.disconnect();
-        this._subViewObserver.disconnect();
-        this._panel.removeEventListener("popupshowing", this);
-        this._panel.removeEventListener("popupshown", this);
-        this._panel.removeEventListener("popuphidden", this);
-        this._subViews.removeEventListener("overflow", this);
-        this._mainViewContainer.removeEventListener("overflow", this);
-        this._clickCapturer.removeEventListener("click", this);
-      ]]></destructor>
-
-      <method name="setMainView">
-        <parameter name="aNewMainView"/>
-        <body><![CDATA[
-        if (this._mainView) {
-          this._mainViewObserver.disconnect();
-          this._subViews.appendChild(this._mainView);
-          this._mainView.removeAttribute("mainview");
-        }
-        this._mainViewId = aNewMainView.id;
-        aNewMainView.setAttribute("mainview", "true");
-        this._mainViewContainer.appendChild(aNewMainView);
-        ]]></body>
-      </method>
-
-      <method name="showMainView">
-        <body><![CDATA[
-          if (this.showingSubView) {
-            let viewNode = this._currentSubView;
-            let evt = document.createEvent("CustomEvent");
-            evt.initCustomEvent("ViewHiding", true, true, viewNode);
-            viewNode.dispatchEvent(evt);
-
-            viewNode.removeAttribute("current");
-            this._currentSubView = null;
-
-            this._subViewObserver.disconnect();
-
-            this._setViewContainerHeight(this._mainViewHeight);
-
-            this.setAttribute("viewtype", "main");
-          }
-
-          this._shiftMainView();
-        ]]></body>
-      </method>
-
-      <method name="showSubView">
-        <parameter name="aViewId"/>
-        <parameter name="aAnchor"/>
-        <body><![CDATA[
-          Task.spawn(function*() {
-            let viewNode = this.querySelector("#" + aViewId);
-            if (!viewNode) {
-              viewNode = document.getElementById(aViewId);
-              if (viewNode) {
-                this._subViews.appendChild(viewNode);
-              } else {
-                throw new Error(`Subview ${aViewId} doesn't exist!`);
-              }
-            }
-            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);
-              },
-            };
-
-            let evt = new CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
-            viewNode.dispatchEvent(evt);
-
-            let cancel = evt.defaultPrevented;
-            if (detail.blockers.size) {
-              try {
-                let results = yield Promise.all(detail.blockers);
-                cancel = cancel || results.some(val => val === false);
-              } catch (e) {
-                Components.utils.reportError(e);
-                cancel = true;
-              }
-            }
-
-            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);
-
-            this._mainViewHeight = this._viewStack.clientHeight;
-
-            let newHeight = this._heightOfSubview(viewNode, this._subViews);
-            this._setViewContainerHeight(newHeight);
-
-            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;
-
-          let onTransitionEnd = () => {
-            container.removeEventListener("transitionend", onTransitionEnd);
-            this._transitioning = false;
-          };
-
-          container.addEventListener("transitionend", onTransitionEnd);
-          container.style.height = `${aHeight}px`;
-        ]]></body>
-      </method>
-
-      <method name="_shiftMainView">
-        <parameter name="aAnchor"/>
-        <body><![CDATA[
-          if (aAnchor) {
-            // We need to find the edge of the anchor, relative to the main panel.
-            // Then we need to add half the width of the anchor. This is the target
-            // that we need to transition to.
-            let anchorRect = aAnchor.getBoundingClientRect();
-            let mainViewRect = this._mainViewContainer.getBoundingClientRect();
-            let center = aAnchor.clientWidth / 2;
-            let direction = aAnchor.ownerGlobal.getComputedStyle(aAnchor).direction;
-            let edge;
-            if (direction == "ltr") {
-              edge = anchorRect.left - mainViewRect.left;
-            } else {
-              edge = mainViewRect.right - anchorRect.right;
-            }
-
-            // If the anchor is an element on the far end of the mainView we
-            // don't want to shift the mainView too far, we would reveal empty
-            // space otherwise.
-            let cstyle = window.getComputedStyle(document.documentElement);
-            let exitSubViewGutterWidth =
-              cstyle.getPropertyValue("--panel-ui-exit-subview-gutter-width");
-            let maxShift = mainViewRect.width - parseInt(exitSubViewGutterWidth);
-            let target = Math.min(maxShift, edge + center);
-
-            let neg = direction == "ltr" ? "-" : "";
-            this._mainViewContainer.style.transform = `translateX(${neg}${target}px)`;
-            aAnchor.setAttribute("panel-multiview-anchor", true);
-          } else {
-            this._mainViewContainer.style.transform = "";
-            if (this.anchorElement)
-              this.anchorElement.removeAttribute("panel-multiview-anchor");
-          }
-          this.anchorElement = aAnchor;
-        ]]></body>
-      </method>
-
-      <method name="handleEvent">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          if (aEvent.type.startsWith("popup") && aEvent.target != this._panel) {
-            // Shouldn't act on e.g. context menus being shown from within the panel.
-            return;
-          }
-          switch (aEvent.type) {
-            case "click":
-              if (aEvent.originalTarget == this._clickCapturer) {
-                this.showMainView();
-              }
-              break;
-            case "overflow":
-              if (aEvent.target.localName == "vbox") {
-                // Resize the right view on the next tick.
-                if (this.showingSubView) {
-                  setTimeout(this._syncContainerWithSubView.bind(this), 0);
-                } else if (!this.transitioning) {
-                  setTimeout(this._syncContainerWithMainView.bind(this), 0);
-                }
-              }
-              break;
-            case "popupshowing":
-              this.setAttribute("panelopen", "true");
-              // Bug 941196 - The panel can get taller when opening a subview. Disabling
-              // autoPositioning means that the panel won't jump around if an opened
-              // subview causes the panel to exceed the dimensions of the screen in the
-              // direction that the panel originally opened in. This property resets
-              // every time the popup closes, which is why we have to set it each time.
-              this._panel.autoPosition = false;
-              this._syncContainerWithMainView();
-
-              this._mainViewObserver.observe(this._mainView, {
-                attributes: true,
-                characterData: true,
-                childList: true,
-                subtree: true
-              });
-
-              break;
-            case "popupshown":
-              this._setMaxHeight();
-              break;
-            case "popuphidden":
-              this.removeAttribute("panelopen");
-              this._mainView.style.removeProperty("height");
-              this.showMainView();
-              this._mainViewObserver.disconnect();
-              break;
-          }
-        ]]></body>
-      </method>
-
-      <method name="_shouldSetPosition">
-        <body><![CDATA[
-          return this.getAttribute("nosubviews") == "true";
-        ]]></body>
-      </method>
-
-      <method name="_shouldSetHeight">
-        <body><![CDATA[
-          return this.getAttribute("nosubviews") != "true";
-        ]]></body>
-      </method>
-
-      <method name="_setMaxHeight">
-        <body><![CDATA[
-          if (!this._shouldSetHeight())
-            return;
-
-          // Ignore the mutation that'll fire when we set the height of
-          // the main view.
-          this.ignoreMutations = true;
-          this._mainView.style.height =
-            this.getBoundingClientRect().height + "px";
-          this.ignoreMutations = false;
-        ]]></body>
-      </method>
-      <method name="_adjustContainerHeight">
-        <body><![CDATA[
-          if (!this.ignoreMutations && !this.showingSubView && !this._transitioning) {
-            let height;
-            if (this.showingSubViewAsMainView) {
-              height = this._heightOfSubview(this._mainView);
-            } else {
-              height = this._mainView.scrollHeight;
-            }
-            this._viewContainer.style.height = height + "px";
-          }
-        ]]></body>
-      </method>
-      <method name="_syncContainerWithSubView">
-        <body><![CDATA[
-          // Check that this panel is still alive:
-          if (!this._panel || !this._panel.parentNode) {
-            return;
-          }
-
-          if (!this.ignoreMutations && this.showingSubView) {
-            let newHeight = this._heightOfSubview(this._currentSubView, this._subViews);
-            this._viewContainer.style.height = newHeight + "px";
-          }
-        ]]></body>
-      </method>
-      <method name="_syncContainerWithMainView">
-        <body><![CDATA[
-          // Check that this panel is still alive:
-          if (!this._panel || !this._panel.parentNode) {
-            return;
-          }
-
-          if (this._shouldSetPosition()) {
-            this._panel.adjustArrowPosition();
-          }
-
-          if (this._shouldSetHeight()) {
-            this._adjustContainerHeight();
-          }
-        ]]></body>
-      </method>
-
-      <!-- Call this when the height of one of your views (the main view or a
-           subview) changes and you want the heights of the multiview and panel
-           to be the same as the view's height.
-           If the caller can give a hint of the expected height change with the
-           optional aExpectedChange parameter, it prevents flicker. -->
-      <method name="setHeightToFit">
-        <parameter name="aExpectedChange"/>
-        <body><![CDATA[
-          // Set the max-height to zero, wait until the height is actually
-          // updated, and then remove it.  If it's not removed, weird things can
-          // happen, like widgets in the panel won't respond to clicks even
-          // though they're visible.
-          let count = 5;
-          let height = getComputedStyle(this).height;
-          if (aExpectedChange)
-            this.style.maxHeight = (parseInt(height) + aExpectedChange) + "px";
-          else
-            this.style.maxHeight = "0";
-          let interval = setInterval(() => {
-            if (height != getComputedStyle(this).height || --count == 0) {
-              clearInterval(interval);
-              this.style.removeProperty("max-height");
-            }
-          }, 0);
-        ]]></body>
-      </method>
-
-      <method name="_heightOfSubview">
-        <parameter name="aSubview"/>
-        <parameter name="aContainerToCheck"/>
-        <body><![CDATA[
-          function getFullHeight(element) {
-            // XXXgijs: unfortunately, scrollHeight rounds values, and there's no alternative
-            // that works with overflow: auto elements. Fortunately for us,
-            // we have exactly 1 (potentially) scrolling element in here (the subview body),
-            // and rounding 1 value is OK - rounding more than 1 and adding them means we get
-            // off-by-1 errors. Now we might be off by a subpixel, but we care less about that.
-            // So, use scrollHeight *only* if the element is vertically scrollable.
-            let height;
-            let elementCS;
-            if (element.scrollTopMax) {
-              height = element.scrollHeight;
-              // Bounding client rects include borders, scrollHeight doesn't:
-              elementCS = win.getComputedStyle(element);
-              height += parseFloat(elementCS.borderTopWidth) +
-                        parseFloat(elementCS.borderBottomWidth);
-            } else {
-              height = element.getBoundingClientRect().height;
-              if (height > 0) {
-                elementCS = win.getComputedStyle(element);
-              }
-            }
-            if (elementCS) {
-              // Include margins - but not borders or paddings because they
-              // were dealt with above.
-              height += parseFloat(elementCS.marginTop) + parseFloat(elementCS.marginBottom);
-            }
-            return height;
-          }
-          let win = aSubview.ownerGlobal;
-          let body = aSubview.querySelector(".panel-subview-body");
-          let height = getFullHeight(body || aSubview);
-          if (body) {
-            let header = aSubview.querySelector(".panel-subview-header");
-            let footer = aSubview.querySelector(".panel-subview-footer");
-            height += (header ? getFullHeight(header) : 0) +
-                      (footer ? getFullHeight(footer) : 0);
-          }
-          if (aContainerToCheck) {
-            let containerCS = win.getComputedStyle(aContainerToCheck);
-            height += parseFloat(containerCS.paddingTop) + parseFloat(containerCS.paddingBottom);
-          }
-          return Math.ceil(height);
-        ]]></body>
-      </method>
-
-    </implementation>
+        const {PanelMultiView} = Components.utils.import("resource:///modules/PanelMultiView.jsm", {});
+        this.instance = new PanelMultiView(this);
+       ]]></constructor>
+ 
+       <destructor><![CDATA[
+        this.instance.destructor();
+       ]]></destructor>
+     </implementation>
   </binding>
 
   <binding id="panelview">
     <implementation>
       <property name="panelMultiView" readonly="true">
         <getter><![CDATA[
           if (this.parentNode.localName != "panelmultiview") {
             return document.getBindingParent(this.parentNode);
--- a/browser/components/customizableui/moz.build
+++ b/browser/components/customizableui/moz.build
@@ -10,16 +10,17 @@ DIRS += [
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 EXTRA_JS_MODULES += [
     'CustomizableUI.jsm',
     'CustomizableWidgets.jsm',
     'CustomizeMode.jsm',
     'DragPositionManager.jsm',
+    'PanelMultiView.jsm',
     'PanelWideWidgetTracker.jsm',
     'ScrollbarSampler.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'):
     DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1
 
 with Files('**'):