--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -36,16 +36,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm",
LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
Log: "resource://gre/modules/Log.jsm",
LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
PageActions: "resource:///modules/PageActions.jsm",
PageThumbs: "resource://gre/modules/PageThumbs.jsm",
+ PanelView: "resource:///modules/PanelMultiView.jsm",
PluralForm: "resource://gre/modules/PluralForm.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.jsm",
PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
ReaderMode: "resource://gre/modules/ReaderMode.jsm",
ReaderParent: "resource:///modules/ReaderParent.jsm",
RecentWindow: "resource:///modules/RecentWindow.jsm",
SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
@@ -7133,31 +7134,29 @@ var gIdentityHandler = {
get _identityBox() {
delete this._identityBox;
return this._identityBox = document.getElementById("identity-box");
},
get _identityPopupMultiView() {
delete this._identityPopupMultiView;
return this._identityPopupMultiView = document.getElementById("identity-popup-multiView");
},
+ get _identityPopupMainView() {
+ delete this._identityPopupMainView;
+ return this._identityPopupMainView = document.getElementById("identity-popup-mainView");
+ },
get _identityPopupContentHosts() {
delete this._identityPopupContentHosts;
- let selector = ".identity-popup-host";
- return this._identityPopupContentHosts = [
- ...this._identityPopupMultiView._mainView.querySelectorAll(selector),
- ...document.querySelectorAll(selector)
- ];
+ return this._identityPopupContentHosts =
+ [...document.querySelectorAll(".identity-popup-host")];
},
get _identityPopupContentHostless() {
delete this._identityPopupContentHostless;
- let selector = ".identity-popup-hostless";
- return this._identityPopupContentHostless = [
- ...this._identityPopupMultiView._mainView.querySelectorAll(selector),
- ...document.querySelectorAll(selector)
- ];
+ return this._identityPopupContentHostless =
+ [...document.querySelectorAll(".identity-popup-hostless")];
},
get _identityPopupContentOwner() {
delete this._identityPopupContentOwner;
return this._identityPopupContentOwner =
document.getElementById("identity-popup-content-owner");
},
get _identityPopupContentSupp() {
delete this._identityPopupContentSupp;
@@ -7389,17 +7388,18 @@ var gIdentityHandler = {
this._identityBox.setAttribute("sharing", sharing);
else
this._identityBox.removeAttribute("sharing");
this._sharingState = tab._sharingState;
if (this._identityPopup.state == "open") {
this.updateSitePermissions();
- this._identityPopupMultiView.descriptionHeightWorkaround();
+ PanelView.forNode(this._identityPopupMainView)
+ .descriptionHeightWorkaround();
}
},
/**
* Attempt to provide proper IDN treatment for host names
*/
getEffectiveHost() {
if (!this._IDNService)
@@ -8038,17 +8038,18 @@ var gIdentityHandler = {
}
}
browser.messageManager.sendAsyncMessage("webrtc:StopSharing", windowId);
webrtcUI.forgetActivePermissionsFromBrowser(gBrowser.selectedBrowser);
}
SitePermissions.remove(gBrowser.currentURI, aPermission.id, browser);
this._permissionReloadHint.removeAttribute("hidden");
- this._identityPopupMultiView.descriptionHeightWorkaround();
+ PanelView.forNode(this._identityPopupMainView)
+ .descriptionHeightWorkaround();
// Set telemetry values for clearing a permission
let histogram = Services.telemetry.getKeyedHistogramById("WEB_PERMISSION_CLEARED");
let permissionType = 0;
if (aPermission.state == SitePermissions.ALLOW &&
aPermission.scope == SitePermissions.SCOPE_PERSISTENT) {
// 1 : clear permanently allowed permission
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -59,16 +59,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
const TRANSITION_PHASES = Object.freeze({
START: 1,
PREPARE: 2,
TRANSITION: 3,
END: 4
});
let gNodeToObjectMap = new WeakMap();
+let gMultiLineElementsMap = new WeakMap();
/**
* Allows associating an object to a node lazily using a weak map.
*
* Classes deriving from this one may be easily converted to Custom Elements,
* although they would lose the ability of being associated lazily.
*/
this.AssociatedToNode = class {
@@ -105,29 +106,45 @@ this.AssociatedToNode = class {
* nsIDOMWindowUtils for the window of this node.
*/
get _dwu() {
if (this.__dwu)
return this.__dwu;
return this.__dwu = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
}
+
+ /**
+ * Dispatches a custom event on this element.
+ *
+ * @param {String} eventName Name of the event to dispatch.
+ * @param {Object} [detail] Event detail object. Optional.
+ * @param {Boolean} cancelable If the event can be canceled.
+ * @return {Boolean} `true` if the event was canceled by an event handler, `false`
+ * otherwise.
+ */
+ dispatchCustomEvent(eventName, detail, cancelable = false) {
+ let event = new this.window.CustomEvent(eventName, {
+ detail,
+ bubbles: true,
+ cancelable,
+ });
+ this.node.dispatchEvent(event);
+ return event.defaultPrevented;
+ }
};
/**
* This is associated to <panelmultiview> elements by the panelUI.xml binding.
*/
this.PanelMultiView = class extends this.AssociatedToNode {
get _panel() {
return this.node.parentNode;
}
- get showingSubView() {
- return this._showingSubView;
- }
get _mainViewId() {
return this.node.getAttribute("mainViewId");
}
get _mainView() {
return this.document.getElementById(this._mainViewId);
}
get _transitioning() {
@@ -177,27 +194,23 @@ this.PanelMultiView = class extends this
get currentShowPromise() {
return this._currentShowPromise || Promise.resolve();
}
get _keyNavigationMap() {
if (!this.__keyNavigationMap)
this.__keyNavigationMap = new Map();
return this.__keyNavigationMap;
}
- get _multiLineElementsMap() {
- if (!this.__multiLineElementsMap)
- this.__multiLineElementsMap = new WeakMap();
- return this.__multiLineElementsMap;
- }
connect() {
this.knownViews = new Set(this.node.getElementsByTagName("panelview"));
this.openViews = [];
this._mainViewHeight = 0;
- this.__transitioning = this._ignoreMutations = this._showingSubView = false;
+ this.__transitioning = false;
+ this.showingSubView = false;
const {document, window} = this;
this._viewContainer =
document.getAnonymousElementByAttribute(this.node, "anonid", "viewContainer");
this._viewStack =
document.getAnonymousElementByAttribute(this.node, "anonid", "viewStack");
this._offscreenViewStack =
@@ -213,35 +226,25 @@ this.PanelMultiView = class extends this
this._panel.addEventListener("popuphidden", this);
this._panel.addEventListener("popupshown", this);
let cs = window.getComputedStyle(document.documentElement);
// Set CSS-determined attributes now to prevent a layout flush when we do
// it when transitioning between panels.
this._dir = cs.direction;
this.showMainView();
- this._showingSubView = false;
-
// 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
- });
- });
- ["goBack", "descriptionHeightWorkaround", "showMainView",
- "showSubView"].forEach(method => {
+ ["goBack", "showMainView", "showSubView"].forEach(method => {
Object.defineProperty(this.node, method, {
enumerable: true,
value: (...args) => this[method](...args)
});
});
- ["current", "currentShowPromise"].forEach(property => {
+ ["current", "currentShowPromise", "showingSubView"].forEach(property => {
Object.defineProperty(this.node, property, {
enumerable: true,
get: () => this[property]
});
});
}
destructor() {
@@ -285,56 +288,16 @@ this.PanelMultiView = class extends this
let subviews = Array.from(viewNodeContainer.childNodes);
for (let subview of subviews) {
// XBL lists the 'children' XBL element explicitly. :-(
if (subview.nodeName != "children")
this._panelViewCache.appendChild(subview);
}
}
- _setHeader(viewNode, titleText) {
- // If the header already exists, update or remove it as requested.
- let header = viewNode.firstChild;
- if (header && header.classList.contains("panel-header")) {
- if (titleText) {
- header.querySelector("label").setAttribute("value", titleText);
- } else {
- header.remove();
- }
- return;
- }
-
- // The header doesn't exist, only create it if needed.
- if (!titleText) {
- return;
- }
-
- header = this.document.createElement("box");
- header.classList.add("panel-header");
-
- let backButton = this.document.createElement("toolbarbutton");
- backButton.className =
- "subviewbutton subviewbutton-iconic subviewbutton-back";
- backButton.setAttribute("closemenu", "none");
- backButton.setAttribute("tabindex", "0");
- backButton.setAttribute("tooltip",
- this.node.getAttribute("data-subviewbutton-tooltip"));
- backButton.addEventListener("command", () => {
- // The panelmultiview element may change if the view is reused.
- viewNode.panelMultiView.goBack();
- backButton.blur();
- });
-
- let label = this.document.createElement("label");
- label.setAttribute("value", titleText);
-
- header.append(backButton, label);
- viewNode.prepend(header);
- }
-
goBack() {
let previous = this.openViews.pop();
let current = this._currentSubView;
return this.showSubView(current, null, previous);
}
/**
* Checks whether it is possible to navigate backwards currently. Returns
@@ -361,34 +324,29 @@ this.PanelMultiView = class extends this
* @param {panelview} [theOne] The panelview DOM node to ensure is visible.
* Optional.
*/
hideAllViewsExcept(theOne = null) {
for (let panelview of this.knownViews) {
// When the panelview was already reparented, don't interfere any more.
if (panelview == theOne || !this.node || panelview.panelMultiView != this.node)
continue;
- if (panelview.hasAttribute("current"))
- this._dispatchViewEvent(panelview, "ViewHiding");
- panelview.removeAttribute("current");
+ PanelView.forNode(panelview).current = false;
}
this._viewShowing = null;
if (!this.node || !theOne)
return;
if (!this.openViews.includes(theOne))
this.openViews.push(theOne);
- if (!theOne.hasAttribute("current")) {
- theOne.setAttribute("current", true);
- this.descriptionHeightWorkaround(theOne);
- this._dispatchViewEvent(theOne, "ViewShown");
- }
- this._showingSubView = theOne.id != this._mainViewId;
+
+ PanelView.forNode(theOne).current = true;
+ this.showingSubView = theOne.id != this._mainViewId;
}
showSubView(aViewId, aAnchor, aPreviousView) {
this._currentShowPromise = (async () => {
// Support passing in the node directly.
let viewNode = typeof aViewId == "string" ? this.node.querySelector("#" + aViewId) : aViewId;
if (!viewNode) {
viewNode = this.document.getElementById(aViewId);
@@ -396,24 +354,25 @@ this.PanelMultiView = class extends this
this._viewStack.appendChild(viewNode);
} else {
throw new Error(`Subview ${aViewId} doesn't exist!`);
}
} else if (viewNode.parentNode == this._panelViewCache) {
this._viewStack.appendChild(viewNode);
}
+ let nextPanelView = PanelView.forNode(viewNode);
this.knownViews.add(viewNode);
viewNode.panelMultiView = this.node;
let reverse = !!aPreviousView;
if (!reverse) {
- this._setHeader(viewNode, viewNode.getAttribute("title") ||
- (aAnchor && aAnchor.getAttribute("label")));
+ nextPanelView.headerText = viewNode.getAttribute("title") ||
+ (aAnchor && aAnchor.getAttribute("label"));
}
let previousViewNode = aPreviousView || this._currentSubView;
// If the panelview to show is the same as the previous one, the 'ViewShowing'
// event has already been dispatched. Don't do it twice.
let showingSameView = viewNode == previousViewNode;
let playTransition = (!!previousViewNode && !showingSameView && this._panel.state == "open");
let isMainView = viewNode.id == this._mainViewId;
@@ -430,20 +389,17 @@ this.PanelMultiView = class extends this
this._mainViewHeight = previousRect.height;
this._viewContainer.style.minHeight = this._mainViewHeight + "px";
}
this._viewShowing = viewNode;
// Because the 'mainview' attribute may be out-of-sync, due to view node
// reparenting in combination with ephemeral PanelMultiView instances,
// this is the best place to correct it (just before showing).
- if (isMainView)
- viewNode.setAttribute("mainview", true);
- else
- viewNode.removeAttribute("mainview");
+ nextPanelView.mainview = isMainView;
if (aAnchor) {
viewNode.classList.add("PanelUI-subView");
}
if (!isMainView && this._mainViewWidth)
viewNode.style.maxWidth = viewNode.style.minWidth = this._mainViewWidth + "px";
if (!showingSameView || !viewNode.hasAttribute("current")) {
@@ -451,17 +407,17 @@ this.PanelMultiView = class extends this
// to lazily populate the subview with things or perhaps even cancel this
// whole operation.
let detail = {
blockers: new Set(),
addBlocker(promise) {
this.blockers.add(promise);
}
};
- let cancel = this._dispatchViewEvent(viewNode, "ViewShowing", aAnchor, detail);
+ let cancel = nextPanelView.dispatchCustomEvent("ViewShowing", detail, true);
if (detail.blockers.size) {
try {
let results = await Promise.all(detail.blockers);
cancel = cancel || results.some(val => val === false);
} catch (e) {
Cu.reportError(e);
cancel = true;
}
@@ -508,16 +464,18 @@ this.PanelMultiView = class extends this
// There's absolutely no need to show off our epic animation skillz when
// the panel's not even open.
if (this._panel.state != "open") {
return;
}
const {window, document} = this;
+ let nextPanelView = PanelView.forNode(viewNode);
+
if (this._autoResizeWorkaroundTimer)
window.clearTimeout(this._autoResizeWorkaroundTimer);
let details = this._transitionDetails = {
phase: TRANSITION_PHASES.START,
previousViewNode, viewNode, reverse, anchor
};
@@ -557,17 +515,17 @@ this.PanelMultiView = class extends this
let oldSibling = viewNode.nextSibling || null;
this._offscreenViewStack.style.minHeight =
this._viewContainer.style.height;
this._offscreenViewStack.appendChild(viewNode);
viewNode.setAttribute("in-transition", true);
// Now that the subview is visible, we can check the height of the
// description elements it contains.
- this.descriptionHeightWorkaround(viewNode);
+ nextPanelView.descriptionHeightWorkaround();
viewRect = await BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {
return this._dwu.getBoundsWithoutFlushing(viewNode);
});
try {
this._viewStack.insertBefore(viewNode, oldSibling);
} catch (ex) {
@@ -707,45 +665,16 @@ this.PanelMultiView = class extends this
// We force 'display: none' on the previous view node to make sure that it
// doesn't cause an annoying flicker whilst resetting the styles above.
previousViewNode.style.display = "none";
await BrowserUtils.promiseLayoutFlushed(this.document, "layout", () => {});
previousViewNode.style.removeProperty("display");
}
}
- /**
- * Helper method to emit an event on a panelview, whilst also making sure that
- * the correct method is called on CustomizableWidget instances.
- *
- * @param {panelview} viewNode Target of the event to dispatch.
- * @param {String} eventName Name of the event to dispatch.
- * @param {DOMNode} [anchor] Node where the panel is anchored to. Optional.
- * @param {Object} [detail] Event detail object. Optional.
- * @return {Boolean} `true` if the event was canceled by an event handler, `false`
- * otherwise.
- */
- _dispatchViewEvent(viewNode, eventName, anchor, detail) {
- let cancel = false;
- if (eventName != "PanelMultiViewHidden") {
- // Don't need to do this for PanelMultiViewHidden event
- CustomizableUI.ensureSubviewListeners(viewNode);
- }
-
- let evt = new this.window.CustomEvent(eventName, {
- detail,
- bubbles: true,
- cancelable: eventName == "ViewShowing"
- });
- viewNode.dispatchEvent(evt);
- if (!cancel)
- cancel = evt.defaultPrevented;
- return cancel;
- }
-
_calculateMaxHeight() {
// While opening the panel, we have to limit the maximum height of any
// view based on the space that will be available. We cannot just use
// window.screen.availTop and availHeight because these may return an
// incorrect value when the window spans multiple screens.
let anchorBox = this._panel.anchorNode.boxObject;
let screen = this._screenManager.screenForRect(anchorBox.screenX,
anchorBox.screenY,
@@ -815,17 +744,17 @@ this.PanelMultiView = class extends this
this._viewStack.style.maxHeight = maxHeight + "px";
this._offscreenViewStack.style.maxHeight = maxHeight + "px";
}
break;
}
case "popupshown":
// Now that the main view is visible, we can check the height of the
// description elements it contains.
- this.descriptionHeightWorkaround();
+ PanelView.forNode(this._mainView).descriptionHeightWorkaround();
break;
case "popuphidden": {
// WebExtensions consumers can hide the popup from viewshowing, or
// mid-transition, which disrupts our state:
this._viewShowing = null;
this._transitioning = false;
this.node.removeAttribute("panelopen");
this.showMainView();
@@ -845,17 +774,17 @@ this.PanelMultiView = class extends this
// when the popup is opened again, e.g. through touch mode sizing.
this._mainViewHeight = 0;
this._mainViewWidth = 0;
this._viewContainer.style.removeProperty("min-height");
this._viewStack.style.removeProperty("max-height");
this._viewContainer.style.removeProperty("min-width");
this._viewContainer.style.removeProperty("max-width");
- this._dispatchViewEvent(this.node, "PanelMultiViewHidden");
+ this.dispatchCustomEvent("PanelMultiViewHidden");
break;
}
}
}
/**
* Based on going up or down, select the previous or next focusable button
* in the current view.
@@ -1038,33 +967,112 @@ this.PanelMultiView = class extends this
* @param {panelview} view the view in which to update keyboard focus.
*/
_updateKeyboardFocus(view) {
let navMap = this._keyNavigationMap.get(view);
if (navMap && navMap.selected && navMap.selected.get()) {
navMap.selected.get().focus();
}
}
+};
+
+/**
+ * This is associated to <panelview> elements.
+ */
+this.PanelView = class extends this.AssociatedToNode {
+ /**
+ * The "mainview" attribute is set before the panel is opened when this view
+ * is displayed as the main view, and is removed before the <panelview> is
+ * displayed as a subview. The same view element can be displayed as a main
+ * view and as a subview at different times.
+ */
+ set mainview(value) {
+ if (value) {
+ this.node.setAttribute("mainview", true);
+ } else {
+ this.node.removeAttribute("mainview");
+ }
+ }
+
+ set current(value) {
+ if (value) {
+ if (!this.node.hasAttribute("current")) {
+ this.node.setAttribute("current", true);
+ this.descriptionHeightWorkaround();
+ this.dispatchCustomEvent("ViewShown");
+ }
+ } else if (this.node.hasAttribute("current")) {
+ this.dispatchCustomEvent("ViewHiding");
+ this.node.removeAttribute("current");
+ }
+ }
+
+ /**
+ * Adds a header with the given title, or removes it if the title is empty.
+ */
+ set headerText(value) {
+ // If the header already exists, update or remove it as requested.
+ let header = this.node.firstChild;
+ if (header && header.classList.contains("panel-header")) {
+ if (value) {
+ header.querySelector("label").setAttribute("value", value);
+ } else {
+ header.remove();
+ }
+ return;
+ }
+
+ // The header doesn't exist, only create it if needed.
+ if (!value) {
+ return;
+ }
+
+ header = this.document.createElement("box");
+ header.classList.add("panel-header");
+
+ let backButton = this.document.createElement("toolbarbutton");
+ backButton.className =
+ "subviewbutton subviewbutton-iconic subviewbutton-back";
+ backButton.setAttribute("closemenu", "none");
+ backButton.setAttribute("tabindex", "0");
+ backButton.setAttribute("tooltip",
+ this.node.getAttribute("data-subviewbutton-tooltip"));
+ backButton.addEventListener("command", () => {
+ // The panelmultiview element may change if the view is reused.
+ this.node.panelMultiView.goBack();
+ backButton.blur();
+ });
+
+ let label = this.document.createElement("label");
+ label.setAttribute("value", value);
+
+ header.append(backButton, label);
+ this.node.prepend(header);
+ }
+
+ /**
+ * Also make sure that the correct method is called on CustomizableWidget.
+ */
+ dispatchCustomEvent(...args) {
+ CustomizableUI.ensureSubviewListeners(this.node);
+ super.dispatchCustomEvent(...args);
+ }
/**
* If the main view or a subview contains wrapping elements, the attribute
* "descriptionheightworkaround" should be set on the view to force all the
* wrapping "description", "label" or "toolbarbutton" elements to a fixed
* height. If the attribute is set and the visibility, contents, or width
* of any of these elements changes, this function should be called to
* refresh the calculated heights.
*
* This may trigger a synchronous layout.
- *
- * @param viewNode
- * Indicates the node to scan for descendant elements. This is the main
- * view if omitted.
*/
- descriptionHeightWorkaround(viewNode = this._mainView) {
- if (!viewNode || !viewNode.hasAttribute("descriptionheightworkaround")) {
+ descriptionHeightWorkaround() {
+ if (!this.node.hasAttribute("descriptionheightworkaround")) {
// This view does not require the workaround.
return;
}
// We batch DOM changes together in order to reduce synchronous layouts.
// First we reset any change we may have made previously. The first time
// this is called, and in the best case scenario, this has no effect.
let items = [];
@@ -1072,26 +1080,26 @@ this.PanelMultiView = class extends this
// and also don't have a value attribute can be multiline (if their
// text content is long enough).
let isMultiline = ":not(:-moz-any([hidden],[value],:empty))";
let selector = [
"description" + isMultiline,
"label" + isMultiline,
"toolbarbutton[wrap]:not([hidden])",
].join(",");
- for (let element of viewNode.querySelectorAll(selector)) {
+ for (let element of this.node.querySelectorAll(selector)) {
// Ignore items in hidden containers.
if (element.closest("[hidden]")) {
continue;
}
// Take the label for toolbarbuttons; it only exists on those elements.
element = element.labelElement || element;
let bounds = element.getBoundingClientRect();
- let previous = this._multiLineElementsMap.get(element);
+ let previous = gMultiLineElementsMap.get(element);
// We don't need to (re-)apply the workaround for invisible elements or
// on elements we've seen before and haven't changed in the meantime.
if (!bounds.width || !bounds.height ||
(previous && element.textContent == previous.textContent &&
bounds.width == previous.bounds.width)) {
continue;
}
@@ -1107,26 +1115,21 @@ this.PanelMultiView = class extends this
// We now read the computed style to store the height of any element that
// may contain wrapping text.
for (let item of items) {
item.bounds = item.element.getBoundingClientRect();
}
// Now we can make all the necessary DOM changes at once.
for (let { element, bounds } of items) {
- this._multiLineElementsMap.set(element, { bounds, textContent: element.textContent });
+ gMultiLineElementsMap.set(element, { bounds, textContent: element.textContent });
element.style.height = bounds.height + "px";
}
}
-};
-/**
- * This is associated to <panelview> elements.
- */
-this.PanelView = class extends this.AssociatedToNode {
/**
* Retrieves the button elements that can be used for navigation using the
* keyboard, that is all enabled buttons including the back button if visible.
*
* @return {Array}
*/
getNavigableElements() {
let buttons = Array.from(this.node.querySelectorAll(".subviewbutton:not([disabled])"));