diff options
Diffstat (limited to 'browser/components/customizableui/content/panelUI.xml')
-rw-r--r-- | browser/components/customizableui/content/panelUI.xml | 509 |
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> |