summaryrefslogtreecommitdiffstats
path: root/browser/components/places/content/places-tree.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/places/content/places-tree.js864
1 files changed, 864 insertions, 0 deletions
diff --git a/browser/components/places/content/places-tree.js b/browser/components/places/content/places-tree.js
new file mode 100644
index 0000000000..28edd775f7
--- /dev/null
+++ b/browser/components/places/content/places-tree.js
@@ -0,0 +1,864 @@
+/* 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/. */
+
+/* import-globals-from controller.js */
+/* import-globals-from treeView.js */
+
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+ /**
+ * Custom element definition for the places tree.
+ */
+ class MozPlacesTree extends customElements.get("tree") {
+ constructor() {
+ super();
+
+ this.addEventListener("focus", event => {
+ this._cachedInsertionPoint = undefined;
+ // See select handler. We need the sidebar's places commandset to be
+ // updated as well
+ document.commandDispatcher.updateCommands("focus");
+ });
+
+ this.addEventListener("select", event => {
+ this._cachedInsertionPoint = undefined;
+
+ // This additional complexity is here for the sidebars
+ var win = window;
+ while (true) {
+ win.document.commandDispatcher.updateCommands("focus");
+ if (win == window.top) {
+ break;
+ }
+
+ win = win.parent;
+ }
+ });
+
+ this.addEventListener("dragstart", event => {
+ if (event.target.localName != "treechildren") {
+ return;
+ }
+
+ if (this.disableUserActions) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ let nodes = this.selectedNodes;
+ for (let i = 0; i < nodes.length; i++) {
+ let node = nodes[i];
+
+ // Disallow dragging the root node of a tree.
+ if (!node.parent) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ // If this node is child of a readonly container or cannot be moved,
+ // we must force a copy.
+ if (!this.controller.canMoveNode(node)) {
+ event.dataTransfer.effectAllowed = "copyLink";
+ break;
+ }
+ }
+
+ // Indicate to drag and drop listeners
+ // whether or not this was the start of the drag
+ this._isDragSource = true;
+
+ this._controller.setDataTransfer(event);
+ event.stopPropagation();
+ });
+
+ this.addEventListener("dragover", event => {
+ if (event.target.localName != "treechildren") {
+ return;
+ }
+
+ let cell = this.getCellAt(event.clientX, event.clientY);
+ let node =
+ cell.row != -1
+ ? this.view.nodeForTreeIndex(cell.row)
+ : this.result.root;
+ // cache the dropTarget for the view
+ PlacesControllerDragHelper.currentDropTarget = node;
+
+ // We have to calculate the orientation since view.canDrop will use
+ // it and we want to be consistent with the dropfeedback.
+ let rowHeight = this.rowHeight;
+ let eventY =
+ event.clientY -
+ this.treeBody.getBoundingClientRect().y -
+ rowHeight * (cell.row - this.getFirstVisibleRow());
+
+ let orientation = Ci.nsITreeView.DROP_BEFORE;
+
+ if (cell.row == -1) {
+ // If the row is not valid we try to insert inside the resultNode.
+ orientation = Ci.nsITreeView.DROP_ON;
+ } else if (
+ PlacesUtils.nodeIsContainer(node) &&
+ eventY > rowHeight * 0.75
+ ) {
+ // If we are below the 75% of a container the treeview we try
+ // to drop after the node.
+ orientation = Ci.nsITreeView.DROP_AFTER;
+ } else if (
+ PlacesUtils.nodeIsContainer(node) &&
+ eventY > rowHeight * 0.25
+ ) {
+ // If we are below the 25% of a container the treeview we try
+ // to drop inside the node.
+ orientation = Ci.nsITreeView.DROP_ON;
+ }
+
+ if (!this.view.canDrop(cell.row, orientation, event.dataTransfer)) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+ });
+
+ this.addEventListener("dragend", event => {
+ this._isDragSource = false;
+ PlacesControllerDragHelper.currentDropTarget = null;
+ });
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+ super.connectedCallback();
+ this._contextMenuShown = false;
+
+ this._active = true;
+
+ // Force an initial build.
+ if (this.place) {
+ // eslint-disable-next-line no-self-assign
+ this.place = this.place;
+ }
+ }
+
+ get controller() {
+ return this._controller;
+ }
+
+ set disableUserActions(val) {
+ if (val) {
+ this.setAttribute("disableUserActions", "true");
+ } else {
+ this.removeAttribute("disableUserActions");
+ }
+ }
+
+ get disableUserActions() {
+ return this.getAttribute("disableUserActions") == "true";
+ }
+ /**
+ * overriding
+ *
+ * @param {PlacesTreeView} val
+ * The parent view
+ */
+ set view(val) {
+ // We save the view so that we can avoid expensive get calls when
+ // we need to get the view again.
+ this._view = val;
+ Object.getOwnPropertyDescriptor(
+ // eslint-disable-next-line no-undef
+ XULTreeElement.prototype,
+ "view"
+ ).set.call(this, val);
+ }
+
+ get view() {
+ return this._view;
+ }
+
+ get associatedElement() {
+ return this;
+ }
+
+ set flatList(val) {
+ if (this.flatList != val) {
+ this.setAttribute("flatList", val);
+ // reload with the last place set
+ if (this.place) {
+ // eslint-disable-next-line no-self-assign
+ this.place = this.place;
+ }
+ }
+ }
+
+ get flatList() {
+ return this.getAttribute("flatList") == "true";
+ }
+
+ get result() {
+ try {
+ return this.view.QueryInterface(Ci.nsINavHistoryResultObserver).result;
+ } catch (e) {
+ return null;
+ }
+ }
+
+ set place(val) {
+ this.setAttribute("place", val);
+
+ let query = {},
+ options = {};
+ PlacesUtils.history.queryStringToQuery(val, query, options);
+ this.load(query.value, options.value);
+ }
+
+ get place() {
+ return this.getAttribute("place");
+ }
+
+ get selectedCount() {
+ return this.view?.selection?.count || 0;
+ }
+
+ get hasSelection() {
+ return this.selectedCount >= 1;
+ }
+
+ get selectedNodes() {
+ let nodes = [];
+ if (!this.hasSelection) {
+ return nodes;
+ }
+
+ let selection = this.view.selection;
+ let rc = selection.getRangeCount();
+ let resultview = this.view;
+ for (let i = 0; i < rc; ++i) {
+ let min = {},
+ max = {};
+ selection.getRangeAt(i, min, max);
+ for (let j = min.value; j <= max.value; ++j) {
+ nodes.push(resultview.nodeForTreeIndex(j));
+ }
+ }
+ return nodes;
+ }
+
+ get removableSelectionRanges() {
+ // This property exists in addition to selectedNodes because it
+ // encodes selection ranges (which only occur in list views) into
+ // the return value. For each removed range, the index at which items
+ // will be re-inserted upon the remove transaction being performed is
+ // the first index of the range, so that the view updates correctly.
+ //
+ // For example, if we remove rows 2,3,4 and 7,8 from a list, when we
+ // undo that operation, if we insert what was at row 3 at row 3 again,
+ // it will show up _after_ the item that was at row 5. So we need to
+ // insert all items at row 2, and the tree view will update correctly.
+ //
+ // Also, this function collapses the selection to remove redundant
+ // data, e.g. when deleting this selection:
+ //
+ // http://www.foo.com/
+ // (-) Some Folder
+ // http://www.bar.com/
+ //
+ // ... returning http://www.bar.com/ as part of the selection is
+ // redundant because it is implied by removing "Some Folder". We
+ // filter out all such redundancies since some partial amount of
+ // the folder's children may be selected.
+ //
+ let nodes = [];
+ if (!this.hasSelection) {
+ return nodes;
+ }
+
+ var selection = this.view.selection;
+ var rc = selection.getRangeCount();
+ var resultview = this.view;
+ // This list is kept independently of the range selected (i.e. OUTSIDE
+ // the for loop) since the row index of a container is unique for the
+ // entire view, and we could have some really wacky selection and we
+ // don't want to blow up.
+ var containers = {};
+ for (var i = 0; i < rc; ++i) {
+ var range = [];
+ var min = {},
+ max = {};
+ selection.getRangeAt(i, min, max);
+
+ for (var j = min.value; j <= max.value; ++j) {
+ if (this.view.isContainer(j)) {
+ containers[j] = true;
+ }
+ if (!(this.view.getParentIndex(j) in containers)) {
+ range.push(resultview.nodeForTreeIndex(j));
+ }
+ }
+ nodes.push(range);
+ }
+ return nodes;
+ }
+
+ get draggableSelection() {
+ return this.selectedNodes;
+ }
+
+ get selectedNode() {
+ if (this.selectedCount != 1) {
+ return null;
+ }
+
+ var selection = this.view.selection;
+ var min = {},
+ max = {};
+ selection.getRangeAt(0, min, max);
+
+ return this.view.nodeForTreeIndex(min.value);
+ }
+
+ get singleClickOpens() {
+ return this.getAttribute("singleclickopens") == "true";
+ }
+
+ get insertionPoint() {
+ // invalidated on selection and focus changes
+ if (this._cachedInsertionPoint !== undefined) {
+ return this._cachedInsertionPoint;
+ }
+
+ // there is no insertion point for history queries
+ // so bail out now and save a lot of work when updating commands
+ var resultNode = this.result.root;
+ if (
+ PlacesUtils.nodeIsQuery(resultNode) &&
+ PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY
+ ) {
+ return (this._cachedInsertionPoint = null);
+ }
+
+ var orientation = Ci.nsITreeView.DROP_BEFORE;
+ // If there is no selection, insert at the end of the container.
+ if (!this.hasSelection) {
+ var index = this.view.rowCount - 1;
+ this._cachedInsertionPoint = this._getInsertionPoint(
+ index,
+ orientation
+ );
+ return this._cachedInsertionPoint;
+ }
+
+ // This is a two-part process. The first part is determining the drop
+ // orientation.
+ // * The default orientation is to drop _before_ the selected item.
+ // * If the selected item is a container, the default orientation
+ // is to drop _into_ that container.
+ //
+ // Warning: It may be tempting to use tree indexes in this code, but
+ // you must not, since the tree is nested and as your tree
+ // index may change when folders before you are opened and
+ // closed. You must convert your tree index to a node, and
+ // then use getChildIndex to find your absolute index in
+ // the parent container instead.
+ //
+ var resultView = this.view;
+ var selection = resultView.selection;
+ var rc = selection.getRangeCount();
+ var min = {},
+ max = {};
+ selection.getRangeAt(rc - 1, min, max);
+
+ // If the sole selection is a container, and we are not in
+ // a flatlist, insert into it.
+ // Note that this only applies to _single_ selections,
+ // if the last element within a multi-selection is a
+ // container, insert _adjacent_ to the selection.
+ //
+ // If the sole selection is the bookmarks toolbar folder, we insert
+ // into it even if it is not opened
+ if (
+ selection.count == 1 &&
+ resultView.isContainer(max.value) &&
+ !this.flatList
+ ) {
+ orientation = Ci.nsITreeView.DROP_ON;
+ }
+
+ this._cachedInsertionPoint = this._getInsertionPoint(
+ max.value,
+ orientation
+ );
+ return this._cachedInsertionPoint;
+ }
+
+ get isDragSource() {
+ return this._isDragSource;
+ }
+
+ get ownerWindow() {
+ return window;
+ }
+
+ set active(val) {
+ this._active = val;
+ }
+
+ get active() {
+ return this._active;
+ }
+
+ applyFilter(filterString, folderRestrict, includeHidden) {
+ // preserve grouping
+ var queryNode = PlacesUtils.asQuery(this.result.root);
+ var options = queryNode.queryOptions.clone();
+
+ // Make sure we're getting uri results.
+ // We do not yet support searching into grouped queries or into
+ // tag containers, so we must fall to the default case.
+ if (
+ PlacesUtils.nodeIsHistoryContainer(queryNode) ||
+ PlacesUtils.nodeIsTagQuery(queryNode) ||
+ options.resultType == options.RESULTS_AS_TAGS_ROOT ||
+ options.resultType == options.RESULTS_AS_ROOTS_QUERY
+ ) {
+ options.resultType = options.RESULTS_AS_URI;
+ }
+
+ var query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = filterString;
+
+ if (folderRestrict) {
+ query.setParents(folderRestrict);
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ Services.telemetry.keyedScalarAdd("sidebar.search", "bookmarks", 1);
+ }
+
+ options.includeHidden = !!includeHidden;
+
+ this.load(query, options);
+ }
+
+ load(query, options) {
+ let result = PlacesUtils.history.executeQuery(query, options);
+
+ if (!this._controller) {
+ this._controller = new PlacesController(this);
+ this._controller.disableUserActions = this.disableUserActions;
+ this.controllers.appendController(this._controller);
+ }
+
+ let treeView = new PlacesTreeView(this);
+
+ // Observer removal is done within the view itself. When the tree
+ // goes away, view.setTree(null) is called, which then
+ // calls removeObserver.
+ result.addObserver(treeView);
+ this.view = treeView;
+
+ if (
+ this.getAttribute("selectfirstnode") == "true" &&
+ treeView.rowCount > 0
+ ) {
+ treeView.selection.select(0);
+ }
+
+ this._cachedInsertionPoint = undefined;
+ }
+
+ /**
+ * Causes a particular node represented by the specified placeURI to be
+ * selected in the tree. All containers above the node in the hierarchy
+ * will be opened, so that the node is visible.
+ *
+ * @param {string} placeURI
+ * The URI that should be selected
+ */
+ selectPlaceURI(placeURI) {
+ // Do nothing if a node matching the given uri is already selected
+ if (this.hasSelection && this.selectedNode.uri == placeURI) {
+ return;
+ }
+
+ function findNode(container, nodesURIChecked) {
+ var containerURI = container.uri;
+ if (containerURI == placeURI) {
+ return container;
+ }
+ if (nodesURIChecked.includes(containerURI)) {
+ return null;
+ }
+
+ // never check the contents of the same query
+ nodesURIChecked.push(containerURI);
+
+ var wasOpen = container.containerOpen;
+ if (!wasOpen) {
+ container.containerOpen = true;
+ }
+ for (var i = 0; i < container.childCount; ++i) {
+ var child = container.getChild(i);
+ var childURI = child.uri;
+ if (childURI == placeURI) {
+ return child;
+ } else if (PlacesUtils.nodeIsContainer(child)) {
+ var nested = findNode(
+ PlacesUtils.asContainer(child),
+ nodesURIChecked
+ );
+ if (nested) {
+ return nested;
+ }
+ }
+ }
+
+ if (!wasOpen) {
+ container.containerOpen = false;
+ }
+
+ return null;
+ }
+
+ var container = this.result.root;
+ console.assert(container, "No result, cannot select place URI!");
+ if (!container) {
+ return;
+ }
+
+ var child = findNode(container, []);
+ if (child) {
+ this.selectNode(child);
+ } else {
+ // If the specified child could not be located, clear the selection
+ var selection = this.view.selection;
+ selection.clearSelection();
+ }
+ }
+
+ /**
+ * Causes a particular node to be selected in the tree, resulting in all
+ * containers above the node in the hierarchy to be opened, so that the
+ * node is visible.
+ *
+ * @param {object} node
+ * The node that should be selected
+ */
+ selectNode(node) {
+ var view = this.view;
+
+ var parent = node.parent;
+ if (parent && !parent.containerOpen) {
+ // Build a list of all of the nodes that are the parent of this one
+ // in the result.
+ var parents = [];
+ var root = this.result.root;
+ while (parent && parent != root) {
+ parents.push(parent);
+ parent = parent.parent;
+ }
+
+ // Walk the list backwards (opening from the root of the hierarchy)
+ // opening each folder as we go.
+ for (var i = parents.length - 1; i >= 0; --i) {
+ let index = view.treeIndexForNode(parents[i]);
+ if (
+ index != -1 &&
+ view.isContainer(index) &&
+ !view.isContainerOpen(index)
+ ) {
+ view.toggleOpenState(index);
+ }
+ }
+ // Select the specified node...
+ }
+
+ let index = view.treeIndexForNode(node);
+ if (index == -1) {
+ return;
+ }
+
+ view.selection.select(index);
+ // ... and ensure it's visible, not scrolled off somewhere.
+ this.ensureRowIsVisible(index);
+ }
+
+ toggleCutNode(aNode, aValue) {
+ this.view.toggleCutNode(aNode, aValue);
+ }
+
+ _getInsertionPoint(index, orientation) {
+ var result = this.result;
+ var resultview = this.view;
+ var container = result.root;
+ var dropNearNode = null;
+ console.assert(container, "null container");
+ // When there's no selection, assume the container is the container
+ // the view is populated from (i.e. the result's itemId).
+ if (index != -1) {
+ var lastSelected = resultview.nodeForTreeIndex(index);
+ if (
+ resultview.isContainer(index) &&
+ orientation == Ci.nsITreeView.DROP_ON
+ ) {
+ // If the last selected item is an open container, append _into_
+ // it, rather than insert adjacent to it.
+ container = lastSelected;
+ index = -1;
+ } else if (
+ lastSelected.containerOpen &&
+ orientation == Ci.nsITreeView.DROP_AFTER &&
+ lastSelected.hasChildren
+ ) {
+ // If the last selected item is an open container and the user is
+ // trying to drag into it as a first item, really insert into it.
+ container = lastSelected;
+ orientation = Ci.nsITreeView.DROP_ON;
+ index = 0;
+ } else {
+ // Use the last-selected node's container.
+ container = lastSelected.parent;
+
+ // See comment in the treeView.js's copy of this method
+ if (!container || !container.containerOpen) {
+ return null;
+ }
+
+ // Avoid the potentially expensive call to getChildIndex
+ // if we know this container doesn't allow insertion
+ if (this.controller.disallowInsertion(container)) {
+ return null;
+ }
+
+ var queryOptions = PlacesUtils.asQuery(result.root).queryOptions;
+ if (
+ queryOptions.sortingMode !=
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE
+ ) {
+ // If we are within a sorted view, insert at the end
+ index = -1;
+ } else if (queryOptions.excludeItems || queryOptions.excludeQueries) {
+ // Some item may be invisible, insert near last selected one.
+ // We don't replace index here to avoid requests to the db,
+ // instead it will be calculated later by the controller.
+ index = -1;
+ dropNearNode = lastSelected;
+ } else {
+ var lsi = container.getChildIndex(lastSelected);
+ index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
+ }
+ }
+ }
+
+ if (this.controller.disallowInsertion(container)) {
+ return null;
+ }
+
+ let tagName = PlacesUtils.nodeIsTagQuery(container)
+ ? PlacesUtils.asQuery(container).query.tags[0]
+ : null;
+
+ return new PlacesInsertionPoint({
+ parentGuid: PlacesUtils.getConcreteItemGuid(container),
+ index,
+ orientation,
+ tagName,
+ dropNearNode,
+ });
+ }
+
+ selectAll() {
+ this.view.selection.selectAll();
+ }
+
+ /**
+ * This method will select the first node in the tree that matches
+ * each given item guid. It will open any folder nodes that it needs
+ * to in order to show the selected items.
+ *
+ * @param {Array} aGuids
+ * Guids to select.
+ * @param {boolean} aOpenContainers
+ * Whether or not to open containers.
+ */
+ selectItems(aGuids, aOpenContainers) {
+ // Never open containers in flat lists.
+ if (this.flatList) {
+ aOpenContainers = false;
+ }
+ // By default, we do search and select within containers which were
+ // closed (note that containers in which nodes were not found are
+ // closed).
+ if (aOpenContainers === undefined) {
+ aOpenContainers = true;
+ }
+
+ var guids = aGuids; // don't manipulate the caller's array
+
+ // Array of nodes found by findNodes which are to be selected
+ var nodes = [];
+
+ // Array of nodes found by findNodes which should be opened
+ var nodesToOpen = [];
+
+ // A set of GUIDs of container-nodes that were previously searched,
+ // and thus shouldn't be searched again. This is empty at the initial
+ // start of the recursion and gets filled in as the recursion
+ // progresses.
+ var checkedGuidsSet = new Set();
+
+ /**
+ * Recursively search through a node's children for items
+ * with the given GUIDs. When a matching item is found, remove its GUID
+ * from the GUIDs array, and add the found node to the nodes dictionary.
+ *
+ * NOTE: This method will leave open any node that had matching items
+ * in its subtree.
+ *
+ * @param {object} node
+ * The node to search.
+ * @returns {boolean}
+ * Returns true if at least one item was found.
+ */
+ function findNodes(node) {
+ var foundOne = false;
+ // See if node matches an ID we wanted; add to results.
+ // For simple folder queries, check both itemId and the concrete
+ // item id.
+ var index = guids.indexOf(node.bookmarkGuid);
+ if (index == -1) {
+ let concreteGuid = PlacesUtils.getConcreteItemGuid(node);
+ if (concreteGuid != node.bookmarkGuid) {
+ index = guids.indexOf(concreteGuid);
+ }
+ }
+
+ if (index != -1) {
+ nodes.push(node);
+ foundOne = true;
+ guids.splice(index, 1);
+ }
+
+ var concreteGuid = PlacesUtils.getConcreteItemGuid(node);
+ if (
+ !guids.length ||
+ !PlacesUtils.nodeIsContainer(node) ||
+ checkedGuidsSet.has(concreteGuid)
+ ) {
+ return foundOne;
+ }
+
+ // Only follow a query if it has been been explicitly opened by the
+ // caller. We support the "AllBookmarks" case to allow callers to
+ // specify just the top-level bookmark folders.
+ let shouldOpen =
+ aOpenContainers &&
+ (PlacesUtils.nodeIsFolder(node) ||
+ (PlacesUtils.nodeIsQuery(node) &&
+ node.bookmarkGuid == PlacesUIUtils.virtualAllBookmarksGuid));
+
+ PlacesUtils.asContainer(node);
+ if (!node.containerOpen && !shouldOpen) {
+ return foundOne;
+ }
+
+ checkedGuidsSet.add(concreteGuid);
+
+ // Remember the beginning state so that we can re-close
+ // this node if we don't find any additional results here.
+ var previousOpenness = node.containerOpen;
+ node.containerOpen = true;
+ for (var child = 0; child < node.childCount && guids.length; child++) {
+ var childNode = node.getChild(child);
+ var found = findNodes(childNode);
+ if (!foundOne) {
+ foundOne = found;
+ }
+ }
+
+ // If we didn't find any additional matches in this node's
+ // subtree, revert the node to its previous openness.
+ if (foundOne) {
+ nodesToOpen.unshift(node);
+ }
+ node.containerOpen = previousOpenness;
+ return foundOne;
+ }
+
+ // Disable notifications while looking for nodes.
+ let result = this.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications) {
+ result.suppressNotifications = true;
+ }
+ try {
+ findNodes(this.result.root);
+ } finally {
+ if (!didSuppressNotifications) {
+ result.suppressNotifications = false;
+ }
+ }
+
+ // For all the nodes we've found, highlight the corresponding
+ // index in the tree.
+ var resultview = this.view;
+ var selection = this.view.selection;
+ selection.selectEventsSuppressed = true;
+ selection.clearSelection();
+ // Open nodes containing found items
+ for (let i = 0; i < nodesToOpen.length; i++) {
+ nodesToOpen[i].containerOpen = true;
+ }
+ let firstValidTreeIndex = -1;
+ for (let i = 0; i < nodes.length; i++) {
+ var index = resultview.treeIndexForNode(nodes[i]);
+ if (index == -1) {
+ continue;
+ }
+ if (firstValidTreeIndex < 0 && index >= 0) {
+ firstValidTreeIndex = index;
+ }
+ selection.rangedSelect(index, index, true);
+ }
+ selection.selectEventsSuppressed = false;
+
+ // Bring the first valid node into view if necessary
+ if (firstValidTreeIndex >= 0) {
+ this.ensureRowIsVisible(firstValidTreeIndex);
+ }
+ }
+
+ buildContextMenu(aPopup) {
+ this._contextMenuShown = true;
+ return this.controller.buildContextMenu(aPopup);
+ }
+
+ destroyContextMenu(aPopup) {}
+ disconnectedCallback() {
+ // Unregister the controller before unlinking the view, otherwise it
+ // may still try to update commands on a view with a null result.
+ if (this._controller) {
+ this._controller.terminate();
+ this.controllers.removeController(this._controller);
+ }
+
+ if (this.view) {
+ this.view.uninit();
+ }
+ // view.setTree(null) will be called upon unsetting the view, which
+ // breaks the reference cycle between the PlacesTreeView and result.
+ // See the "setTree" method of PlacesTreeView in treeView.js.
+ this.view = null;
+ }
+ }
+
+ customElements.define("places-tree", MozPlacesTree, {
+ extends: "tree",
+ });
+}