Files
tubestation/browser/components/downloads/DownloadsSubview.jsm
Brian Grinstead 1c86f46ecd Bug 1479125 - Migrate calls that expect an element to be returned to use element variation firstChild etc to firstElementChild etc;r=Paolo
This allows the JS to work in HTML documents, where whitespace is preserved. In XUL
documents, whitespace is ignored when parsing so text nodes are generally not returned.

The following changes were made, with manual cleanups as necessary (i.e. when firstChild actually
refers to a text node, or when firstChild is used in a loop to empty out an element):

  firstChild->firstElementChild
  lastChild->lastElementChild
  nextSibling->nextElementSibling
  previousSibling->previousElementSibling
  childNodes->children

MozReview-Commit-ID: 95NQ8syBhYw
2018-08-08 15:22:53 -07:00

462 lines
16 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/. */
"use strict";
var EXPORTED_SYMBOLS = [
"DownloadsSubview",
];
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.defineModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
ChromeUtils.defineModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
ChromeUtils.defineModuleGetter(this, "DownloadsCommon",
"resource:///modules/DownloadsCommon.jsm");
ChromeUtils.defineModuleGetter(this, "DownloadsViewUI",
"resource:///modules/DownloadsViewUI.jsm");
ChromeUtils.defineModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
ChromeUtils.defineModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
let gPanelViewInstances = new WeakMap();
const kRefreshBatchSize = 10;
const kMaxWaitForIdleMs = 200;
XPCOMUtils.defineLazyGetter(this, "kButtonLabels", () => {
return {
show: DownloadsCommon.strings[AppConstants.platform == "macosx" ? "showMacLabel" : "showLabel"],
open: DownloadsCommon.strings.openFileLabel,
retry: DownloadsCommon.strings.retryLabel,
};
});
class DownloadsSubview extends DownloadsViewUI.BaseView {
constructor(panelview) {
super();
this.document = panelview.ownerDocument;
this.window = panelview.ownerGlobal;
this.context = "panelDownloadsContextMenu";
this.panelview = panelview;
this.container = this.document.getElementById("panelMenu_downloadsMenu");
while (this.container.lastChild) {
this.container.lastChild.remove();
}
this.panelview.addEventListener("click", DownloadsSubview.onClick);
this.panelview.addEventListener("ViewHiding", DownloadsSubview.onViewHiding);
this._viewItemsForDownloads = new WeakMap();
let contextMenu = this.document.getElementById(this.context);
if (!contextMenu) {
contextMenu = this.document.getElementById("downloadsContextMenu").cloneNode(true);
contextMenu.setAttribute("closemenu", "none");
contextMenu.setAttribute("id", this.context);
contextMenu.removeAttribute("onpopupshown");
contextMenu.setAttribute("onpopupshowing",
"DownloadsSubview.updateContextMenu(document.popupNode, this);");
contextMenu.setAttribute("onpopuphidden", "DownloadsSubview.onContextMenuHidden(this);");
let clearButton = contextMenu.querySelector("menuitem[command='downloadsCmd_clearDownloads']");
clearButton.hidden = false;
clearButton.previousElementSibling.hidden = true;
contextMenu.querySelector("menuitem[command='cmd_delete']")
.setAttribute("command", "downloadsCmd_delete");
}
this.panelview.appendChild(contextMenu);
this.container.setAttribute("context", this.context);
this._downloadsData = DownloadsCommon.getData(this.window, true, true, true);
this._downloadsData.addView(this);
}
destructor(event) {
this.panelview.removeEventListener("click", DownloadsSubview.onClick);
this.panelview.removeEventListener("ViewHiding", DownloadsSubview.onViewHiding);
this._downloadsData.removeView(this);
gPanelViewInstances.delete(this);
this.destroyed = true;
}
/**
* DataView handler; invoked when a batch of downloads is being passed in -
* usually when this instance is added as a view in the constructor.
*/
onDownloadBatchStarting() {
this.batchFragment = this.document.createDocumentFragment();
this.window.clearTimeout(this._batchTimeout);
}
/**
* DataView handler; invoked when the view stopped feeding its current list of
* downloads.
*/
onDownloadBatchEnded() {
let {window} = this;
window.clearTimeout(this._batchTimeout);
let waitForMs = 200;
if (this.batchFragment.childElementCount) {
// Prepend the batch fragment.
this.container.insertBefore(this.batchFragment, this.container.firstElementChild || null);
waitForMs = 0;
}
// Wait a wee bit to dispatch the event, because another batch may start
// right away.
this._batchTimeout = window.setTimeout(() => {
this._updateStatsFromDisk();
this.panelview.dispatchEvent(new window.CustomEvent("DownloadsLoaded"));
}, waitForMs);
this.batchFragment = null;
}
/**
* DataView handler; invoked when a new download is added to the list.
*
* @param {Download} download
* @param {DOMNode} [options.insertBefore]
*/
onDownloadAdded(download, { insertBefore } = {}) {
let shell = new DownloadsSubview.Button(download, this.document);
this._viewItemsForDownloads.set(download, shell);
// Triggger the code that update all attributes to match the downloads'
// current state.
shell.onChanged();
// 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.container).prepend(shell.element);
}
}
/**
* DataView Handler; invoked when the state of a download changed.
*
* @param {Download} download
*/
onDownloadChanged(download) {
this._viewItemsForDownloads.get(download).onChanged();
}
/**
* DataView handler; invoked when a download is removed.
*
* @param {Download} download
*/
onDownloadRemoved(download) {
this._viewItemsForDownloads.get(download).element.remove();
}
/**
* Schedule a refresh of the downloads that were added, which is mainly about
* checking whether the target file still exists.
* We're doing this during idle time and in chunks.
*/
async _updateStatsFromDisk() {
if (this._updatingStats)
return;
this._updatingStats = true;
try {
let idleOptions = { timeout: kMaxWaitForIdleMs };
// Start with getting an idle moment to (maybe) refresh the list of downloads.
await new Promise(resolve => this.window.requestIdleCallback(resolve), idleOptions);
// In the meantime, this instance could have been destroyed, so take note.
if (this.destroyed)
return;
let count = 0;
for (let button of this.container.children) {
if (this.destroyed)
return;
if (!button._shell)
continue;
await button._shell.refresh();
// Make sure to request a new idle moment every `kRefreshBatchSize` buttons.
if (++count % kRefreshBatchSize === 0) {
await new Promise(resolve => this.window.requestIdleCallback(resolve, idleOptions));
}
}
} catch (ex) {
Cu.reportError(ex);
} finally {
this._updatingStats = false;
}
}
// ----- Static methods. -----
/**
* Show the Downloads subview panel and listen for events that will trigger
* building the dynamic part of the view.
*
* @param {DOMNode} anchor The button that was commanded to trigger this function.
*/
static show(anchor) {
let document = anchor.ownerDocument;
let window = anchor.ownerGlobal;
let panelview = document.getElementById("PanelUI-downloads");
anchor.setAttribute("closemenu", "none");
gPanelViewInstances.set(panelview, new DownloadsSubview(panelview));
// Since the DownloadsLists are propagated asynchronously, we need to wait a
// little to get the view propagated.
panelview.addEventListener("ViewShowing", event => {
event.detail.addBlocker(new Promise(resolve => {
panelview.addEventListener("DownloadsLoaded", resolve, { once: true });
}));
}, { once: true });
window.PanelUI.showSubView("PanelUI-downloads", anchor);
}
/**
* Handler method; reveal the users' download directory using the OS specific
* method.
*/
static async onShowDownloads() {
// Retrieve the user's default download directory.
let preferredDir = await Downloads.getPreferredDownloadsDirectory();
DownloadsCommon.showDirectory(new FileUtils.File(preferredDir));
}
/**
* Handler method; clear the list downloads finished and old(er) downloads,
* just like in the Library.
*
* @param {DOMNode} button Button that was clicked to call this method.
*/
static onClearDownloads(button) {
let instance = gPanelViewInstances.get(button.closest("panelview"));
if (!instance)
return;
instance._downloadsData.removeFinished();
PlacesUtils.history.removeVisitsByFilter({
transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD
}).catch(Cu.reportError);
}
/**
* Just before showing the context menu, anchored to a download item, we need
* to set the right properties to make sure the right menu-items are visible.
*
* @param {DOMNode} button The Button the context menu will be anchored to.
* @param {DOMNode} menu The context menu.
*/
static updateContextMenu(button, menu) {
while (!button._shell) {
button = button.parentNode;
}
menu.setAttribute("state", button.getAttribute("state"));
if (button.hasAttribute("exists"))
menu.setAttribute("exists", button.getAttribute("exists"));
else
menu.removeAttribute("exists");
menu.classList.toggle("temporary-block", button.classList.contains("temporary-block"));
for (let menuitem of menu.getElementsByTagName("menuitem")) {
let command = menuitem.getAttribute("command");
if (!command)
continue;
if (command == "downloadsCmd_clearDownloads") {
menuitem.disabled = !DownloadsSubview.canClearDownloads(button);
} else {
menuitem.disabled = !button._shell.isCommandEnabled(command);
}
}
// The menu anchorNode property is not available long enough to be used elsewhere,
// so tack it another property name.
menu._anchorNode = button;
}
/**
* Right after the context menu was hidden, perform a bit of cleanup.
*
* @param {DOMNode} menu The context menu.
*/
static onContextMenuHidden(menu) {
delete menu._anchorNode;
}
/**
* Static version of DownloadsSubview#canClearDownloads().
*
* @param {DOMNode} button Button that we'll use to find the right
* DownloadsSubview instance.
*/
static canClearDownloads(button) {
let instance = gPanelViewInstances.get(button.closest("panelview"));
if (!instance)
return false;
return instance.canClearDownloads(instance.container);
}
/**
* Handler method; invoked when the Downloads panel is hidden and should be
* torn down & cleaned up.
*
* @param {DOMEvent} event
*/
static onViewHiding(event) {
let instance = gPanelViewInstances.get(event.target);
if (!instance)
return;
instance.destructor(event);
}
/**
* Handler method; invoked when anything is clicked inside the Downloads panel.
* Depending on the context, it will find the appropriate command to invoke.
*
* We don't have a command dispatcher registered for this view, so we don't go
* through the goDoCommand path like we do for the other views.
*
* @param {DOMMouseEvent} event
*/
static onClick(event) {
// Middle clicks fall through and are regarded as left clicks.
if (event.button > 1)
return;
let button = event.originalTarget;
if (!button.hasAttribute || button.classList.contains("subviewbutton-back"))
return;
let command = "downloadsCmd_open";
if (button.classList.contains("action-button")) {
button = button.parentNode;
command = button.hasAttribute("showLabel") ? "downloadsCmd_show" : "downloadsCmd_retry";
} else if (button.localName == "menuitem") {
command = button.getAttribute("command");
button = button.parentNode._anchorNode;
}
while (button && !button._shell && button != this.panelview &&
(!button.hasAttribute || !button.hasAttribute("oncommand"))) {
button = button.parentNode;
}
// We don't need to do anything when no button was clicked, like a separator
// or a blank panel area. Also, when 'oncommand' is set, the button will invoke
// its own, custom command handler.
if (!button || button == this.panelview || button.hasAttribute("oncommand"))
return;
if (command == "downloadsCmd_clearDownloads") {
DownloadsSubview.onClearDownloads(button);
} else if (button._shell.isCommandEnabled(command)) {
button._shell[command]();
}
}
}
DownloadsSubview.Button = class extends DownloadsViewUI.DownloadElementShell {
constructor(download, document) {
super();
this.download = download;
this.element = document.createElement("toolbarbutton");
this.element._shell = this;
this.element.classList.add("subviewbutton", "subviewbutton-iconic", "download",
"download-state");
}
get browserWindow() {
return this.element.ownerGlobal;
}
async refresh() {
if (this._targetFileChecked)
return;
try {
await this.download.refresh();
} catch (ex) {
Cu.reportError(ex);
} finally {
this._targetFileChecked = true;
}
}
/**
* Handle state changes of a download.
*/
onStateChanged() {
// Since the state changed, we may need to check the target file again.
this._targetFileChecked = false;
this._updateState();
}
/**
* Handler method; invoked when any state attribute of a download changed.
*/
onChanged() {
let newState = DownloadsCommon.stateOfDownload(this.download);
if (this._downloadState !== newState) {
this._downloadState = newState;
this.onStateChanged();
} else {
this._updateState();
}
// This cannot be placed within onStateChanged because when a download goes
// from hasBlockedData to !hasBlockedData it will still remain in the same state.
this.element.classList.toggle("temporary-block",
!!this.download.hasBlockedData);
}
/**
* Update the DOM representation of this download to match the current, recently
* updated, state.
*/
_updateState() {
super._updateState();
this.element.setAttribute("label", this.element.getAttribute("displayName"));
this.element.setAttribute("tooltiptext", this.element.getAttribute("fullStatus"));
if (this.isCommandEnabled("downloadsCmd_show")) {
this.element.setAttribute("openLabel", kButtonLabels.open);
this.element.setAttribute("showLabel", kButtonLabels.show);
this.element.removeAttribute("retryLabel");
} else if (this.isCommandEnabled("downloadsCmd_retry")) {
this.element.setAttribute("retryLabel", kButtonLabels.retry);
this.element.removeAttribute("openLabel");
this.element.removeAttribute("showLabel");
} else {
this.element.removeAttribute("openLabel");
this.element.removeAttribute("retryLabel");
this.element.removeAttribute("showLabel");
}
this._updateVisibility();
}
_updateVisibility() {
let state = this.element.getAttribute("state");
// This view only show completed and failed downloads.
this.element.hidden = !(state == DownloadsCommon.DOWNLOAD_FINISHED ||
state == DownloadsCommon.DOWNLOAD_FAILED);
}
/**
* Command handler; copy the download URL to the OS general clipboard.
*/
downloadsCmd_copyLocation() {
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper);
clipboard.copyString(this.download.source.url);
}
};