Bug 1354159 - Part 2 - Introduce a new Places view type, PlacesPanelview, which can visualize query results inside panelview nodes. r?mak,Gijs
MozReview-Commit-ID: Ft1RC7dsqKD
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -200,16 +200,25 @@ this.PanelMultiView = class {
.getInterface(Ci.nsIDOMWindowUtils);
}
get _screenManager() {
if (this.__screenManager)
return this.__screenManager;
return this.__screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
.getService(Ci.nsIScreenManager);
}
+ /**
+ * Getter that returns the currently visible subview OR the subview that is
+ * about to be shown whilst a 'ViewShowing' event is being dispatched.
+ *
+ * @return {panelview}
+ */
+ get current() {
+ return this._viewShowing || this._currentSubView
+ }
get _currentSubView() {
return this.panelViews ? this.panelViews.currentView : this.__currentSubView;
}
set _currentSubView(panel) {
if (this.panelViews)
this.panelViews.currentView = panel;
else
this.__currentSubView = panel;
@@ -290,16 +299,20 @@ this.PanelMultiView = class {
});
["goBack", "descriptionHeightWorkaround", "setMainView", "showMainView",
"showSubView"].forEach(method => {
Object.defineProperty(this.node, method, {
enumerable: true,
value: (...args) => this[method](...args)
});
});
+ Object.defineProperty(this.node, "current", {
+ enumerable: true,
+ get: () => this.current
+ });
}
destructor() {
// Guard against re-entrancy.
if (!this.node)
return;
if (this._mainView) {
@@ -315,16 +328,17 @@ this.PanelMultiView = class {
this._moveOutKids(this._viewStack);
this.panelViews.clear();
} else {
this._clickCapturer.removeEventListener("click", this);
}
this._panel.removeEventListener("popupshowing", this);
this._panel.removeEventListener("popupshown", this);
this._panel.removeEventListener("popuphidden", this);
+ this.node.dispatchEvent(new this.window.CustomEvent("destructed"));
this.node = this._clickCapturer = this._viewContainer = this._mainViewContainer =
this._subViews = this._viewStack = this.__dwu = this._panelViewCache = null;
}
/**
* Remove any child subviews into the panelViewCache, to ensure
* they remain usable even if this panelmultiview instance is removed
* from the DOM.
@@ -339,16 +353,26 @@ this.PanelMultiView = class {
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);
}
}
+ _placeSubView(viewNode) {
+ if (this.panelViews) {
+ this._viewStack.appendChild(viewNode);
+ if (!this.panelViews.includes(viewNode))
+ this.panelViews.push(viewNode);
+ } else {
+ this._subViews.appendChild(viewNode);
+ }
+ }
+
goBack(target) {
let [current, previous] = this.panelViews.back();
return this.showSubView(current, target, previous);
}
/**
* Checks whether it is possible to navigate backwards currently. Returns
* false if this is the panelmultiview's mainview, true otherwise.
@@ -404,25 +428,22 @@ this.PanelMultiView = class {
showSubView(aViewId, aAnchor, aPreviousView) {
const {document, window} = this;
return (async () => {
// Support passing in the node directly.
let viewNode = typeof aViewId == "string" ? this.node.querySelector("#" + aViewId) : aViewId;
if (!viewNode) {
viewNode = document.getElementById(aViewId);
if (viewNode) {
- if (this.panelViews) {
- this._viewStack.appendChild(viewNode);
- this.panelViews.push(viewNode);
- } else {
- this._subViews.appendChild(viewNode);
- }
+ this._placeSubView(viewNode);
} else {
throw new Error(`Subview ${aViewId} doesn't exist!`);
}
+ } else if (viewNode.parentNode == this._panelViewCache) {
+ this._placeSubView(viewNode);
}
let reverse = !!aPreviousView;
let previousViewNode = aPreviousView || this._currentSubView;
let playTransition = (!!previousViewNode && previousViewNode != viewNode);
let dwu, previousRect;
if (playTransition || this.panelViews) {
@@ -467,16 +488,17 @@ this.PanelMultiView = class {
if (custWidget.onInit)
custWidget.onInit(aAnchor);
custWidget.onViewShowing({ target: viewNode, preventDefault: () => cancel = true, detail });
}
}
if (this.panelViews && this._mainViewWidth)
viewNode.style.maxWidth = viewNode.style.minWidth = this._mainViewWidth + "px";
+ this._viewShowing = viewNode;
let evt = new window.CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
viewNode.dispatchEvent(evt);
if (!cancel)
cancel = evt.defaultPrevented;
if (detail.blockers.size) {
try {
let results = await Promise.all(detail.blockers);
@@ -486,16 +508,17 @@ this.PanelMultiView = class {
cancel = true;
}
}
if (cancel) {
return;
}
+ this._viewShowing = null;
this._currentSubView = viewNode;
viewNode.setAttribute("current", true);
if (this.panelViews) {
this.node.setAttribute("viewtype", "subview");
if (!playTransition)
this.descriptionHeightWorkaround(viewNode);
}
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -659,16 +659,20 @@ this.PlacesUIUtils = {
* Returns the closet ancestor places view for the given DOM node
* @param aNode
* a DOM node
* @return the closet ancestor places view if exists, null otherwsie.
*/
getViewForNode: function PUIU_getViewForNode(aNode) {
let node = aNode;
+ if (node.localName == "panelview" && node._placesView) {
+ return node._placesView;
+ }
+
// The view for a <menu> of which its associated menupopup is a places
// view, is the menupopup.
if (node.localName == "menu" && !node._placesNode &&
node.lastChild._placesView)
return node.lastChild._placesView;
while (node instanceof Ci.nsIDOMElement) {
if (node._placesView)
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -7,20 +7,24 @@
Components.utils.import("resource://gre/modules/AppConstants.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
/**
* The base view implements everything that's common to the toolbar and
* menu views.
*/
-function PlacesViewBase(aPlace, aOptions) {
- this.place = aPlace;
+function PlacesViewBase(aPlace, aOptions = {}) {
+ if ("rootElt" in aOptions)
+ this._rootElt = aOptions.rootElt;
+ if ("viewElt" in aOptions)
+ this._viewElt = aOptions.viewElt;
this.options = aOptions;
this._controller = new PlacesController(this);
+ this.place = aPlace;
this._viewElt.controllers.appendController(this._controller);
}
PlacesViewBase.prototype = {
// The xul element that holds the entire view.
_viewElt: null,
get viewElt() {
return this._viewElt;
@@ -228,16 +232,19 @@ PlacesViewBase.prototype = {
return this.controller.buildContextMenu(aPopup);
},
destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
this._contextMenuShown = null;
},
_cleanPopup: function PVB_cleanPopup(aPopup, aDelay) {
+ // Ensure markers are here when `invalidateContainer` is called before the
+ // popup is shown, which may the case for panelviews, for example.
+ this._ensureMarkers(aPopup);
// Remove Places nodes from the popup.
let child = aPopup._startMarker;
while (child.nextSibling != aPopup._endMarker) {
let sibling = child.nextSibling;
if (sibling._placesNode && !aDelay) {
aPopup.removeChild(sibling);
} else if (sibling._placesNode && aDelay) {
// HACK (bug 733419): the popups originating from the OS X native
@@ -312,18 +319,18 @@ PlacesViewBase.prototype = {
} else {
aPopup.removeAttribute("emptyplacesresult");
try {
aPopup.removeChild(aPopup._emptyMenuitem);
} catch (ex) {}
}
},
- _createMenuItemForPlacesNode:
- function PVB__createMenuItemForPlacesNode(aPlacesNode) {
+ _createDOMNodeForPlacesNode:
+ function PVB__createDOMNodeForPlacesNode(aPlacesNode) {
this._domNodes.delete(aPlacesNode);
let element;
let type = aPlacesNode.type;
if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
element = document.createElement("menuseparator");
element.setAttribute("class", "small-separator");
} else {
@@ -387,17 +394,17 @@ PlacesViewBase.prototype = {
if (!this._domNodes.has(aPlacesNode))
this._domNodes.set(aPlacesNode, element);
return element;
},
_insertNewItemToPopup:
function PVB__insertNewItemToPopup(aNewChild, aPopup, aBefore) {
- let element = this._createMenuItemForPlacesNode(aNewChild);
+ let element = this._createDOMNodeForPlacesNode(aNewChild);
let before = aBefore || aPopup._endMarker;
if (element.localName == "menuitem" || element.localName == "menu") {
if (typeof this.options.extraClasses.entry == "string")
element.classList.add(this.options.extraClasses.entry);
}
aPopup.insertBefore(element, before);
@@ -711,22 +718,33 @@ PlacesViewBase.prototype = {
if (child.accessCount)
this._getDOMNodeForPlacesNode(child).setAttribute("visited", true);
else
this._getDOMNodeForPlacesNode(child).removeAttribute("visited");
}
}, Components.utils.reportError);
},
+ /**
+ * Checks whether the popup associated with the provided element is open.
+ * This method may be overridden by classes that extend this base class.
+ *
+ * @param {nsIDOMElement} elt
+ * @return {Boolean}
+ */
+ _isPopupOpen(elt) {
+ return !!elt.parentNode.open;
+ },
+
invalidateContainer: function PVB_invalidateContainer(aPlacesNode) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
elt._built = false;
// If the menupopup is open we should live-update it.
- if (elt.parentNode.open)
+ if (this._isPopupOpen(elt))
this._rebuildPopup(elt);
},
uninit: function PVB_uninit() {
if (this._result) {
this._result.removeObserver(this);
this._resultNode.containerOpen = false;
this._resultNode = null;
@@ -850,17 +868,17 @@ PlacesViewBase.prototype = {
// _startMarker is an hidden menuseparator that lives before places nodes.
aPopup._startMarker = document.createElement("menuseparator");
aPopup._startMarker.hidden = true;
aPopup.insertBefore(aPopup._startMarker, aPopup.firstChild);
// _endMarker is a DOM node that lives after places nodes, specified with
// the 'insertionPoint' option or will be a hidden menuseparator.
- let node = ("insertionPoint" in this.options) ?
+ let node = this.options.insertionPoint ?
aPopup.querySelector(this.options.insertionPoint) : null;
if (node) {
aPopup._endMarker = node;
} else {
aPopup._endMarker = document.createElement("menuseparator");
aPopup._endMarker.hidden = true;
}
aPopup.appendChild(aPopup._endMarker);
@@ -907,24 +925,24 @@ PlacesViewBase.prototype = {
if (!popup._built)
this._rebuildPopup(popup);
this._mayAddCommandsItems(popup);
}
},
_addEventListeners:
- function PVB__addEventListeners(aObject, aEventNames, aCapturing) {
+ function PVB__addEventListeners(aObject, aEventNames, aCapturing = false) {
for (let i = 0; i < aEventNames.length; i++) {
aObject.addEventListener(aEventNames[i], this, aCapturing);
}
},
_removeEventListeners:
- function PVB__removeEventListeners(aObject, aEventNames, aCapturing) {
+ function PVB__removeEventListeners(aObject, aEventNames, aCapturing = false) {
for (let i = 0; i < aEventNames.length; i++) {
aObject.removeEventListener(aEventNames[i], this, aCapturing);
}
},
};
function PlacesToolbar(aPlace) {
let startTime = Date.now();
@@ -1953,8 +1971,201 @@ PlacesPanelMenuView.prototype = {
this._rootElt.firstChild.remove();
}
for (let i = 0; i < this._resultNode.childCount; ++i) {
this._insertNewItem(this._resultNode.getChild(i), null);
}
}
};
+
+class PlacesPanelview extends PlacesViewBase {
+ constructor(container, panelview, place, options = {}) {
+ options.rootElt = container;
+ options.viewElt = panelview;
+ super(place, options);
+ this._viewElt._placesView = this;
+ // We're simulating a popup show, because a panelview may only be shown when
+ // its containing popup is already shown.
+ this._onPopupShowing({ originalTarget: this._viewElt });
+ this._addEventListeners(window, ["unload"]);
+ this._rootElt.setAttribute("context", "placesContext");
+ }
+
+ get events() {
+ if (this._events)
+ return this._events;
+ return this._events = ["command", "destructed", "dragend", "dragstart",
+ "ViewHiding", "ViewShowing", "ViewShown"];
+ }
+
+ get panel() {
+ return this.panelMultiView.parentNode;
+ }
+
+ get panelMultiView() {
+ return this._viewElt.panelMultiView;
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "command":
+ this._onCommand(event);
+ break;
+ case "destructed":
+ this._onDestructed(event);
+ break;
+ case "dragend":
+ this._onDragEnd(event);
+ break;
+ case "dragstart":
+ this._onDragStart(event);
+ break;
+ case "unload":
+ this.uninit(event);
+ break;
+ case "ViewHiding":
+ this._onPopupHidden(event);
+ break;
+ case "ViewShowing":
+ this._onPopupShowing(event);
+ break;
+ case "ViewShown":
+ this._onViewShown(event);
+ break;
+ }
+ }
+
+ _onCommand(event) {
+ let button = event.originalTarget;
+ if (!button._placesNode)
+ return;
+
+ PlacesUIUtils.openNodeWithEvent(button._placesNode, event);
+ }
+
+ _onDestructed(event) {
+ // The panelmultiview is ephemeral, so let's keep an eye out when the root
+ // element is showing again.
+ this._removeEventListeners(event.target, this.events);
+ this._addEventListeners(this._viewElt, ["ViewShowing"]);
+ }
+
+ _onDragEnd() {
+ this._draggedElt = null;
+ }
+
+ _onDragStart(event) {
+ let draggedElt = event.originalTarget;
+ if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode)
+ return;
+
+ // Activate the view and cache the dragged element.
+ this._draggedElt = draggedElt._placesNode;
+ this._rootElt.focus();
+
+ this._controller.setDataTransfer(event);
+ event.stopPropagation();
+ }
+
+ uninit(event) {
+ this._removeEventListeners(this.panelMultiView, this.events);
+ this._removeEventListeners(this._viewElt, ["ViewShowing"]);
+ this._removeEventListeners(window, ["unload"]);
+ super.uninit(event);
+ }
+
+ _createDOMNodeForPlacesNode(placesNode) {
+ this._domNodes.delete(placesNode);
+
+ let element;
+ let type = placesNode.type;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ element = document.createElement("toolbarseparator");
+ } else {
+ if (type != Ci.nsINavHistoryResultNode.RESULT_TYPE_URI)
+ throw "Unexpected node";
+
+ element = document.createElement("toolbarbutton");
+ element.classList.add("subviewbutton", "subviewbutton-iconic", "subview-bookmark-item");
+ element.setAttribute("scheme", PlacesUIUtils.guessUrlSchemeForUI(placesNode.uri));
+ element.setAttribute("label", PlacesUIUtils.getBestTitle(placesNode));
+
+ let icon = placesNode.icon;
+ if (icon)
+ element.setAttribute("image", icon);
+ }
+
+ element._placesNode = placesNode;
+ if (!this._domNodes.has(placesNode))
+ this._domNodes.set(placesNode, element);
+
+ return element;
+ }
+
+ _setEmptyPopupStatus(panelview, empty = false) {
+ if (!panelview._emptyMenuitem) {
+ let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
+ panelview._emptyMenuitem = document.createElement("toolbarbutton");
+ panelview._emptyMenuitem.setAttribute("label", label);
+ panelview._emptyMenuitem.setAttribute("disabled", true);
+ panelview._emptyMenuitem.className = "subviewbutton";
+ if (typeof this.options.extraClasses.entry == "string")
+ panelview._emptyMenuitem.classList.add(this.options.extraClasses.entry);
+ }
+
+ if (empty) {
+ panelview.setAttribute("emptyplacesresult", "true");
+ // Don't add the menuitem if there is static content.
+ if (!panelview._startMarker.previousSibling &&
+ !panelview._endMarker.nextSibling)
+ panelview.insertBefore(panelview._emptyMenuitem, panelview._endMarker);
+ } else {
+ panelview.removeAttribute("emptyplacesresult");
+ try {
+ panelview.removeChild(panelview._emptyMenuitem);
+ } catch (ex) {}
+ }
+ }
+
+ _isPopupOpen() {
+ return this.panel.state == "open" && this.panelMultiView.current == this._viewElt;
+ }
+
+ _onPopupHidden(event) {
+ let panelview = event.originalTarget;
+ let placesNode = panelview._placesNode;
+ // Avoid handling ViewHiding of inner views
+ if (placesNode && PlacesUIUtils.getViewForNode(panelview) == this) {
+ // UI performance: folder queries are cheap, keep the resultnode open
+ // so we don't rebuild its contents whenever the popup is reopened.
+ // Though, we want to always close feed containers so their expiration
+ // status will be checked at next opening.
+ if (!PlacesUtils.nodeIsFolder(placesNode) ||
+ this.controller.hasCachedLivemarkInfo(placesNode)) {
+ placesNode.containerOpen = false;
+ }
+ }
+ }
+
+ _onPopupShowing(event) {
+ // If the event came from the root element, this is a sign that the panelmultiview
+ // was just instantiated (see `_onDestructed` above) or this is the first time
+ // we ever get here.
+ if (event.originalTarget == this._viewElt) {
+ this._removeEventListeners(this._viewElt, ["ViewShowing"]);
+ // Start listening for events from all panels inside the panelmultiview.
+ this._addEventListeners(this.panelMultiView, this.events);
+ }
+ super._onPopupShowing(event);
+ }
+
+ _onViewShown(event) {
+ if (event.originalTarget != this._viewElt)
+ return;
+
+ // Because PanelMultiView reparents the panelview internally, the controller
+ // may get lost. In that case we'll append it again, because we certainly
+ // need it later!
+ if (!this.controllers.getControllerCount() && this._controller)
+ this.controllers.appendController(this._controller);
+ }
+}