tor-browser

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

treeView.js (59966B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /* import-globals-from controller.js */
      6 
      7 /**
      8 * This returns the key for any node/details object.
      9 *
     10 * @param {object} nodeOrDetails
     11 *        A node, or an object containing the following properties:
     12 *        - uri
     13 *        - time
     14 *        - itemId
     15 *        In case any of these is missing, an empty string will be returned. This is
     16 *        to facilitate easy delete statements which occur due to assignment to items in `this._rows`,
     17 *        since the item we are deleting may be undefined in the array.
     18 *
     19 * @returns {string} key or empty string.
     20 */
     21 function makeNodeDetailsKey(nodeOrDetails) {
     22  if (
     23    nodeOrDetails &&
     24    typeof nodeOrDetails === "object" &&
     25    "uri" in nodeOrDetails &&
     26    "time" in nodeOrDetails &&
     27    "itemId" in nodeOrDetails
     28  ) {
     29    return `${nodeOrDetails.uri}*${nodeOrDetails.time}*${nodeOrDetails.itemId}`;
     30  }
     31  return "";
     32 }
     33 
     34 function PlacesTreeView(aContainer) {
     35  this._tree = null;
     36  this._result = null;
     37  this._selection = null;
     38  this._rootNode = null;
     39  this._rows = [];
     40  this._flatList = aContainer.flatList;
     41  this._nodeDetails = new Map();
     42  this._element = aContainer;
     43  this._controller = aContainer._controller;
     44 }
     45 
     46 PlacesTreeView.prototype = {
     47  get wrappedJSObject() {
     48    return this;
     49  },
     50 
     51  QueryInterface: ChromeUtils.generateQI([
     52    "nsITreeView",
     53    "nsINavHistoryResultObserver",
     54    "nsISupportsWeakReference",
     55  ]),
     56 
     57  /**
     58   * This is called once both the result and the tree are set.
     59   */
     60  _finishInit: function PTV__finishInit() {
     61    let selection = this.selection;
     62    if (selection) {
     63      selection.selectEventsSuppressed = true;
     64    }
     65 
     66    if (!this._rootNode.containerOpen) {
     67      // This triggers containerStateChanged which then builds the visible
     68      // section.
     69      this._rootNode.containerOpen = true;
     70    } else {
     71      this.invalidateContainer(this._rootNode);
     72    }
     73 
     74    // "Activate" the sorting column and update commands.
     75    this.sortingChanged(this._result.sortingMode);
     76 
     77    if (selection) {
     78      selection.selectEventsSuppressed = false;
     79    }
     80  },
     81 
     82  uninit() {
     83    if (this._editingObservers) {
     84      for (let observer of this._editingObservers.values()) {
     85        observer.disconnect();
     86      }
     87      delete this._editingObservers;
     88    }
     89    // Break the reference cycle between the PlacesTreeView and result.
     90    if (this._result) {
     91      this._result.removeObserver(this);
     92    }
     93  },
     94 
     95  /**
     96   * Plain Container: container result nodes which may never include sub
     97   * hierarchies.
     98   *
     99   * When the rows array is constructed, we don't set the children of plain
    100   * containers.  Instead, we keep placeholders for these children.  We then
    101   * build these children lazily as the tree asks us for information about each
    102   * row.  Luckily, the tree doesn't ask about rows outside the visible area.
    103   *
    104   * It's guaranteed that all containers are listed in the rows
    105   * elements array.  It's also guaranteed that separators (if they're not
    106   * filtered, see below) are listed in the visible elements array, because
    107   * bookmark folders are never built lazily, as described above.
    108   *
    109   * @see {@link PlacesTreeView._getNodeForRow} and
    110   * {@link PlacesTreeView._getRowForNode} for the actual magic.
    111   *
    112   * @param {object} aContainer
    113   *        A container result node.
    114   *
    115   * @returns {boolean} true if aContainer is a plain container, false otherwise.
    116   */
    117  _isPlainContainer: function PTV__isPlainContainer(aContainer) {
    118    // We don't know enough about non-query containers.
    119    if (!(aContainer instanceof Ci.nsINavHistoryQueryResultNode)) {
    120      return false;
    121    }
    122 
    123    switch (aContainer.queryOptions.resultType) {
    124      case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
    125      case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
    126      case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
    127      case Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT:
    128      case Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY:
    129      case Ci.nsINavHistoryQueryOptions.RESULTS_AS_LEFT_PANE_QUERY:
    130        return false;
    131    }
    132 
    133    // If it's a folder, it's not a plain container.
    134    let nodeType = aContainer.type;
    135    return (
    136      nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER &&
    137      nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT
    138    );
    139  },
    140 
    141  /**
    142   * Gets the row number for a given node.  Assumes that the given node is
    143   * visible (i.e. it's not an obsolete node).
    144   *
    145   * If aParentRow and aNodeIndex are passed and parent is a plain
    146   * container, this method will just return a calculated row value, without
    147   * making assumptions on existence of the node at that position.
    148   *
    149   * @param {object} aNode
    150   *        A result node.  Do not pass an obsolete node, or any
    151   *        node which isn't supposed to be in the tree (e.g. separators in
    152   *        sorted trees).
    153   * @param {boolean} [aForceBuild]
    154   *        See {@link _isPlainContainer}.
    155   *        If true, the row will be computed even if the node still isn't set
    156   *        in our rows array.
    157   * @param {object} [aParentRow]
    158   *        The row of aNode's parent. Ignored for the root node.
    159   * @param {number} [aNodeIndex]
    160   *        The index of aNode in its parent.  Only used if aParentRow is
    161   *        set too.
    162   *
    163   * @throws if aNode is invisible.
    164   * @returns {object} aNode's row if it's in the rows list or if aForceBuild is set, -1
    165   *         otherwise.
    166   */
    167  _getRowForNode: function PTV__getRowForNode(
    168    aNode,
    169    aForceBuild,
    170    aParentRow,
    171    aNodeIndex
    172  ) {
    173    if (aNode == this._rootNode) {
    174      throw new Error("The root node is never visible");
    175    }
    176 
    177    // A node is removed form the view either if it has no parent or if its
    178    // root-ancestor is not the root node (in which case that's the node
    179    // for which nodeRemoved was called).
    180    let ancestors = Array.from(PlacesUtils.nodeAncestors(aNode));
    181    if (
    182      !ancestors.length ||
    183      ancestors[ancestors.length - 1] != this._rootNode
    184    ) {
    185      throw new Error("Removed node passed to _getRowForNode");
    186    }
    187 
    188    // Ensure that the entire chain is open, otherwise that node is invisible.
    189    for (let ancestor of ancestors) {
    190      if (!ancestor.containerOpen) {
    191        throw new Error("Invisible node passed to _getRowForNode");
    192      }
    193    }
    194 
    195    // Non-plain containers are initially built with their contents.
    196    let parent = aNode.parent;
    197    let parentIsPlain = this._isPlainContainer(parent);
    198    if (!parentIsPlain) {
    199      if (parent == this._rootNode) {
    200        return this._rows.indexOf(aNode);
    201      }
    202 
    203      return this._rows.indexOf(aNode, aParentRow);
    204    }
    205 
    206    let row = -1;
    207    let useNodeIndex = typeof aNodeIndex == "number";
    208    if (parent == this._rootNode) {
    209      row = useNodeIndex ? aNodeIndex : this._rootNode.getChildIndex(aNode);
    210    } else if (useNodeIndex && typeof aParentRow == "number") {
    211      // If we have both the row of the parent node, and the node's index, we
    212      // can avoid searching the rows array if the parent is a plain container.
    213      row = aParentRow + aNodeIndex + 1;
    214    } else {
    215      // Look for the node in the nodes array.  Start the search at the parent
    216      // row.  If the parent row isn't passed, we'll pass undefined to indexOf,
    217      // which is fine.
    218      row = this._rows.indexOf(aNode, aParentRow);
    219      if (row == -1 && aForceBuild) {
    220        let parentRow =
    221          typeof aParentRow == "number"
    222            ? aParentRow
    223            : this._getRowForNode(parent);
    224        row = parentRow + parent.getChildIndex(aNode) + 1;
    225      }
    226    }
    227 
    228    if (row != -1) {
    229      this._nodeDetails.delete(makeNodeDetailsKey(this._rows[row]));
    230      this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode);
    231      this._rows[row] = aNode;
    232    }
    233 
    234    return row;
    235  },
    236 
    237  /**
    238   * Given a row, finds and returns the parent details of the associated node.
    239   *
    240   * @param {number} aChildRow
    241   *        Row number.
    242   * @returns {Array} [parentNode, parentRow]
    243   */
    244  _getParentByChildRow: function PTV__getParentByChildRow(aChildRow) {
    245    let node = this._getNodeForRow(aChildRow);
    246    let parent = node === null ? this._rootNode : node.parent;
    247 
    248    // The root node is never visible
    249    if (parent == this._rootNode) {
    250      return [this._rootNode, -1];
    251    }
    252 
    253    let parentRow = this._rows.lastIndexOf(parent, aChildRow - 1);
    254    return [parent, parentRow];
    255  },
    256 
    257  /**
    258   * Gets the node at a given row.
    259   *
    260   * @param {number} aRow
    261   *   The index of the row to set
    262   * @returns {object}
    263   */
    264  _getNodeForRow: function PTV__getNodeForRow(aRow) {
    265    if (aRow < 0) {
    266      return null;
    267    }
    268 
    269    let node = this._rows[aRow];
    270    if (node !== undefined) {
    271      return node;
    272    }
    273 
    274    // Find the nearest node.
    275    let rowNode, row;
    276    for (let i = aRow - 1; i >= 0 && rowNode === undefined; i--) {
    277      rowNode = this._rows[i];
    278      row = i;
    279    }
    280 
    281    // If there's no container prior to the given row, it's a child of
    282    // the root node (remember: all containers are listed in the rows array).
    283    if (!rowNode) {
    284      let newNode = this._rootNode.getChild(aRow);
    285      this._nodeDetails.delete(makeNodeDetailsKey(this._rows[aRow]));
    286      this._nodeDetails.set(makeNodeDetailsKey(newNode), newNode);
    287      return (this._rows[aRow] = newNode);
    288    }
    289 
    290    // Unset elements may exist only in plain containers.  Thus, if the nearest
    291    // node is a container, it's the row's parent, otherwise, it's a sibling.
    292    if (rowNode instanceof Ci.nsINavHistoryContainerResultNode) {
    293      let newNode = rowNode.getChild(aRow - row - 1);
    294      this._nodeDetails.delete(makeNodeDetailsKey(this._rows[aRow]));
    295      this._nodeDetails.set(makeNodeDetailsKey(newNode), newNode);
    296      return (this._rows[aRow] = newNode);
    297    }
    298 
    299    let [parent, parentRow] = this._getParentByChildRow(row);
    300    let newNode = parent.getChild(aRow - parentRow - 1);
    301    this._nodeDetails.delete(makeNodeDetailsKey(this._rows[aRow]));
    302    this._nodeDetails.set(makeNodeDetailsKey(newNode), newNode);
    303    return (this._rows[aRow] = newNode);
    304  },
    305 
    306  /**
    307   * This takes a container and recursively appends our rows array per its
    308   * contents.  Assumes that the rows arrays has no rows for the given
    309   * container.
    310   *
    311   * @param {object} aContainer
    312   *        A container result node.
    313   * @param {object} aFirstChildRow
    314   *        The first row at which nodes may be inserted to the row array.
    315   *        In other words, that's aContainer's row + 1.
    316   * @param {Array} aToOpen
    317   *        An array of containers to open once the build is done (out param)
    318   *
    319   * @returns {number} the number of rows which were inserted.
    320   */
    321  _buildVisibleSection: function PTV__buildVisibleSection(
    322    aContainer,
    323    aFirstChildRow,
    324    aToOpen
    325  ) {
    326    // There's nothing to do if the container is closed.
    327    if (!aContainer.containerOpen) {
    328      return 0;
    329    }
    330 
    331    // Inserting the new elements into the rows array in one shot (by
    332    // Array.prototype.concat) is faster than resizing the array (by splice) on each loop
    333    // iteration.
    334    let cc = aContainer.childCount;
    335    let newElements = new Array(cc);
    336    // We need to clean up the node details from aFirstChildRow + 1 to the end of rows.
    337    for (let i = aFirstChildRow + 1; i < this._rows.length; i++) {
    338      this._nodeDetails.delete(makeNodeDetailsKey(this._rows[i]));
    339    }
    340    this._rows = this._rows
    341      .splice(0, aFirstChildRow)
    342      .concat(newElements, this._rows);
    343 
    344    if (this._isPlainContainer(aContainer)) {
    345      return cc;
    346    }
    347 
    348    let sortingMode = this._result.sortingMode;
    349 
    350    let rowsInserted = 0;
    351    for (let i = 0; i < cc; i++) {
    352      let curChild = aContainer.getChild(i);
    353      let curChildType = curChild.type;
    354 
    355      let row = aFirstChildRow + rowsInserted;
    356 
    357      // Don't display separators when sorted.
    358      if (curChildType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
    359        if (sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
    360          // Remove the element for the filtered separator.
    361          // Notice that the rows array was initially resized to include all
    362          // children.
    363          this._nodeDetails.delete(makeNodeDetailsKey(this._rows[row]));
    364          this._rows.splice(row, 1);
    365          continue;
    366        }
    367      }
    368 
    369      this._nodeDetails.delete(makeNodeDetailsKey(this._rows[row]));
    370      this._nodeDetails.set(makeNodeDetailsKey(curChild), curChild);
    371      this._rows[row] = curChild;
    372      rowsInserted++;
    373 
    374      // Recursively do containers.
    375      if (
    376        !this._flatList &&
    377        curChild instanceof Ci.nsINavHistoryContainerResultNode
    378      ) {
    379        let uri = curChild.uri;
    380        let isopen = false;
    381 
    382        if (uri) {
    383          let val = Services.xulStore.getValue(
    384            document.documentURI,
    385            PlacesUIUtils.obfuscateUrlForXulStore(uri),
    386            "open"
    387          );
    388          isopen = val == "true";
    389        }
    390 
    391        if (isopen != curChild.containerOpen) {
    392          aToOpen.push(curChild);
    393        } else if (curChild.containerOpen && curChild.childCount > 0) {
    394          rowsInserted += this._buildVisibleSection(curChild, row + 1, aToOpen);
    395        }
    396      }
    397    }
    398 
    399    return rowsInserted;
    400  },
    401 
    402  /**
    403   * This counts how many rows a node takes in the tree.  For containers it
    404   * will count the node itself plus any child node following it.
    405   *
    406   * @param {number} aNodeRow
    407   *   The row of the node to count
    408   * @returns {number}
    409   */
    410  _countVisibleRowsForNodeAtRow: function PTV__countVisibleRowsForNodeAtRow(
    411    aNodeRow
    412  ) {
    413    let node = this._rows[aNodeRow];
    414 
    415    // If it's not listed yet, we know that it's a leaf node (instanceof also
    416    // null-checks).
    417    if (!(node instanceof Ci.nsINavHistoryContainerResultNode)) {
    418      return 1;
    419    }
    420 
    421    let outerLevel = node.indentLevel;
    422    for (let i = aNodeRow + 1; i < this._rows.length; i++) {
    423      let rowNode = this._rows[i];
    424      if (rowNode && rowNode.indentLevel <= outerLevel) {
    425        return i - aNodeRow;
    426      }
    427    }
    428 
    429    // This node plus its children take up the bottom of the list.
    430    return this._rows.length - aNodeRow;
    431  },
    432 
    433  _getSelectedNodesInRange: function PTV__getSelectedNodesInRange(
    434    aFirstRow,
    435    aLastRow
    436  ) {
    437    let selection = this.selection;
    438    let rc = selection.getRangeCount();
    439    if (rc == 0) {
    440      return [];
    441    }
    442 
    443    // The visible-area borders are needed for checking whether a
    444    // selected row is also visible.
    445    let firstVisibleRow = this._tree.getFirstVisibleRow();
    446    let lastVisibleRow = this._tree.getLastVisibleRow();
    447 
    448    let nodesInfo = [];
    449    for (let rangeIndex = 0; rangeIndex < rc; rangeIndex++) {
    450      let min = {},
    451        max = {};
    452      selection.getRangeAt(rangeIndex, min, max);
    453 
    454      // If this range does not overlap the replaced chunk, we don't need to
    455      // persist the selection.
    456      if (max.value < aFirstRow || min.value > aLastRow) {
    457        continue;
    458      }
    459 
    460      let firstRow = Math.max(min.value, aFirstRow);
    461      let lastRow = Math.min(max.value, aLastRow);
    462      for (let i = firstRow; i <= lastRow; i++) {
    463        nodesInfo.push({
    464          node: this._rows[i],
    465          oldRow: i,
    466          wasVisible: i >= firstVisibleRow && i <= lastVisibleRow,
    467        });
    468      }
    469    }
    470 
    471    return nodesInfo;
    472  },
    473 
    474  /**
    475   * Tries to find an equivalent node for a node which was removed.  We first
    476   * look for the original node, in case it was just relocated.  Then, if we
    477   * that node was not found, we look for a node that has the same itemId, uri
    478   * and time values.
    479   *
    480   * @param {object} aOldNode
    481   *        The node which was removed.
    482   *
    483   * @returns {number} the row number of an equivalent node for aOldOne, if one was
    484   *         found, -1 otherwise.
    485   */
    486  _getNewRowForRemovedNode: function PTV__getNewRowForRemovedNode(aOldNode) {
    487    let parent = aOldNode.parent;
    488    if (parent) {
    489      // If the node's parent is still set, the node is not obsolete
    490      // and we should just find out its new position.
    491      // However, if any of the node's ancestor is closed, the node is
    492      // invisible.
    493      let ancestors = PlacesUtils.nodeAncestors(aOldNode);
    494      for (let ancestor of ancestors) {
    495        if (!ancestor.containerOpen) {
    496          return -1;
    497        }
    498      }
    499 
    500      return this._getRowForNode(aOldNode, true);
    501    }
    502 
    503    // There's a broken edge case here.
    504    // If a visit appears in two queries, and the second one was
    505    // the old node, we'll select the first one after refresh.  There's
    506    // nothing we could do about that, because aOldNode.parent is
    507    // gone by the time invalidateContainer is called.
    508    let newNode = this._nodeDetails.get(makeNodeDetailsKey(aOldNode));
    509 
    510    if (!newNode) {
    511      return -1;
    512    }
    513 
    514    return this._getRowForNode(newNode, true);
    515  },
    516 
    517  /**
    518   * Restores a given selection state as near as possible to the original
    519   * selection state.
    520   *
    521   * @param {Array} aNodesInfo
    522   *        The persisted selection state as returned by
    523   *        _getSelectedNodesInRange.
    524   */
    525  _restoreSelection: function PTV__restoreSelection(aNodesInfo) {
    526    if (!aNodesInfo.length) {
    527      return;
    528    }
    529 
    530    let selection = this.selection;
    531 
    532    // Attempt to ensure that previously-visible selection will be visible
    533    // if it's re-selected.  However, we can only ensure that for one row.
    534    let scrollToRow = -1;
    535    for (let i = 0; i < aNodesInfo.length; i++) {
    536      let nodeInfo = aNodesInfo[i];
    537      let row = this._getNewRowForRemovedNode(nodeInfo.node);
    538      // Select the found node, if any.
    539      if (row != -1) {
    540        selection.rangedSelect(row, row, true);
    541        if (nodeInfo.wasVisible && scrollToRow == -1) {
    542          scrollToRow = row;
    543        }
    544      }
    545    }
    546 
    547    // If only one node was previously selected and there's no selection now,
    548    // select the node at its old row, if any.
    549    if (aNodesInfo.length == 1 && selection.count == 0) {
    550      let row = Math.min(aNodesInfo[0].oldRow, this._rows.length - 1);
    551      if (row != -1) {
    552        selection.rangedSelect(row, row, true);
    553        if (aNodesInfo[0].wasVisible && scrollToRow == -1) {
    554          scrollToRow = aNodesInfo[0].oldRow;
    555        }
    556      }
    557    }
    558 
    559    if (scrollToRow != -1) {
    560      this._tree.ensureRowIsVisible(scrollToRow);
    561    }
    562  },
    563 
    564  _convertPRTimeToString: function PTV__convertPRTimeToString(aTime) {
    565    const MS_PER_MINUTE = 60000;
    566    const MS_PER_DAY = 86400000;
    567    let timeMs = aTime / 1000; // PRTime is in microseconds
    568 
    569    // Date is calculated starting from midnight, so the modulo with a day are
    570    // milliseconds from today's midnight.
    571    // getTimezoneOffset corrects that based on local time, notice midnight
    572    // can have a different offset during DST-change days.
    573    let dateObj = new Date();
    574    let now = dateObj.getTime() - dateObj.getTimezoneOffset() * MS_PER_MINUTE;
    575    let midnight = now - (now % MS_PER_DAY);
    576    midnight += new Date(midnight).getTimezoneOffset() * MS_PER_MINUTE;
    577 
    578    let timeObj = new Date(timeMs);
    579    return timeMs >= midnight
    580      ? this._todayFormatter.format(timeObj)
    581      : this._dateFormatter.format(timeObj);
    582  },
    583 
    584  // We use a different formatter for times within the current day,
    585  // so we cache both a "today" formatter and a general date formatter.
    586  __todayFormatter: null,
    587  get _todayFormatter() {
    588    if (!this.__todayFormatter) {
    589      const dtOptions = { timeStyle: "short" };
    590      this.__todayFormatter = new Services.intl.DateTimeFormat(
    591        undefined,
    592        dtOptions
    593      );
    594    }
    595    return this.__todayFormatter;
    596  },
    597 
    598  __dateFormatter: null,
    599  get _dateFormatter() {
    600    if (!this.__dateFormatter) {
    601      const dtOptions = {
    602        dateStyle: "short",
    603        timeStyle: "short",
    604      };
    605      this.__dateFormatter = new Services.intl.DateTimeFormat(
    606        undefined,
    607        dtOptions
    608      );
    609    }
    610    return this.__dateFormatter;
    611  },
    612 
    613  COLUMN_TYPE_UNKNOWN: 0,
    614  COLUMN_TYPE_TITLE: 1,
    615  COLUMN_TYPE_URI: 2,
    616  COLUMN_TYPE_DATE: 3,
    617  COLUMN_TYPE_VISITCOUNT: 4,
    618  COLUMN_TYPE_DATEADDED: 5,
    619  COLUMN_TYPE_LASTMODIFIED: 6,
    620  COLUMN_TYPE_TAGS: 7,
    621 
    622  _getColumnType: function PTV__getColumnType(aColumn) {
    623    let columnType = aColumn.element.getAttribute("anonid") || aColumn.id;
    624 
    625    switch (columnType) {
    626      case "title":
    627        return this.COLUMN_TYPE_TITLE;
    628      case "url":
    629        return this.COLUMN_TYPE_URI;
    630      case "date":
    631        return this.COLUMN_TYPE_DATE;
    632      case "visitCount":
    633        return this.COLUMN_TYPE_VISITCOUNT;
    634      case "dateAdded":
    635        return this.COLUMN_TYPE_DATEADDED;
    636      case "lastModified":
    637        return this.COLUMN_TYPE_LASTMODIFIED;
    638      case "tags":
    639        return this.COLUMN_TYPE_TAGS;
    640    }
    641    return this.COLUMN_TYPE_UNKNOWN;
    642  },
    643 
    644  _sortTypeToColumnType: function PTV__sortTypeToColumnType(aSortType) {
    645    switch (aSortType) {
    646      case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING:
    647        return [this.COLUMN_TYPE_TITLE, false];
    648      case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING:
    649        return [this.COLUMN_TYPE_TITLE, true];
    650      case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING:
    651        return [this.COLUMN_TYPE_DATE, false];
    652      case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING:
    653        return [this.COLUMN_TYPE_DATE, true];
    654      case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING:
    655        return [this.COLUMN_TYPE_URI, false];
    656      case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING:
    657        return [this.COLUMN_TYPE_URI, true];
    658      case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING:
    659        return [this.COLUMN_TYPE_VISITCOUNT, false];
    660      case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING:
    661        return [this.COLUMN_TYPE_VISITCOUNT, true];
    662      case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING:
    663        return [this.COLUMN_TYPE_DATEADDED, false];
    664      case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING:
    665        return [this.COLUMN_TYPE_DATEADDED, true];
    666      case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING:
    667        return [this.COLUMN_TYPE_LASTMODIFIED, false];
    668      case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING:
    669        return [this.COLUMN_TYPE_LASTMODIFIED, true];
    670      case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING:
    671        return [this.COLUMN_TYPE_TAGS, false];
    672      case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING:
    673        return [this.COLUMN_TYPE_TAGS, true];
    674    }
    675    return [this.COLUMN_TYPE_UNKNOWN, false];
    676  },
    677 
    678  // nsINavHistoryResultObserver
    679  nodeInserted: function PTV_nodeInserted(aParentNode, aNode, aNewIndex) {
    680    console.assert(this._result, "Got a notification but have no result!");
    681    if (!this._tree || !this._result) {
    682      return;
    683    }
    684 
    685    // Bail out for hidden separators.
    686    if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) {
    687      return;
    688    }
    689 
    690    let parentRow;
    691    if (aParentNode != this._rootNode) {
    692      parentRow = this._getRowForNode(aParentNode);
    693 
    694      // Update parent when inserting the first item, since twisty has changed.
    695      if (aParentNode.childCount == 1) {
    696        this._tree.invalidateRow(parentRow);
    697      }
    698    }
    699 
    700    // Compute the new row number of the node.
    701    let row = -1;
    702    let cc = aParentNode.childCount;
    703    if (aNewIndex == 0 || this._isPlainContainer(aParentNode) || cc == 0) {
    704      // We don't need to worry about sub hierarchies of the parent node
    705      // if it's a plain container, or if the new node is its first child.
    706      if (aParentNode == this._rootNode) {
    707        row = aNewIndex;
    708      } else {
    709        row = parentRow + aNewIndex + 1;
    710      }
    711    } else {
    712      // Here, we try to find the next visible element in the child list so we
    713      // can set the new visible index to be right before that.  Note that we
    714      // have to search down instead of up, because some siblings could have
    715      // children themselves that would be in the way.
    716      let separatorsAreHidden =
    717        PlacesUtils.nodeIsSeparator(aNode) && this.isSorted();
    718      for (let i = aNewIndex + 1; i < cc; i++) {
    719        let node = aParentNode.getChild(i);
    720        if (!separatorsAreHidden || PlacesUtils.nodeIsSeparator(node)) {
    721          // The children have not been shifted so the next item will have what
    722          // should be our index.
    723          row = this._getRowForNode(node, false, parentRow, i);
    724          break;
    725        }
    726      }
    727      if (row < 0) {
    728        // At the end of the child list without finding a visible sibling. This
    729        // is a little harder because we don't know how many rows the last item
    730        // in our list takes up (it could be a container with many children).
    731        let prevChild = aParentNode.getChild(aNewIndex - 1);
    732        let prevIndex = this._getRowForNode(
    733          prevChild,
    734          false,
    735          parentRow,
    736          aNewIndex - 1
    737        );
    738        row = prevIndex + this._countVisibleRowsForNodeAtRow(prevIndex);
    739      }
    740    }
    741 
    742    this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode);
    743    this._rows.splice(row, 0, aNode);
    744    this._tree.rowCountChanged(row, 1);
    745 
    746    if (
    747      PlacesUtils.nodeIsContainer(aNode) &&
    748      PlacesUtils.asContainer(aNode).containerOpen
    749    ) {
    750      this.invalidateContainer(aNode);
    751    }
    752  },
    753 
    754  /**
    755   * THIS FUNCTION DOES NOT HANDLE cases where a collapsed node is being
    756   * removed but the node it is collapsed with is not being removed (this then
    757   * just swap out the removee with its collapsing partner). The only time
    758   * when we really remove things is when deleting URIs, which will apply to
    759   * all collapsees. This function is called sometimes when resorting items.
    760   * However, we won't do this when sorted by date because dates will never
    761   * change for visits, and date sorting is the only time things are collapsed.
    762   *
    763   * @param {object} aParentNode
    764   *   The parent node of the node being removed.
    765   * @param {object} aNode
    766   *   The node to remove from the tree.
    767   * @param {number} aOldIndex
    768   *   The old index of the node in the parent.
    769   */
    770  nodeRemoved: function PTV_nodeRemoved(aParentNode, aNode, aOldIndex) {
    771    console.assert(this._result, "Got a notification but have no result!");
    772    if (!this._tree || !this._result) {
    773      return;
    774    }
    775 
    776    // XXX bug 517701: We don't know what to do when the root node is removed.
    777    if (aNode == this._rootNode) {
    778      throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
    779    }
    780 
    781    // Bail out for hidden separators.
    782    if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) {
    783      return;
    784    }
    785 
    786    let parentRow =
    787      aParentNode == this._rootNode
    788        ? undefined
    789        : this._getRowForNode(aParentNode, true);
    790    let oldRow = this._getRowForNode(aNode, true, parentRow, aOldIndex);
    791    if (oldRow < 0) {
    792      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
    793    }
    794 
    795    // If the node was exclusively selected, the node next to it will be
    796    // selected.
    797    let selectNext = false;
    798    let selection = this.selection;
    799    if (selection.getRangeCount() == 1) {
    800      let min = {},
    801        max = {};
    802      selection.getRangeAt(0, min, max);
    803      if (min.value == max.value && this.nodeForTreeIndex(min.value) == aNode) {
    804        selectNext = true;
    805      }
    806    }
    807 
    808    // Remove the node and its children, if any.
    809    let count = this._countVisibleRowsForNodeAtRow(oldRow);
    810    for (let splicedNode of this._rows.splice(oldRow, count)) {
    811      this._nodeDetails.delete(makeNodeDetailsKey(splicedNode));
    812    }
    813    this._tree.rowCountChanged(oldRow, -count);
    814 
    815    // Redraw the parent if its twisty state has changed.
    816    if (aParentNode != this._rootNode && !aParentNode.hasChildren) {
    817      parentRow = oldRow - 1;
    818      this._tree.invalidateRow(parentRow);
    819    }
    820 
    821    // Restore selection if the node was exclusively selected.
    822    if (!selectNext) {
    823      return;
    824    }
    825 
    826    // Restore selection.
    827    let rowToSelect = Math.min(oldRow, this._rows.length - 1);
    828    if (rowToSelect != -1) {
    829      this.selection.rangedSelect(rowToSelect, rowToSelect, true);
    830    }
    831  },
    832 
    833  nodeMoved: function PTV_nodeMoved(
    834    aNode,
    835    aOldParent,
    836    aOldIndex,
    837    aNewParent,
    838    aNewIndex
    839  ) {
    840    console.assert(this._result, "Got a notification but have no result!");
    841    if (!this._tree || !this._result) {
    842      return;
    843    }
    844 
    845    // Bail out for hidden separators.
    846    if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) {
    847      return;
    848    }
    849 
    850    // Note that at this point the node has already been moved by the backend,
    851    // so we must give hints to _getRowForNode to get the old row position.
    852    let oldParentRow =
    853      aOldParent == this._rootNode
    854        ? undefined
    855        : this._getRowForNode(aOldParent, true);
    856    let oldRow = this._getRowForNode(aNode, true, oldParentRow, aOldIndex);
    857    if (oldRow < 0) {
    858      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
    859    }
    860 
    861    // If this node is a container it could take up more than one row.
    862    let count = this._countVisibleRowsForNodeAtRow(oldRow);
    863 
    864    // Persist selection state.
    865    let nodesToReselect = this._getSelectedNodesInRange(
    866      oldRow,
    867      oldRow + count - 1
    868    );
    869    if (nodesToReselect.length) {
    870      this.selection.selectEventsSuppressed = true;
    871    }
    872 
    873    // Redraw the parent if its twisty state has changed.
    874    if (aOldParent != this._rootNode && !aOldParent.hasChildren) {
    875      let parentRow = oldRow - 1;
    876      this._tree.invalidateRow(parentRow);
    877    }
    878 
    879    // Remove node and its children, if any, from the old position.
    880    for (let splicedNode of this._rows.splice(oldRow, count)) {
    881      this._nodeDetails.delete(makeNodeDetailsKey(splicedNode));
    882    }
    883    this._tree.rowCountChanged(oldRow, -count);
    884 
    885    // Insert the node into the new position.
    886    this.nodeInserted(aNewParent, aNode, aNewIndex);
    887 
    888    // Restore selection.
    889    if (nodesToReselect.length) {
    890      this._restoreSelection(nodesToReselect);
    891      this.selection.selectEventsSuppressed = false;
    892    }
    893  },
    894 
    895  _invalidateCellValue: function PTV__invalidateCellValue(aNode, aColumnType) {
    896    console.assert(this._result, "Got a notification but have no result!");
    897    if (!this._tree || !this._result) {
    898      return;
    899    }
    900 
    901    // Nothing to do for the root node.
    902    if (aNode == this._rootNode) {
    903      return;
    904    }
    905 
    906    let row = this._getRowForNode(aNode);
    907    if (row == -1) {
    908      return;
    909    }
    910 
    911    let column = this._findColumnByType(aColumnType);
    912    if (column && !column.element.hidden) {
    913      if (aColumnType == this.COLUMN_TYPE_TITLE) {
    914        this._tree.removeImageCacheEntry(row, column);
    915      }
    916      this._tree.invalidateCell(row, column);
    917    }
    918 
    919    // Last modified time is altered for almost all node changes.
    920    if (aColumnType != this.COLUMN_TYPE_LASTMODIFIED) {
    921      let lastModifiedColumn = this._findColumnByType(
    922        this.COLUMN_TYPE_LASTMODIFIED
    923      );
    924      if (lastModifiedColumn && !lastModifiedColumn.hidden) {
    925        this._tree.invalidateCell(row, lastModifiedColumn);
    926      }
    927    }
    928  },
    929 
    930  nodeTitleChanged: function PTV_nodeTitleChanged(aNode) {
    931    this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
    932  },
    933 
    934  nodeURIChanged: function PTV_nodeURIChanged(aNode, aOldURI) {
    935    this._nodeDetails.delete(
    936      makeNodeDetailsKey({
    937        uri: aOldURI,
    938        itemId: aNode.itemId,
    939        time: aNode.time,
    940      })
    941    );
    942    this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode);
    943    this._invalidateCellValue(aNode, this.COLUMN_TYPE_URI);
    944  },
    945 
    946  nodeIconChanged: function PTV_nodeIconChanged(aNode) {
    947    this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
    948  },
    949 
    950  nodeHistoryDetailsChanged: function PTV_nodeHistoryDetailsChanged(
    951    aNode,
    952    aOldVisitDate
    953  ) {
    954    this._nodeDetails.delete(
    955      makeNodeDetailsKey({
    956        uri: aNode.uri,
    957        itemId: aNode.itemId,
    958        time: aOldVisitDate,
    959      })
    960    );
    961    this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode);
    962 
    963    this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATE);
    964    this._invalidateCellValue(aNode, this.COLUMN_TYPE_VISITCOUNT);
    965  },
    966 
    967  nodeTagsChanged: function PTV_nodeTagsChanged(aNode) {
    968    this._invalidateCellValue(aNode, this.COLUMN_TYPE_TAGS);
    969  },
    970 
    971  nodeKeywordChanged() {},
    972 
    973  nodeDateAddedChanged: function PTV_nodeDateAddedChanged(aNode) {
    974    this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATEADDED);
    975  },
    976 
    977  nodeLastModifiedChanged: function PTV_nodeLastModifiedChanged(aNode) {
    978    this._invalidateCellValue(aNode, this.COLUMN_TYPE_LASTMODIFIED);
    979  },
    980 
    981  containerStateChanged: function PTV_containerStateChanged(aNode) {
    982    this.invalidateContainer(aNode);
    983  },
    984 
    985  invalidateContainer: function PTV_invalidateContainer(aContainer) {
    986    console.assert(this._result, "Need to have a result to update");
    987    if (!this._tree) {
    988      return;
    989    }
    990 
    991    // If we are currently editing, don't invalidate the container until we
    992    // finish.
    993    if (this._tree.getAttribute("editing")) {
    994      if (!this._editingObservers) {
    995        this._editingObservers = new Map();
    996      }
    997      if (!this._editingObservers.has(aContainer)) {
    998        let mutationObserver = new MutationObserver(() => {
    999          Services.tm.dispatchToMainThread(() =>
   1000            this.invalidateContainer(aContainer)
   1001          );
   1002          let observer = this._editingObservers.get(aContainer);
   1003          observer.disconnect();
   1004          this._editingObservers.delete(aContainer);
   1005        });
   1006 
   1007        mutationObserver.observe(this._tree, {
   1008          attributes: true,
   1009          attributeFilter: ["editing"],
   1010        });
   1011 
   1012        this._editingObservers.set(aContainer, mutationObserver);
   1013      }
   1014      return;
   1015    }
   1016 
   1017    let startReplacement, replaceCount;
   1018    if (aContainer == this._rootNode) {
   1019      startReplacement = 0;
   1020      replaceCount = this._rows.length;
   1021 
   1022      // If the root node is now closed, the tree is empty.
   1023      if (!this._rootNode.containerOpen) {
   1024        this._nodeDetails.clear();
   1025        this._rows = [];
   1026        if (replaceCount) {
   1027          this._tree.rowCountChanged(startReplacement, -replaceCount);
   1028        }
   1029 
   1030        return;
   1031      }
   1032    } else {
   1033      // Update the twisty state.
   1034      let row = this._getRowForNode(aContainer);
   1035      this._tree.invalidateRow(row);
   1036 
   1037      // We don't replace the container node itself, so we should decrease the
   1038      // replaceCount by 1.
   1039      startReplacement = row + 1;
   1040      replaceCount = this._countVisibleRowsForNodeAtRow(row) - 1;
   1041    }
   1042 
   1043    // Persist selection state.
   1044    let nodesToReselect = this._getSelectedNodesInRange(
   1045      startReplacement,
   1046      startReplacement + replaceCount
   1047    );
   1048 
   1049    // Now update the number of elements.
   1050    this.selection.selectEventsSuppressed = true;
   1051 
   1052    // First remove the old elements
   1053    for (let splicedNode of this._rows.splice(startReplacement, replaceCount)) {
   1054      this._nodeDetails.delete(makeNodeDetailsKey(splicedNode));
   1055    }
   1056 
   1057    // If the container is now closed, we're done.
   1058    if (!aContainer.containerOpen) {
   1059      let oldSelectionCount = this.selection.count;
   1060      if (replaceCount) {
   1061        this._tree.rowCountChanged(startReplacement, -replaceCount);
   1062      }
   1063 
   1064      // Select the row next to the closed container if any of its
   1065      // children were selected, and nothing else is selected.
   1066      if (
   1067        nodesToReselect.length &&
   1068        nodesToReselect.length == oldSelectionCount
   1069      ) {
   1070        this.selection.rangedSelect(startReplacement, startReplacement, true);
   1071        this._tree.ensureRowIsVisible(startReplacement);
   1072      }
   1073 
   1074      this.selection.selectEventsSuppressed = false;
   1075      return;
   1076    }
   1077 
   1078    // Otherwise, start a batch first.
   1079    this._tree.beginUpdateBatch();
   1080    if (replaceCount) {
   1081      this._tree.rowCountChanged(startReplacement, -replaceCount);
   1082    }
   1083 
   1084    let toOpenElements = [];
   1085    let elementsAddedCount = this._buildVisibleSection(
   1086      aContainer,
   1087      startReplacement,
   1088      toOpenElements
   1089    );
   1090    if (elementsAddedCount) {
   1091      this._tree.rowCountChanged(startReplacement, elementsAddedCount);
   1092    }
   1093 
   1094    if (!this._flatList) {
   1095      // Now, open any containers that were persisted.
   1096      for (let i = 0; i < toOpenElements.length; i++) {
   1097        let item = toOpenElements[i];
   1098        let parent = item.parent;
   1099 
   1100        // Avoid recursively opening containers.
   1101        while (parent) {
   1102          if (parent.uri == item.uri) {
   1103            break;
   1104          }
   1105          parent = parent.parent;
   1106        }
   1107 
   1108        // If we don't have a parent, we made it all the way to the root
   1109        // and didn't find a match, so we can open our item.
   1110        if (!parent && !item.containerOpen) {
   1111          item.containerOpen = true;
   1112        }
   1113      }
   1114    }
   1115 
   1116    this._tree.endUpdateBatch();
   1117 
   1118    // Restore selection.
   1119    this._restoreSelection(nodesToReselect);
   1120    this.selection.selectEventsSuppressed = false;
   1121  },
   1122 
   1123  _columns: [],
   1124  _findColumnByType: function PTV__findColumnByType(aColumnType) {
   1125    if (this._columns[aColumnType]) {
   1126      return this._columns[aColumnType];
   1127    }
   1128 
   1129    let columns = this._tree.columns;
   1130    let colCount = columns.count;
   1131    for (let i = 0; i < colCount; i++) {
   1132      let column = columns.getColumnAt(i);
   1133      let columnType = this._getColumnType(column);
   1134      this._columns[columnType] = column;
   1135      if (columnType == aColumnType) {
   1136        return column;
   1137      }
   1138    }
   1139 
   1140    // That's completely valid.  Most of our trees actually include just the
   1141    // title column.
   1142    return null;
   1143  },
   1144 
   1145  sortingChanged: function PTV__sortingChanged(aSortingMode) {
   1146    if (!this._tree || !this._result) {
   1147      return;
   1148    }
   1149 
   1150    // Depending on the sort mode, certain commands may be disabled.
   1151    window.updateCommands("sort");
   1152 
   1153    let columns = this._tree.columns;
   1154 
   1155    // Clear old sorting indicator.
   1156    let sortedColumn = columns.getSortedColumn();
   1157    if (sortedColumn) {
   1158      sortedColumn.element.removeAttribute("sortDirection");
   1159    }
   1160 
   1161    // Set new sorting indicator by looking through all columns for ours.
   1162    if (aSortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
   1163      return;
   1164    }
   1165 
   1166    let [desiredColumn, desiredIsDescending] =
   1167      this._sortTypeToColumnType(aSortingMode);
   1168    let column = this._findColumnByType(desiredColumn);
   1169    if (column) {
   1170      let sortDir = desiredIsDescending ? "descending" : "ascending";
   1171      column.element.setAttribute("sortDirection", sortDir);
   1172    }
   1173  },
   1174 
   1175  _inBatchMode: false,
   1176  batching: function PTV__batching(aToggleMode) {
   1177    if (this._inBatchMode != aToggleMode) {
   1178      this._inBatchMode = this.selection.selectEventsSuppressed = aToggleMode;
   1179      if (this._inBatchMode) {
   1180        this._tree.beginUpdateBatch();
   1181      } else {
   1182        this._tree.endUpdateBatch();
   1183      }
   1184    }
   1185  },
   1186 
   1187  get result() {
   1188    return this._result;
   1189  },
   1190  set result(val) {
   1191    if (this._result) {
   1192      this._result.removeObserver(this);
   1193      this._rootNode.containerOpen = false;
   1194    }
   1195 
   1196    if (val) {
   1197      this._result = val;
   1198      this._rootNode = this._result.root;
   1199      this._cellProperties = new Map();
   1200      this._cuttingNodes = new Set();
   1201    } else if (this._result) {
   1202      delete this._result;
   1203      delete this._rootNode;
   1204      delete this._cellProperties;
   1205      delete this._cuttingNodes;
   1206    }
   1207 
   1208    // If the tree is not set yet, setTree will call finishInit.
   1209    if (this._tree && val) {
   1210      this._finishInit();
   1211    }
   1212  },
   1213 
   1214  /**
   1215   * This allows you to get at the real node for a given row index. This is
   1216   * only valid when a tree is attached.
   1217   *
   1218   * @param {Integer} aIndex The index for the node to get.
   1219   * @returns {Ci.nsINavHistoryResultNode} The node.
   1220   * @throws Cr.NS_ERROR_INVALID_ARG if the index is greater than the number of
   1221   *                                 rows.
   1222   */
   1223  nodeForTreeIndex(aIndex) {
   1224    if (aIndex > this._rows.length) {
   1225      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   1226    }
   1227 
   1228    return this._getNodeForRow(aIndex);
   1229  },
   1230 
   1231  /**
   1232   * Reverse of nodeForTreeIndex, returns the row index for a given result node.
   1233   * The node should be part of the tree.
   1234   *
   1235   * @param {Ci.nsINavHistoryResultNode} aNode The node to look for in the tree.
   1236   * @returns {Integer} The found index, or -1 if the item is not visible or not found.
   1237   */
   1238  treeIndexForNode(aNode) {
   1239    // The API allows passing invisible nodes.
   1240    try {
   1241      return this._getRowForNode(aNode, true);
   1242    } catch (ex) {}
   1243 
   1244    return -1;
   1245  },
   1246 
   1247  // nsITreeView
   1248  get rowCount() {
   1249    return this._rows.length;
   1250  },
   1251  get selection() {
   1252    return this._selection;
   1253  },
   1254  set selection(val) {
   1255    this._selection = val;
   1256  },
   1257 
   1258  getRowProperties() {
   1259    return "";
   1260  },
   1261 
   1262  getCellProperties: function PTV_getCellProperties(aRow, aColumn) {
   1263    // for anonid-trees, we need to add the column-type manually
   1264    var props = "";
   1265    let columnType = aColumn.element.getAttribute("anonid");
   1266    if (columnType) {
   1267      props += columnType;
   1268    } else {
   1269      columnType = aColumn.id;
   1270    }
   1271 
   1272    // Set the "ltr" property on url cells
   1273    if (columnType == "url") {
   1274      props += " ltr";
   1275    }
   1276 
   1277    if (columnType != "title") {
   1278      return props;
   1279    }
   1280 
   1281    let node = this._getNodeForRow(aRow);
   1282 
   1283    if (this._cuttingNodes.has(node)) {
   1284      props += " cutting";
   1285    }
   1286 
   1287    let properties = this._cellProperties.get(node);
   1288    if (properties === undefined) {
   1289      properties = "";
   1290      let itemId = node.itemId;
   1291      let nodeType = node.type;
   1292      if (PlacesUtils.containerTypes.includes(nodeType)) {
   1293        if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
   1294          properties += " query";
   1295          if (PlacesUtils.nodeIsTagQuery(node)) {
   1296            properties += " tagContainer";
   1297          } else if (PlacesUtils.nodeIsDay(node)) {
   1298            properties += " dayContainer";
   1299          } else if (PlacesUtils.nodeIsHost(node)) {
   1300            properties += " hostContainer";
   1301          }
   1302        }
   1303 
   1304        if (itemId == -1) {
   1305          switch (node.bookmarkGuid) {
   1306            case PlacesUtils.bookmarks.virtualToolbarGuid:
   1307              properties += ` queryFolder_${PlacesUtils.bookmarks.toolbarGuid}`;
   1308              break;
   1309            case PlacesUtils.bookmarks.virtualMenuGuid:
   1310              properties += ` queryFolder_${PlacesUtils.bookmarks.menuGuid}`;
   1311              break;
   1312            case PlacesUtils.bookmarks.virtualUnfiledGuid:
   1313              properties += ` queryFolder_${PlacesUtils.bookmarks.unfiledGuid}`;
   1314              break;
   1315            case PlacesUtils.virtualAllBookmarksGuid:
   1316            case PlacesUtils.virtualHistoryGuid:
   1317            case PlacesUtils.virtualDownloadsGuid:
   1318            case PlacesUtils.virtualTagsGuid:
   1319              properties += ` OrganizerQuery_${node.bookmarkGuid}`;
   1320              break;
   1321          }
   1322        }
   1323      } else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
   1324        properties += " separator";
   1325      } else if (PlacesUtils.nodeIsURI(node)) {
   1326        properties += " " + PlacesUIUtils.guessUrlSchemeForUI(node.uri);
   1327      }
   1328 
   1329      this._cellProperties.set(node, properties);
   1330    }
   1331 
   1332    return props + " " + properties;
   1333  },
   1334 
   1335  getColumnProperties() {
   1336    return "";
   1337  },
   1338 
   1339  isContainer: function PTV_isContainer(aRow) {
   1340    // Only leaf nodes aren't listed in the rows array.
   1341    let node = this._rows[aRow];
   1342    if (node === undefined || !PlacesUtils.nodeIsContainer(node)) {
   1343      return false;
   1344    }
   1345 
   1346    // Flat-lists may ignore expandQueries and other query options when
   1347    // they are asked to open a container.
   1348    if (this._flatList) {
   1349      return true;
   1350    }
   1351 
   1352    // Treat non-expandable childless queries as non-containers, unless they
   1353    // are tags.
   1354    if (PlacesUtils.nodeIsQuery(node) && !PlacesUtils.nodeIsTagQuery(node)) {
   1355      return (
   1356        PlacesUtils.asQuery(node).queryOptions.expandQueries || node.hasChildren
   1357      );
   1358    }
   1359    return true;
   1360  },
   1361 
   1362  isContainerOpen: function PTV_isContainerOpen(aRow) {
   1363    if (this._flatList) {
   1364      return false;
   1365    }
   1366 
   1367    // All containers are listed in the rows array.
   1368    return this._rows[aRow].containerOpen;
   1369  },
   1370 
   1371  isContainerEmpty: function PTV_isContainerEmpty(aRow) {
   1372    if (this._flatList) {
   1373      return true;
   1374    }
   1375 
   1376    // All containers are listed in the rows array.
   1377    return !this._rows[aRow].hasChildren;
   1378  },
   1379 
   1380  isSeparator: function PTV_isSeparator(aRow) {
   1381    // All separators are listed in the rows array.
   1382    let node = this._rows[aRow];
   1383    return node && PlacesUtils.nodeIsSeparator(node);
   1384  },
   1385 
   1386  isSorted: function PTV_isSorted() {
   1387    return (
   1388      this._result.sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE
   1389    );
   1390  },
   1391 
   1392  canDrop: function PTV_canDrop(aRow, aOrientation, aDataTransfer) {
   1393    if (!this._result) {
   1394      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
   1395    }
   1396 
   1397    if (this._controller.disableUserActions) {
   1398      return false;
   1399    }
   1400 
   1401    // Drop position into a sorted treeview would be wrong.
   1402    if (this.isSorted()) {
   1403      return false;
   1404    }
   1405 
   1406    let ip = this._getInsertionPoint(aRow, aOrientation);
   1407    return ip && PlacesControllerDragHelper.canDrop(ip, aDataTransfer);
   1408  },
   1409 
   1410  _getInsertionPoint: function PTV__getInsertionPoint(index, orientation) {
   1411    let container = this._result.root;
   1412    let dropNearNode = null;
   1413    // When there's no selection, assume the container is the container
   1414    // the view is populated from (i.e. the result's itemId).
   1415    if (index != -1) {
   1416      let lastSelected = this.nodeForTreeIndex(index);
   1417      if (this.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) {
   1418        // If the last selected item is an open container, append _into_
   1419        // it, rather than insert adjacent to it.
   1420        container = lastSelected;
   1421        index = -1;
   1422      } else if (
   1423        lastSelected.containerOpen &&
   1424        orientation == Ci.nsITreeView.DROP_AFTER &&
   1425        lastSelected.hasChildren
   1426      ) {
   1427        // If the last selected node is an open container and the user is
   1428        // trying to drag into it as a first node, really insert into it.
   1429        container = lastSelected;
   1430        orientation = Ci.nsITreeView.DROP_ON;
   1431        index = 0;
   1432      } else {
   1433        // Use the last-selected node's container.
   1434        container = lastSelected.parent;
   1435 
   1436        // During its Drag & Drop operation, the tree code closes-and-opens
   1437        // containers very often (part of the XUL "spring-loaded folders"
   1438        // implementation).  And in certain cases, we may reach a closed
   1439        // container here.  However, we can simply bail out when this happens,
   1440        // because we would then be back here in less than a millisecond, when
   1441        // the container had been reopened.
   1442        if (!container || !container.containerOpen) {
   1443          return null;
   1444        }
   1445 
   1446        // Don't show an insertion point if the index is contained
   1447        // within the selection and drag source is the same
   1448        if (
   1449          this._element.isDragSource &&
   1450          this._element.view.selection.isSelected(index)
   1451        ) {
   1452          return null;
   1453        }
   1454 
   1455        // Avoid the potentially expensive call to getChildIndex
   1456        // if we know this container doesn't allow insertion.
   1457        if (this._controller.disallowInsertion(container)) {
   1458          return null;
   1459        }
   1460 
   1461        let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
   1462        if (
   1463          queryOptions.sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE
   1464        ) {
   1465          // If we are within a sorted view, insert at the end.
   1466          index = -1;
   1467        } else if (queryOptions.excludeItems || queryOptions.excludeQueries) {
   1468          // Some item may be invisible, insert near last selected one.
   1469          // We don't replace index here to avoid requests to the db,
   1470          // instead it will be calculated later by the controller.
   1471          index = -1;
   1472          dropNearNode = lastSelected;
   1473        } else {
   1474          let lastSelectedIndex = container.getChildIndex(lastSelected);
   1475          index =
   1476            orientation == Ci.nsITreeView.DROP_BEFORE
   1477              ? lastSelectedIndex
   1478              : lastSelectedIndex + 1;
   1479        }
   1480      }
   1481    }
   1482 
   1483    if (this._controller.disallowInsertion(container)) {
   1484      return null;
   1485    }
   1486 
   1487    let tagName = PlacesUtils.nodeIsTagQuery(container)
   1488      ? PlacesUtils.asQuery(container).query.tags[0]
   1489      : null;
   1490 
   1491    return new PlacesInsertionPoint({
   1492      parentGuid: PlacesUtils.getConcreteItemGuid(container),
   1493      index,
   1494      orientation,
   1495      tagName,
   1496      dropNearNode,
   1497    });
   1498  },
   1499 
   1500  async drop(aRow, aOrientation, aDataTransfer) {
   1501    if (this._controller.disableUserActions) {
   1502      return;
   1503    }
   1504 
   1505    // We are responsible for translating the |index| and |orientation|
   1506    // parameters into a container id and index within the container,
   1507    // since this information is specific to the tree view.
   1508    let ip = this._getInsertionPoint(aRow, aOrientation);
   1509    if (ip) {
   1510      try {
   1511        await PlacesControllerDragHelper.onDrop(ip, aDataTransfer, this._tree);
   1512      } catch (ex) {
   1513        console.error(ex);
   1514      } finally {
   1515        // We should only clear the drop target once
   1516        // the onDrop is complete, as it is an async function.
   1517        PlacesControllerDragHelper.currentDropTarget = null;
   1518      }
   1519    }
   1520  },
   1521 
   1522  getParentIndex: function PTV_getParentIndex(aRow) {
   1523    let [, parentRow] = this._getParentByChildRow(aRow);
   1524    return parentRow;
   1525  },
   1526 
   1527  hasNextSibling: function PTV_hasNextSibling(aRow, aAfterIndex) {
   1528    if (aRow == this._rows.length - 1) {
   1529      // The last row has no sibling.
   1530      return false;
   1531    }
   1532 
   1533    let node = this._rows[aRow];
   1534    if (node === undefined || this._isPlainContainer(node.parent)) {
   1535      // The node is a child of a plain container.
   1536      // If the next row is either unset or has the same parent,
   1537      // it's a sibling.
   1538      let nextNode = this._rows[aRow + 1];
   1539      return nextNode == undefined || nextNode.parent == node.parent;
   1540    }
   1541 
   1542    let thisLevel = node.indentLevel;
   1543    for (let i = aAfterIndex + 1; i < this._rows.length; ++i) {
   1544      let rowNode = this._getNodeForRow(i);
   1545      let nextLevel = rowNode.indentLevel;
   1546      if (nextLevel == thisLevel) {
   1547        return true;
   1548      }
   1549      if (nextLevel < thisLevel) {
   1550        break;
   1551      }
   1552    }
   1553 
   1554    return false;
   1555  },
   1556 
   1557  getLevel(aRow) {
   1558    return this._getNodeForRow(aRow).indentLevel;
   1559  },
   1560 
   1561  getImageSrc: function PTV_getImageSrc(aRow, aColumn) {
   1562    // Only the title column has an image.
   1563    if (this._getColumnType(aColumn) != this.COLUMN_TYPE_TITLE) {
   1564      return "";
   1565    }
   1566 
   1567    let node = this._getNodeForRow(aRow);
   1568    return node.icon;
   1569  },
   1570 
   1571  getCellValue() {},
   1572 
   1573  getCellText: function PTV_getCellText(aRow, aColumn) {
   1574    let node = this._getNodeForRow(aRow);
   1575    switch (this._getColumnType(aColumn)) {
   1576      case this.COLUMN_TYPE_TITLE:
   1577        // normally, this is just the title, but we don't want empty items in
   1578        // the tree view so return a special string if the title is empty.
   1579        // Do it here so that callers can still get at the 0 length title
   1580        // if they go through the "result" API.
   1581        if (PlacesUtils.nodeIsSeparator(node)) {
   1582          return "";
   1583        }
   1584        return PlacesUIUtils.getBestTitle(node, true);
   1585      case this.COLUMN_TYPE_TAGS:
   1586        return node.tags?.replace(",", ", ");
   1587      case this.COLUMN_TYPE_URI:
   1588        if (PlacesUtils.nodeIsURI(node)) {
   1589          return node.uri;
   1590        }
   1591        return "";
   1592      case this.COLUMN_TYPE_DATE: {
   1593        let nodeTime = node.time;
   1594        if (nodeTime == 0 || !PlacesUtils.nodeIsURI(node)) {
   1595          // hosts and days shouldn't have a value for the date column.
   1596          // Actually, you could argue this point, but looking at the
   1597          // results, seeing the most recently visited date is not what
   1598          // I expect, and gives me no information I know how to use.
   1599          // Only show this for URI-based items.
   1600          return "";
   1601        }
   1602 
   1603        return this._convertPRTimeToString(nodeTime);
   1604      }
   1605      case this.COLUMN_TYPE_VISITCOUNT:
   1606        return node.accessCount;
   1607      case this.COLUMN_TYPE_DATEADDED:
   1608        if (node.dateAdded) {
   1609          return this._convertPRTimeToString(node.dateAdded);
   1610        }
   1611        return "";
   1612      case this.COLUMN_TYPE_LASTMODIFIED:
   1613        if (node.lastModified) {
   1614          return this._convertPRTimeToString(node.lastModified);
   1615        }
   1616        return "";
   1617    }
   1618    return "";
   1619  },
   1620 
   1621  setTree: function PTV_setTree(aTree) {
   1622    // If we are replacing the tree during a batch, there is a concrete risk
   1623    // that the treeView goes out of sync, thus it's safer to end the batch now.
   1624    // This is a no-op if we are not batching.
   1625    this.batching(false);
   1626 
   1627    let hasOldTree = this._tree != null;
   1628    this._tree = aTree;
   1629 
   1630    if (this._result) {
   1631      if (hasOldTree) {
   1632        // detach from result when we are detaching from the tree.
   1633        // This breaks the reference cycle between us and the result.
   1634        if (!aTree) {
   1635          // Close the root container to free up memory and stop live updates.
   1636          this._rootNode.containerOpen = false;
   1637        }
   1638      }
   1639      if (aTree) {
   1640        this._finishInit();
   1641      }
   1642    }
   1643  },
   1644 
   1645  toggleOpenState: function PTV_toggleOpenState(aRow) {
   1646    if (!this._result) {
   1647      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
   1648    }
   1649 
   1650    let node = this._rows[aRow];
   1651    if (this._flatList && this._element) {
   1652      let event = new CustomEvent("onOpenFlatContainer", { detail: node });
   1653      this._element.dispatchEvent(event);
   1654      return;
   1655    }
   1656 
   1657    let uri = node.uri;
   1658 
   1659    if (uri) {
   1660      let docURI = document.documentURI;
   1661 
   1662      if (node.containerOpen) {
   1663        Services.xulStore.removeValue(
   1664          docURI,
   1665          PlacesUIUtils.obfuscateUrlForXulStore(uri),
   1666          "open"
   1667        );
   1668      } else {
   1669        Services.xulStore.setValue(
   1670          docURI,
   1671          PlacesUIUtils.obfuscateUrlForXulStore(uri),
   1672          "open",
   1673          "true"
   1674        );
   1675      }
   1676    }
   1677 
   1678    node.containerOpen = !node.containerOpen;
   1679  },
   1680 
   1681  cycleHeader: function PTV_cycleHeader(aColumn) {
   1682    if (!this._result) {
   1683      throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
   1684    }
   1685 
   1686    // Sometimes you want a tri-state sorting, and sometimes you don't. This
   1687    // rule allows tri-state sorting when the root node is a folder. This will
   1688    // catch the most common cases. When you are looking at folders, you want
   1689    // the third state to reset the sorting to the natural bookmark order. When
   1690    // you are looking at history, that third state has no meaning so we try
   1691    // to disallow it.
   1692    //
   1693    // The problem occurs when you have a query that results in bookmark
   1694    // folders. One example of this is the subscriptions view. In these cases,
   1695    // this rule doesn't allow you to sort those sub-folders by their natural
   1696    // order.
   1697    let allowTriState = PlacesUtils.nodeIsFolderOrShortcut(this._result.root);
   1698 
   1699    let oldSort = this._result.sortingMode;
   1700    let newSort;
   1701    const NHQO = Ci.nsINavHistoryQueryOptions;
   1702    switch (this._getColumnType(aColumn)) {
   1703      case this.COLUMN_TYPE_TITLE:
   1704        if (oldSort == NHQO.SORT_BY_TITLE_ASCENDING) {
   1705          newSort = NHQO.SORT_BY_TITLE_DESCENDING;
   1706        } else if (allowTriState && oldSort == NHQO.SORT_BY_TITLE_DESCENDING) {
   1707          newSort = NHQO.SORT_BY_NONE;
   1708        } else {
   1709          newSort = NHQO.SORT_BY_TITLE_ASCENDING;
   1710        }
   1711 
   1712        break;
   1713      case this.COLUMN_TYPE_URI:
   1714        if (oldSort == NHQO.SORT_BY_URI_ASCENDING) {
   1715          newSort = NHQO.SORT_BY_URI_DESCENDING;
   1716        } else if (allowTriState && oldSort == NHQO.SORT_BY_URI_DESCENDING) {
   1717          newSort = NHQO.SORT_BY_NONE;
   1718        } else {
   1719          newSort = NHQO.SORT_BY_URI_ASCENDING;
   1720        }
   1721 
   1722        break;
   1723      case this.COLUMN_TYPE_DATE:
   1724        if (oldSort == NHQO.SORT_BY_DATE_ASCENDING) {
   1725          newSort = NHQO.SORT_BY_DATE_DESCENDING;
   1726        } else if (allowTriState && oldSort == NHQO.SORT_BY_DATE_DESCENDING) {
   1727          newSort = NHQO.SORT_BY_NONE;
   1728        } else {
   1729          newSort = NHQO.SORT_BY_DATE_ASCENDING;
   1730        }
   1731 
   1732        break;
   1733      case this.COLUMN_TYPE_VISITCOUNT:
   1734        // visit count default is unusual because we sort by descending
   1735        // by default because you are most likely to be looking for
   1736        // highly visited sites when you click it
   1737        if (oldSort == NHQO.SORT_BY_VISITCOUNT_DESCENDING) {
   1738          newSort = NHQO.SORT_BY_VISITCOUNT_ASCENDING;
   1739        } else if (
   1740          allowTriState &&
   1741          oldSort == NHQO.SORT_BY_VISITCOUNT_ASCENDING
   1742        ) {
   1743          newSort = NHQO.SORT_BY_NONE;
   1744        } else {
   1745          newSort = NHQO.SORT_BY_VISITCOUNT_DESCENDING;
   1746        }
   1747 
   1748        break;
   1749      case this.COLUMN_TYPE_DATEADDED:
   1750        if (oldSort == NHQO.SORT_BY_DATEADDED_ASCENDING) {
   1751          newSort = NHQO.SORT_BY_DATEADDED_DESCENDING;
   1752        } else if (
   1753          allowTriState &&
   1754          oldSort == NHQO.SORT_BY_DATEADDED_DESCENDING
   1755        ) {
   1756          newSort = NHQO.SORT_BY_NONE;
   1757        } else {
   1758          newSort = NHQO.SORT_BY_DATEADDED_ASCENDING;
   1759        }
   1760 
   1761        break;
   1762      case this.COLUMN_TYPE_LASTMODIFIED:
   1763        if (oldSort == NHQO.SORT_BY_LASTMODIFIED_ASCENDING) {
   1764          newSort = NHQO.SORT_BY_LASTMODIFIED_DESCENDING;
   1765        } else if (
   1766          allowTriState &&
   1767          oldSort == NHQO.SORT_BY_LASTMODIFIED_DESCENDING
   1768        ) {
   1769          newSort = NHQO.SORT_BY_NONE;
   1770        } else {
   1771          newSort = NHQO.SORT_BY_LASTMODIFIED_ASCENDING;
   1772        }
   1773 
   1774        break;
   1775      case this.COLUMN_TYPE_TAGS:
   1776        if (oldSort == NHQO.SORT_BY_TAGS_ASCENDING) {
   1777          newSort = NHQO.SORT_BY_TAGS_DESCENDING;
   1778        } else if (allowTriState && oldSort == NHQO.SORT_BY_TAGS_DESCENDING) {
   1779          newSort = NHQO.SORT_BY_NONE;
   1780        } else {
   1781          newSort = NHQO.SORT_BY_TAGS_ASCENDING;
   1782        }
   1783 
   1784        break;
   1785      default:
   1786        throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
   1787    }
   1788    this._result.sortingMode = newSort;
   1789  },
   1790 
   1791  isEditable: function PTV_isEditable(aRow, aColumn) {
   1792    // At this point we only support editing the title field.
   1793    if (aColumn.index != 0) {
   1794      return false;
   1795    }
   1796 
   1797    let node = this._rows[aRow];
   1798    if (!node) {
   1799      console.error("isEditable called for an unbuilt row.");
   1800      return false;
   1801    }
   1802    let itemGuid = node.bookmarkGuid;
   1803 
   1804    // Only bookmark-nodes are editable.
   1805    if (!itemGuid) {
   1806      return false;
   1807    }
   1808 
   1809    // The following items are also not editable, even though they are bookmark
   1810    // items.
   1811    // * places-roots
   1812    // * the left pane special folders and queries (those are place: uri
   1813    //   bookmarks)
   1814    // * separators
   1815    //
   1816    // Note that concrete itemIds aren't used intentionally.  For example, we
   1817    // have no reason to disallow renaming a shortcut to the Bookmarks Toolbar,
   1818    // except for the one under All Bookmarks.
   1819    if (
   1820      PlacesUtils.nodeIsSeparator(node) ||
   1821      PlacesUtils.isRootItem(itemGuid) ||
   1822      PlacesUtils.nodeIsQueryGeneratedFolder(node)
   1823    ) {
   1824      return false;
   1825    }
   1826 
   1827    return true;
   1828  },
   1829 
   1830  setCellText: function PTV_setCellText(aRow, aColumn, aText) {
   1831    // We may only get here if the cell is editable.
   1832    let node = this._rows[aRow];
   1833    if (node.title != aText) {
   1834      PlacesTransactions.EditTitle({ guid: node.bookmarkGuid, title: aText })
   1835        .transact()
   1836        .catch(console.error);
   1837    }
   1838  },
   1839 
   1840  toggleCutNode: function PTV_toggleCutNode(aNode, aValue) {
   1841    let currentVal = this._cuttingNodes.has(aNode);
   1842    if (currentVal != aValue) {
   1843      if (aValue) {
   1844        this._cuttingNodes.add(aNode);
   1845      } else {
   1846        this._cuttingNodes.delete(aNode);
   1847      }
   1848 
   1849      this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
   1850    }
   1851  },
   1852 
   1853  selectionChanged() {},
   1854  cycleCell() {},
   1855 };