tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

places-tree.js (27045B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /* import-globals-from controller.js */
      6 /* import-globals-from treeView.js */
      7 
      8 // This is loaded into all XUL windows. Wrap in a block to prevent
      9 // leaking to window scope.
     10 {
     11  /**
     12   * Custom element definition for the places tree.
     13   */
     14  class MozPlacesTree extends customElements.get("tree") {
     15    constructor() {
     16      super();
     17 
     18      this.addEventListener("focus", () => {
     19        this._cachedInsertionPoint = undefined;
     20        // See select handler. We need the sidebar's places commandset to be
     21        // updated as well
     22        document.commandDispatcher.updateCommands("focus");
     23      });
     24 
     25      this.addEventListener("select", () => {
     26        this._cachedInsertionPoint = undefined;
     27 
     28        // This additional complexity is here for the sidebars
     29        var win = window;
     30        while (true) {
     31          win.document.commandDispatcher.updateCommands("focus");
     32          if (win == window.top) {
     33            break;
     34          }
     35 
     36          win = win.parent;
     37        }
     38      });
     39 
     40      this.addEventListener("dragstart", event => {
     41        if (event.target.localName != "treechildren") {
     42          return;
     43        }
     44 
     45        if (this.disableUserActions) {
     46          event.preventDefault();
     47          event.stopPropagation();
     48          return;
     49        }
     50 
     51        let nodes = this.selectedNodes;
     52        for (let i = 0; i < nodes.length; i++) {
     53          let node = nodes[i];
     54 
     55          // Disallow dragging the root node of a tree.
     56          if (!node.parent) {
     57            event.preventDefault();
     58            event.stopPropagation();
     59            return;
     60          }
     61 
     62          // If this node is child of a readonly container or cannot be moved,
     63          // we must force a copy.
     64          if (!this.controller.canMoveNode(node)) {
     65            event.dataTransfer.effectAllowed = "copyLink";
     66            break;
     67          }
     68        }
     69 
     70        // Indicate to drag and drop listeners
     71        // whether or not this was the start of the drag
     72        this._isDragSource = true;
     73 
     74        this._controller.setDataTransfer(event);
     75        event.stopPropagation();
     76      });
     77 
     78      this.addEventListener("dragover", event => {
     79        if (event.target.localName != "treechildren") {
     80          return;
     81        }
     82 
     83        let cell = this.getCellAt(event.clientX, event.clientY);
     84        let node =
     85          cell.row != -1
     86            ? this.view.nodeForTreeIndex(cell.row)
     87            : this.result.root;
     88        // cache the dropTarget for the view
     89        PlacesControllerDragHelper.currentDropTarget = node;
     90 
     91        // We have to calculate the orientation since view.canDrop will use
     92        // it and we want to be consistent with the dropfeedback.
     93        let rowHeight = this.rowHeight;
     94        let eventY =
     95          event.clientY -
     96          this.treeBody.getBoundingClientRect().y -
     97          rowHeight * (cell.row - this.getFirstVisibleRow());
     98 
     99        let orientation = Ci.nsITreeView.DROP_BEFORE;
    100 
    101        if (cell.row == -1) {
    102          // If the row is not valid we try to insert inside the resultNode.
    103          orientation = Ci.nsITreeView.DROP_ON;
    104        } else if (
    105          PlacesUtils.nodeIsContainer(node) &&
    106          eventY > rowHeight * 0.75
    107        ) {
    108          // If we are below the 75% of a container the treeview we try
    109          // to drop after the node.
    110          orientation = Ci.nsITreeView.DROP_AFTER;
    111        } else if (
    112          PlacesUtils.nodeIsContainer(node) &&
    113          eventY > rowHeight * 0.25
    114        ) {
    115          // If we are below the 25% of a container the treeview we try
    116          // to drop inside the node.
    117          orientation = Ci.nsITreeView.DROP_ON;
    118        }
    119 
    120        if (!this.view.canDrop(cell.row, orientation, event.dataTransfer)) {
    121          return;
    122        }
    123 
    124        event.preventDefault();
    125        event.stopPropagation();
    126      });
    127 
    128      this.addEventListener("dragend", () => {
    129        this._isDragSource = false;
    130        PlacesControllerDragHelper.currentDropTarget = null;
    131      });
    132    }
    133 
    134    connectedCallback() {
    135      if (this.delayConnectedCallback()) {
    136        return;
    137      }
    138      super.connectedCallback();
    139      this._contextMenuShown = false;
    140 
    141      this._active = true;
    142 
    143      // Force an initial build.
    144      if (this.place) {
    145        // eslint-disable-next-line no-self-assign
    146        this.place = this.place;
    147      }
    148 
    149      window.addEventListener("unload", this.disconnectedCallback);
    150    }
    151 
    152    get controller() {
    153      return this._controller;
    154    }
    155 
    156    set disableUserActions(val) {
    157      if (val) {
    158        this.setAttribute("disableUserActions", "true");
    159      } else {
    160        this.removeAttribute("disableUserActions");
    161      }
    162    }
    163 
    164    get disableUserActions() {
    165      return this.getAttribute("disableUserActions") == "true";
    166    }
    167    /**
    168     * overriding
    169     *
    170     * @param {PlacesTreeView} val
    171     *   The parent view
    172     */
    173    set view(val) {
    174      // We save the view so that we can avoid expensive get calls when
    175      // we need to get the view again.
    176      this._view = val;
    177      Object.getOwnPropertyDescriptor(
    178        // eslint-disable-next-line no-undef
    179        XULTreeElement.prototype,
    180        "view"
    181      ).set.call(this, val);
    182    }
    183 
    184    get view() {
    185      return this._view;
    186    }
    187 
    188    get associatedElement() {
    189      return this;
    190    }
    191 
    192    set flatList(val) {
    193      if (this.flatList != val) {
    194        this.setAttribute("flatList", val);
    195        // reload with the last place set
    196        if (this.place) {
    197          // eslint-disable-next-line no-self-assign
    198          this.place = this.place;
    199        }
    200      }
    201    }
    202 
    203    get flatList() {
    204      return this.getAttribute("flatList") == "true";
    205    }
    206 
    207    get result() {
    208      try {
    209        return this.view.QueryInterface(Ci.nsINavHistoryResultObserver).result;
    210      } catch (e) {
    211        return null;
    212      }
    213    }
    214 
    215    set place(val) {
    216      this.setAttribute("place", val);
    217 
    218      let query = {},
    219        options = {};
    220      PlacesUtils.history.queryStringToQuery(val, query, options);
    221      this.load(query.value, options.value);
    222    }
    223 
    224    get place() {
    225      return this.getAttribute("place");
    226    }
    227 
    228    get selectedCount() {
    229      return this.view?.selection?.count || 0;
    230    }
    231 
    232    get hasSelection() {
    233      return this.selectedCount >= 1;
    234    }
    235 
    236    get selectedNodes() {
    237      let nodes = [];
    238      if (!this.hasSelection) {
    239        return nodes;
    240      }
    241 
    242      let selection = this.view.selection;
    243      let rc = selection.getRangeCount();
    244      let resultview = this.view;
    245      for (let i = 0; i < rc; ++i) {
    246        let min = {},
    247          max = {};
    248        selection.getRangeAt(i, min, max);
    249        for (let j = min.value; j <= max.value; ++j) {
    250          nodes.push(resultview.nodeForTreeIndex(j));
    251        }
    252      }
    253      return nodes;
    254    }
    255 
    256    get removableSelectionRanges() {
    257      // This property exists in addition to selectedNodes because it
    258      // encodes selection ranges (which only occur in list views) into
    259      // the return value. For each removed range, the index at which items
    260      // will be re-inserted upon the remove transaction being performed is
    261      // the first index of the range, so that the view updates correctly.
    262      //
    263      // For example, if we remove rows 2,3,4 and 7,8 from a list, when we
    264      // undo that operation, if we insert what was at row 3 at row 3 again,
    265      // it will show up _after_ the item that was at row 5. So we need to
    266      // insert all items at row 2, and the tree view will update correctly.
    267      //
    268      // Also, this function collapses the selection to remove redundant
    269      // data, e.g. when deleting this selection:
    270      //
    271      //      http://www.foo.com/
    272      //  (-) Some Folder
    273      //        http://www.bar.com/
    274      //
    275      // ... returning http://www.bar.com/ as part of the selection is
    276      // redundant because it is implied by removing "Some Folder". We
    277      // filter out all such redundancies since some partial amount of
    278      // the folder's children may be selected.
    279      //
    280      let nodes = [];
    281      if (!this.hasSelection) {
    282        return nodes;
    283      }
    284 
    285      var selection = this.view.selection;
    286      var rc = selection.getRangeCount();
    287      var resultview = this.view;
    288      // This list is kept independently of the range selected (i.e. OUTSIDE
    289      // the for loop) since the row index of a container is unique for the
    290      // entire view, and we could have some really wacky selection and we
    291      // don't want to blow up.
    292      var containers = {};
    293      for (var i = 0; i < rc; ++i) {
    294        var range = [];
    295        var min = {},
    296          max = {};
    297        selection.getRangeAt(i, min, max);
    298 
    299        for (var j = min.value; j <= max.value; ++j) {
    300          if (this.view.isContainer(j)) {
    301            containers[j] = true;
    302          }
    303          if (!(this.view.getParentIndex(j) in containers)) {
    304            range.push(resultview.nodeForTreeIndex(j));
    305          }
    306        }
    307        nodes.push(range);
    308      }
    309      return nodes;
    310    }
    311 
    312    get draggableSelection() {
    313      return this.selectedNodes;
    314    }
    315 
    316    get selectedNode() {
    317      if (this.selectedCount != 1) {
    318        return null;
    319      }
    320 
    321      var selection = this.view.selection;
    322      var min = {},
    323        max = {};
    324      selection.getRangeAt(0, min, max);
    325 
    326      return this.view.nodeForTreeIndex(min.value);
    327    }
    328 
    329    get singleClickOpens() {
    330      return this.getAttribute("singleclickopens") == "true";
    331    }
    332 
    333    get insertionPoint() {
    334      // invalidated on selection and focus changes
    335      if (this._cachedInsertionPoint !== undefined) {
    336        return this._cachedInsertionPoint;
    337      }
    338 
    339      // there is no insertion point for history queries
    340      // so bail out now and save a lot of work when updating commands
    341      var resultNode = this.result.root;
    342      if (
    343        PlacesUtils.nodeIsQuery(resultNode) &&
    344        PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
    345          Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY
    346      ) {
    347        return (this._cachedInsertionPoint = null);
    348      }
    349 
    350      var orientation = Ci.nsITreeView.DROP_BEFORE;
    351      // If there is no selection, insert at the end of the container.
    352      if (!this.hasSelection) {
    353        var index = this.view.rowCount - 1;
    354        this._cachedInsertionPoint = this._getInsertionPoint(
    355          index,
    356          orientation
    357        );
    358        return this._cachedInsertionPoint;
    359      }
    360 
    361      // This is a two-part process. The first part is determining the drop
    362      // orientation.
    363      // * The default orientation is to drop _before_ the selected item.
    364      // * If the selected item is a container, the default orientation
    365      //   is to drop _into_ that container.
    366      //
    367      // Warning: It may be tempting to use tree indexes in this code, but
    368      //          you must not, since the tree is nested and as your tree
    369      //          index may change when folders before you are opened and
    370      //          closed. You must convert your tree index to a node, and
    371      //          then use getChildIndex to find your absolute index in
    372      //          the parent container instead.
    373      //
    374      var resultView = this.view;
    375      var selection = resultView.selection;
    376      var rc = selection.getRangeCount();
    377      var min = {},
    378        max = {};
    379      selection.getRangeAt(rc - 1, min, max);
    380 
    381      // If the sole selection is a container, and we are not in
    382      // a flatlist, insert into it.
    383      // Note that this only applies to _single_ selections,
    384      // if the last element within a multi-selection is a
    385      // container, insert _adjacent_ to the selection.
    386      //
    387      // If the sole selection is the bookmarks toolbar folder, we insert
    388      // into it even if it is not opened
    389      if (
    390        selection.count == 1 &&
    391        resultView.isContainer(max.value) &&
    392        !this.flatList
    393      ) {
    394        orientation = Ci.nsITreeView.DROP_ON;
    395      }
    396 
    397      this._cachedInsertionPoint = this._getInsertionPoint(
    398        max.value,
    399        orientation
    400      );
    401      return this._cachedInsertionPoint;
    402    }
    403 
    404    get isDragSource() {
    405      return this._isDragSource;
    406    }
    407 
    408    get ownerWindow() {
    409      return window;
    410    }
    411 
    412    set active(val) {
    413      this._active = val;
    414    }
    415 
    416    get active() {
    417      return this._active;
    418    }
    419 
    420    applyFilter(filterString, folderRestrict, includeHidden) {
    421      // preserve grouping
    422      var queryNode = PlacesUtils.asQuery(this.result.root);
    423      var options = queryNode.queryOptions.clone();
    424 
    425      // Make sure we're getting uri results.
    426      // We do not yet support searching into grouped queries or into
    427      // tag containers, so we must fall to the default case.
    428      if (
    429        PlacesUtils.nodeIsHistoryContainer(queryNode) ||
    430        PlacesUtils.nodeIsTagQuery(queryNode) ||
    431        options.resultType == options.RESULTS_AS_TAGS_ROOT ||
    432        options.resultType == options.RESULTS_AS_ROOTS_QUERY
    433      ) {
    434        options.resultType = options.RESULTS_AS_URI;
    435      }
    436 
    437      var query = PlacesUtils.history.getNewQuery();
    438      query.searchTerms = filterString;
    439 
    440      if (folderRestrict) {
    441        query.setParents(folderRestrict);
    442        options.queryType = options.QUERY_TYPE_BOOKMARKS;
    443        Glean.sidebar.search.bookmarks.add(1);
    444      }
    445 
    446      options.includeHidden = !!includeHidden;
    447 
    448      this.load(query, options);
    449    }
    450 
    451    load(query, options) {
    452      let result = PlacesUtils.history.executeQuery(query, options);
    453 
    454      if (!this._controller) {
    455        this._controller = new PlacesController(this);
    456        this._controller.disableUserActions = this.disableUserActions;
    457        this.controllers.appendController(this._controller);
    458      }
    459 
    460      let treeView = new PlacesTreeView(this);
    461 
    462      // Observer removal is done within the view itself.  When the tree
    463      // goes away, view.setTree(null) is called, which then
    464      // calls removeObserver.
    465      result.addObserver(treeView);
    466      this.view = treeView;
    467 
    468      if (
    469        this.getAttribute("selectfirstnode") == "true" &&
    470        treeView.rowCount > 0
    471      ) {
    472        treeView.selection.select(0);
    473      }
    474 
    475      this._cachedInsertionPoint = undefined;
    476    }
    477 
    478    /**
    479     * Causes a particular node represented by the specified placeURI to be
    480     * selected in the tree. All containers above the node in the hierarchy
    481     * will be opened, so that the node is visible.
    482     *
    483     * @param {string} placeURI
    484     *   The URI that should be selected
    485     */
    486    selectPlaceURI(placeURI) {
    487      // Do nothing if a node matching the given uri is already selected
    488      if (this.hasSelection && this.selectedNode.uri == placeURI) {
    489        return;
    490      }
    491 
    492      function findNode(container, nodesURIChecked) {
    493        var containerURI = container.uri;
    494        if (containerURI == placeURI) {
    495          return container;
    496        }
    497        if (nodesURIChecked.includes(containerURI)) {
    498          return null;
    499        }
    500 
    501        // never check the contents of the same query
    502        nodesURIChecked.push(containerURI);
    503 
    504        var wasOpen = container.containerOpen;
    505        if (!wasOpen) {
    506          container.containerOpen = true;
    507        }
    508        for (let i = 0, count = container.childCount; i < count; ++i) {
    509          var child = container.getChild(i);
    510          var childURI = child.uri;
    511          if (childURI == placeURI) {
    512            return child;
    513          } else if (PlacesUtils.nodeIsContainer(child)) {
    514            var nested = findNode(
    515              PlacesUtils.asContainer(child),
    516              nodesURIChecked
    517            );
    518            if (nested) {
    519              return nested;
    520            }
    521          }
    522        }
    523 
    524        if (!wasOpen) {
    525          container.containerOpen = false;
    526        }
    527 
    528        return null;
    529      }
    530 
    531      var container = this.result.root;
    532      console.assert(container, "No result, cannot select place URI!");
    533      if (!container) {
    534        return;
    535      }
    536 
    537      var child = findNode(container, []);
    538      if (child) {
    539        this.selectNode(child);
    540      } else {
    541        // If the specified child could not be located, clear the selection
    542        var selection = this.view.selection;
    543        selection.clearSelection();
    544      }
    545    }
    546 
    547    /**
    548     * Causes a particular node to be selected in the tree, resulting in all
    549     * containers above the node in the hierarchy to be opened, so that the
    550     * node is visible.
    551     *
    552     * @param {object} node
    553     *   The node that should be selected
    554     */
    555    selectNode(node) {
    556      var view = this.view;
    557 
    558      var parent = node.parent;
    559      if (parent && !parent.containerOpen) {
    560        // Build a list of all of the nodes that are the parent of this one
    561        // in the result.
    562        var parents = [];
    563        var root = this.result.root;
    564        while (parent && parent != root) {
    565          parents.push(parent);
    566          parent = parent.parent;
    567        }
    568 
    569        // Walk the list backwards (opening from the root of the hierarchy)
    570        // opening each folder as we go.
    571        for (var i = parents.length - 1; i >= 0; --i) {
    572          let index = view.treeIndexForNode(parents[i]);
    573          if (
    574            index != -1 &&
    575            view.isContainer(index) &&
    576            !view.isContainerOpen(index)
    577          ) {
    578            view.toggleOpenState(index);
    579          }
    580        }
    581        // Select the specified node...
    582      }
    583 
    584      let index = view.treeIndexForNode(node);
    585      if (index == -1) {
    586        return;
    587      }
    588 
    589      view.selection.select(index);
    590      // ... and ensure it's visible, not scrolled off somewhere.
    591      this.ensureRowIsVisible(index);
    592    }
    593 
    594    toggleCutNode(aNode, aValue) {
    595      this.view.toggleCutNode(aNode, aValue);
    596    }
    597 
    598    _getInsertionPoint(index, orientation) {
    599      var result = this.result;
    600      var resultview = this.view;
    601      var container = result.root;
    602      var dropNearNode = null;
    603      console.assert(container, "null container");
    604      // When there's no selection, assume the container is the container
    605      // the view is populated from (i.e. the result's itemId).
    606      if (index != -1) {
    607        var lastSelected = resultview.nodeForTreeIndex(index);
    608        if (
    609          resultview.isContainer(index) &&
    610          orientation == Ci.nsITreeView.DROP_ON
    611        ) {
    612          // If the last selected item is an open container, append _into_
    613          // it, rather than insert adjacent to it.
    614          container = lastSelected;
    615          index = -1;
    616        } else if (
    617          lastSelected.containerOpen &&
    618          orientation == Ci.nsITreeView.DROP_AFTER &&
    619          lastSelected.hasChildren
    620        ) {
    621          // If the last selected item is an open container and the user is
    622          // trying to drag into it as a first item, really insert into it.
    623          container = lastSelected;
    624          orientation = Ci.nsITreeView.DROP_ON;
    625          index = 0;
    626        } else {
    627          // Use the last-selected node's container.
    628          container = lastSelected.parent;
    629 
    630          // See comment in the treeView.js's copy of this method
    631          if (!container || !container.containerOpen) {
    632            return null;
    633          }
    634 
    635          // Avoid the potentially expensive call to getChildIndex
    636          // if we know this container doesn't allow insertion
    637          if (this.controller.disallowInsertion(container)) {
    638            return null;
    639          }
    640 
    641          var queryOptions = PlacesUtils.asQuery(result.root).queryOptions;
    642          if (
    643            queryOptions.sortingMode !=
    644            Ci.nsINavHistoryQueryOptions.SORT_BY_NONE
    645          ) {
    646            // If we are within a sorted view, insert at the end
    647            index = -1;
    648          } else if (queryOptions.excludeItems || queryOptions.excludeQueries) {
    649            // Some item may be invisible, insert near last selected one.
    650            // We don't replace index here to avoid requests to the db,
    651            // instead it will be calculated later by the controller.
    652            index = -1;
    653            dropNearNode = lastSelected;
    654          } else {
    655            var lsi = container.getChildIndex(lastSelected);
    656            index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
    657          }
    658        }
    659      }
    660 
    661      if (this.controller.disallowInsertion(container)) {
    662        return null;
    663      }
    664 
    665      let tagName = PlacesUtils.nodeIsTagQuery(container)
    666        ? PlacesUtils.asQuery(container).query.tags[0]
    667        : null;
    668 
    669      return new PlacesInsertionPoint({
    670        parentGuid: PlacesUtils.getConcreteItemGuid(container),
    671        index,
    672        orientation,
    673        tagName,
    674        dropNearNode,
    675      });
    676    }
    677 
    678    selectAll() {
    679      this.view.selection.selectAll();
    680    }
    681 
    682    /**
    683     * This method will select the first node in the tree that matches
    684     * each given item guid. It will open any folder nodes that it needs
    685     * to in order to show the selected items.
    686     *
    687     * @param {Array} aGuids
    688     *   Guids to select.
    689     * @param {boolean} aOpenContainers
    690     *   Whether or not to open containers.
    691     */
    692    selectItems(aGuids, aOpenContainers) {
    693      // Never open containers in flat lists.
    694      if (this.flatList) {
    695        aOpenContainers = false;
    696      }
    697      // By default, we do search and select within containers which were
    698      // closed (note that containers in which nodes were not found are
    699      // closed).
    700      if (aOpenContainers === undefined) {
    701        aOpenContainers = true;
    702      }
    703 
    704      var guids = aGuids; // don't manipulate the caller's array
    705 
    706      // Array of nodes found by findNodes which are to be selected
    707      var nodes = [];
    708 
    709      // Array of nodes found by findNodes which should be opened
    710      var nodesToOpen = [];
    711 
    712      // A set of GUIDs of container-nodes that were previously searched,
    713      // and thus shouldn't be searched again. This is empty at the initial
    714      // start of the recursion and gets filled in as the recursion
    715      // progresses.
    716      var checkedGuidsSet = new Set();
    717 
    718      /**
    719       * Recursively search through a node's children for items
    720       * with the given GUIDs. When a matching item is found, remove its GUID
    721       * from the GUIDs array, and add the found node to the nodes dictionary.
    722       *
    723       * NOTE: This method will leave open any node that had matching items
    724       * in its subtree.
    725       *
    726       * @param {object} node
    727       *   The node to search.
    728       * @returns {boolean}
    729       *   Returns true if at least one item was found.
    730       */
    731      function findNodes(node) {
    732        var foundOne = false;
    733        // See if node matches an ID we wanted; add to results.
    734        // For simple folder queries, check both itemId and the concrete
    735        // item id.
    736        var index = guids.indexOf(node.bookmarkGuid);
    737        if (index == -1) {
    738          let concreteGuid = PlacesUtils.getConcreteItemGuid(node);
    739          if (concreteGuid != node.bookmarkGuid) {
    740            index = guids.indexOf(concreteGuid);
    741          }
    742        }
    743 
    744        if (index != -1) {
    745          nodes.push(node);
    746          foundOne = true;
    747          guids.splice(index, 1);
    748        }
    749 
    750        var concreteGuid = PlacesUtils.getConcreteItemGuid(node);
    751        if (
    752          !guids.length ||
    753          !PlacesUtils.nodeIsContainer(node) ||
    754          checkedGuidsSet.has(concreteGuid)
    755        ) {
    756          return foundOne;
    757        }
    758 
    759        // Only follow a query if it has been been explicitly opened by the
    760        // caller. We support the "AllBookmarks" case to allow callers to
    761        // specify just the top-level bookmark folders.
    762        let shouldOpen =
    763          aOpenContainers &&
    764          (PlacesUtils.nodeIsFolderOrShortcut(node) ||
    765            (PlacesUtils.nodeIsQuery(node) &&
    766              node.bookmarkGuid == PlacesUIUtils.virtualAllBookmarksGuid));
    767 
    768        PlacesUtils.asContainer(node);
    769        if (!node.containerOpen && !shouldOpen) {
    770          return foundOne;
    771        }
    772 
    773        checkedGuidsSet.add(concreteGuid);
    774 
    775        // Remember the beginning state so that we can re-close
    776        // this node if we don't find any additional results here.
    777        let previousOpenness = node.containerOpen;
    778        node.containerOpen = true;
    779        for (
    780          let i = 0, count = node.childCount;
    781          i < count && guids.length;
    782          ++i
    783        ) {
    784          let childNode = node.getChild(i);
    785          let found = findNodes(childNode);
    786          if (!foundOne) {
    787            foundOne = found;
    788          }
    789        }
    790 
    791        // If we didn't find any additional matches in this node's
    792        // subtree, revert the node to its previous openness.
    793        if (foundOne) {
    794          nodesToOpen.unshift(node);
    795        }
    796        node.containerOpen = previousOpenness;
    797        return foundOne;
    798      }
    799 
    800      // Disable notifications while looking for nodes.
    801      let result = this.result;
    802      let didSuppressNotifications = result.suppressNotifications;
    803      if (!didSuppressNotifications) {
    804        result.suppressNotifications = true;
    805      }
    806      try {
    807        findNodes(this.result.root);
    808      } finally {
    809        if (!didSuppressNotifications) {
    810          result.suppressNotifications = false;
    811        }
    812      }
    813 
    814      // For all the nodes we've found, highlight the corresponding
    815      // index in the tree.
    816      var resultview = this.view;
    817      var selection = this.view.selection;
    818      selection.selectEventsSuppressed = true;
    819      selection.clearSelection();
    820      // Open nodes containing found items
    821      for (let i = 0; i < nodesToOpen.length; i++) {
    822        nodesToOpen[i].containerOpen = true;
    823      }
    824      let firstValidTreeIndex = -1;
    825      for (let i = 0; i < nodes.length; i++) {
    826        var index = resultview.treeIndexForNode(nodes[i]);
    827        if (index == -1) {
    828          continue;
    829        }
    830        if (firstValidTreeIndex < 0 && index >= 0) {
    831          firstValidTreeIndex = index;
    832        }
    833        selection.rangedSelect(index, index, true);
    834      }
    835      selection.selectEventsSuppressed = false;
    836 
    837      // Bring the first valid node into view if necessary
    838      if (firstValidTreeIndex >= 0) {
    839        this.ensureRowIsVisible(firstValidTreeIndex);
    840      }
    841    }
    842 
    843    buildContextMenu(aPopup) {
    844      this._contextMenuShown = true;
    845      return this.controller.buildContextMenu(aPopup);
    846    }
    847 
    848    destroyContextMenu() {}
    849 
    850    disconnectedCallback() {
    851      window.removeEventListener("unload", this.disconnectedCallback);
    852      // Unregister the controller before unlinking the view, otherwise it
    853      // may still try to update commands on a view with a null result.
    854      if (this._controller) {
    855        this._controller.terminate();
    856        this.controllers.removeController(this._controller);
    857      }
    858 
    859      if (this.view) {
    860        this.view.uninit();
    861        this.view = null;
    862      }
    863    }
    864  }
    865 
    866  customElements.define("places-tree", MozPlacesTree, {
    867    extends: "tree",
    868  });
    869 }