tor-browser

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

allDownloadsView.js (30199B)


      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 var { XPCOMUtils } = ChromeUtils.importESModule(
      6  "resource://gre/modules/XPCOMUtils.sys.mjs"
      7 );
      8 
      9 ChromeUtils.defineESModuleGetters(this, {
     10  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     11  Downloads: "resource://gre/modules/Downloads.sys.mjs",
     12  DownloadsCommon:
     13    "moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
     14  DownloadsViewUI:
     15    "moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
     16  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
     17  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
     18  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     19 });
     20 
     21 const CLIPBOARD_URL_FLAVORS = ["text/x-moz-url", "text/plain"];
     22 
     23 /**
     24 * A download element shell is responsible for handling the commands and the
     25 * displayed data for a single download view element.
     26 *
     27 * The shell may contain a session download, a history download, or both.  When
     28 * both a history and a session download are present, the session download gets
     29 * priority and its information is displayed.
     30 *
     31 * On construction, a new richlistitem is created, and can be accessed through
     32 * the |element| getter. The shell doesn't insert the item in a richlistbox, the
     33 * caller must do it and remove the element when it's no longer needed.
     34 *
     35 * The caller is also responsible for forwarding status notifications, calling
     36 * the onChanged method.
     37 *
     38 * @param download
     39 *        The Download object from the DownloadHistoryList.
     40 */
     41 function HistoryDownloadElementShell(download) {
     42  this._download = download;
     43 
     44  this.element = document.createXULElement("richlistitem");
     45  this.element._shell = this;
     46 
     47  this.element.classList.add("download");
     48  this.element.classList.add("download-state");
     49 }
     50 
     51 HistoryDownloadElementShell.prototype = {
     52  /**
     53   * Overrides the base getter to return the Download or HistoryDownload object
     54   * for displaying information and executing commands in the user interface.
     55   */
     56  get download() {
     57    return this._download;
     58  },
     59 
     60  onStateChanged() {
     61    // Since the state changed, we may need to check the target file again.
     62    this._targetFileChecked = false;
     63 
     64    this._updateState();
     65 
     66    if (this.element.selected) {
     67      goUpdateDownloadCommands();
     68    } else {
     69      // If a state change occurs in an item that is not currently selected,
     70      // this is the only command that may be affected.
     71      goUpdateCommand("downloadsCmd_clearDownloads");
     72    }
     73  },
     74 
     75  onChanged() {
     76    // There is nothing to do if the item has always been invisible.
     77    if (!this.active) {
     78      return;
     79    }
     80 
     81    let newState = DownloadsCommon.stateOfDownload(this.download);
     82    if (this._downloadState !== newState) {
     83      this._downloadState = newState;
     84      this.onStateChanged();
     85    } else {
     86      this._updateStateInner();
     87    }
     88  },
     89  _downloadState: null,
     90 
     91  isCommandEnabled(aCommand) {
     92    // The only valid command for inactive elements is cmd_delete.
     93    if (!this.active && aCommand != "cmd_delete") {
     94      return false;
     95    }
     96    return DownloadsViewUI.DownloadElementShell.prototype.isCommandEnabled.call(
     97      this,
     98      aCommand
     99    );
    100  },
    101 
    102  downloadsCmd_unblock() {
    103    this.confirmUnblock(window, "unblock");
    104  },
    105  downloadsCmd_unblockAndSave() {
    106    this.confirmUnblock(window, "unblock");
    107  },
    108 
    109  downloadsCmd_chooseUnblock() {
    110    this.confirmUnblock(window, "chooseUnblock");
    111  },
    112 
    113  downloadsCmd_chooseOpen() {
    114    this.confirmUnblock(window, "chooseOpen");
    115  },
    116 
    117  // Returns whether or not the download handled by this shell should
    118  // show up in the search results for the given term.  Both the display
    119  // name for the download and the url are searched.
    120  matchesSearchTerm(aTerm) {
    121    if (!aTerm) {
    122      return true;
    123    }
    124    aTerm = aTerm.toLowerCase();
    125    let displayName = DownloadsViewUI.getDisplayName(this.download);
    126    return (
    127      displayName.toLowerCase().includes(aTerm) ||
    128      (this.download.source.originalUrl || this.download.source.url)
    129        .toLowerCase()
    130        .includes(aTerm)
    131    );
    132  },
    133 
    134  // Handles double-click and return keypress on the element (the keypress
    135  // listener is set in the DownloadsPlacesView object).
    136  doDefaultCommand(event) {
    137    let command = this.currentDefaultCommandName;
    138    if (
    139      command == "downloadsCmd_open" &&
    140      event &&
    141      (event.shiftKey || event.ctrlKey || event.metaKey || event.button == 1)
    142    ) {
    143      // We adjust the command for supported modifiers to suggest where the download may
    144      // be opened.
    145      let browserWin = BrowserWindowTracker.getTopWindow();
    146      let openWhere = browserWin
    147        ? BrowserUtils.whereToOpenLink(event, false, true)
    148        : "window";
    149      if (["window", "tabshifted", "tab"].includes(openWhere)) {
    150        command += ":" + openWhere;
    151      }
    152    }
    153 
    154    if (command && this.isCommandEnabled(command)) {
    155      this.doCommand(command);
    156    }
    157  },
    158 
    159  /**
    160   * This method is called by the outer download view, after the controller
    161   * commands have already been updated. In case we did not check for the
    162   * existence of the target file already, we can do it now and then update
    163   * the commands as needed.
    164   */
    165  onSelect() {
    166    if (!this.active) {
    167      return;
    168    }
    169 
    170    // If this is a history download for which no target file information is
    171    // available, we cannot retrieve information about the target file.
    172    if (!this.download.target.path) {
    173      return;
    174    }
    175 
    176    // Start checking for existence.  This may be done twice if onSelect is
    177    // called again before the information is collected.
    178    if (!this._targetFileChecked) {
    179      this.download
    180        .refresh()
    181        .catch(console.error)
    182        .then(() => {
    183          // Do not try to check for existence again even if this failed.
    184          this._targetFileChecked = true;
    185        });
    186    }
    187  },
    188 };
    189 Object.setPrototypeOf(
    190  HistoryDownloadElementShell.prototype,
    191  DownloadsViewUI.DownloadElementShell.prototype
    192 );
    193 
    194 /**
    195 * Relays commands from the download.xml binding to the selected items.
    196 */
    197 var DownloadsView = {
    198  onDownloadButton(event) {
    199    event.target.closest("richlistitem")._shell.onButton();
    200  },
    201 
    202  onDownloadClick() {},
    203 };
    204 
    205 /**
    206 * A Downloads Places View is a places view designed to show a places query
    207 * for history downloads alongside the session downloads.
    208 *
    209 * As we don't use the places controller, some methods implemented by other
    210 * places views are not implemented by this view.
    211 *
    212 * A richlistitem in this view can represent either a past download or a session
    213 * download, or both. Session downloads are shown first in the view, and as long
    214 * as they exist they "collapses" their history "counterpart" (So we don't show two
    215 * items for every download).
    216 */
    217 function DownloadsPlacesView(
    218  aRichListBox,
    219  aActive = true,
    220  aSuppressionFlag = DownloadsCommon.SUPPRESS_ALL_DOWNLOADS_OPEN
    221 ) {
    222  this._richlistbox = aRichListBox;
    223  this._richlistbox._placesView = this;
    224  window.controllers.insertControllerAt(0, this);
    225 
    226  // Map downloads to their element shells.
    227  this._viewItemsForDownloads = new WeakMap();
    228 
    229  this._searchTerm = "";
    230 
    231  this._active = aActive;
    232 
    233  // Register as a downloads view. The places data will be initialized by
    234  // the places setter.
    235  this._initiallySelectedElement = null;
    236  this._downloadsData = DownloadsCommon.getData(window.opener || window, true);
    237  this._waitingForInitialData = true;
    238  this._downloadsData.addView(this);
    239 
    240  // Pause the download indicator as user is interacting with downloads. This is
    241  // skipped on about:downloads because it handles this by itself.
    242  if (aSuppressionFlag === DownloadsCommon.SUPPRESS_ALL_DOWNLOADS_OPEN) {
    243    DownloadsCommon.getIndicatorData(window).attentionSuppressed |=
    244      aSuppressionFlag;
    245  }
    246 
    247  // Make sure to unregister the view if the window is closed.
    248  window.addEventListener(
    249    "unload",
    250    () => {
    251      window.controllers.removeController(this);
    252      // Unpause the main window's download indicator.
    253      DownloadsCommon.getIndicatorData(window).attentionSuppressed &=
    254        ~aSuppressionFlag;
    255      this._downloadsData.removeView(this);
    256      this.result = null;
    257    },
    258    true
    259  );
    260  // Resizing the window may change items visibility.
    261  window.addEventListener(
    262    "resize",
    263    () => {
    264      this._ensureVisibleElementsAreActive(true);
    265    },
    266    true
    267  );
    268 }
    269 
    270 DownloadsPlacesView.prototype = {
    271  get associatedElement() {
    272    return this._richlistbox;
    273  },
    274 
    275  get active() {
    276    return this._active;
    277  },
    278  set active(val) {
    279    this._active = val;
    280    if (this._active) {
    281      this._ensureVisibleElementsAreActive(true);
    282    }
    283  },
    284 
    285  /**
    286   * Ensure the custom element contents are created and shown for each
    287   * visible element in the list.
    288   *
    289   * @param debounce whether to use a short timeout rather than running
    290   *                 immediately. The default is running immediately. If you
    291   *                 pass `true`, we'll run on a 10ms timeout. This is used to
    292   *                 avoid running this code lots while scrolling or resizing.
    293   */
    294  _ensureVisibleElementsAreActive(debounce = false) {
    295    if (
    296      !this.active ||
    297      (debounce && this._ensureVisibleTimer) ||
    298      !this._richlistbox.firstChild
    299    ) {
    300      return;
    301    }
    302 
    303    if (debounce) {
    304      this._ensureVisibleTimer = setTimeout(() => {
    305        this._internalEnsureVisibleElementsAreActive();
    306      }, 10);
    307    } else {
    308      this._internalEnsureVisibleElementsAreActive();
    309    }
    310  },
    311 
    312  _internalEnsureVisibleElementsAreActive() {
    313    // If there are no children, we can't do anything so bail out.
    314    // However, avoid clearing the timer because there may be children
    315    // when the timer fires.
    316    if (!this._richlistbox.firstChild) {
    317      // If we were called asynchronously (debounced), we need to delete
    318      // the timer variable to ensure we are called again if another
    319      // debounced call comes in.
    320      delete this._ensureVisibleTimer;
    321      return;
    322    }
    323 
    324    if (this._ensureVisibleTimer) {
    325      clearTimeout(this._ensureVisibleTimer);
    326      delete this._ensureVisibleTimer;
    327    }
    328 
    329    let rlbRect = this._richlistbox.getBoundingClientRect();
    330    let winUtils = window.windowUtils;
    331    let nodes = winUtils.nodesFromRect(
    332      rlbRect.left,
    333      rlbRect.top,
    334      0,
    335      rlbRect.width,
    336      rlbRect.height,
    337      0,
    338      true,
    339      false,
    340      false
    341    );
    342    // nodesFromRect returns nodes in z-index order, and for the same z-index
    343    // sorts them in inverted DOM order, thus starting from the one that would
    344    // be on top.
    345    let firstVisibleNode, lastVisibleNode;
    346    for (let node of nodes) {
    347      if (node.localName === "richlistitem" && node._shell) {
    348        node._shell.ensureActive();
    349        // The first visible node is the last match.
    350        firstVisibleNode = node;
    351        // While the last visible node is the first match.
    352        if (!lastVisibleNode) {
    353          lastVisibleNode = node;
    354        }
    355      }
    356    }
    357 
    358    // Also activate the first invisible nodes in both boundaries (that is,
    359    // above and below the visible area) to ensure proper keyboard navigation
    360    // in both directions.
    361    let nodeBelowVisibleArea = lastVisibleNode && lastVisibleNode.nextSibling;
    362    if (nodeBelowVisibleArea && nodeBelowVisibleArea._shell) {
    363      nodeBelowVisibleArea._shell.ensureActive();
    364    }
    365 
    366    let nodeAboveVisibleArea =
    367      firstVisibleNode && firstVisibleNode.previousSibling;
    368    if (nodeAboveVisibleArea && nodeAboveVisibleArea._shell) {
    369      nodeAboveVisibleArea._shell.ensureActive();
    370    }
    371  },
    372 
    373  _place: "",
    374  get place() {
    375    return this._place;
    376  },
    377  set place(val) {
    378    if (this._place == val) {
    379      // XXXmano: places.js relies on this behavior (see Bug 822203).
    380      this.searchTerm = "";
    381    } else {
    382      this._place = val;
    383    }
    384  },
    385 
    386  get selectedNodes() {
    387    return Array.prototype.filter.call(
    388      this._richlistbox.selectedItems,
    389      element => element._shell.download.placesNode
    390    );
    391  },
    392 
    393  get selectedNode() {
    394    let selectedNodes = this.selectedNodes;
    395    return selectedNodes.length == 1 ? selectedNodes[0] : null;
    396  },
    397 
    398  get hasSelection() {
    399    return !!this.selectedNodes.length;
    400  },
    401 
    402  get controller() {
    403    return this._richlistbox.controller;
    404  },
    405 
    406  get searchTerm() {
    407    return this._searchTerm;
    408  },
    409  set searchTerm(aValue) {
    410    if (this._searchTerm != aValue) {
    411      // Always clear selection on a new search, since the user is starting a
    412      // different workflow. This also solves the fact we could end up
    413      // retaining selection on hidden elements.
    414      this._richlistbox.clearSelection();
    415      for (let element of this._richlistbox.childNodes) {
    416        element.hidden = !element._shell.matchesSearchTerm(aValue);
    417      }
    418      this._ensureVisibleElementsAreActive();
    419    }
    420    this._searchTerm = aValue;
    421  },
    422 
    423  /**
    424   * When the view loads, we want to select the first item.
    425   * However, because session downloads, for which the data is loaded
    426   * asynchronously, always come first in the list, and because the list
    427   * may (or may not) already contain history downloads at that point, it
    428   * turns out that by the time we can select the first item, the user may
    429   * have already started using the view.
    430   * To make things even more complicated, in other cases, the places data
    431   * may be loaded after the session downloads data.  Thus we cannot rely on
    432   * the order in which the data comes in.
    433   * We work around this by attempting to select the first element twice,
    434   * once after the places data is loaded and once when the session downloads
    435   * data is done loading.  However, if the selection has changed in-between,
    436   * we assume the user has already started using the view and give up.
    437   */
    438  _ensureInitialSelection() {
    439    // Either they're both null, or the selection has not changed in between.
    440    if (this._richlistbox.selectedItem == this._initiallySelectedElement) {
    441      let firstDownloadElement = this._richlistbox.firstChild;
    442      if (firstDownloadElement != this._initiallySelectedElement) {
    443        // We may be called before _ensureVisibleElementsAreActive,
    444        // therefore, ensure the first item is activated.
    445        firstDownloadElement._shell.ensureActive();
    446        this._richlistbox.selectedItem = firstDownloadElement;
    447        this._richlistbox.currentItem = firstDownloadElement;
    448        this._initiallySelectedElement = firstDownloadElement;
    449      }
    450    }
    451  },
    452 
    453  /**
    454   * DocumentFragment object that contains all the new elements added during a
    455   * batch operation, or null if no batch is in progress.
    456   *
    457   * Since newest downloads are displayed at the top, elements are normally
    458   * prepended to the fragment, and then the fragment is prepended to the list.
    459   */
    460  batchFragment: null,
    461 
    462  onDownloadBatchStarting() {
    463    this.batchFragment = document.createDocumentFragment();
    464 
    465    this.oldSuppressOnSelect = this._richlistbox.suppressOnSelect;
    466    this._richlistbox.suppressOnSelect = true;
    467  },
    468 
    469  onDownloadBatchEnded() {
    470    this._richlistbox.suppressOnSelect = this.oldSuppressOnSelect;
    471    delete this.oldSuppressOnSelect;
    472 
    473    if (this.batchFragment.childElementCount) {
    474      this._prependBatchFragment();
    475    }
    476    this.batchFragment = null;
    477 
    478    this._ensureInitialSelection();
    479    this._ensureVisibleElementsAreActive();
    480    goUpdateDownloadCommands();
    481    if (this._waitingForInitialData) {
    482      this._waitingForInitialData = false;
    483      this._richlistbox.dispatchEvent(
    484        new CustomEvent("InitialDownloadsLoaded")
    485      );
    486    }
    487  },
    488 
    489  _prependBatchFragment() {
    490    // Workaround multiple reflows hang by removing the richlistbox
    491    // and adding it back when we're done.
    492 
    493    // Hack for bug 836283: reset xbl fields to their old values after the
    494    // binding is reattached to avoid breaking the selection state
    495    let xblFields = new Map();
    496    for (let key of Object.getOwnPropertyNames(this._richlistbox)) {
    497      let value = this._richlistbox[key];
    498      xblFields.set(key, value);
    499    }
    500 
    501    let oldActiveElement = document.activeElement;
    502    let parentNode = this._richlistbox.parentNode;
    503    let nextSibling = this._richlistbox.nextSibling;
    504    parentNode.removeChild(this._richlistbox);
    505    this._richlistbox.prepend(this.batchFragment);
    506    parentNode.insertBefore(this._richlistbox, nextSibling);
    507    if (oldActiveElement && oldActiveElement != document.activeElement) {
    508      oldActiveElement.focus();
    509    }
    510 
    511    for (let [key, value] of xblFields) {
    512      this._richlistbox[key] = value;
    513    }
    514  },
    515 
    516  onDownloadAdded(download, { insertBefore } = {}) {
    517    let shell = new HistoryDownloadElementShell(download);
    518    this._viewItemsForDownloads.set(download, shell);
    519 
    520    // Since newest downloads are displayed at the top, either prepend the new
    521    // element or insert it after the one indicated by the insertBefore option.
    522    if (insertBefore) {
    523      this._viewItemsForDownloads
    524        .get(insertBefore)
    525        .element.insertAdjacentElement("afterend", shell.element);
    526    } else {
    527      (this.batchFragment || this._richlistbox).prepend(shell.element);
    528    }
    529 
    530    if (this.searchTerm) {
    531      shell.element.hidden = !shell.matchesSearchTerm(this.searchTerm);
    532    }
    533 
    534    // Don't update commands and visible elements during a batch change.
    535    if (!this.batchFragment) {
    536      this._ensureVisibleElementsAreActive();
    537      goUpdateCommand("downloadsCmd_clearDownloads");
    538    }
    539  },
    540 
    541  onDownloadChanged(download) {
    542    this._viewItemsForDownloads.get(download).onChanged();
    543  },
    544 
    545  onDownloadRemoved(download) {
    546    let element = this._viewItemsForDownloads.get(download).element;
    547 
    548    // If the element was selected exclusively, select its next
    549    // sibling first, if not, try for previous sibling, if any.
    550    if (
    551      (element.nextSibling || element.previousSibling) &&
    552      this._richlistbox.selectedItems &&
    553      this._richlistbox.selectedItems.length == 1 &&
    554      this._richlistbox.selectedItems[0] == element
    555    ) {
    556      this._richlistbox.selectItem(
    557        element.nextSibling || element.previousSibling
    558      );
    559    }
    560 
    561    this._richlistbox.removeItemFromSelection(element);
    562    element.remove();
    563 
    564    // Don't update commands and visible elements during a batch change.
    565    if (!this.batchFragment) {
    566      this._ensureVisibleElementsAreActive();
    567      goUpdateCommand("downloadsCmd_clearDownloads");
    568    }
    569  },
    570 
    571  // nsIController
    572  supportsCommand(aCommand) {
    573    // Firstly, determine if this is a command that we can handle.
    574    if (!DownloadsViewUI.isCommandName(aCommand)) {
    575      return false;
    576    }
    577    if (
    578      !(aCommand in this) &&
    579      !(aCommand in HistoryDownloadElementShell.prototype)
    580    ) {
    581      return false;
    582    }
    583    // If this function returns true, other controllers won't get a chance to
    584    // process the command even if isCommandEnabled returns false, so it's
    585    // important to check if the list is focused here to handle common commands
    586    // like copy and paste correctly. The clear downloads command, instead, is
    587    // specific to the downloads list but can be invoked from the toolbar, so we
    588    // can just return true unconditionally.
    589    return (
    590      aCommand == "downloadsCmd_clearDownloads" ||
    591      document.activeElement == this._richlistbox
    592    );
    593  },
    594 
    595  // nsIController
    596  isCommandEnabled(aCommand) {
    597    switch (aCommand) {
    598      case "cmd_copy":
    599        return Array.prototype.some.call(
    600          this._richlistbox.selectedItems,
    601          element => {
    602            const { source } = element._shell.download;
    603            return !!(source?.originalUrl || source?.url);
    604          }
    605        );
    606      case "downloadsCmd_openReferrer":
    607      case "downloadShowMenuItem":
    608        return this._richlistbox.selectedItems.length == 1;
    609      case "cmd_selectAll":
    610        return true;
    611      case "cmd_paste":
    612        // We check later whether content is valid for pasting, or ignore it.
    613        return Services.clipboard.hasDataMatchingFlavors(
    614          CLIPBOARD_URL_FLAVORS,
    615          Ci.nsIClipboard.kGlobalClipboard
    616        );
    617      case "downloadsCmd_clearDownloads":
    618        return this.canClearDownloads(this._richlistbox);
    619      default:
    620        return Array.prototype.every.call(
    621          this._richlistbox.selectedItems,
    622          element => element._shell.isCommandEnabled(aCommand)
    623        );
    624    }
    625  },
    626 
    627  _copySelectedDownloadsToClipboard() {
    628    let urls = Array.from(this._richlistbox.selectedItems, element => {
    629      const { source } = element._shell.download;
    630      return source?.originalUrl || source?.url;
    631    }).filter(Boolean);
    632 
    633    Cc["@mozilla.org/widget/clipboardhelper;1"]
    634      .getService(Ci.nsIClipboardHelper)
    635      .copyString(urls.join("\n"));
    636  },
    637 
    638  _getURLFromClipboardData() {
    639    let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
    640      Ci.nsITransferable
    641    );
    642    trans.init(null);
    643 
    644    CLIPBOARD_URL_FLAVORS.forEach(trans.addDataFlavor);
    645 
    646    Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
    647 
    648    // Getting the data or creating the nsIURI might fail.
    649    try {
    650      let data = {};
    651      trans.getAnyTransferData({}, data);
    652      let [url, name] = data.value
    653        .QueryInterface(Ci.nsISupportsString)
    654        .data.split("\n");
    655      if (url) {
    656        return [NetUtil.newURI(url).spec, name];
    657      }
    658    } catch (ex) {}
    659 
    660    return ["", ""];
    661  },
    662 
    663  // nsIController
    664  doCommand(aCommand) {
    665    // Commands may be invoked with keyboard shortcuts even if disabled.
    666    if (!this.isCommandEnabled(aCommand)) {
    667      return;
    668    }
    669 
    670    // If this command is not selection-specific, execute it.
    671    if (aCommand in this) {
    672      this[aCommand]();
    673      return;
    674    }
    675 
    676    // Cloning the nodelist into an array to get a frozen list of selected items.
    677    // Otherwise, the selectedItems nodelist is live and doCommand may alter the
    678    // selection while we are trying to do one particular action, like removing
    679    // items from history.
    680    let selectedElements = [...this._richlistbox.selectedItems];
    681    for (let element of selectedElements) {
    682      element._shell.doCommand(aCommand);
    683    }
    684  },
    685 
    686  // nsIController
    687  onEvent() {},
    688 
    689  cmd_copy() {
    690    this._copySelectedDownloadsToClipboard();
    691  },
    692 
    693  cmd_selectAll() {
    694    if (!this.searchTerm) {
    695      this._richlistbox.selectAll();
    696      return;
    697    }
    698    // If there is a filtering search term, some rows are hidden and should not
    699    // be selected.
    700    let oldSuppressOnSelect = this._richlistbox.suppressOnSelect;
    701    this._richlistbox.suppressOnSelect = true;
    702    this._richlistbox.clearSelection();
    703    var item = this._richlistbox.getItemAtIndex(0);
    704    while (item) {
    705      if (!item.hidden) {
    706        this._richlistbox.addItemToSelection(item);
    707      }
    708      item = this._richlistbox.getNextItem(item, 1);
    709    }
    710    this._richlistbox.suppressOnSelect = oldSuppressOnSelect;
    711  },
    712 
    713  cmd_paste() {
    714    let [url, name] = this._getURLFromClipboardData();
    715    if (url) {
    716      let browserWin = BrowserWindowTracker.getTopWindow();
    717      let initiatingDoc = browserWin ? browserWin.document : document;
    718      DownloadURL(url, name, initiatingDoc);
    719    }
    720  },
    721 
    722  downloadsCmd_clearDownloads() {
    723    this._downloadsData.removeFinished();
    724    if (this._place) {
    725      PlacesUtils.history
    726        .removeVisitsByFilter({
    727          transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
    728        })
    729        .catch(console.error);
    730    }
    731    // There may be no selection or focus change as a result
    732    // of these change, and we want the command updated immediately.
    733    goUpdateCommand("downloadsCmd_clearDownloads");
    734  },
    735 
    736  onContextMenu() {
    737    let element = this._richlistbox.selectedItem;
    738    if (!element || !element._shell) {
    739      return false;
    740    }
    741 
    742    let contextMenu = document.getElementById("downloadsContextMenu");
    743    DownloadsViewUI.updateContextMenuForElement(contextMenu, element);
    744    // Hide the copy location item if there is somehow no URL. We have to do
    745    // this here instead of in DownloadsViewUI because DownloadsView doesn't
    746    // allow selecting multiple downloads, so in that view the menuitem will be
    747    // shown according to whether just the selected item has a source URL.
    748    contextMenu.querySelector(".downloadCopyLocationMenuItem").hidden =
    749      !Array.prototype.some.call(
    750        this._richlistbox.selectedItems,
    751        el => !!el._shell.download.source?.url
    752      );
    753 
    754    let download = element._shell.download;
    755    if (!download.stopped) {
    756      // The hasPartialData property of a download may change at any time after
    757      // it has started, so ensure we update the related command now.
    758      goUpdateCommand("downloadsCmd_pauseResume");
    759    }
    760 
    761    return true;
    762  },
    763 
    764  onKeyPress(aEvent) {
    765    let selectedElements = this._richlistbox.selectedItems;
    766    if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
    767      // In the content tree, opening bookmarks by pressing return is only
    768      // supported when a single item is selected. To be consistent, do the
    769      // same here.
    770      if (selectedElements.length == 1) {
    771        let element = selectedElements[0];
    772        if (element._shell) {
    773          element._shell.doDefaultCommand(aEvent);
    774        }
    775      }
    776    } else if (aEvent.charCode == " ".charCodeAt(0)) {
    777      let atLeastOneDownloadToggled = false;
    778      // Pause/Resume every selected download
    779      for (let element of selectedElements) {
    780        if (element._shell.isCommandEnabled("downloadsCmd_pauseResume")) {
    781          element._shell.doCommand("downloadsCmd_pauseResume");
    782          atLeastOneDownloadToggled = true;
    783        }
    784      }
    785 
    786      if (atLeastOneDownloadToggled) {
    787        aEvent.preventDefault();
    788      }
    789    }
    790  },
    791 
    792  onDoubleClick(aEvent) {
    793    if (aEvent.button != 0) {
    794      return;
    795    }
    796 
    797    let selectedElements = this._richlistbox.selectedItems;
    798    if (selectedElements.length != 1) {
    799      return;
    800    }
    801 
    802    let element = selectedElements[0];
    803    if (element._shell) {
    804      element._shell.doDefaultCommand(aEvent);
    805    }
    806  },
    807 
    808  onScroll() {
    809    this._ensureVisibleElementsAreActive(true);
    810  },
    811 
    812  onSelect() {
    813    goUpdateDownloadCommands();
    814 
    815    let selectedElements = this._richlistbox.selectedItems;
    816    for (let elt of selectedElements) {
    817      if (elt._shell) {
    818        elt._shell.onSelect();
    819      }
    820    }
    821  },
    822 
    823  onDragStart(aEvent) {
    824    // TODO Bug 831358: Support d&d for multiple selection.
    825    // For now, we just drag the first element.
    826    let selectedItem = this._richlistbox.selectedItem;
    827    if (!selectedItem) {
    828      return;
    829    }
    830 
    831    let targetPath = selectedItem._shell.download.target.path;
    832    if (!targetPath) {
    833      return;
    834    }
    835 
    836    // We must check for existence synchronously because this is a DOM event.
    837    let file = new FileUtils.File(targetPath);
    838    if (!file.exists()) {
    839      return;
    840    }
    841 
    842    let dt = aEvent.dataTransfer;
    843    dt.mozSetDataAt("application/x-moz-file", file, 0);
    844    let url = Services.io.newFileURI(file).spec;
    845    dt.setData("text/uri-list", url);
    846    dt.effectAllowed = "copyMove";
    847    dt.addElement(selectedItem);
    848  },
    849 
    850  onDragOver(aEvent) {
    851    let types = aEvent.dataTransfer.types;
    852    if (
    853      types.includes("text/uri-list") ||
    854      types.includes("text/x-moz-url") ||
    855      types.includes("text/plain")
    856    ) {
    857      aEvent.preventDefault();
    858    }
    859  },
    860 
    861  onDrop(aEvent) {
    862    let dt = aEvent.dataTransfer;
    863    // If dragged item is from our source, do not try to
    864    // redownload already downloaded file.
    865    if (dt.mozGetDataAt("application/x-moz-file", 0)) {
    866      return;
    867    }
    868 
    869    let links = Services.droppedLinkHandler.dropLinks(aEvent);
    870    if (!links.length) {
    871      return;
    872    }
    873    aEvent.preventDefault();
    874    let browserWin = BrowserWindowTracker.getTopWindow();
    875    let initiatingDoc = browserWin ? browserWin.document : document;
    876    for (let link of links) {
    877      if (link.url.startsWith("about:")) {
    878        continue;
    879      }
    880      DownloadURL(link.url, link.name, initiatingDoc);
    881    }
    882  },
    883 };
    884 Object.setPrototypeOf(
    885  DownloadsPlacesView.prototype,
    886  DownloadsViewUI.BaseView.prototype
    887 );
    888 
    889 for (let methodName of ["load", "applyFilter", "selectNode", "selectItems"]) {
    890  DownloadsPlacesView.prototype[methodName] = function () {
    891    throw new Error(
    892      "|" + methodName + "| is not implemented by the downloads view."
    893    );
    894  };
    895 }
    896 
    897 function goUpdateDownloadCommands() {
    898  function updateCommandsForObject(object) {
    899    for (let name in object) {
    900      if (DownloadsViewUI.isCommandName(name)) {
    901        goUpdateCommand(name);
    902      }
    903    }
    904  }
    905  updateCommandsForObject(DownloadsPlacesView.prototype);
    906  updateCommandsForObject(HistoryDownloadElementShell.prototype);
    907 }
    908 
    909 document.addEventListener("DOMContentLoaded", function () {
    910  let richListBox = document.getElementById("downloadsListBox");
    911  richListBox.addEventListener("scroll", function () {
    912    return this._placesView.onScroll();
    913  });
    914  richListBox.addEventListener("keypress", function (event) {
    915    return this._placesView.onKeyPress(event);
    916  });
    917  richListBox.addEventListener("dblclick", function (event) {
    918    return this._placesView.onDoubleClick(event);
    919  });
    920  richListBox.addEventListener("contextmenu", function (event) {
    921    return this._placesView.onContextMenu(event);
    922  });
    923  richListBox.addEventListener("dragstart", function (event) {
    924    this._placesView.onDragStart(event);
    925  });
    926  let dropNode = richListBox;
    927  // In about:downloads, also allow drops if the list is empty, by
    928  // adding the listener to the document, as the richlistbox is
    929  // hidden when it is empty.
    930  if (document.documentElement.id == "contentAreaDownloadsView") {
    931    dropNode = richListBox.parentNode;
    932  }
    933  dropNode.addEventListener("dragover", function (event) {
    934    richListBox._placesView.onDragOver(event);
    935  });
    936  dropNode.addEventListener("drop", function (event) {
    937    richListBox._placesView.onDrop(event);
    938  });
    939  richListBox.addEventListener("select", function () {
    940    this._placesView.onSelect();
    941  });
    942  richListBox.addEventListener("focus", goUpdateDownloadCommands);
    943  richListBox.addEventListener("blur", goUpdateDownloadCommands);
    944 });