tor-browser

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

indicator.js (19334B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set ts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 /**
      8 * Handles the indicator that displays the progress of ongoing downloads, which
      9 * is also used as the anchor for the downloads panel.
     10 *
     11 * This module includes the following constructors and global objects:
     12 *
     13 * DownloadsButton
     14 * Main entry point for the downloads indicator.  Depending on how the toolbars
     15 * have been customized, this object determines if we should show a fully
     16 * functional indicator, a placeholder used during customization and in the
     17 * customization palette, or a neutral view as a temporary anchor for the
     18 * downloads panel.
     19 *
     20 * DownloadsIndicatorView
     21 * Builds and updates the actual downloads status widget, responding to changes
     22 * in the global status data, or provides a neutral view if the indicator is
     23 * removed from the toolbars and only used as a temporary anchor.  In addition,
     24 * handles the user interaction events raised by the widget.
     25 */
     26 
     27 "use strict";
     28 
     29 // DownloadsButton
     30 
     31 /**
     32 * Main entry point for the downloads indicator.  Depending on how the toolbars
     33 * have been customized, this object determines if we should show a fully
     34 * functional indicator, a placeholder used during customization and in the
     35 * customization palette, or a neutral view as a temporary anchor for the
     36 * downloads panel.
     37 */
     38 const DownloadsButton = {
     39  /**
     40   * Returns a reference to the downloads button position placeholder, or null
     41   * if not available because it has been removed from the toolbars.
     42   */
     43  get _placeholder() {
     44    return document.getElementById("downloads-button");
     45  },
     46 
     47  /**
     48   * Indicates whether toolbar customization is in progress.
     49   */
     50  _customizing: false,
     51 
     52  /**
     53   * This function is called asynchronously just after window initialization.
     54   *
     55   * NOTE: This function should limit the input/output it performs to improve
     56   *       startup time.
     57   */
     58  initializeIndicator() {
     59    DownloadsIndicatorView.ensureInitialized();
     60  },
     61 
     62  /**
     63   * Determines the position where the indicator should appear, and moves its
     64   * associated element to the new position.
     65   *
     66   * @return Anchor element, or null if the indicator is not visible.
     67   */
     68  _getAnchorInternal() {
     69    let indicator = DownloadsIndicatorView.indicator;
     70    if (!indicator) {
     71      // Exit now if the button is not in the document.
     72      return null;
     73    }
     74 
     75    indicator.open = this._anchorRequested;
     76 
     77    let widget = CustomizableUI.getWidget("downloads-button");
     78    // Determine if the indicator is located on an invisible toolbar.
     79    if (
     80      !isElementVisible(indicator.parentNode) &&
     81      widget.areaType == CustomizableUI.TYPE_TOOLBAR
     82    ) {
     83      return null;
     84    }
     85 
     86    return DownloadsIndicatorView.indicatorAnchor;
     87  },
     88 
     89  /**
     90   * Indicates whether we should try and show the indicator temporarily as an
     91   * anchor for the panel, even if the indicator would be hidden by default.
     92   */
     93  _anchorRequested: false,
     94 
     95  /**
     96   * Ensures that there is an anchor available for the panel.
     97   *
     98   * @return Anchor element where the panel should be anchored, or null if an
     99   *         anchor is not available (for example because both the tab bar and
    100   *         the navigation bar are hidden).
    101   */
    102  getAnchor() {
    103    // Do not allow anchoring the panel to the element while customizing.
    104    if (this._customizing) {
    105      return null;
    106    }
    107 
    108    this._anchorRequested = true;
    109    return this._getAnchorInternal();
    110  },
    111 
    112  /**
    113   * Allows the temporary anchor to be hidden.
    114   */
    115  releaseAnchor() {
    116    this._anchorRequested = false;
    117    this._getAnchorInternal();
    118  },
    119 
    120  /**
    121   * Unhide the button. Generally, this only needs to use the placeholder.
    122   * However, when starting customize mode, if the button is in the palette,
    123   * we need to unhide it before customize mode is entered, otherwise it
    124   * gets ignored by customize mode. To do this, we pass true for
    125   * `includePalette`. We don't always look in the palette because it's
    126   * inefficient (compared to getElementById), shouldn't be necessary, and
    127   * if _placeholder returned the node even if in the palette, other checks
    128   * would break.
    129   *
    130   * @param includePalette  whether to search the palette, too. Defaults to false.
    131   */
    132  unhide(includePalette = false) {
    133    let button = this._placeholder;
    134    let wasHidden = false;
    135    if (!button && includePalette) {
    136      button = gNavToolbox.palette.querySelector("#downloads-button");
    137    }
    138    if (button && button.hasAttribute("hidden")) {
    139      button.removeAttribute("hidden");
    140      if (this._navBar.contains(button)) {
    141        this._navBar.setAttribute("downloadsbuttonshown", "true");
    142      }
    143      wasHidden = true;
    144    }
    145    return wasHidden;
    146  },
    147 
    148  hide() {
    149    let button = this._placeholder;
    150    if (this.autoHideDownloadsButton && button && button.closest("toolbar")) {
    151      DownloadsPanel.hidePanel();
    152      button.hidden = true;
    153      this._navBar.removeAttribute("downloadsbuttonshown");
    154    }
    155  },
    156 
    157  startAutoHide() {
    158    if (DownloadsIndicatorView.hasDownloads) {
    159      this.unhide();
    160    } else {
    161      this.hide();
    162    }
    163  },
    164 
    165  checkForAutoHide() {
    166    let button = this._placeholder;
    167    if (
    168      !this._customizing &&
    169      this.autoHideDownloadsButton &&
    170      button &&
    171      button.closest("toolbar")
    172    ) {
    173      this.startAutoHide();
    174    } else {
    175      this.unhide();
    176    }
    177  },
    178 
    179  // Callback from CustomizableUI when nodes get moved around.
    180  // We use this to track whether our node has moved somewhere
    181  // where we should (not) autohide it.
    182  onWidgetAfterDOMChange(node) {
    183    if (node == this._placeholder) {
    184      this.checkForAutoHide();
    185    }
    186  },
    187 
    188  /**
    189   * This function is called when toolbar customization starts.
    190   *
    191   * During customization, we never show the actual download progress indication
    192   * or the event notifications, but we show a neutral placeholder.  The neutral
    193   * placeholder is an ordinary button defined in the browser window that can be
    194   * moved freely between the toolbars and the customization palette.
    195   */
    196  onCustomizeStart(win) {
    197    if (win == window) {
    198      // Prevent the indicator from being displayed as a temporary anchor
    199      // during customization, even if requested using the getAnchor method.
    200      this._customizing = true;
    201      this._anchorRequested = false;
    202      this.unhide(true);
    203    }
    204  },
    205 
    206  onCustomizeEnd(win) {
    207    if (win == window) {
    208      this._customizing = false;
    209      this.checkForAutoHide();
    210      DownloadsIndicatorView.afterCustomize();
    211    }
    212  },
    213 
    214  init() {
    215    XPCOMUtils.defineLazyPreferenceGetter(
    216      this,
    217      "autoHideDownloadsButton",
    218      "browser.download.autohideButton",
    219      true,
    220      this.checkForAutoHide.bind(this)
    221    );
    222 
    223    CustomizableUI.addListener(this);
    224    this.checkForAutoHide();
    225  },
    226 
    227  uninit() {
    228    CustomizableUI.removeListener(this);
    229  },
    230 
    231  get _tabsToolbar() {
    232    delete this._tabsToolbar;
    233    return (this._tabsToolbar = document.getElementById("TabsToolbar"));
    234  },
    235 
    236  get _navBar() {
    237    delete this._navBar;
    238    return (this._navBar = document.getElementById("nav-bar"));
    239  },
    240 };
    241 
    242 Object.defineProperty(this, "DownloadsButton", {
    243  value: DownloadsButton,
    244  enumerable: true,
    245  writable: false,
    246 });
    247 
    248 // DownloadsIndicatorView
    249 
    250 /**
    251 * Builds and updates the actual downloads status widget, responding to changes
    252 * in the global status data, or provides a neutral view if the indicator is
    253 * removed from the toolbars and only used as a temporary anchor.  In addition,
    254 * handles the user interaction events raised by the widget.
    255 */
    256 const DownloadsIndicatorView = {
    257  /**
    258   * True when the view is connected with the underlying downloads data.
    259   */
    260  _initialized: false,
    261 
    262  /**
    263   * True when the user interface elements required to display the indicator
    264   * have finished loading in the browser window, and can be referenced.
    265   */
    266  _operational: false,
    267 
    268  /**
    269   * Prepares the downloads indicator to be displayed.
    270   */
    271  ensureInitialized() {
    272    if (this._initialized) {
    273      return;
    274    }
    275    this._initialized = true;
    276 
    277    window.addEventListener("unload", this);
    278    window.addEventListener("visibilitychange", this);
    279    DownloadsCommon.getIndicatorData(window).addView(this);
    280  },
    281 
    282  /**
    283   * Frees the internal resources related to the indicator.
    284   */
    285  ensureTerminated() {
    286    if (!this._initialized) {
    287      return;
    288    }
    289    this._initialized = false;
    290 
    291    window.removeEventListener("unload", this);
    292    window.removeEventListener("visibilitychange", this);
    293    DownloadsCommon.getIndicatorData(window).removeView(this);
    294 
    295    // Reset the view properties, so that a neutral indicator is displayed if we
    296    // are visible only temporarily as an anchor.
    297    this.percentComplete = 0;
    298    this.attention = DownloadsCommon.ATTENTION_NONE;
    299  },
    300 
    301  /**
    302   * Ensures that the user interface elements required to display the indicator
    303   * are loaded.
    304   */
    305  _ensureOperational() {
    306    if (this._operational) {
    307      return;
    308    }
    309 
    310    // If we don't have a _placeholder, there's no chance that everything
    311    // will load correctly: bail (and don't set _operational to true!)
    312    if (!DownloadsButton._placeholder) {
    313      return;
    314    }
    315 
    316    this._operational = true;
    317 
    318    // If the view is initialized, we need to update the elements now that
    319    // they are finally available in the document.
    320    if (this._initialized) {
    321      DownloadsCommon.getIndicatorData(window).refreshView(this);
    322    }
    323  },
    324 
    325  // Direct control functions
    326 
    327  /**
    328   * Set to the type ("start" or "finish") when display of a notification is in-progress
    329   */
    330  _currentNotificationType: null,
    331 
    332  /**
    333   * Set to the type ("start" or "finish") when a notification arrives while we
    334   * are waiting for the timeout of the previous notification
    335   */
    336  _nextNotificationType: null,
    337 
    338  /**
    339   * Check if the panel containing aNode is open.
    340   *
    341   * @param aNode
    342   *        the node whose panel we're interested in.
    343   */
    344  _isAncestorPanelOpen(aNode) {
    345    while (aNode && aNode.localName != "panel") {
    346      aNode = aNode.parentNode;
    347    }
    348    return aNode && aNode.state == "open";
    349  },
    350 
    351  /**
    352   * Display or enqueue a visual notification of a relevant event, like a new download.
    353   *
    354   * @param aType
    355   *        Set to "start" for new downloads, "finish" for completed downloads.
    356   */
    357  showEventNotification(aType) {
    358    if (!this._initialized) {
    359      return;
    360    }
    361 
    362    // enqueue this notification while the current one is being displayed
    363    if (this._currentNotificationType) {
    364      // only queue up the notification if it is different to the current one
    365      if (this._currentNotificationType != aType) {
    366        this._nextNotificationType = aType;
    367      }
    368    } else {
    369      this._showNotification(aType);
    370    }
    371  },
    372 
    373  /**
    374   * If the status indicator is visible in its assigned position, shows for a
    375   * brief time a visual notification of a relevant event, like a new download.
    376   *
    377   * @param aType
    378   *        Set to "start" for new downloads, "finish" for completed downloads.
    379   */
    380  _showNotification(aType) {
    381    let anchor = DownloadsButton._placeholder;
    382    if (!anchor || !isElementVisible(anchor.parentNode)) {
    383      // Our container isn't visible, so can't show the animation:
    384      return;
    385    }
    386 
    387    if (anchor.ownerGlobal.matchMedia("(prefers-reduced-motion)").matches) {
    388      // User has prefers-reduced-motion enabled, so we shouldn't show the animation.
    389      return;
    390    }
    391 
    392    anchor.setAttribute("notification", aType);
    393    anchor.setAttribute("animate", "");
    394 
    395    // are we animating from an initially-hidden state?
    396    anchor.toggleAttribute("washidden", !!this._wasHidden);
    397    delete this._wasHidden;
    398 
    399    this._currentNotificationType = aType;
    400 
    401    const onNotificationAnimEnd = event => {
    402      if (
    403        event.animationName !== "downloadsButtonNotification" &&
    404        event.animationName !== "downloadsButtonFinishedNotification"
    405      ) {
    406        return;
    407      }
    408      anchor.removeEventListener("animationend", onNotificationAnimEnd);
    409 
    410      requestAnimationFrame(() => {
    411        anchor.removeAttribute("notification");
    412        anchor.removeAttribute("animate");
    413 
    414        requestAnimationFrame(() => {
    415          let nextType = this._nextNotificationType;
    416          this._currentNotificationType = null;
    417          this._nextNotificationType = null;
    418          if (nextType && isElementVisible(anchor.parentNode)) {
    419            this._showNotification(nextType);
    420          }
    421        });
    422      });
    423    };
    424    anchor.addEventListener("animationend", onNotificationAnimEnd);
    425  },
    426 
    427  // Callback functions from DownloadsIndicatorData
    428 
    429  /**
    430   * Indicates whether the indicator should be shown because there are some
    431   * downloads to be displayed.
    432   */
    433  set hasDownloads(aValue) {
    434    if (this._hasDownloads != aValue || (!this._operational && aValue)) {
    435      this._hasDownloads = aValue;
    436 
    437      // If there is at least one download, ensure that the view elements are
    438      // operational
    439      if (aValue) {
    440        this._wasHidden = DownloadsButton.unhide();
    441        this._ensureOperational();
    442      } else {
    443        DownloadsButton.checkForAutoHide();
    444      }
    445    }
    446  },
    447  get hasDownloads() {
    448    return this._hasDownloads;
    449  },
    450  _hasDownloads: false,
    451 
    452  /**
    453   * Progress indication to display, from 0 to 100, or -1 if unknown.
    454   * Progress is not visible if the current progress is unknown.
    455   */
    456  set percentComplete(aValue) {
    457    if (!this._operational) {
    458      return;
    459    }
    460    aValue = Math.min(100, aValue);
    461    if (this._percentComplete !== aValue) {
    462      // Initial progress may fire before the start event gets to us.
    463      // To avoid flashing, trip the start event first.
    464      if (this._percentComplete < 0 && aValue >= 0) {
    465        this.showEventNotification("start");
    466      }
    467      this._percentComplete = aValue;
    468      this._refreshAttention();
    469      this._maybeScheduleProgressUpdate();
    470    }
    471  },
    472 
    473  _maybeScheduleProgressUpdate() {
    474    if (
    475      this.indicator &&
    476      !this._progressRaf &&
    477      document.visibilityState == "visible"
    478    ) {
    479      this._progressRaf = requestAnimationFrame(() => {
    480        // indeterminate downloads (unknown content-length) will show up as aValue = 0
    481        if (this._percentComplete >= 0) {
    482          if (!this.indicator.hasAttribute("progress")) {
    483            this.indicator.setAttribute("progress", "true");
    484          }
    485          // For arrow type only: Set the % complete on the pie-chart.
    486          // We use a minimum of 10% to ensure something is always visible
    487          this._progressIcon.style.setProperty(
    488            "--download-progress-pcent",
    489            `${Math.max(10, this._percentComplete)}%`
    490          );
    491        } else {
    492          this.indicator.removeAttribute("progress");
    493          this._progressIcon.style.setProperty(
    494            "--download-progress-pcent",
    495            "0%"
    496          );
    497        }
    498        this._progressRaf = null;
    499      });
    500    }
    501  },
    502  _percentComplete: -1,
    503 
    504  /**
    505   * Set when the indicator should draw user attention to itself.
    506   */
    507  set attention(aValue) {
    508    if (!this._operational) {
    509      return;
    510    }
    511    if (this._attention != aValue) {
    512      this._attention = aValue;
    513      this._refreshAttention();
    514    }
    515  },
    516 
    517  _refreshAttention() {
    518    // Check if the downloads button is in the menu panel, to determine which
    519    // button needs to get a badge.
    520    let widgetGroup = CustomizableUI.getWidget("downloads-button");
    521    let inMenu = widgetGroup.areaType == CustomizableUI.TYPE_PANEL;
    522 
    523    // For arrow-Styled indicator, suppress success attention if we have
    524    // progress in toolbar
    525    let suppressAttention =
    526      !inMenu &&
    527      this._attention == DownloadsCommon.ATTENTION_SUCCESS &&
    528      this._percentComplete >= 0;
    529 
    530    if (
    531      suppressAttention ||
    532      this._attention == DownloadsCommon.ATTENTION_NONE
    533    ) {
    534      this.indicator.removeAttribute("attention");
    535    } else {
    536      this.indicator.setAttribute("attention", this._attention);
    537    }
    538  },
    539  _attention: DownloadsCommon.ATTENTION_NONE,
    540 
    541  // User interface event functions
    542  handleEvent(aEvent) {
    543    switch (aEvent.type) {
    544      case "unload":
    545        this.ensureTerminated();
    546        break;
    547 
    548      case "visibilitychange":
    549        this._maybeScheduleProgressUpdate();
    550        break;
    551    }
    552  },
    553 
    554  onCommand(aEvent) {
    555    if (
    556      // On Mac, ctrl-click will send a context menu event from the widget, so
    557      // we don't want to bring up the panel when ctrl key is pressed.
    558      (aEvent.type == "mousedown" &&
    559        (aEvent.button != 0 ||
    560          (AppConstants.platform == "macosx" && aEvent.ctrlKey))) ||
    561      (aEvent.type == "keypress" && aEvent.key != " " && aEvent.key != "Enter")
    562    ) {
    563      return;
    564    }
    565 
    566    DownloadsPanel.showPanel(
    567      /* openedManually */ true,
    568      aEvent.type.startsWith("key")
    569    );
    570    aEvent.stopPropagation();
    571  },
    572 
    573  onDragOver(aEvent) {
    574    ToolbarDropHandler.onDragOver(aEvent);
    575  },
    576 
    577  onDrop(aEvent) {
    578    let dt = aEvent.dataTransfer;
    579    // If dragged item is from our source, do not try to
    580    // redownload already downloaded file.
    581    if (dt.mozGetDataAt("application/x-moz-file", 0)) {
    582      return;
    583    }
    584 
    585    let links = Services.droppedLinkHandler.dropLinks(aEvent);
    586    if (!links.length) {
    587      return;
    588    }
    589    let sourceDoc = dt.mozSourceNode
    590      ? dt.mozSourceNode.ownerDocument
    591      : document;
    592    let handled = false;
    593    for (let link of links) {
    594      if (link.url.startsWith("about:")) {
    595        continue;
    596      }
    597      saveURL(
    598        link.url,
    599        null,
    600        link.name,
    601        null,
    602        true,
    603        true,
    604        null,
    605        null,
    606        sourceDoc
    607      );
    608      handled = true;
    609    }
    610    if (handled) {
    611      aEvent.preventDefault();
    612    }
    613  },
    614 
    615  _indicator: null,
    616  __progressIcon: null,
    617 
    618  /**
    619   * Returns a reference to the main indicator element, or null if the element
    620   * is not present in the browser window yet.
    621   */
    622  get indicator() {
    623    if (!this._indicator) {
    624      this._indicator = document.getElementById("downloads-button");
    625    }
    626 
    627    return this._indicator;
    628  },
    629 
    630  get indicatorAnchor() {
    631    let widgetGroup = CustomizableUI.getWidget("downloads-button");
    632    if (widgetGroup.areaType == CustomizableUI.TYPE_PANEL) {
    633      let overflowIcon = widgetGroup.forWindow(window).anchor;
    634      return overflowIcon.icon;
    635    }
    636 
    637    return this.indicator.badgeStack;
    638  },
    639 
    640  get _progressIcon() {
    641    return (
    642      this.__progressIcon ||
    643      (this.__progressIcon = document.getElementById(
    644        "downloads-indicator-progress-inner"
    645      ))
    646    );
    647  },
    648 
    649  _onCustomizedAway() {
    650    this._indicator = null;
    651    this.__progressIcon = null;
    652  },
    653 
    654  afterCustomize() {
    655    // If the cached indicator is not the one currently in the document,
    656    // invalidate our references
    657    if (this._indicator != document.getElementById("downloads-button")) {
    658      this._onCustomizedAway();
    659      this._operational = false;
    660      this.ensureTerminated();
    661      this.ensureInitialized();
    662    }
    663  },
    664 };
    665 
    666 Object.defineProperty(this, "DownloadsIndicatorView", {
    667  value: DownloadsIndicatorView,
    668  enumerable: true,
    669  writable: false,
    670 });