Source code

Revision control

Copy as Markdown

Other Tools

/* 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(
);
ChromeUtils.defineESModuleGetters(this, {
});
/**
* 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() {
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 () {
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 () {
this._placesView.onSelect();
});
richListBox.addEventListener("focus", goUpdateDownloadCommands);
richListBox.addEventListener("blur", goUpdateDownloadCommands);
});