--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -105,16 +105,36 @@ 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);
}
+
+ /**
+ * Helper method to emit an event on an element, whilst also making sure that
+ * the correct method is called on CustomizableWidget instances.
+ *
+ * @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 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
@@ -296,56 +316,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
@@ -372,33 +352,28 @@ 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");
- }
+
+ 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) {
@@ -407,24 +382,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 newPanelView = 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")));
+ newPanelView.setHeader(newPanelView.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;
@@ -445,20 +421,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");
+ newPanelView.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")) {
@@ -466,17 +439,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 = newPanelView.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;
}
@@ -722,45 +695,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,
@@ -859,17 +803,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.
@@ -1068,17 +1012,121 @@ this.PanelMultiView = class extends this
*
* 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")) {
+ if (!viewNode) {
+ return;
+ }
+
+ PanelView.forNode(viewNode).descriptionHeightWorkaround();
+ }
+};
+
+/**
+ * This is associated to <panelview> elements.
+ */
+this.PanelView = class extends this.AssociatedToNode {
+ get title() {
+ return this.node.getAttribute("title");
+ }
+
+ /**
+ * The "mainview" attribute is set before the panel is opened when this view
+ * is displayed as the main view, and is reset before the <panelview> is
+ * displayed as a subview.
+ */
+ 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");
+ }
+ }
+
+ /**
+ * Also make sure that the correct method is called on CustomizableWidget.
+ */
+ dispatchCustomEvent(...args) {
+ CustomizableUI.ensureSubviewListeners(this.node);
+ super.dispatchCustomEvent(...args);
+ }
+
+ /**
+ * Adds a header with the given title, or removes it if the title is empty.
+ */
+ setHeader(titleText) {
+ // If the header already exists, update or remove it as requested.
+ let header = this.node.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.
+ this.node.panelMultiView.goBack();
+ backButton.blur();
+ });
+
+ let label = this.document.createElement("label");
+ label.setAttribute("value", titleText);
+
+ header.append(backButton, label);
+ this.node.prepend(header);
+ }
+
+ /**
+ * 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.
+ */
+ 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 = [];
@@ -1086,17 +1134,17 @@ 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();
@@ -1125,22 +1173,17 @@ this.PanelMultiView = class extends this
}
// 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 });
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])"));