Source: src/js/views/MetricView.js

define(["backbone", "views/MetricModalView", "semantic"], (
  Backbone,
  MetricModalView,
  Semantic,
) => {
  "use strict";

  const METRIC_NAMES = ["Citations", "Downloads", "Views"];
  const SEM_VARIATIONS = Semantic.CLASS_NAMES.variations;

  const CLASS_NAMES = {
    metricButton: "metrics",
    metricIcon: "metric-icon",
    metricName: "metric-name",
    metricValue: "metric-value",
    metricsButtonDisabled: "metrics-button-disabled",
    buttonLinkNeutral: "btn-link--neutral",
    // Icon classes
    iconSpinner: "icon-spinner",
    iconSpin: "icon-spin",
    iconExclamationSign: "icon-exclamation-sign",
    moreInfo: "more-info",
    // Bootstrap classes
    button: "btn",
    buttonLink: "btn-link",
    icon: "icon",
    iconOnLeft: "icon-on-left",
  };

  /**
   * @class MetricView
   * @classdesc the display of the dataset citation and usage metrics on the
   * dataset landing page
   * @classcategory Views
   * @screenshot views/MetricView.png
   * @augments Backbone.View
   */
  const MetricView = Backbone.View.extend(
    /** @lends MetricView.prototype */ {
      tagName: "a",

      /**
       * Class name to be applied to the metric buttons
       * @type {string}
       */
      className: `${CLASS_NAMES.metricButton} ${CLASS_NAMES.button} ${CLASS_NAMES.buttonLink} ${CLASS_NAMES.buttonLinkNeutral}`,

      /**
       * Attribute to indicate the type of metric
       * @type {string}
       */
      metricName: null,

      /**
       * The Metric Model associated with this view
       * @type {MetricsModel}
       */
      model: null,

      /**
       * The font-awesome icons associated with each metric
       * @type {object}
       * @param {string} Citations - The icon for citations metric
       * @param {string} Downloads - The icon for downloads metric
       * @param {string} Views - The icon for views metric
       * @since 2.36.0
       */
      metricsIcons: {
        Citations: "quote-right",
        Downloads: "cloud-download",
        Views: "eye-open",
      },

      /**
       * The descriptions associated with each metric for tool-tips
       * @type {object}
       * @param {string} Citations - The description for citations metric
       * @param {string} Downloads - The description for downloads metric
       * @param {string} Views - The description for views metric
       * @since 2.36.0
       */
      metricsTooltips: {
        Citations:
          "For all the versions of this dataset, the number of times that all or part of this dataset was cited.",
        Downloads:
          "For all the versions of this dataset, the number of times that all or part of this dataset was downloaded.",
        Views:
          "For all the versions of this dataset, the number of times that all or part of this dataset was viewed.",
      },

      /**
       * Settings passed to the Formantic UI popup module to configure a tooltip
       * shown over the metric button.
       * @see https://fomantic-ui.com/modules/popup.html#/settings
       * @type {object|boolean}
       * @since 2.36.0
       */
      tooltipSettings: {
        variation: `${SEM_VARIATIONS.mini} ${SEM_VARIATIONS.inverted}`,
        position: "top center",
        on: "hover",
        hoverable: true,
        delay: {
          show: 500,
          hide: 40,
        },
      },

      /**
       * The template for the metric button
       * @param {object} options - A literal object with options for the
       * template
       * @param {string} options.metricValue - The value of the metric to be
       * displayed
       * @param {string} options.metricIcon - The icon class for the metric
       * @param {string} options.metricName - The name of the metric
       * @returns {string} - The HTML string for the metric button
       */
      metricButtonTemplate: ({ metricValue, metricIcon, metricName }) => {
        const loaderIconClasses = [
          CLASS_NAMES.icon,
          CLASS_NAMES.metricIcon,
          CLASS_NAMES.iconSpinner,
          CLASS_NAMES.iconSpin,
        ].join(" ");
        const iconClasses = [
          CLASS_NAMES.icon,
          CLASS_NAMES.metricIcon,
          CLASS_NAMES.iconOnLeft,
          metricIcon,
        ].join(" ");
        const val = metricValue || `<i class='${loaderIconClasses}'></i>`;
        return `
          <i class='${iconClasses}'></i>
          <span class='${CLASS_NAMES.metricValue}'>${val}</span>
          <span class='${CLASS_NAMES.metricName}'>${metricName}</span>
        `.trim();
      },

      /** @inheritdoc */
      events: {
        click: "showMetricModal",
      },

      /**
       * @param {object} options - A literal object with options to pass to the
       * view
       * @param {MetricsModel} options.model - The MetricsModel object
       * associated with this view
       * @param {string} options.metricName - The name of the metric view
       * @param {string} options.pid - Associated dataset identifier with this
       * view
       */
      initialize(options = {}) {
        let name = options.metricName || "";
        name = name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
        this.metricName = name;
        this.model = options.model;
        this.pid = options.pid;
      },

      /**
       * Renders the apprpriate metric view on the UI
       * @returns {MetricView} - The MetricView object
       */
      render() {
        const icon = this.metricsIcons[this.metricName];

        if (!METRIC_NAMES.includes(this.metricName)) {
          this.el.innerHTML = "";
          this.removeTooltip();
          return this;
        }

        this.el.innerHTML = this.metricButtonTemplate({
          metricValue: "",
          metricIcon: `icon-${icon}`,
          metricName: this.metricName,
        })
          // remove all the white spaces and new lines for consistent spacing
          // between elements with the same classes (e.g. assessment report button
          // from DatasetControlsView)
          .trim()
          .replace(/\s+/g, " ")
          .replace(/>\s+</g, "><");

        // Adding tool-tip for the buttons
        if (MetacatUI.appModel.get("displayDatasetMetricsTooltip")) {
          const tooltipText = this.metricsTooltips[this.metricName];
          this.addTooltip(tooltipText);
        } else {
          this.removeTooltip();
        }

        this.stopListening(this.model);

        // waiting for the fetch() call to succeed.
        this.listenTo(this.model, "sync", this.renderResults);
        // in case when there is an error for the fetch call.
        this.listenTo(this.model, "error", this.renderError);

        return this;
      },

      /**
       * Handles the click functions and displays appropriate modals on click
       * events
       * @param {Event} _e - The click event object
       */
      showMetricModal(_e) {
        if (MetacatUI.appModel.get("displayMetricModals")) {
          const modalView = new MetricModalView({
            metricName: this.metricName,
            metricsModel: this.model,
            pid: this.pid,
          });
          modalView.render();
          this.modalView = modalView;

          if (Array.isArray(this.subviews)) {
            this.subviews.push(modalView);
          } else {
            this.subviews = [modalView];
          }

          // Track this event
          MetacatUI.analytics?.trackEvent(
            "metrics",
            "Click metric",
            this.metricName,
          );
        }
      },

      /**
       * Displays the metrics count and badge on the landing page
       */
      renderResults() {
        const total = this.model.get(`total${this.metricName}`);
        // Check if the metric object exists in results obtained from the
        // service If it does, get its total value else set the total count to 0

        // Replacing the metric total count with the spinning icon.

        this.$(`.${CLASS_NAMES.metricValue}`).text(
          MetacatUI.appView.numberAbbreviator(total, 1),
        );
        this.$(`.${CLASS_NAMES.metricValue}`);
      },

      /**
       * Manages error handling in case Metrics Service does not responsd
       */
      renderError() {
        // Replacing the spinning icon with a question-mark when the metrics are
        // not loaded
        const iconEl = this.$(`.${CLASS_NAMES.metricValue}`).find(
          `.${CLASS_NAMES.metricIcon}`,
        );
        iconEl.removeClass(CLASS_NAMES.iconSpinner);
        iconEl.removeClass(CLASS_NAMES.iconSpin);
        iconEl.addClass(CLASS_NAMES.iconExclamationSign);
        iconEl.addClass(CLASS_NAMES.moreInfo);

        this.$el.addClass(CLASS_NAMES.metricsButtonDisabled);

        // Setting the error tool-tip
        this.addTooltip(
          "Metrics are currently unavailable. Please try again later.",
        );
      },

      /**
       * Add a tooltip to the button with the given text
       * @param {string} tooltipText - The text to show in the tooltip
       */
      addTooltip(tooltipText) {
        this.tooltipSettings.content = tooltipText;
        this.$el.popup(this.tooltipSettings);
      },

      /** Remove any tooltip set on the button */
      removeTooltip() {
        this.$el.popup("destroy");
      },

      /**
       * Cleans up listeners from this view
       */
      onClose() {
        this.subviews?.forEach((view) => {
          if (typeof view.onClose === "function") {
            view.onClose();
          }
        });
        this.removeTooltip();
        this.stopListening();
      },
    },
  );

  return MetricView;
});