/* 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/. */
/* eslint-env mozilla/browser-window */
/**
* The base view implements everything that's common to the toolbar and
* menu views.
*/
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.interfaces = [
Ci.nsINavHistoryResultObserver,
Ci.nsISupportsWeakReference,
];
PlacesViewBase.prototype = {
// The xul element that holds the entire view.
_viewElt: null,
get viewElt() {
return this._viewElt;
},
get associatedElement() {
return this._viewElt;
},
get controllers() {
return this._viewElt.controllers;
},
// The xul element that represents the root container.
_rootElt: null,
// Set to true for views that are represented by native widgets (i.e.
// the native mac menu).
_nativeView: false,
QueryInterface: ChromeUtils.generateQI(PlacesViewBase.interfaces),
_place: "",
get place() {
return this._place;
},
set place(val) {
this._place = val;
let history = PlacesUtils.history;
let query = {},
options = {};
history.queryStringToQuery(val, query, options);
let result = history.executeQuery(query.value, options.value);
result.addObserver(this);
},
_result: null,
get result() {
return this._result;
},
set result(val) {
if (this._result == val) {
return;
}
if (this._result) {
this._result.removeObserver(this);
this._resultNode.containerOpen = false;
}
if (this._rootElt.localName == "menupopup") {
this._rootElt._built = false;
}
this._result = val;
if (val) {
this._resultNode = val.root;
this._rootElt._placesNode = this._resultNode;
this._domNodes = new Map();
this._domNodes.set(this._resultNode, this._rootElt);
// This calls _rebuild through invalidateContainer.
this._resultNode.containerOpen = true;
} else {
this._resultNode = null;
delete this._domNodes;
}
},
_options: null,
get options() {
return this._options;
},
set options(val) {
if (!val) {
val = {};
}
if (!("extraClasses" in val)) {
val.extraClasses = {};
}
this._options = val;
},
/**
* Gets the DOM node used for the given places node.
*
* @param aPlacesNode
* a places result node.
* @param aAllowMissing
* whether the node may be missing
* @throws if there is no DOM node set for aPlacesNode.
*/
_getDOMNodeForPlacesNode: function PVB__getDOMNodeForPlacesNode(
aPlacesNode,
aAllowMissing = false
) {
let node = this._domNodes.get(aPlacesNode, null);
if (!node && !aAllowMissing) {
throw new Error(
"No DOM node set for aPlacesNode.\nnode.type: " +
aPlacesNode.type +
". node.parent: " +
aPlacesNode
);
}
return node;
},
get controller() {
return this._controller;
},
get selType() {
return "single";
},
selectItems() {},
selectAll() {},
get selectedNode() {
if (this._contextMenuShown) {
let anchor = this._contextMenuShown.triggerNode;
if (!anchor) {
return null;
}
if (anchor._placesNode) {
return this._rootElt == anchor ? null : anchor._placesNode;
}
anchor = anchor.parentNode;
return this._rootElt == anchor ? null : anchor._placesNode || null;
}
return null;
},
get hasSelection() {
return this.selectedNode != null;
},
get selectedNodes() {
let selectedNode = this.selectedNode;
return selectedNode ? [selectedNode] : [];
},
get removableSelectionRanges() {
// On static content the current selectedNode would be the selection's
// parent node. We don't want to allow removing a node when the
// selection is not explicit.
if (
document.popupNode &&
(document.popupNode == "menupopup" || !document.popupNode._placesNode)
) {
return [];
}
return [this.selectedNodes];
},
get draggableSelection() {
return [this._draggedElt];
},
get insertionPoint() {
// There is no insertion point for history queries, so bail out now and
// save a lot of work when updating commands.
let resultNode = this._resultNode;
if (
PlacesUtils.nodeIsQuery(resultNode) &&
PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY
) {
return null;
}
// By default, the insertion point is at the top level, at the end.
let index = PlacesUtils.bookmarks.DEFAULT_INDEX;
let container = this._resultNode;
let orientation = Ci.nsITreeView.DROP_BEFORE;
let tagName = null;
let selectedNode = this.selectedNode;
if (selectedNode) {
let popup = document.popupNode;
if (
!popup._placesNode ||
popup._placesNode == this._resultNode ||
popup._placesNode.itemId == -1 ||
!selectedNode.parent
) {
// If a static menuitem is selected, or if the root node is selected,
// the insertion point is inside the folder, at the end.
container = selectedNode;
orientation = Ci.nsITreeView.DROP_ON;
} else {
// In all other cases the insertion point is before that node.
container = selectedNode.parent;
index = container.getChildIndex(selectedNode);
if (PlacesUtils.nodeIsTagQuery(container)) {
tagName = PlacesUtils.asQuery(container).query.tags[0];
}
}
}
if (this.controller.disallowInsertion(container)) {
return null;
}
return new PlacesInsertionPoint({
parentId: PlacesUtils.getConcreteItemId(container),
parentGuid: PlacesUtils.getConcreteItemGuid(container),
index,
orientation,
tagName,
});
},
buildContextMenu: function PVB_buildContextMenu(aPopup) {
this._contextMenuShown = aPopup;
window.updateCommands("places");
// Ensure that an existing "Show Other Bookmarks" item is removed before adding it
// again. This item should only be added when gBookmarksToolbar2h2020 is true, but
// its possible the pref could be toggled off in the same window. This results in
// the "Show Other Bookmarks" menu item still being visible even when the pref is
// set to false.
let existingOtherBookmarksItem = aPopup.querySelector(
"#show-other-bookmarks_PersonalToolbar"
);
existingOtherBookmarksItem?.remove();
// Add the View menu for the Bookmarks Toolbar and "Show Other Bookmarks" menu item
// if the click originated from the Bookmarks Toolbar.
if (gBookmarksToolbar2h2020) {
let existingSubmenu = aPopup.querySelector("#toggle_PersonalToolbar");
existingSubmenu?.remove();
let bookmarksToolbar = document.getElementById("PersonalToolbar");
if (bookmarksToolbar?.contains(aPopup.triggerNode)) {
let menu = BookmarkingUI.buildBookmarksToolbarSubmenu(bookmarksToolbar);
aPopup.appendChild(menu);
if (
aPopup.triggerNode.id === "OtherBookmarks" ||
aPopup.triggerNode.id === "PlacesChevron" ||
aPopup.triggerNode.id === "PlacesToolbarItems"
) {
let otherBookmarksMenuItem = BookmarkingUI.buildShowOtherBookmarksMenuItem();
if (otherBookmarksMenuItem) {
aPopup.insertBefore(
otherBookmarksMenuItem,
menu.nextElementSibling
);
}
}
}
}
return this.controller.buildContextMenu(aPopup);
},
destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
this._contextMenuShown = null;
},
clearAllContents(aPopup) {
let kid = aPopup.firstElementChild;
while (kid) {
let next = kid.nextElementSibling;
if (!kid.classList.contains("panel-header")) {
kid.remove();
}
kid = next;
}
aPopup._emptyMenuitem = aPopup._startMarker = aPopup._endMarker = 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.nextElementSibling != aPopup._endMarker) {
let sibling = child.nextElementSibling;
if (sibling._placesNode && !aDelay) {
aPopup.removeChild(sibling);
} else if (sibling._placesNode && aDelay) {
// HACK (bug 733419): the popups originating from the OS X native
// menubar don't live-update while open, thus we don't clean it
// until the next popupshowing, to avoid zombie menuitems.
if (!aPopup._delayedRemovals) {
aPopup._delayedRemovals = [];
}
aPopup._delayedRemovals.push(sibling);
child = child.nextElementSibling;
} else {
child = child.nextElementSibling;
}
}
},
_rebuildPopup: function PVB__rebuildPopup(aPopup) {
let resultNode = aPopup._placesNode;
if (!resultNode.containerOpen) {
return;
}
this._cleanPopup(aPopup);
let cc = resultNode.childCount;
if (cc > 0) {
this._setEmptyPopupStatus(aPopup, false);
let fragment = document.createDocumentFragment();
for (let i = 0; i < cc; ++i) {
let child = resultNode.getChild(i);
this._insertNewItemToPopup(child, fragment);
}
aPopup.insertBefore(fragment, aPopup._endMarker);
} else {
this._setEmptyPopupStatus(aPopup, true);
}
aPopup._built = true;
},
_removeChild: function PVB__removeChild(aChild) {
// If document.popupNode pointed to this child, null it out,
// otherwise controller's command-updating may rely on the removed
// item still being "selected".
if (document.popupNode == aChild) {
document.popupNode = null;
}
aChild.remove();
},
_setEmptyPopupStatus: function PVB__setEmptyPopupStatus(aPopup, aEmpty) {
if (!aPopup._emptyMenuitem) {
let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
aPopup._emptyMenuitem = document.createXULElement("menuitem");
aPopup._emptyMenuitem.setAttribute("label", label);
aPopup._emptyMenuitem.setAttribute("disabled", true);
aPopup._emptyMenuitem.className = "bookmark-item";
if (typeof this.options.extraClasses.entry == "string") {
aPopup._emptyMenuitem.classList.add(this.options.extraClasses.entry);
}
}
if (aEmpty) {
aPopup.setAttribute("emptyplacesresult", "true");
// Don't add the menuitem if there is static content.
if (
!aPopup._startMarker.previousElementSibling &&
!aPopup._endMarker.nextElementSibling
) {
aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker);
}
} else {
aPopup.removeAttribute("emptyplacesresult");
try {
aPopup.removeChild(aPopup._emptyMenuitem);
} catch (ex) {}
}
},
_createDOMNodeForPlacesNode: function PVB__createDOMNodeForPlacesNode(
aPlacesNode
) {
this._domNodes.delete(aPlacesNode);
let element;
let type = aPlacesNode.type;
if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
element = document.createXULElement("menuseparator");
element.setAttribute("class", "small-separator");
} else {
if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
element = document.createXULElement("menuitem");
element.className =
"menuitem-iconic bookmark-item menuitem-with-favicon";
element.setAttribute(
"scheme",
PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri)
);
} else if (PlacesUtils.containerTypes.includes(type)) {
element = document.createXULElement("menu");
element.setAttribute("container", "true");
if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
element.setAttribute("query", "true");
if (PlacesUtils.nodeIsTagQuery(aPlacesNode)) {
element.setAttribute("tagContainer", "true");
} else if (PlacesUtils.nodeIsDay(aPlacesNode)) {
element.setAttribute("dayContainer", "true");
} else if (PlacesUtils.nodeIsHost(aPlacesNode)) {
element.setAttribute("hostContainer", "true");
}
}
let popup = document.createXULElement("menupopup", {
is: "places-popup",
});
popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
if (!this._nativeView) {
popup.setAttribute("placespopup", "true");
}
element.appendChild(popup);
element.className = "menu-iconic bookmark-item";
if (typeof this.options.extraClasses.entry == "string") {
element.classList.add(this.options.extraClasses.entry);
}
this._domNodes.set(aPlacesNode, popup);
} else {
throw new Error("Unexpected node");
}
element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
let icon = aPlacesNode.icon;
if (icon) {
element.setAttribute("image", icon);
}
}
element._placesNode = aPlacesNode;
if (!this._domNodes.has(aPlacesNode)) {
this._domNodes.set(aPlacesNode, element);
}
return element;
},
_insertNewItemToPopup: function PVB__insertNewItemToPopup(
aNewChild,
aInsertionNode,
aBefore = null
) {
let element = this._createDOMNodeForPlacesNode(aNewChild);
if (element.localName == "menuitem" || element.localName == "menu") {
if (typeof this.options.extraClasses.entry == "string") {
element.classList.add(this.options.extraClasses.entry);
}
}
aInsertionNode.insertBefore(element, aBefore);
return element;
},
toggleCutNode: function PVB_toggleCutNode(aPlacesNode, aValue) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// We may get the popup for menus, but we need the menu itself.
if (elt.localName == "menupopup") {
elt = elt.parentNode;
}
if (aValue) {
elt.setAttribute("cutting", "true");
} else {
elt.removeAttribute("cutting");
}
},
nodeURIChanged: function PVB_nodeURIChanged(aPlacesNode, aURIString) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// Here we need the