869 lines
26 KiB
JavaScript
869 lines
26 KiB
JavaScript
/* 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", () => {
|
|
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", () => {
|
|
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", () => {
|
|
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;
|
|
}
|
|
|
|
window.addEventListener("unload", this.disconnectedCallback);
|
|
}
|
|
|
|
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;
|
|
Glean.sidebar.search.bookmarks.add(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 (let i = 0, count = container.childCount; i < count; ++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.nodeIsFolderOrShortcut(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.
|
|
let previousOpenness = node.containerOpen;
|
|
node.containerOpen = true;
|
|
for (
|
|
let i = 0, count = node.childCount;
|
|
i < count && guids.length;
|
|
++i
|
|
) {
|
|
let childNode = node.getChild(i);
|
|
let 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() {}
|
|
|
|
disconnectedCallback() {
|
|
window.removeEventListener("unload", this.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();
|
|
this.view = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define("places-tree", MozPlacesTree, {
|
|
extends: "tree",
|
|
});
|
|
}
|