Source: src/js/views/DownloadButtonView.js

"use strict";

define([
  "jquery",
  "underscore",
  "backbone",
  "models/SolrResult",
  "models/DataONEObject",
  "models/PackageModel",
], ($, _, Backbone, SolrResult, DataONEObject, PackageModel) => {
  /**
   * @class DownloadButtonView
   * @classdesc A Backbone View for rendering a download button for individual
   * or package downloads.
   * @classcategory Views
   * @augments Backbone.View
   * // @screenshot TODO
   */
  const DownloadButtonView = Backbone.View.extend(
    /** @lends DownloadButtonView.prototype */ {
      /**
       * The HTML tag name for this view's root element.
       * @type {string}
       */
      tagName: "a",

      /**
       * The CSS class name(s) for this view's root element.
       * @type {string}
       */
      className: "btn download",

      /**
       * Initializes the view with options.
       * @param {object} [options] - The options for the view.
       * @param {string} [options.view] - The context view for this button.
       * @param {string} [options.id] - The ID of the button.
       * @param {Backbone.Model} [options.model] - The model associated with this
       * button.
       * @param {boolean} [options.nested] - Whether this button is nested.
       */
      initialize(options = {}) {
        this.view = options.view || null;
        this.id = options.id || null;
        this.model = options.model || new SolrResult();
        this.nested = options.nested || false;
      },

      /**
       * The DOM events bound to this view.
       * @type {object}
       */
      events: {
        click: "download",
      },

      /**
       * Renders the download button. Adds attributes, styles, and event listeners
       * based on the model and context.
       * @returns {DownloadButtonView} - Returns the view instance.
       */
      render() {
        let fileName = this.model.get("fileName") || "";

        if (typeof fileName === "string") {
          fileName = fileName.trim();
        }

        // Add the href and id attributes
        let hrefLink = this.model.get("url");
        if (
          this.model instanceof DataONEObject &&
          (this.model.get("formatType") === "RESOURCE" ||
            this.model.get("type") === "DataPackage")
        ) {
          hrefLink = this.model.getPackageURL();
        }
        if (
          this.model instanceof PackageModel &&
          (this.model.get("formatType") === "RESOURCE" ||
            this.model.get("type") === "DataPackage" ||
            this.model.get("type") === "Package")
        ) {
          hrefLink = this.model.getURL();
        }
        this.$el
          .attr("href", hrefLink)
          .attr("data-id", this.model.get("id"))
          .attr("download", fileName);

        // Check for CORS downloads. For CORS, the 'download' attribute may not
        // work, so open in a new tab.
        if (hrefLink?.indexOf(window.location.origin) === -1) {
          this.$el.attr("target", "_blank");
        }

        // For packages
        if (this.view === "actionsView") {
          this.$el.append(
            $(document.createElement("i")).addClass(
              "icon icon-large icon-cloud-download",
            ),
          );
          this.$el.addClass("btn-rounded");
          this.$el.addClass("action");
          this.$el.addClass("downloadAction");
        } else {
          if (this.model.type === "Package") {
            this.$el.text("Download All").addClass("btn-primary");

            // if the Package Model has no Solr index document associated with it,
            // then we can assume the resource map object is private. So disable
            // the download button.
            if (!this.model.get("indexDoc")) {
              this.$el
                .attr("disabled", "disabled")
                .addClass("disabled")
                .attr("href", "")
                .tooltip({
                  trigger: "hover",
                  placement: "top",
                  delay: 500,
                  title:
                    "This dataset may contain private data, so each data file should be downloaded individually.",
                });
            }
          }
          // For individual DataONEObjects
          else {
            this.$el.text("Download");
          }

          // Add a download icon
          this.$el.append(
            $(document.createElement("i")).addClass("icon icon-cloud-download"),
          );
        }

        // If this is a Download All button for a package but it's too large, then
        // disable the button with a message
        if (
          this.model.type === "Package" &&
          this.model.getTotalSize() > MetacatUI.appModel.get("maxDownloadSize")
        ) {
          this.$el
            .addClass("tooltip-this")
            .attr("disabled", "disabled")
            .attr(
              "data-title",
              "This dataset is too large to download as a package. Please download the files individually or contact us for alternate data access.",
            )
            .attr("data-placement", "top")
            .attr("data-trigger", "hover")
            .attr("data-container", "body");

          // Removing the `href` attribute while disabling the download button.
          this.$el.removeAttr("href");

          // Removing pointer as cursor and setting to default
          this.$el.css("cursor", "default");
        }

        return this;
      },

      /**
       * Handles the download event when the button is clicked. Checks for
       * conditions like authentication, public/private access, and download size
       * before proceeding.
       * @param {Event} e - The click event.
       */
      download(e) {
        // Checking if the Download All button is disabled because the package is
        // too large
        const isDownloadDisabled = !!(
          this.$el.attr("disabled") === "disabled" || this.$el.is(".disabled")
        );

        // Do nothing if the `disabled` attribute is set!. If the download is
        // already in progress, don't try to download again
        if (isDownloadDisabled || this.$el.is(".in-progress")) {
          e.preventDefault();
          return;
        }

        // If the user isn't logged in, let the browser handle the download
        // normally
        if (
          MetacatUI.appUserModel.get("tokenChecked") &&
          !MetacatUI.appUserModel.get("loggedIn")
        ) {
          return;
        }
        // If the authentication hasn't been checked yet, wait for it
        if (!MetacatUI.appUserModel.get("tokenChecked")) {
          const view = this;
          this.listenTo(MetacatUI.appUserModel, "change:tokenChecked", () => {
            view.download(e);
          });
          return;
        }
        // If the user is logged in but the object is public, download normally
        if (this.model.get("isPublic")) {
          // If this is a "Download All" button for a package, and at least object
          // is private, then we need to download via XHR with credentials
          if (this.model.type === "Package") {
            // If we found a private object, download the package via XHR so we
            // can send the auth token.
            const privateObject = _.find(
              this.model.get("members"),
              (m) => m.get("isPublic") !== true,
            );
            // If no private object is found, download normally. This may still
            // fail when there is a private object that the logged-in user doesn't
            // have access to.
            if (!privateObject) {
              return;
            }
          }
          // All other object types (data and metadata objects) can be downloaded
          // normally
          else {
            return;
          }
        }

        e.preventDefault();

        // Show that the download has started
        this.$el.addClass("in-progress");
        const buttonHTML = this.$el.html();
        if (this.view === "actionsView") {
          this.$el.html(
            "<i class='icon icon-on-right icon-spinner icon-spin'></i>",
          );
        } else {
          this.$el.html(
            "Downloading... <i class='icon icon-on-right icon-spinner icon-spin'></i>",
          );
        }

        const thisRef = this;

        this.listenToOnce(this.model, "downloadComplete", () => {
          let iconEl = "<i class='icon icon-on-right icon-ok'></i>";
          let downloadEl =
            "<i class='icon icon-large icon-cloud-download'></i>";

          if (thisRef.view !== "actionsView") {
            iconEl += "Complete ";
            downloadEl = buttonHTML;
          }

          // Show that the download is complete
          thisRef.$el
            .html(iconEl)
            .addClass("complete")
            .removeClass("in-progress error");

          // Put the download button back to normal
          setTimeout(() => {
            // After one second, change the background color with an animation
            thisRef.$el.removeClass("complete").html(downloadEl);
          }, 2000);
        });

        this.listenToOnce(this.model, "downloadError", () => {
          let iconEl = "<i class='icon icon-on-right icon-warning-sign'></i>";

          if (thisRef.view !== "actionsView") {
            iconEl += "Error ";
          }

          // Show that the download failed to compelete.
          thisRef.$el
            .html(iconEl)
            .addClass("error")
            .removeClass("in-progress")
            .tooltip({
              trigger: "hover",
              placement: "top",
              title:
                "Something went wrong while trying to download. Click to try again.",
            });
        });

        // Fire the download event via the SolrResult model
        this.model.downloadWithCredentials();
      },
    },
  );

  return DownloadButtonView;
});