summaryrefslogtreecommitdiffstats
path: root/browser/components/customizableui/content/panelUI.xml
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/customizableui/content/panelUI.xml')
-rw-r--r--browser/components/customizableui/content/panelUI.xml509
1 files changed, 509 insertions, 0 deletions
diff --git a/browser/components/customizableui/content/panelUI.xml b/browser/components/customizableui/content/panelUI.xml
new file mode 100644
index 000000000..6893bd8ff
--- /dev/null
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -0,0 +1,509 @@
+<?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/. -->
+
+<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">
+
+ <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"/>
+
+ <!-- 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"/>
+
+ <!-- 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>
+
+ <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>
+ <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);
+ 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.ownerDocument.defaultView.getComputedStyle(aAnchor, null).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, null);
+ 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.ownerDocument.defaultView;
+ 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>