forked from mirrors/gecko-dev
		
	Most usage is a straight replacement but gtk needs extra changes as it transfers plain text in UTF8 natively and needs to be converted into UTF16, and Windows uses single-byte characters for RTF and CF_HTML formats so we preserve this. Differential Revision: https://phabricator.services.mozilla.com/D158587
		
			
				
	
	
		
			952 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			952 lines
		
	
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | 
						|
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
/* eslint-env mozilla/browser-window */
 | 
						|
 | 
						|
var { XPCOMUtils } = ChromeUtils.importESModule(
 | 
						|
  "resource://gre/modules/XPCOMUtils.sys.mjs"
 | 
						|
);
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(this, {
 | 
						|
  Downloads: "resource://gre/modules/Downloads.sys.mjs",
 | 
						|
  DownloadsCommon: "resource:///modules/DownloadsCommon.sys.mjs",
 | 
						|
  DownloadsViewUI: "resource:///modules/DownloadsViewUI.sys.mjs",
 | 
						|
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
 | 
						|
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetters(this, {
 | 
						|
  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
 | 
						|
  NetUtil: "resource://gre/modules/NetUtil.jsm",
 | 
						|
});
 | 
						|
 | 
						|
/**
 | 
						|
 * A download element shell is responsible for handling the commands and the
 | 
						|
 * displayed data for a single download view element.
 | 
						|
 *
 | 
						|
 * The shell may contain a session download, a history download, or both.  When
 | 
						|
 * both a history and a session download are present, the session download gets
 | 
						|
 * priority and its information is displayed.
 | 
						|
 *
 | 
						|
 * On construction, a new richlistitem is created, and can be accessed through
 | 
						|
 * the |element| getter. The shell doesn't insert the item in a richlistbox, the
 | 
						|
 * caller must do it and remove the element when it's no longer needed.
 | 
						|
 *
 | 
						|
 * The caller is also responsible for forwarding status notifications, calling
 | 
						|
 * the onChanged method.
 | 
						|
 *
 | 
						|
 * @param download
 | 
						|
 *        The Download object from the DownloadHistoryList.
 | 
						|
 */
 | 
						|
function HistoryDownloadElementShell(download) {
 | 
						|
  this._download = download;
 | 
						|
 | 
						|
  this.element = document.createXULElement("richlistitem");
 | 
						|
  this.element._shell = this;
 | 
						|
 | 
						|
  this.element.classList.add("download");
 | 
						|
  this.element.classList.add("download-state");
 | 
						|
}
 | 
						|
 | 
						|
HistoryDownloadElementShell.prototype = {
 | 
						|
  /**
 | 
						|
   * Overrides the base getter to return the Download or HistoryDownload object
 | 
						|
   * for displaying information and executing commands in the user interface.
 | 
						|
   */
 | 
						|
  get download() {
 | 
						|
    return this._download;
 | 
						|
  },
 | 
						|
 | 
						|
  onStateChanged() {
 | 
						|
    // Since the state changed, we may need to check the target file again.
 | 
						|
    this._targetFileChecked = false;
 | 
						|
 | 
						|
    this._updateState();
 | 
						|
 | 
						|
    if (this.element.selected) {
 | 
						|
      goUpdateDownloadCommands();
 | 
						|
    } else {
 | 
						|
      // If a state change occurs in an item that is not currently selected,
 | 
						|
      // this is the only command that may be affected.
 | 
						|
      goUpdateCommand("downloadsCmd_clearDownloads");
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onChanged() {
 | 
						|
    // There is nothing to do if the item has always been invisible.
 | 
						|
    if (!this.active) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let newState = DownloadsCommon.stateOfDownload(this.download);
 | 
						|
    if (this._downloadState !== newState) {
 | 
						|
      this._downloadState = newState;
 | 
						|
      this.onStateChanged();
 | 
						|
    } else {
 | 
						|
      this._updateStateInner();
 | 
						|
    }
 | 
						|
  },
 | 
						|
  _downloadState: null,
 | 
						|
 | 
						|
  isCommandEnabled(aCommand) {
 | 
						|
    // The only valid command for inactive elements is cmd_delete.
 | 
						|
    if (!this.active && aCommand != "cmd_delete") {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    return DownloadsViewUI.DownloadElementShell.prototype.isCommandEnabled.call(
 | 
						|
      this,
 | 
						|
      aCommand
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  downloadsCmd_unblock() {
 | 
						|
    this.confirmUnblock(window, "unblock");
 | 
						|
  },
 | 
						|
  downloadsCmd_unblockAndSave() {
 | 
						|
    this.confirmUnblock(window, "unblock");
 | 
						|
  },
 | 
						|
 | 
						|
  downloadsCmd_chooseUnblock() {
 | 
						|
    this.confirmUnblock(window, "chooseUnblock");
 | 
						|
  },
 | 
						|
 | 
						|
  downloadsCmd_chooseOpen() {
 | 
						|
    this.confirmUnblock(window, "chooseOpen");
 | 
						|
  },
 | 
						|
 | 
						|
  // Returns whether or not the download handled by this shell should
 | 
						|
  // show up in the search results for the given term.  Both the display
 | 
						|
  // name for the download and the url are searched.
 | 
						|
  matchesSearchTerm(aTerm) {
 | 
						|
    if (!aTerm) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    aTerm = aTerm.toLowerCase();
 | 
						|
    let displayName = DownloadsViewUI.getDisplayName(this.download);
 | 
						|
    return (
 | 
						|
      displayName.toLowerCase().includes(aTerm) ||
 | 
						|
      (this.download.source.originalUrl || this.download.source.url)
 | 
						|
        .toLowerCase()
 | 
						|
        .includes(aTerm)
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  // Handles double-click and return keypress on the element (the keypress
 | 
						|
  // listener is set in the DownloadsPlacesView object).
 | 
						|
  doDefaultCommand(event) {
 | 
						|
    let command = this.currentDefaultCommandName;
 | 
						|
    if (
 | 
						|
      command == "downloadsCmd_open" &&
 | 
						|
      event &&
 | 
						|
      (event.shiftKey || event.ctrlKey || event.metaKey || event.button == 1)
 | 
						|
    ) {
 | 
						|
      // We adjust the command for supported modifiers to suggest where the download may
 | 
						|
      // be opened.
 | 
						|
      let browserWin = BrowserWindowTracker.getTopWindow();
 | 
						|
      let openWhere = browserWin
 | 
						|
        ? browserWin.whereToOpenLink(event, false, true)
 | 
						|
        : "window";
 | 
						|
      if (["window", "tabshifted", "tab"].includes(openWhere)) {
 | 
						|
        command += ":" + openWhere;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (command && this.isCommandEnabled(command)) {
 | 
						|
      this.doCommand(command);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * This method is called by the outer download view, after the controller
 | 
						|
   * commands have already been updated. In case we did not check for the
 | 
						|
   * existence of the target file already, we can do it now and then update
 | 
						|
   * the commands as needed.
 | 
						|
   */
 | 
						|
  onSelect() {
 | 
						|
    if (!this.active) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // If this is a history download for which no target file information is
 | 
						|
    // available, we cannot retrieve information about the target file.
 | 
						|
    if (!this.download.target.path) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Start checking for existence.  This may be done twice if onSelect is
 | 
						|
    // called again before the information is collected.
 | 
						|
    if (!this._targetFileChecked) {
 | 
						|
      this.download
 | 
						|
        .refresh()
 | 
						|
        .catch(console.error)
 | 
						|
        .then(() => {
 | 
						|
          // Do not try to check for existence again even if this failed.
 | 
						|
          this._targetFileChecked = true;
 | 
						|
        });
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
Object.setPrototypeOf(
 | 
						|
  HistoryDownloadElementShell.prototype,
 | 
						|
  DownloadsViewUI.DownloadElementShell.prototype
 | 
						|
);
 | 
						|
 | 
						|
/**
 | 
						|
 * Relays commands from the download.xml binding to the selected items.
 | 
						|
 */
 | 
						|
var DownloadsView = {
 | 
						|
  onDownloadButton(event) {
 | 
						|
    event.target.closest("richlistitem")._shell.onButton();
 | 
						|
  },
 | 
						|
 | 
						|
  onDownloadClick() {},
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * A Downloads Places View is a places view designed to show a places query
 | 
						|
 * for history downloads alongside the session downloads.
 | 
						|
 *
 | 
						|
 * As we don't use the places controller, some methods implemented by other
 | 
						|
 * places views are not implemented by this view.
 | 
						|
 *
 | 
						|
 * A richlistitem in this view can represent either a past download or a session
 | 
						|
 * download, or both. Session downloads are shown first in the view, and as long
 | 
						|
 * as they exist they "collapses" their history "counterpart" (So we don't show two
 | 
						|
 * items for every download).
 | 
						|
 */
 | 
						|
function DownloadsPlacesView(
 | 
						|
  aRichListBox,
 | 
						|
  aActive = true,
 | 
						|
  aSuppressionFlag = DownloadsCommon.SUPPRESS_ALL_DOWNLOADS_OPEN
 | 
						|
) {
 | 
						|
  this._richlistbox = aRichListBox;
 | 
						|
  this._richlistbox._placesView = this;
 | 
						|
  window.controllers.insertControllerAt(0, this);
 | 
						|
 | 
						|
  // Map downloads to their element shells.
 | 
						|
  this._viewItemsForDownloads = new WeakMap();
 | 
						|
 | 
						|
  this._searchTerm = "";
 | 
						|
 | 
						|
  this._active = aActive;
 | 
						|
 | 
						|
  // Register as a downloads view. The places data will be initialized by
 | 
						|
  // the places setter.
 | 
						|
  this._initiallySelectedElement = null;
 | 
						|
  this._downloadsData = DownloadsCommon.getData(window.opener || window, true);
 | 
						|
  this._waitingForInitialData = true;
 | 
						|
  this._downloadsData.addView(this);
 | 
						|
 | 
						|
  // Pause the download indicator as user is interacting with downloads. This is
 | 
						|
  // skipped on about:downloads because it handles this by itself.
 | 
						|
  if (aSuppressionFlag === DownloadsCommon.SUPPRESS_ALL_DOWNLOADS_OPEN) {
 | 
						|
    DownloadsCommon.getIndicatorData(
 | 
						|
      window
 | 
						|
    ).attentionSuppressed |= aSuppressionFlag;
 | 
						|
  }
 | 
						|
 | 
						|
  // Make sure to unregister the view if the window is closed.
 | 
						|
  window.addEventListener(
 | 
						|
    "unload",
 | 
						|
    () => {
 | 
						|
      window.controllers.removeController(this);
 | 
						|
      // Unpause the main window's download indicator.
 | 
						|
      DownloadsCommon.getIndicatorData(
 | 
						|
        window
 | 
						|
      ).attentionSuppressed &= ~aSuppressionFlag;
 | 
						|
      this._downloadsData.removeView(this);
 | 
						|
      this.result = null;
 | 
						|
    },
 | 
						|
    true
 | 
						|
  );
 | 
						|
  // Resizing the window may change items visibility.
 | 
						|
  window.addEventListener(
 | 
						|
    "resize",
 | 
						|
    () => {
 | 
						|
      this._ensureVisibleElementsAreActive(true);
 | 
						|
    },
 | 
						|
    true
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
DownloadsPlacesView.prototype = {
 | 
						|
  get associatedElement() {
 | 
						|
    return this._richlistbox;
 | 
						|
  },
 | 
						|
 | 
						|
  get active() {
 | 
						|
    return this._active;
 | 
						|
  },
 | 
						|
  set active(val) {
 | 
						|
    this._active = val;
 | 
						|
    if (this._active) {
 | 
						|
      this._ensureVisibleElementsAreActive(true);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Ensure the custom element contents are created and shown for each
 | 
						|
   * visible element in the list.
 | 
						|
   *
 | 
						|
   * @param debounce whether to use a short timeout rather than running
 | 
						|
   *                 immediately. The default is running immediately. If you
 | 
						|
   *                 pass `true`, we'll run on a 10ms timeout. This is used to
 | 
						|
   *                 avoid running this code lots while scrolling or resizing.
 | 
						|
   */
 | 
						|
  _ensureVisibleElementsAreActive(debounce = false) {
 | 
						|
    if (
 | 
						|
      !this.active ||
 | 
						|
      (debounce && this._ensureVisibleTimer) ||
 | 
						|
      !this._richlistbox.firstChild
 | 
						|
    ) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (debounce) {
 | 
						|
      this._ensureVisibleTimer = setTimeout(() => {
 | 
						|
        this._internalEnsureVisibleElementsAreActive();
 | 
						|
      }, 10);
 | 
						|
    } else {
 | 
						|
      this._internalEnsureVisibleElementsAreActive();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _internalEnsureVisibleElementsAreActive() {
 | 
						|
    // If there are no children, we can't do anything so bail out.
 | 
						|
    // However, avoid clearing the timer because there may be children
 | 
						|
    // when the timer fires.
 | 
						|
    if (!this._richlistbox.firstChild) {
 | 
						|
      // If we were called asynchronously (debounced), we need to delete
 | 
						|
      // the timer variable to ensure we are called again if another
 | 
						|
      // debounced call comes in.
 | 
						|
      delete this._ensureVisibleTimer;
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this._ensureVisibleTimer) {
 | 
						|
      clearTimeout(this._ensureVisibleTimer);
 | 
						|
      delete this._ensureVisibleTimer;
 | 
						|
    }
 | 
						|
 | 
						|
    let rlbRect = this._richlistbox.getBoundingClientRect();
 | 
						|
    let winUtils = window.windowUtils;
 | 
						|
    let nodes = winUtils.nodesFromRect(
 | 
						|
      rlbRect.left,
 | 
						|
      rlbRect.top,
 | 
						|
      0,
 | 
						|
      rlbRect.width,
 | 
						|
      rlbRect.height,
 | 
						|
      0,
 | 
						|
      true,
 | 
						|
      false,
 | 
						|
      false
 | 
						|
    );
 | 
						|
    // nodesFromRect returns nodes in z-index order, and for the same z-index
 | 
						|
    // sorts them in inverted DOM order, thus starting from the one that would
 | 
						|
    // be on top.
 | 
						|
    let firstVisibleNode, lastVisibleNode;
 | 
						|
    for (let node of nodes) {
 | 
						|
      if (node.localName === "richlistitem" && node._shell) {
 | 
						|
        node._shell.ensureActive();
 | 
						|
        // The first visible node is the last match.
 | 
						|
        firstVisibleNode = node;
 | 
						|
        // While the last visible node is the first match.
 | 
						|
        if (!lastVisibleNode) {
 | 
						|
          lastVisibleNode = node;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Also activate the first invisible nodes in both boundaries (that is,
 | 
						|
    // above and below the visible area) to ensure proper keyboard navigation
 | 
						|
    // in both directions.
 | 
						|
    let nodeBelowVisibleArea = lastVisibleNode && lastVisibleNode.nextSibling;
 | 
						|
    if (nodeBelowVisibleArea && nodeBelowVisibleArea._shell) {
 | 
						|
      nodeBelowVisibleArea._shell.ensureActive();
 | 
						|
    }
 | 
						|
 | 
						|
    let nodeAboveVisibleArea =
 | 
						|
      firstVisibleNode && firstVisibleNode.previousSibling;
 | 
						|
    if (nodeAboveVisibleArea && nodeAboveVisibleArea._shell) {
 | 
						|
      nodeAboveVisibleArea._shell.ensureActive();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _place: "",
 | 
						|
  get place() {
 | 
						|
    return this._place;
 | 
						|
  },
 | 
						|
  set place(val) {
 | 
						|
    if (this._place == val) {
 | 
						|
      // XXXmano: places.js relies on this behavior (see Bug 822203).
 | 
						|
      this.searchTerm = "";
 | 
						|
    } else {
 | 
						|
      this._place = val;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  get selectedNodes() {
 | 
						|
    return Array.prototype.filter.call(
 | 
						|
      this._richlistbox.selectedItems,
 | 
						|
      element => element._shell.download.placesNode
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  get selectedNode() {
 | 
						|
    let selectedNodes = this.selectedNodes;
 | 
						|
    return selectedNodes.length == 1 ? selectedNodes[0] : null;
 | 
						|
  },
 | 
						|
 | 
						|
  get hasSelection() {
 | 
						|
    return !!this.selectedNodes.length;
 | 
						|
  },
 | 
						|
 | 
						|
  get controller() {
 | 
						|
    return this._richlistbox.controller;
 | 
						|
  },
 | 
						|
 | 
						|
  get searchTerm() {
 | 
						|
    return this._searchTerm;
 | 
						|
  },
 | 
						|
  set searchTerm(aValue) {
 | 
						|
    if (this._searchTerm != aValue) {
 | 
						|
      // Always clear selection on a new search, since the user is starting a
 | 
						|
      // different workflow. This also solves the fact we could end up
 | 
						|
      // retaining selection on hidden elements.
 | 
						|
      this._richlistbox.clearSelection();
 | 
						|
      for (let element of this._richlistbox.childNodes) {
 | 
						|
        element.hidden = !element._shell.matchesSearchTerm(aValue);
 | 
						|
      }
 | 
						|
      this._ensureVisibleElementsAreActive();
 | 
						|
    }
 | 
						|
    this._searchTerm = aValue;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * When the view loads, we want to select the first item.
 | 
						|
   * However, because session downloads, for which the data is loaded
 | 
						|
   * asynchronously, always come first in the list, and because the list
 | 
						|
   * may (or may not) already contain history downloads at that point, it
 | 
						|
   * turns out that by the time we can select the first item, the user may
 | 
						|
   * have already started using the view.
 | 
						|
   * To make things even more complicated, in other cases, the places data
 | 
						|
   * may be loaded after the session downloads data.  Thus we cannot rely on
 | 
						|
   * the order in which the data comes in.
 | 
						|
   * We work around this by attempting to select the first element twice,
 | 
						|
   * once after the places data is loaded and once when the session downloads
 | 
						|
   * data is done loading.  However, if the selection has changed in-between,
 | 
						|
   * we assume the user has already started using the view and give up.
 | 
						|
   */
 | 
						|
  _ensureInitialSelection() {
 | 
						|
    // Either they're both null, or the selection has not changed in between.
 | 
						|
    if (this._richlistbox.selectedItem == this._initiallySelectedElement) {
 | 
						|
      let firstDownloadElement = this._richlistbox.firstChild;
 | 
						|
      if (firstDownloadElement != this._initiallySelectedElement) {
 | 
						|
        // We may be called before _ensureVisibleElementsAreActive,
 | 
						|
        // therefore, ensure the first item is activated.
 | 
						|
        firstDownloadElement._shell.ensureActive();
 | 
						|
        this._richlistbox.selectedItem = firstDownloadElement;
 | 
						|
        this._richlistbox.currentItem = firstDownloadElement;
 | 
						|
        this._initiallySelectedElement = firstDownloadElement;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * DocumentFragment object that contains all the new elements added during a
 | 
						|
   * batch operation, or null if no batch is in progress.
 | 
						|
   *
 | 
						|
   * Since newest downloads are displayed at the top, elements are normally
 | 
						|
   * prepended to the fragment, and then the fragment is prepended to the list.
 | 
						|
   */
 | 
						|
  batchFragment: null,
 | 
						|
 | 
						|
  onDownloadBatchStarting() {
 | 
						|
    this.batchFragment = document.createDocumentFragment();
 | 
						|
 | 
						|
    this.oldSuppressOnSelect = this._richlistbox.suppressOnSelect;
 | 
						|
    this._richlistbox.suppressOnSelect = true;
 | 
						|
  },
 | 
						|
 | 
						|
  onDownloadBatchEnded() {
 | 
						|
    this._richlistbox.suppressOnSelect = this.oldSuppressOnSelect;
 | 
						|
    delete this.oldSuppressOnSelect;
 | 
						|
 | 
						|
    if (this.batchFragment.childElementCount) {
 | 
						|
      this._prependBatchFragment();
 | 
						|
    }
 | 
						|
    this.batchFragment = null;
 | 
						|
 | 
						|
    this._ensureInitialSelection();
 | 
						|
    this._ensureVisibleElementsAreActive();
 | 
						|
    goUpdateDownloadCommands();
 | 
						|
    if (this._waitingForInitialData) {
 | 
						|
      this._waitingForInitialData = false;
 | 
						|
      this._richlistbox.dispatchEvent(
 | 
						|
        new CustomEvent("InitialDownloadsLoaded")
 | 
						|
      );
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _prependBatchFragment() {
 | 
						|
    // Workaround multiple reflows hang by removing the richlistbox
 | 
						|
    // and adding it back when we're done.
 | 
						|
 | 
						|
    // Hack for bug 836283: reset xbl fields to their old values after the
 | 
						|
    // binding is reattached to avoid breaking the selection state
 | 
						|
    let xblFields = new Map();
 | 
						|
    for (let key of Object.getOwnPropertyNames(this._richlistbox)) {
 | 
						|
      let value = this._richlistbox[key];
 | 
						|
      xblFields.set(key, value);
 | 
						|
    }
 | 
						|
 | 
						|
    let oldActiveElement = document.activeElement;
 | 
						|
    let parentNode = this._richlistbox.parentNode;
 | 
						|
    let nextSibling = this._richlistbox.nextSibling;
 | 
						|
    parentNode.removeChild(this._richlistbox);
 | 
						|
    this._richlistbox.prepend(this.batchFragment);
 | 
						|
    parentNode.insertBefore(this._richlistbox, nextSibling);
 | 
						|
    if (oldActiveElement && oldActiveElement != document.activeElement) {
 | 
						|
      oldActiveElement.focus();
 | 
						|
    }
 | 
						|
 | 
						|
    for (let [key, value] of xblFields) {
 | 
						|
      this._richlistbox[key] = value;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onDownloadAdded(download, { insertBefore } = {}) {
 | 
						|
    let shell = new HistoryDownloadElementShell(download);
 | 
						|
    this._viewItemsForDownloads.set(download, shell);
 | 
						|
 | 
						|
    // Since newest downloads are displayed at the top, either prepend the new
 | 
						|
    // element or insert it after the one indicated by the insertBefore option.
 | 
						|
    if (insertBefore) {
 | 
						|
      this._viewItemsForDownloads
 | 
						|
        .get(insertBefore)
 | 
						|
        .element.insertAdjacentElement("afterend", shell.element);
 | 
						|
    } else {
 | 
						|
      (this.batchFragment || this._richlistbox).prepend(shell.element);
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.searchTerm) {
 | 
						|
      shell.element.hidden = !shell.matchesSearchTerm(this.searchTerm);
 | 
						|
    }
 | 
						|
 | 
						|
    // Don't update commands and visible elements during a batch change.
 | 
						|
    if (!this.batchFragment) {
 | 
						|
      this._ensureVisibleElementsAreActive();
 | 
						|
      goUpdateCommand("downloadsCmd_clearDownloads");
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onDownloadChanged(download) {
 | 
						|
    this._viewItemsForDownloads.get(download).onChanged();
 | 
						|
  },
 | 
						|
 | 
						|
  onDownloadRemoved(download) {
 | 
						|
    let element = this._viewItemsForDownloads.get(download).element;
 | 
						|
 | 
						|
    // If the element was selected exclusively, select its next
 | 
						|
    // sibling first, if not, try for previous sibling, if any.
 | 
						|
    if (
 | 
						|
      (element.nextSibling || element.previousSibling) &&
 | 
						|
      this._richlistbox.selectedItems &&
 | 
						|
      this._richlistbox.selectedItems.length == 1 &&
 | 
						|
      this._richlistbox.selectedItems[0] == element
 | 
						|
    ) {
 | 
						|
      this._richlistbox.selectItem(
 | 
						|
        element.nextSibling || element.previousSibling
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    this._richlistbox.removeItemFromSelection(element);
 | 
						|
    element.remove();
 | 
						|
 | 
						|
    // Don't update commands and visible elements during a batch change.
 | 
						|
    if (!this.batchFragment) {
 | 
						|
      this._ensureVisibleElementsAreActive();
 | 
						|
      goUpdateCommand("downloadsCmd_clearDownloads");
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  // nsIController
 | 
						|
  supportsCommand(aCommand) {
 | 
						|
    // Firstly, determine if this is a command that we can handle.
 | 
						|
    if (!DownloadsViewUI.isCommandName(aCommand)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    if (
 | 
						|
      !(aCommand in this) &&
 | 
						|
      !(aCommand in HistoryDownloadElementShell.prototype)
 | 
						|
    ) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    // If this function returns true, other controllers won't get a chance to
 | 
						|
    // process the command even if isCommandEnabled returns false, so it's
 | 
						|
    // important to check if the list is focused here to handle common commands
 | 
						|
    // like copy and paste correctly. The clear downloads command, instead, is
 | 
						|
    // specific to the downloads list but can be invoked from the toolbar, so we
 | 
						|
    // can just return true unconditionally.
 | 
						|
    return (
 | 
						|
      aCommand == "downloadsCmd_clearDownloads" ||
 | 
						|
      document.activeElement == this._richlistbox
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  // nsIController
 | 
						|
  isCommandEnabled(aCommand) {
 | 
						|
    switch (aCommand) {
 | 
						|
      case "cmd_copy":
 | 
						|
        return Array.prototype.some.call(
 | 
						|
          this._richlistbox.selectedItems,
 | 
						|
          element => {
 | 
						|
            const { source } = element._shell.download;
 | 
						|
            return !!(source?.originalUrl || source?.url);
 | 
						|
          }
 | 
						|
        );
 | 
						|
      case "downloadsCmd_openReferrer":
 | 
						|
      case "downloadShowMenuItem":
 | 
						|
        return this._richlistbox.selectedItems.length == 1;
 | 
						|
      case "cmd_selectAll":
 | 
						|
        return true;
 | 
						|
      case "cmd_paste":
 | 
						|
        return this._canDownloadClipboardURL();
 | 
						|
      case "downloadsCmd_clearDownloads":
 | 
						|
        return this.canClearDownloads(this._richlistbox);
 | 
						|
      default:
 | 
						|
        return Array.prototype.every.call(
 | 
						|
          this._richlistbox.selectedItems,
 | 
						|
          element => element._shell.isCommandEnabled(aCommand)
 | 
						|
        );
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _copySelectedDownloadsToClipboard() {
 | 
						|
    let urls = Array.from(this._richlistbox.selectedItems, element => {
 | 
						|
      const { source } = element._shell.download;
 | 
						|
      return source?.originalUrl || source?.url;
 | 
						|
    }).filter(Boolean);
 | 
						|
 | 
						|
    Cc["@mozilla.org/widget/clipboardhelper;1"]
 | 
						|
      .getService(Ci.nsIClipboardHelper)
 | 
						|
      .copyString(urls.join("\n"));
 | 
						|
  },
 | 
						|
 | 
						|
  _getURLFromClipboardData() {
 | 
						|
    let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
 | 
						|
      Ci.nsITransferable
 | 
						|
    );
 | 
						|
    trans.init(null);
 | 
						|
 | 
						|
    let flavors = ["text/x-moz-url", "text/plain"];
 | 
						|
    flavors.forEach(trans.addDataFlavor);
 | 
						|
 | 
						|
    Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
 | 
						|
 | 
						|
    // Getting the data or creating the nsIURI might fail.
 | 
						|
    try {
 | 
						|
      let data = {};
 | 
						|
      trans.getAnyTransferData({}, data);
 | 
						|
      let [url, name] = data.value
 | 
						|
        .QueryInterface(Ci.nsISupportsString)
 | 
						|
        .data.split("\n");
 | 
						|
      if (url) {
 | 
						|
        return [NetUtil.newURI(url).spec, name];
 | 
						|
      }
 | 
						|
    } catch (ex) {}
 | 
						|
 | 
						|
    return ["", ""];
 | 
						|
  },
 | 
						|
 | 
						|
  _canDownloadClipboardURL() {
 | 
						|
    let [url /* ,name */] = this._getURLFromClipboardData();
 | 
						|
    return url != "";
 | 
						|
  },
 | 
						|
 | 
						|
  _downloadURLFromClipboard() {
 | 
						|
    let [url, name] = this._getURLFromClipboardData();
 | 
						|
    let browserWin = BrowserWindowTracker.getTopWindow();
 | 
						|
    let initiatingDoc = browserWin ? browserWin.document : document;
 | 
						|
    DownloadURL(url, name, initiatingDoc);
 | 
						|
  },
 | 
						|
 | 
						|
  // nsIController
 | 
						|
  doCommand(aCommand) {
 | 
						|
    // Commands may be invoked with keyboard shortcuts even if disabled.
 | 
						|
    if (!this.isCommandEnabled(aCommand)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // If this command is not selection-specific, execute it.
 | 
						|
    if (aCommand in this) {
 | 
						|
      this[aCommand]();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Cloning the nodelist into an array to get a frozen list of selected items.
 | 
						|
    // Otherwise, the selectedItems nodelist is live and doCommand may alter the
 | 
						|
    // selection while we are trying to do one particular action, like removing
 | 
						|
    // items from history.
 | 
						|
    let selectedElements = [...this._richlistbox.selectedItems];
 | 
						|
    for (let element of selectedElements) {
 | 
						|
      element._shell.doCommand(aCommand);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  // nsIController
 | 
						|
  onEvent() {},
 | 
						|
 | 
						|
  cmd_copy() {
 | 
						|
    this._copySelectedDownloadsToClipboard();
 | 
						|
  },
 | 
						|
 | 
						|
  cmd_selectAll() {
 | 
						|
    if (!this.searchTerm) {
 | 
						|
      this._richlistbox.selectAll();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    // If there is a filtering search term, some rows are hidden and should not
 | 
						|
    // be selected.
 | 
						|
    let oldSuppressOnSelect = this._richlistbox.suppressOnSelect;
 | 
						|
    this._richlistbox.suppressOnSelect = true;
 | 
						|
    this._richlistbox.clearSelection();
 | 
						|
    var item = this._richlistbox.getItemAtIndex(0);
 | 
						|
    while (item) {
 | 
						|
      if (!item.hidden) {
 | 
						|
        this._richlistbox.addItemToSelection(item);
 | 
						|
      }
 | 
						|
      item = this._richlistbox.getNextItem(item, 1);
 | 
						|
    }
 | 
						|
    this._richlistbox.suppressOnSelect = oldSuppressOnSelect;
 | 
						|
  },
 | 
						|
 | 
						|
  cmd_paste() {
 | 
						|
    this._downloadURLFromClipboard();
 | 
						|
  },
 | 
						|
 | 
						|
  downloadsCmd_clearDownloads() {
 | 
						|
    this._downloadsData.removeFinished();
 | 
						|
    if (this._place) {
 | 
						|
      PlacesUtils.history
 | 
						|
        .removeVisitsByFilter({
 | 
						|
          transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
 | 
						|
        })
 | 
						|
        .catch(console.error);
 | 
						|
    }
 | 
						|
    // There may be no selection or focus change as a result
 | 
						|
    // of these change, and we want the command updated immediately.
 | 
						|
    goUpdateCommand("downloadsCmd_clearDownloads");
 | 
						|
  },
 | 
						|
 | 
						|
  onContextMenu(aEvent) {
 | 
						|
    let element = this._richlistbox.selectedItem;
 | 
						|
    if (!element || !element._shell) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    let contextMenu = document.getElementById("downloadsContextMenu");
 | 
						|
    DownloadsViewUI.updateContextMenuForElement(contextMenu, element);
 | 
						|
    // Hide the copy location item if there is somehow no URL. We have to do
 | 
						|
    // this here instead of in DownloadsViewUI because DownloadsView doesn't
 | 
						|
    // allow selecting multiple downloads, so in that view the menuitem will be
 | 
						|
    // shown according to whether just the selected item has a source URL.
 | 
						|
    contextMenu.querySelector(
 | 
						|
      ".downloadCopyLocationMenuItem"
 | 
						|
    ).hidden = !Array.prototype.some.call(
 | 
						|
      this._richlistbox.selectedItems,
 | 
						|
      el => !!el._shell.download.source?.url
 | 
						|
    );
 | 
						|
 | 
						|
    let download = element._shell.download;
 | 
						|
    if (!download.stopped) {
 | 
						|
      // The hasPartialData property of a download may change at any time after
 | 
						|
      // it has started, so ensure we update the related command now.
 | 
						|
      goUpdateCommand("downloadsCmd_pauseResume");
 | 
						|
    }
 | 
						|
 | 
						|
    return true;
 | 
						|
  },
 | 
						|
 | 
						|
  onKeyPress(aEvent) {
 | 
						|
    let selectedElements = this._richlistbox.selectedItems;
 | 
						|
    if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
 | 
						|
      // In the content tree, opening bookmarks by pressing return is only
 | 
						|
      // supported when a single item is selected. To be consistent, do the
 | 
						|
      // same here.
 | 
						|
      if (selectedElements.length == 1) {
 | 
						|
        let element = selectedElements[0];
 | 
						|
        if (element._shell) {
 | 
						|
          element._shell.doDefaultCommand(aEvent);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    } else if (aEvent.charCode == " ".charCodeAt(0)) {
 | 
						|
      let atLeastOneDownloadToggled = false;
 | 
						|
      // Pause/Resume every selected download
 | 
						|
      for (let element of selectedElements) {
 | 
						|
        if (element._shell.isCommandEnabled("downloadsCmd_pauseResume")) {
 | 
						|
          element._shell.doCommand("downloadsCmd_pauseResume");
 | 
						|
          atLeastOneDownloadToggled = true;
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      if (atLeastOneDownloadToggled) {
 | 
						|
        aEvent.preventDefault();
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onDoubleClick(aEvent) {
 | 
						|
    if (aEvent.button != 0) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let selectedElements = this._richlistbox.selectedItems;
 | 
						|
    if (selectedElements.length != 1) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let element = selectedElements[0];
 | 
						|
    if (element._shell) {
 | 
						|
      element._shell.doDefaultCommand(aEvent);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onScroll() {
 | 
						|
    this._ensureVisibleElementsAreActive(true);
 | 
						|
  },
 | 
						|
 | 
						|
  onSelect() {
 | 
						|
    goUpdateDownloadCommands();
 | 
						|
 | 
						|
    let selectedElements = this._richlistbox.selectedItems;
 | 
						|
    for (let elt of selectedElements) {
 | 
						|
      if (elt._shell) {
 | 
						|
        elt._shell.onSelect();
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onDragStart(aEvent) {
 | 
						|
    // TODO Bug 831358: Support d&d for multiple selection.
 | 
						|
    // For now, we just drag the first element.
 | 
						|
    let selectedItem = this._richlistbox.selectedItem;
 | 
						|
    if (!selectedItem) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let targetPath = selectedItem._shell.download.target.path;
 | 
						|
    if (!targetPath) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // We must check for existence synchronously because this is a DOM event.
 | 
						|
    let file = new FileUtils.File(targetPath);
 | 
						|
    if (!file.exists()) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let dt = aEvent.dataTransfer;
 | 
						|
    dt.mozSetDataAt("application/x-moz-file", file, 0);
 | 
						|
    let url = Services.io.newFileURI(file).spec;
 | 
						|
    dt.setData("text/uri-list", url);
 | 
						|
    dt.setData("text/plain", url);
 | 
						|
    dt.effectAllowed = "copyMove";
 | 
						|
    dt.addElement(selectedItem);
 | 
						|
  },
 | 
						|
 | 
						|
  onDragOver(aEvent) {
 | 
						|
    let types = aEvent.dataTransfer.types;
 | 
						|
    if (
 | 
						|
      types.includes("text/uri-list") ||
 | 
						|
      types.includes("text/x-moz-url") ||
 | 
						|
      types.includes("text/plain")
 | 
						|
    ) {
 | 
						|
      aEvent.preventDefault();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onDrop(aEvent) {
 | 
						|
    let dt = aEvent.dataTransfer;
 | 
						|
    // If dragged item is from our source, do not try to
 | 
						|
    // redownload already downloaded file.
 | 
						|
    if (dt.mozGetDataAt("application/x-moz-file", 0)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let links = Services.droppedLinkHandler.dropLinks(aEvent);
 | 
						|
    if (!links.length) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    aEvent.preventDefault();
 | 
						|
    let browserWin = BrowserWindowTracker.getTopWindow();
 | 
						|
    let initiatingDoc = browserWin ? browserWin.document : document;
 | 
						|
    for (let link of links) {
 | 
						|
      if (link.url.startsWith("about:")) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      DownloadURL(link.url, link.name, initiatingDoc);
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
Object.setPrototypeOf(
 | 
						|
  DownloadsPlacesView.prototype,
 | 
						|
  DownloadsViewUI.BaseView.prototype
 | 
						|
);
 | 
						|
 | 
						|
for (let methodName of ["load", "applyFilter", "selectNode", "selectItems"]) {
 | 
						|
  DownloadsPlacesView.prototype[methodName] = function() {
 | 
						|
    throw new Error(
 | 
						|
      "|" + methodName + "| is not implemented by the downloads view."
 | 
						|
    );
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
function goUpdateDownloadCommands() {
 | 
						|
  function updateCommandsForObject(object) {
 | 
						|
    for (let name in object) {
 | 
						|
      if (DownloadsViewUI.isCommandName(name)) {
 | 
						|
        goUpdateCommand(name);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
  updateCommandsForObject(DownloadsPlacesView.prototype);
 | 
						|
  updateCommandsForObject(HistoryDownloadElementShell.prototype);
 | 
						|
}
 | 
						|
 | 
						|
document.addEventListener("DOMContentLoaded", function() {
 | 
						|
  let richListBox = document.getElementById("downloadsListBox");
 | 
						|
  richListBox.addEventListener("scroll", function(event) {
 | 
						|
    return this._placesView.onScroll();
 | 
						|
  });
 | 
						|
  richListBox.addEventListener("keypress", function(event) {
 | 
						|
    return this._placesView.onKeyPress(event);
 | 
						|
  });
 | 
						|
  richListBox.addEventListener("dblclick", function(event) {
 | 
						|
    return this._placesView.onDoubleClick(event);
 | 
						|
  });
 | 
						|
  richListBox.addEventListener("contextmenu", function(event) {
 | 
						|
    return this._placesView.onContextMenu(event);
 | 
						|
  });
 | 
						|
  richListBox.addEventListener("dragstart", function(event) {
 | 
						|
    this._placesView.onDragStart(event);
 | 
						|
  });
 | 
						|
  let dropNode = richListBox;
 | 
						|
  // In about:downloads, also allow drops if the list is empty, by
 | 
						|
  // adding the listener to the document, as the richlistbox is
 | 
						|
  // hidden when it is empty.
 | 
						|
  if (document.documentElement.id == "contentAreaDownloadsView") {
 | 
						|
    dropNode = richListBox.parentNode;
 | 
						|
  }
 | 
						|
  dropNode.addEventListener("dragover", function(event) {
 | 
						|
    richListBox._placesView.onDragOver(event);
 | 
						|
  });
 | 
						|
  dropNode.addEventListener("drop", function(event) {
 | 
						|
    richListBox._placesView.onDrop(event);
 | 
						|
  });
 | 
						|
  richListBox.addEventListener("select", function(event) {
 | 
						|
    this._placesView.onSelect();
 | 
						|
  });
 | 
						|
  richListBox.addEventListener("focus", goUpdateDownloadCommands);
 | 
						|
  richListBox.addEventListener("blur", goUpdateDownloadCommands);
 | 
						|
});
 |