author | Mike de Boer <mdeboer@mozilla.com> |
Mon, 08 May 2017 16:42:16 -0400 | |
changeset 357148 | e283471c9bfe14ad030181cbb4290596a7f0969c |
parent 357147 | 042c7c020a3b90f0136ed8b9fdce4e630ff2a61a |
child 357149 | 0bd894decfae5a32c2971d8cbb0835e51c77507c |
push id | 31783 |
push user | cbook@mozilla.com |
push date | Tue, 09 May 2017 12:03:48 +0000 |
treeherder | mozilla-central@b0ff0c5c0a35 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | Gijs |
bugs | 1363178 |
milestone | 55.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
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('**'):