670 lines
19 KiB
JavaScript
670 lines
19 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set ts=2 et sw=2 tw=80: */
|
|
/* 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 */
|
|
|
|
/**
|
|
* Handles the indicator that displays the progress of ongoing downloads, which
|
|
* is also used as the anchor for the downloads panel.
|
|
*
|
|
* This module includes the following constructors and global objects:
|
|
*
|
|
* DownloadsButton
|
|
* Main entry point for the downloads indicator. Depending on how the toolbars
|
|
* have been customized, this object determines if we should show a fully
|
|
* functional indicator, a placeholder used during customization and in the
|
|
* customization palette, or a neutral view as a temporary anchor for the
|
|
* downloads panel.
|
|
*
|
|
* DownloadsIndicatorView
|
|
* Builds and updates the actual downloads status widget, responding to changes
|
|
* in the global status data, or provides a neutral view if the indicator is
|
|
* removed from the toolbars and only used as a temporary anchor. In addition,
|
|
* handles the user interaction events raised by the widget.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
// DownloadsButton
|
|
|
|
/**
|
|
* Main entry point for the downloads indicator. Depending on how the toolbars
|
|
* have been customized, this object determines if we should show a fully
|
|
* functional indicator, a placeholder used during customization and in the
|
|
* customization palette, or a neutral view as a temporary anchor for the
|
|
* downloads panel.
|
|
*/
|
|
const DownloadsButton = {
|
|
/**
|
|
* Returns a reference to the downloads button position placeholder, or null
|
|
* if not available because it has been removed from the toolbars.
|
|
*/
|
|
get _placeholder() {
|
|
return document.getElementById("downloads-button");
|
|
},
|
|
|
|
/**
|
|
* Indicates whether toolbar customization is in progress.
|
|
*/
|
|
_customizing: false,
|
|
|
|
/**
|
|
* This function is called asynchronously just after window initialization.
|
|
*
|
|
* NOTE: This function should limit the input/output it performs to improve
|
|
* startup time.
|
|
*/
|
|
initializeIndicator() {
|
|
DownloadsIndicatorView.ensureInitialized();
|
|
},
|
|
|
|
/**
|
|
* Determines the position where the indicator should appear, and moves its
|
|
* associated element to the new position.
|
|
*
|
|
* @return Anchor element, or null if the indicator is not visible.
|
|
*/
|
|
_getAnchorInternal() {
|
|
let indicator = DownloadsIndicatorView.indicator;
|
|
if (!indicator) {
|
|
// Exit now if the button is not in the document.
|
|
return null;
|
|
}
|
|
|
|
indicator.open = this._anchorRequested;
|
|
|
|
let widget = CustomizableUI.getWidget("downloads-button");
|
|
// Determine if the indicator is located on an invisible toolbar.
|
|
if (
|
|
!isElementVisible(indicator.parentNode) &&
|
|
widget.areaType == CustomizableUI.TYPE_TOOLBAR
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return DownloadsIndicatorView.indicatorAnchor;
|
|
},
|
|
|
|
/**
|
|
* Indicates whether we should try and show the indicator temporarily as an
|
|
* anchor for the panel, even if the indicator would be hidden by default.
|
|
*/
|
|
_anchorRequested: false,
|
|
|
|
/**
|
|
* Ensures that there is an anchor available for the panel.
|
|
*
|
|
* @return Anchor element where the panel should be anchored, or null if an
|
|
* anchor is not available (for example because both the tab bar and
|
|
* the navigation bar are hidden).
|
|
*/
|
|
getAnchor() {
|
|
// Do not allow anchoring the panel to the element while customizing.
|
|
if (this._customizing) {
|
|
return null;
|
|
}
|
|
|
|
this._anchorRequested = true;
|
|
return this._getAnchorInternal();
|
|
},
|
|
|
|
/**
|
|
* Allows the temporary anchor to be hidden.
|
|
*/
|
|
releaseAnchor() {
|
|
this._anchorRequested = false;
|
|
this._getAnchorInternal();
|
|
},
|
|
|
|
/**
|
|
* Unhide the button. Generally, this only needs to use the placeholder.
|
|
* However, when starting customize mode, if the button is in the palette,
|
|
* we need to unhide it before customize mode is entered, otherwise it
|
|
* gets ignored by customize mode. To do this, we pass true for
|
|
* `includePalette`. We don't always look in the palette because it's
|
|
* inefficient (compared to getElementById), shouldn't be necessary, and
|
|
* if _placeholder returned the node even if in the palette, other checks
|
|
* would break.
|
|
*
|
|
* @param includePalette whether to search the palette, too. Defaults to false.
|
|
*/
|
|
unhide(includePalette = false) {
|
|
let button = this._placeholder;
|
|
let wasHidden = false;
|
|
if (!button && includePalette) {
|
|
button = gNavToolbox.palette.querySelector("#downloads-button");
|
|
}
|
|
if (button && button.hasAttribute("hidden")) {
|
|
button.removeAttribute("hidden");
|
|
if (this._navBar.contains(button)) {
|
|
this._navBar.setAttribute("downloadsbuttonshown", "true");
|
|
}
|
|
wasHidden = true;
|
|
}
|
|
return wasHidden;
|
|
},
|
|
|
|
hide() {
|
|
let button = this._placeholder;
|
|
if (this.autoHideDownloadsButton && button && button.closest("toolbar")) {
|
|
DownloadsPanel.hidePanel();
|
|
button.hidden = true;
|
|
this._navBar.removeAttribute("downloadsbuttonshown");
|
|
}
|
|
},
|
|
|
|
startAutoHide() {
|
|
if (DownloadsIndicatorView.hasDownloads) {
|
|
this.unhide();
|
|
} else {
|
|
this.hide();
|
|
}
|
|
},
|
|
|
|
checkForAutoHide() {
|
|
let button = this._placeholder;
|
|
if (
|
|
!this._customizing &&
|
|
this.autoHideDownloadsButton &&
|
|
button &&
|
|
button.closest("toolbar")
|
|
) {
|
|
this.startAutoHide();
|
|
} else {
|
|
this.unhide();
|
|
}
|
|
},
|
|
|
|
// Callback from CustomizableUI when nodes get moved around.
|
|
// We use this to track whether our node has moved somewhere
|
|
// where we should (not) autohide it.
|
|
onWidgetAfterDOMChange(node) {
|
|
if (node == this._placeholder) {
|
|
this.checkForAutoHide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* This function is called when toolbar customization starts.
|
|
*
|
|
* During customization, we never show the actual download progress indication
|
|
* or the event notifications, but we show a neutral placeholder. The neutral
|
|
* placeholder is an ordinary button defined in the browser window that can be
|
|
* moved freely between the toolbars and the customization palette.
|
|
*/
|
|
onCustomizeStart(win) {
|
|
if (win == window) {
|
|
// Prevent the indicator from being displayed as a temporary anchor
|
|
// during customization, even if requested using the getAnchor method.
|
|
this._customizing = true;
|
|
this._anchorRequested = false;
|
|
this.unhide(true);
|
|
}
|
|
},
|
|
|
|
onCustomizeEnd(win) {
|
|
if (win == window) {
|
|
this._customizing = false;
|
|
this.checkForAutoHide();
|
|
DownloadsIndicatorView.afterCustomize();
|
|
}
|
|
},
|
|
|
|
init() {
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
this,
|
|
"autoHideDownloadsButton",
|
|
"browser.download.autohideButton",
|
|
true,
|
|
this.checkForAutoHide.bind(this)
|
|
);
|
|
|
|
CustomizableUI.addListener(this);
|
|
this.checkForAutoHide();
|
|
},
|
|
|
|
uninit() {
|
|
CustomizableUI.removeListener(this);
|
|
},
|
|
|
|
get _tabsToolbar() {
|
|
delete this._tabsToolbar;
|
|
return (this._tabsToolbar = document.getElementById("TabsToolbar"));
|
|
},
|
|
|
|
get _navBar() {
|
|
delete this._navBar;
|
|
return (this._navBar = document.getElementById("nav-bar"));
|
|
},
|
|
};
|
|
|
|
Object.defineProperty(this, "DownloadsButton", {
|
|
value: DownloadsButton,
|
|
enumerable: true,
|
|
writable: false,
|
|
});
|
|
|
|
// DownloadsIndicatorView
|
|
|
|
/**
|
|
* Builds and updates the actual downloads status widget, responding to changes
|
|
* in the global status data, or provides a neutral view if the indicator is
|
|
* removed from the toolbars and only used as a temporary anchor. In addition,
|
|
* handles the user interaction events raised by the widget.
|
|
*/
|
|
const DownloadsIndicatorView = {
|
|
/**
|
|
* True when the view is connected with the underlying downloads data.
|
|
*/
|
|
_initialized: false,
|
|
|
|
/**
|
|
* True when the user interface elements required to display the indicator
|
|
* have finished loading in the browser window, and can be referenced.
|
|
*/
|
|
_operational: false,
|
|
|
|
/**
|
|
* Prepares the downloads indicator to be displayed.
|
|
*/
|
|
ensureInitialized() {
|
|
if (this._initialized) {
|
|
return;
|
|
}
|
|
this._initialized = true;
|
|
|
|
window.addEventListener("unload", this);
|
|
window.addEventListener("visibilitychange", this);
|
|
DownloadsCommon.getIndicatorData(window).addView(this);
|
|
},
|
|
|
|
/**
|
|
* Frees the internal resources related to the indicator.
|
|
*/
|
|
ensureTerminated() {
|
|
if (!this._initialized) {
|
|
return;
|
|
}
|
|
this._initialized = false;
|
|
|
|
window.removeEventListener("unload", this);
|
|
window.removeEventListener("visibilitychange", this);
|
|
DownloadsCommon.getIndicatorData(window).removeView(this);
|
|
|
|
// Reset the view properties, so that a neutral indicator is displayed if we
|
|
// are visible only temporarily as an anchor.
|
|
this.percentComplete = 0;
|
|
this.attention = DownloadsCommon.ATTENTION_NONE;
|
|
},
|
|
|
|
/**
|
|
* Ensures that the user interface elements required to display the indicator
|
|
* are loaded.
|
|
*/
|
|
_ensureOperational() {
|
|
if (this._operational) {
|
|
return;
|
|
}
|
|
|
|
// If we don't have a _placeholder, there's no chance that everything
|
|
// will load correctly: bail (and don't set _operational to true!)
|
|
if (!DownloadsButton._placeholder) {
|
|
return;
|
|
}
|
|
|
|
this._operational = true;
|
|
|
|
// If the view is initialized, we need to update the elements now that
|
|
// they are finally available in the document.
|
|
if (this._initialized) {
|
|
DownloadsCommon.getIndicatorData(window).refreshView(this);
|
|
}
|
|
},
|
|
|
|
// Direct control functions
|
|
|
|
/**
|
|
* Set to the type ("start" or "finish") when display of a notification is in-progress
|
|
*/
|
|
_currentNotificationType: null,
|
|
|
|
/**
|
|
* Set to the type ("start" or "finish") when a notification arrives while we
|
|
* are waiting for the timeout of the previous notification
|
|
*/
|
|
_nextNotificationType: null,
|
|
|
|
/**
|
|
* Check if the panel containing aNode is open.
|
|
* @param aNode
|
|
* the node whose panel we're interested in.
|
|
*/
|
|
_isAncestorPanelOpen(aNode) {
|
|
while (aNode && aNode.localName != "panel") {
|
|
aNode = aNode.parentNode;
|
|
}
|
|
return aNode && aNode.state == "open";
|
|
},
|
|
|
|
/**
|
|
* Display or enqueue a visual notification of a relevant event, like a new download.
|
|
*
|
|
* @param aType
|
|
* Set to "start" for new downloads, "finish" for completed downloads.
|
|
*/
|
|
showEventNotification(aType) {
|
|
if (!this._initialized) {
|
|
return;
|
|
}
|
|
|
|
// enqueue this notification while the current one is being displayed
|
|
if (this._currentNotificationType) {
|
|
// only queue up the notification if it is different to the current one
|
|
if (this._currentNotificationType != aType) {
|
|
this._nextNotificationType = aType;
|
|
}
|
|
} else {
|
|
this._showNotification(aType);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* If the status indicator is visible in its assigned position, shows for a
|
|
* brief time a visual notification of a relevant event, like a new download.
|
|
*
|
|
* @param aType
|
|
* Set to "start" for new downloads, "finish" for completed downloads.
|
|
*/
|
|
_showNotification(aType) {
|
|
let anchor = DownloadsButton._placeholder;
|
|
if (!anchor || !isElementVisible(anchor.parentNode)) {
|
|
// Our container isn't visible, so can't show the animation:
|
|
return;
|
|
}
|
|
|
|
if (anchor.ownerGlobal.matchMedia("(prefers-reduced-motion)").matches) {
|
|
// User has prefers-reduced-motion enabled, so we shouldn't show the animation.
|
|
return;
|
|
}
|
|
|
|
anchor.setAttribute("notification", aType);
|
|
anchor.setAttribute("animate", "");
|
|
|
|
// are we animating from an initially-hidden state?
|
|
anchor.toggleAttribute("washidden", !!this._wasHidden);
|
|
delete this._wasHidden;
|
|
|
|
this._currentNotificationType = aType;
|
|
|
|
const onNotificationAnimEnd = event => {
|
|
if (
|
|
event.animationName !== "downloadsButtonNotification" &&
|
|
event.animationName !== "downloadsButtonFinishedNotification"
|
|
) {
|
|
return;
|
|
}
|
|
anchor.removeEventListener("animationend", onNotificationAnimEnd);
|
|
|
|
requestAnimationFrame(() => {
|
|
anchor.removeAttribute("notification");
|
|
anchor.removeAttribute("animate");
|
|
|
|
requestAnimationFrame(() => {
|
|
let nextType = this._nextNotificationType;
|
|
this._currentNotificationType = null;
|
|
this._nextNotificationType = null;
|
|
if (nextType && isElementVisible(anchor.parentNode)) {
|
|
this._showNotification(nextType);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
anchor.addEventListener("animationend", onNotificationAnimEnd);
|
|
},
|
|
|
|
// Callback functions from DownloadsIndicatorData
|
|
|
|
/**
|
|
* Indicates whether the indicator should be shown because there are some
|
|
* downloads to be displayed.
|
|
*/
|
|
set hasDownloads(aValue) {
|
|
if (this._hasDownloads != aValue || (!this._operational && aValue)) {
|
|
this._hasDownloads = aValue;
|
|
|
|
// If there is at least one download, ensure that the view elements are
|
|
// operational
|
|
if (aValue) {
|
|
this._wasHidden = DownloadsButton.unhide();
|
|
this._ensureOperational();
|
|
} else {
|
|
DownloadsButton.checkForAutoHide();
|
|
}
|
|
}
|
|
},
|
|
get hasDownloads() {
|
|
return this._hasDownloads;
|
|
},
|
|
_hasDownloads: false,
|
|
|
|
/**
|
|
* Progress indication to display, from 0 to 100, or -1 if unknown.
|
|
* Progress is not visible if the current progress is unknown.
|
|
*/
|
|
set percentComplete(aValue) {
|
|
if (!this._operational) {
|
|
return;
|
|
}
|
|
aValue = Math.min(100, aValue);
|
|
if (this._percentComplete !== aValue) {
|
|
// Initial progress may fire before the start event gets to us.
|
|
// To avoid flashing, trip the start event first.
|
|
if (this._percentComplete < 0 && aValue >= 0) {
|
|
this.showEventNotification("start");
|
|
}
|
|
this._percentComplete = aValue;
|
|
this._refreshAttention();
|
|
this._maybeScheduleProgressUpdate();
|
|
}
|
|
},
|
|
|
|
_maybeScheduleProgressUpdate() {
|
|
if (
|
|
this.indicator &&
|
|
!this._progressRaf &&
|
|
document.visibilityState == "visible"
|
|
) {
|
|
this._progressRaf = requestAnimationFrame(() => {
|
|
// indeterminate downloads (unknown content-length) will show up as aValue = 0
|
|
if (this._percentComplete >= 0) {
|
|
if (!this.indicator.hasAttribute("progress")) {
|
|
this.indicator.setAttribute("progress", "true");
|
|
}
|
|
// For arrow type only: Set the % complete on the pie-chart.
|
|
// We use a minimum of 10% to ensure something is always visible
|
|
this._progressIcon.style.setProperty(
|
|
"--download-progress-pcent",
|
|
`${Math.max(10, this._percentComplete)}%`
|
|
);
|
|
} else {
|
|
this.indicator.removeAttribute("progress");
|
|
this._progressIcon.style.setProperty(
|
|
"--download-progress-pcent",
|
|
"0%"
|
|
);
|
|
}
|
|
this._progressRaf = null;
|
|
});
|
|
}
|
|
},
|
|
_percentComplete: -1,
|
|
|
|
/**
|
|
* Set when the indicator should draw user attention to itself.
|
|
*/
|
|
set attention(aValue) {
|
|
if (!this._operational) {
|
|
return;
|
|
}
|
|
if (this._attention != aValue) {
|
|
this._attention = aValue;
|
|
this._refreshAttention();
|
|
}
|
|
},
|
|
|
|
_refreshAttention() {
|
|
// Check if the downloads button is in the menu panel, to determine which
|
|
// button needs to get a badge.
|
|
let widgetGroup = CustomizableUI.getWidget("downloads-button");
|
|
let inMenu = widgetGroup.areaType == CustomizableUI.TYPE_PANEL;
|
|
|
|
// For arrow-Styled indicator, suppress success attention if we have
|
|
// progress in toolbar
|
|
let suppressAttention =
|
|
!inMenu &&
|
|
this._attention == DownloadsCommon.ATTENTION_SUCCESS &&
|
|
this._percentComplete >= 0;
|
|
|
|
if (
|
|
suppressAttention ||
|
|
this._attention == DownloadsCommon.ATTENTION_NONE
|
|
) {
|
|
this.indicator.removeAttribute("attention");
|
|
} else {
|
|
this.indicator.setAttribute("attention", this._attention);
|
|
}
|
|
},
|
|
_attention: DownloadsCommon.ATTENTION_NONE,
|
|
|
|
// User interface event functions
|
|
handleEvent(aEvent) {
|
|
switch (aEvent.type) {
|
|
case "unload":
|
|
this.ensureTerminated();
|
|
break;
|
|
|
|
case "visibilitychange":
|
|
this._maybeScheduleProgressUpdate();
|
|
break;
|
|
}
|
|
},
|
|
|
|
onCommand(aEvent) {
|
|
if (
|
|
// On Mac, ctrl-click will send a context menu event from the widget, so
|
|
// we don't want to bring up the panel when ctrl key is pressed.
|
|
(aEvent.type == "mousedown" &&
|
|
(aEvent.button != 0 ||
|
|
(AppConstants.platform == "macosx" && aEvent.ctrlKey))) ||
|
|
(aEvent.type == "keypress" && aEvent.key != " " && aEvent.key != "Enter")
|
|
) {
|
|
return;
|
|
}
|
|
|
|
DownloadsPanel.showPanel(
|
|
/* openedManually */ true,
|
|
aEvent.type.startsWith("key")
|
|
);
|
|
aEvent.stopPropagation();
|
|
},
|
|
|
|
onDragOver(aEvent) {
|
|
ToolbarDropHandler.onDragOver(aEvent);
|
|
},
|
|
|
|
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;
|
|
}
|
|
let sourceDoc = dt.mozSourceNode
|
|
? dt.mozSourceNode.ownerDocument
|
|
: document;
|
|
let handled = false;
|
|
for (let link of links) {
|
|
if (link.url.startsWith("about:")) {
|
|
continue;
|
|
}
|
|
saveURL(
|
|
link.url,
|
|
null,
|
|
link.name,
|
|
null,
|
|
true,
|
|
true,
|
|
null,
|
|
null,
|
|
sourceDoc
|
|
);
|
|
handled = true;
|
|
}
|
|
if (handled) {
|
|
aEvent.preventDefault();
|
|
}
|
|
},
|
|
|
|
_indicator: null,
|
|
__progressIcon: null,
|
|
|
|
/**
|
|
* Returns a reference to the main indicator element, or null if the element
|
|
* is not present in the browser window yet.
|
|
*/
|
|
get indicator() {
|
|
if (!this._indicator) {
|
|
this._indicator = document.getElementById("downloads-button");
|
|
}
|
|
|
|
return this._indicator;
|
|
},
|
|
|
|
get indicatorAnchor() {
|
|
let widgetGroup = CustomizableUI.getWidget("downloads-button");
|
|
if (widgetGroup.areaType == CustomizableUI.TYPE_PANEL) {
|
|
let overflowIcon = widgetGroup.forWindow(window).anchor;
|
|
return overflowIcon.icon;
|
|
}
|
|
|
|
return this.indicator.badgeStack;
|
|
},
|
|
|
|
get _progressIcon() {
|
|
return (
|
|
this.__progressIcon ||
|
|
(this.__progressIcon = document.getElementById(
|
|
"downloads-indicator-progress-inner"
|
|
))
|
|
);
|
|
},
|
|
|
|
_onCustomizedAway() {
|
|
this._indicator = null;
|
|
this.__progressIcon = null;
|
|
},
|
|
|
|
afterCustomize() {
|
|
// If the cached indicator is not the one currently in the document,
|
|
// invalidate our references
|
|
if (this._indicator != document.getElementById("downloads-button")) {
|
|
this._onCustomizedAway();
|
|
this._operational = false;
|
|
this.ensureTerminated();
|
|
this.ensureInitialized();
|
|
}
|
|
},
|
|
};
|
|
|
|
Object.defineProperty(this, "DownloadsIndicatorView", {
|
|
value: DownloadsIndicatorView,
|
|
enumerable: true,
|
|
writable: false,
|
|
});
|