Source: src/js/views/DownloadButtonView.js

"use strict";

define(["jquery", "backbone", "models/SolrResult", "common/UrlUtilities"], (
  $,
  Backbone,
  SolrResult,
  UrlUtilities,
) => {
  /**
   * @class DownloadButtonView
   * @classdesc A Backbone View for rendering a download button for one object
   * @classcategory Views
   * @augments Backbone.View
   */
  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 {Backbone.Model} [options.model] The model associated with this
       * button
       */
      initialize(options = {}) {
        this.model = options.model || new SolrResult();
      },

      /**
       * 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} The view instance
       */
      render() {
        const { model } = this;

        let fileName =
          model.get("fileName") || model.get("title") || model.get("id") || "";
        if (typeof fileName === "string") fileName = fileName.trim();

        const id = model.get("id");
        const hrefLink = id
          ? model.get("url") ||
            UrlUtilities.getObjectDownloadUrl(id, {
              baseUrl:
                MetacatUI.appModel.get("objectServiceUrl") ||
                MetacatUI.appModel.get("resolveServiceUrl") ||
                "",
            })
          : "";
        this.$el
          .attr("href", hrefLink)
          .attr("data-id", model.get("id"))
          .attr("download", fileName)
          .text("Download")
          .append(
            $(document.createElement("i")).addClass("icon icon-cloud-download"),
          );

        // For CORS downloads the `download` attribute may be ignored, so open
        // cross-origin links in a new tab.
        if (hrefLink && hrefLink.indexOf(window.location.origin) === -1) {
          this.$el.attr("target", "_blank");
        }

        return this;
      },

      /**
       * Prevents the download button from being clickable and adds a tooltip
       * with a message explaining why.
       * @param {string} [message] The message to display in the tooltip
       * @since 2.36.2
       */
      inactivate(message = "This file is not available for download.") {
        this.$el.addClass("disabled").attr("disabled", "disabled");
        this.$el.css("cursor", "default");
        this.$el.removeAttr("href");

        this.$el.tooltip({
          trigger: "hover",
          placement: "top",
          delay: 100,
          title: message,
          container: "body",
        });
      },

      /**
       * Handles the download event when the button is clicked. Checks for
       * authentication and public/private access before proceeding.
       * @param {Event} e The click event
       */
      download(e) {
        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 authentication hasn't been checked yet, wait for it
        if (!MetacatUI.appUserModel.get("tokenChecked")) {
          e.preventDefault();
          this.listenToOnce(MetacatUI.appUserModel, "change:tokenChecked", () =>
            this.el.click(),
          );
          return;
        }
        // Known-public objects can use the browser's normal download path.
        if (this.model.get("isPublic") === true) {
          return;
        }

        e.preventDefault();

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

        this.listenToOnce(this.model, "downloadComplete", () => {
          // Show that the download is complete
          this.$el
            .html("<i class='icon icon-on-right icon-ok'></i>Complete ")
            .addClass("complete")
            .removeClass("in-progress error");

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

        this.listenToOnce(this.model, "downloadError", (error) => {
          if (error?.status === 401 || error?.status === 403) {
            this.$el.html(buttonHTML).removeClass("in-progress error");
            this.inactivate(
              "This file is not publicly accessible. Sign in with an account that has access.",
            );
            return;
          }

          // Show that the download failed to complete.
          this.$el
            .html("<i class='icon icon-on-right icon-warning-sign'></i>Error ")
            .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;
});