--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -1,12 +1,49 @@
/* 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/. */
+/**
+ * Allows a popup panel to host multiple subviews. The main view shown when the
+ * panel is opened may slide out to display a subview, which in turn may lead to
+ * other subviews in a cascade menu pattern.
+ *
+ * The <panel> element should contain a <panelmultiview> element. Views are
+ * declared using <panelview> elements that are usually children of the main
+ * <panelmultiview> element, although they don't need to be, as views can also
+ * be imported into the panel from other panels or popup sets.
+ *
+ * The main view can be declared using the mainViewId attribute, and specific
+ * subviews can slide in using the showSubView method. Backwards navigation can
+ * be done using the goBack method or through a button in the subview headers.
+ *
+ * This diagram shows how <panelview> nodes move during navigation:
+ *
+ * In this <panelmultiview> In other panels Action
+ * ┌───┬───┬───┐ ┌───┬───┐
+ * │(A)│ B │ C │ │ D │ E │ Open panel
+ * └───┴───┴───┘ └───┴───┘
+ * ┌───┬───┬───┐ ┌───┬───┐
+ * │ A │(C)│ B │ │ D │ E │ Show subview C
+ * └───┴───┴───┘ └───┴───┘
+ * ┌───┬───┬───┬───┐ ┌───┐
+ * │ A │ C │(D)│ B │ │ E │ Show subview D
+ * └───┴───┴───┴───┘ └───┘
+ * ┌───┬───┬───┬───┐ ┌───┐
+ * │ A │(C)│ D │ B │ │ E │ Go back
+ * └───┴───┴───┴───┘ └───┘
+ * │
+ * └── Currently visible view
+ *
+ * If the <panelmultiview> element is "ephemeral", imported subviews will be
+ * moved out again to the element specified by the viewCacheId attribute, so
+ * that the panel element can be removed safely.
+ */
+
"use strict";
this.EXPORTED_SYMBOLS = ["PanelMultiView"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
@@ -19,135 +56,16 @@ XPCOMUtils.defineLazyModuleGetter(this,
const TRANSITION_PHASES = Object.freeze({
START: 1,
PREPARE: 2,
TRANSITION: 3,
END: 4
});
/**
- * Simple implementation of the sliding window pattern; panels are added to a
- * linked list, in-order, and the currently shown panel is remembered using a
- * marker. The marker shifts as navigation between panels is continued, where
- * the panel at index 0 is always the starting point:
- * ┌────┬────┬────┬────┐
- * │▓▓▓▓│ │ │ │ Start
- * └────┴────┴────┴────┘
- * ┌────┬────┬────┬────┐
- * │ │▓▓▓▓│ │ │ Forward
- * └────┴────┴────┴────┘
- * ┌────┬────┬────┬────┐
- * │ │ │▓▓▓▓│ │ Forward
- * └────┴────┴────┴────┘
- * ┌────┬────┬────┬────┐
- * │ │▓▓▓▓│ │ │ Back
- * └────┴────┴────┴────┘
- */
-class SlidingPanelViews extends Array {
- constructor() {
- super();
- this._marker = 0;
- }
-
- /**
- * Get the index that points to the currently selected view.
- *
- * @return {Number}
- */
- get current() {
- return this._marker;
- }
-
- /**
- * Setter for the current index, which changes the order of elements and
- * updates the internal marker for the currently selected view.
- * We're manipulating the array directly to have it reflect the order of
- * navigation, instead of continuously growing the array with the next selected
- * view to keep memory usage within reasonable proportions. With this method,
- * the data structure grows no larger than the number of panels inside the
- * panelMultiView.
- *
- * @param {Number} index Index of the item to move to the current position.
- * @return {Number} The new marker index.
- */
- set current(index) {
- if (index == this._marker) {
- // Never change a winning team.
- return index;
- }
- if (index == -1 || index > (this.length - 1)) {
- throw new Error(`SlidingPanelViews :: index ${index} out of bounds`);
- }
-
- let view = this.splice(index, 1)[0];
- if (this._marker > index) {
- // Correct the current marker if the view-to-select was removed somewhere
- // before it.
- --this._marker;
- }
- // Then add the view-to-select right after the currently selected view.
- this.splice(++this._marker, 0, view);
- return this._marker;
- }
-
- /**
- * Getter for the currently selected view node.
- *
- * @return {panelview}
- */
- get currentView() {
- return this[this._marker];
- }
-
- /**
- * Setter for the currently selected view node.
- *
- * @param {panelview} view
- * @return {Number} Index of the currently selected view.
- */
- set currentView(view) {
- if (!view)
- return this.current;
- // This will throw an error if the view could not be found.
- return this.current = this.indexOf(view);
- }
-
- /**
- * Getter for the previous view, which is always positioned one position after
- * the current view.
- *
- * @return {panelview}
- */
- get previousView() {
- return this[this._marker + 1];
- }
-
- /**
- * Going back is an explicit action on the data structure, moving the marker
- * one step back.
- *
- * @return {Array} A list of two items: the newly selected view and the previous one.
- */
- back() {
- if (this._marker > 0)
- --this._marker;
- return [this.currentView, this.previousView];
- }
-
- /**
- * Reset the data structure to its original construct, removing all references
- * to view nodes.
- */
- clear() {
- this._marker = 0;
- this.splice(0, this.length);
- }
-}
-
-/**
* 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}
@@ -195,24 +113,16 @@ this.PanelMultiView = class {
* @return {Boolean} |true| when the 'ephemeral' attribute is set, which means
* that this instance should be ready to be thrown away at
* any time.
*/
get _ephemeral() {
return this.node.hasAttribute("ephemeral");
}
- get panelViews() {
- if (this._panelViews)
- return this._panelViews;
-
- this._panelViews = new SlidingPanelViews();
- this._panelViews.push(...this.node.getElementsByTagName("panelview"));
- return this._panelViews;
- }
get _dwu() {
if (this.__dwu)
return this.__dwu;
return this.__dwu = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
}
get _screenManager() {
if (this.__screenManager)
@@ -224,20 +134,19 @@ this.PanelMultiView = class {
* @return {panelview} the currently visible subview OR the subview that is
* about to be shown whilst a 'ViewShowing' event is being
* dispatched.
*/
get current() {
return this._viewShowing || this._currentSubView;
}
get _currentSubView() {
- return this.panelViews.currentView;
- }
- set _currentSubView(panel) {
- this.panelViews.currentView = panel;
+ // Peek the top of the stack, but fall back to the main view if the list of
+ // opened views is currently empty.
+ return this.openViews[this.openViews.length - 1] || this._mainView;
}
/**
* @return {Promise} showSubView() returns a promise, which is kept here for
* random access.
*/
get currentShowPromise() {
return this._currentShowPromise || Promise.resolve();
}
@@ -254,17 +163,18 @@ this.PanelMultiView = class {
constructor(xulNode, testMode = false) {
this.node = xulNode;
// If `testMode` is `true`, the consumer is only interested in accessing the
// methods of this instance. (E.g. in unit tests.)
if (testMode)
return;
- this._currentSubView = this._subViewObserver = null;
+ this.knownViews = new Set(this.node.getElementsByTagName("panelview"));
+ this.openViews = [];
this._mainViewHeight = 0;
this.__transitioning = this._ignoreMutations = this._showingSubView = false;
const {document, window} = this;
this._viewContainer =
document.getAnonymousElementByAttribute(this.node, "anonid", "viewContainer");
this._viewStack =
@@ -324,17 +234,16 @@ this.PanelMultiView = class {
let mainView = this._mainView;
if (mainView) {
if (this._panelViewCache)
this._panelViewCache.appendChild(mainView);
mainView.removeAttribute("mainview");
}
this._moveOutKids(this._viewStack);
- this.panelViews.clear();
this._panel.removeEventListener("mousemove", this);
this._panel.removeEventListener("popupshowing", this);
this._panel.removeEventListener("popuppositioned", this);
this._panel.removeEventListener("popupshown", this);
this._panel.removeEventListener("popuphidden", this);
this.window.removeEventListener("keydown", this);
this.node = this._viewContainer = this._viewStack = this.__dwu =
this._panelViewCache = this._transitionDetails = null;
@@ -396,61 +305,63 @@ this.PanelMultiView = class {
let label = this.document.createElement("label");
label.setAttribute("value", titleText);
header.append(backButton, label);
viewNode.prepend(header);
}
goBack() {
- let [current, previous] = this.panelViews.back();
+ 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
* false if this is the panelmultiview's mainview, true otherwise.
*
* @param {panelview} view View to check, defaults to the currently active view.
* @return {Boolean}
*/
_canGoBack(view = this._currentSubView) {
return view.id != this._mainViewId;
}
showMainView() {
- if (!this._mainViewId)
+ if (!this.node || !this._mainViewId)
return Promise.resolve();
return this.showSubView(this._mainView);
}
/**
* Ensures that all the panelviews, that are currently part of this instance,
* are hidden, except one specifically.
*
* @param {panelview} [theOne] The panelview DOM node to ensure is visible.
* Optional.
*/
hideAllViewsExcept(theOne = null) {
- for (let panelview of this.panelViews) {
+ 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");
}
this._viewShowing = null;
if (!this.node || !theOne)
return;
- this._currentSubView = theOne;
+ 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;
}
@@ -464,18 +375,17 @@ this.PanelMultiView = class {
this._viewStack.appendChild(viewNode);
} else {
throw new Error(`Subview ${aViewId} doesn't exist!`);
}
} else if (viewNode.parentNode == this._panelViewCache) {
this._viewStack.appendChild(viewNode);
}
- if (!this.panelViews.includes(viewNode))
- this.panelViews.push(viewNode);
+ this.knownViews.add(viewNode);
viewNode.panelMultiView = this.node;
let reverse = !!aPreviousView;
if (!reverse) {
this._setHeader(viewNode, viewNode.getAttribute("title") ||
(aAnchor && aAnchor.getAttribute("label")));
}