Source: src/js/views/CitationView.js

define([
  "jquery",
  "underscore",
  "backbone",
  "models/CitationModel",
  "text!templates/citations/citationAPA.html",
  "text!templates/citations/citationFullArchived.html",
  "text!templates/citations/citationAPAInText.html",
  "text!templates/citations/citationAPAInTextArchived.html",
], function (
  $,
  _,
  Backbone,
  CitationModel,
  APATemplate,
  ArchivedTemplate,
  APAInTextTemplate,
  InTextArchivedTemplate
) {
  "use strict";

  /**
   * @class CitationView
   * @classdesc The CitationView shows a formatted citation for a package,
   * including title, authors, year, UUID/DOI, etc.
   * @classcategory Views
   * @extends Backbone.View
   * @screenshot views/CitationView.png
   * @constructor
   */
  var CitationView = Backbone.View.extend(
    /** @lends CitationView.prototype */ {
      /**
       * The name of this type of view
       * @type {string}
       */
      type: "Citation",

      /**
       * The HTML tag name for this view's element
       * @type {string}
       */
      tagName: "cite",

      /**
       * The HTML classes to use for this view's element
       * @type {string}
       */
      className: "citation",

      /**
       * The HTML classes to use for the title element. This will be passed to
       * the template and will be used to identify the title element if the
       * createTitleLink option is set to true.
       * @since 2.23.0
       * @type {string}
       */
      titleClass: "title",

      /**
       * The HTML classes to use for the element that will contain the citation
       * metadata. The citation metadata are the citations that cite the main
       * citation. This class will be passed to the template and will be used to
       * identify the container element.
       * @since 2.23.0
       * @type {string}
       */
      citationMetadataClass: "citation-metadata",

      /**
       * The CitationModel that this view is displaying. The view can be
       * instantiated by passing in a CitationModel, or a SolrResult,
       * DataONEObject, or an extension of those, and the view will create a
       * CitationModel from it. To change the model after the view has been
       * instantiated, use the {@link CitationView#setModel} method.
       * @type {CitationModel}
       */
      model: null,

      /**
       * Defines how to render a citation style for only it's in-text or full
       * citation format.
       * @typedef {Object} ContextDefinition
       * @property {Underscore.Template} template - The Underscore.js template
       * to use. HTML files are converted to Underscore.js templates.
       * @property {Underscore.Template} archivedTemplate - The Underscore.js
       * template to use when the object is archived.
       * @property {string} render - The name of the method in this view to use
       * to render the citation. This method will be passed the template (or
       * archived template if the object is archived), as well as the template
       * options.
       * @since 2.23.0
       * @example
       * {
       *  inText: {
       *    template: _.template(InTextAPATemplate),
       *    archivedTemplate: _.template(InTextAPAArchivedTemplate),
       *    render: "renderAPAInText",
       * }
       */

      /**
       * Defines how to render a citation style for both it's in-text and full
       * contexts.
       * @typedef {Object} StyleDefinition
       * @property {CitationView#ContextDefinition} full - The full citation
       * format.
       * @property {CitationView#ContextDefinition} inText - The in-text
       * citation format.
       * @since 2.23.0
       */

      /**
       * The format and layout options that are available for this view.
       * @type {Object}
       * @property {CitationView#StyleDefinition} styleName - Each property in
       * the styles object maps a style name to a StyleOption object.
       * @since 2.23.0
       */
      styles: {
        apa: {
          full: {
            template: _.template(APATemplate),
            archivedTemplate: _.template(ArchivedTemplate),
            render: "renderAPA",
          },
          inText: {
            template: _.template(APAInTextTemplate),
            archivedTemplate: _.template(InTextArchivedTemplate),
            render: "renderAPAInText",
          },
        },
        apaAllAuthors: {
          full: {
            template: _.template(APATemplate),
            archivedTemplate: _.template(ArchivedTemplate),
            render: "renderAPAAllAuthors",
          },
          inText: {
            template: _.template(APAInTextTemplate),
            archivedTemplate: _.template(InTextArchivedTemplate),
            render: "renderAPAInText",
          },
        },
      },

      /**
       * The citation style to use. Any style that is defined in the
       * {@link CitationView#styles} property can be used. If the style is not
       * defined then the view will not render.
       * @type {string}
       * @since 2.23.0
       */
      style: "apa",

      /**
       * The context to use when rendering the citation. This can be either
       * "inText" or "full". Configure this in the {@link CitationView#styles}
       * property.
       * @type {string}
       * @since 2.23.0
       */
      context: "full",

      /**
       * Set this to true to create a link to the object's landing page around
       * the entire citation. This will override the createTitleLink option.
       * @type {boolean}
       */
      createLink: false,

      /**
       * Set this to true to create a link to the object's landing page around
       * the title. This will be ignored if the createLink option is set to
       * true.
       * @type {boolean}
       */
      createTitleLink: true,

      /**
       * When links are created as part of this view, whether to open them in a
       * new tab or not. If the URL begins with "http" then it will always open
       * in a new tab. When this option is true, then relative URLs will also
       * open in a new tab.
       * @type {boolean}
       * @since 2.23.0
       */
      openLinkInNewTab: false,

      /** A default title for when the Citation Model does not have a title.
       * @type {string}
       * @since 2.23.0
       */
      defaultTitle: null,

      /**
       * Executed when a new CitationView is created. Options that are available
       * but which are not defined as properties of this view are defined below.
       * @param {Object} options - A literal object with options to pass to the
       * view.
       * @param {Backbone.Model} [options.metadata] - This option is allowed for
       * backwards compatibility, but it is recommended to use the model option
       * instead. This will be ignored if a model is set. A model passed in this
       * option will be used to populate a CitationModel.
       * @param {string} [options.title] - Allowed for backwards compatibility.
       * Setting this option will set the default title for this view, on the
       * {@link CitationView#defaultTitle} property.
       * @param {string} [options.id] - When no model and no metadata are
       * provided, this option can be used to query for an object to cite. If a
       * model or metadata model is provided, then the ID will be ignored.
       */
      initialize: function (options) {
        try {
          options = !options || typeof options != "object" ? {} : options;
          const optKeys = Object.keys(options);

          // Identify the model from the options, for backwards compatibility.
          const modelOpt = options.model;
          const metadataOpt = options.metadata;
          const idOpt = options.id;

          // Convert deprecated options to the new options.
          if (optKeys.includes("title") && !optKeys.includes("defaultTitle")) {
            options.defaultTitle = options.title;
          }

          // Don't set any of the deprecated on the the model.
          delete options.model;
          delete options.metadata;
          delete options.id;
          delete options.title;

          // Get all the options and apply them to this view.
          Object.keys(options).forEach(function (key) {
            this[key] = options[key];
          }, this);

          this.setModel(modelOpt, metadataOpt, idOpt, false);
        } catch (error) {
          console.log("Error initializing CitationView", error);
        }
      },

      /**
       * Use this method to set or change the model for this view, and
       * re-render. If a CitationModel is provided, then it will be used. If a
       * SolrResult, DataONEObject, or an extension of those is provided as
       * either the first or second argument, then a CitationModel will be
       * created from it. Otherwise, if there is an ID provided, then a
       * CitationModel will be created from a SolrResult with that ID. If none
       * of those are provided, then a new, empty CitationModel will be created.
       * @param {CitationModel} [newModel] - The new model to set on this view.
       * @param {Backbone.Model} [metadata] - This option is allowed for
       * backwards compatibility, but it is recommended to use the model option
       * instead. This will be ignored if a model is set. A model passed in this
       * option will be used to populate a CitationModel.
       * @param {string} [id] - When no model and no metadata are provided, this
       * option can be used to query for an object to cite.
       * @param {boolean} [render=true] - Whether to re-render the view after
       * setting the model.
       * @since 2.23.0
       */
      setModel: function (newModel, metadata, id, render = true) {
        try {
          this.stopListening(this.model);

          let model = newModel;
          let sourceModel = newModel || metadata;
          if (!model || !(model instanceof CitationModel)) {
            model = new CitationModel();
            if (!sourceModel && id) {
              require(["models/SolrResult"], function (SolrResult) {
                sourceModel = new SolrResult({ id: id });
                sourceModel.getCitationInfo();
                model.setSourceModel(sourceModel);
              });
            } else {
              model.setSourceModel(sourceModel);
            }
          }
          this.model = model;
          // Set up listeners to re-render when there are any changes to the model
          this.listenTo(this.model, "change", this.render);
          if (render) this.render();
        } catch (error) {
          console.log("Error setting the model for the CitationView: ", error);
        }
      },

      /**
       * Renders the view.
       * @return {CitationView} Returns the view.
       */
      render: function () {
        try {
          // TODO - start with a placeholder.

          // Cases where we don't want to render.
          if (!this.model) return this.clear();

          // If the model is still uploading, then don't re-render.
          if (
            this.el.children.length &&
            this.model.getUploadStatus &&
            this.model.getUploadStatus() == "p"
          ) {
            return this;
          }

          // Get the attributes for the style and context
          let styleAttrs = this.style ? this.styles[this.style] : null;

          // Can't render without a set style
          if (!styleAttrs) return this.clear();

          // In text or full citation?
          styleAttrs = styleAttrs[this.context || "full"];

          // Can't render without a set style
          if (!styleAttrs) return this.clear();

          // Get the template for this style. If object is archived & not
          // indexed, use the archived template.
          let template = styleAttrs.template;
          if (
            this.model.isArchivedAndNotIndexed() &&
            styleAttrs.archivedTemplate
          ) {
            template = styleAttrs.archivedTemplate;
          }

          // If for some reason there is no template, then render an empty view
          if (!template) return this.clear();

          // Options to pass to the template
          const options = {
            titleClass: this.titleClass,
            citationMetadataClass: this.citationMetadataClass,
            ...this.model.toJSON(),
          };
          if (!options.title) options.title = this.defaultTitle || "";

          // PANGAEA specific override. If this is a PANGAEA object, then do not
          // show the UUID if the seriesId is a DOI.
          if (
            this.model.isFromNode("urn:node:PANGAEA") &&
            this.model.isDOI(options.seriesId)
          ) {
            options.pid = "";
          }

          // Find the render method to use that is in the style definition
          let renderMethod = styleAttrs.render;

          // Run the render method, if it exists
          if (typeof renderMethod == "function") {
            renderMethod.call(this, options, template);
          } else if (typeof this[renderMethod] == "function") {
            this[renderMethod](options, template);
          } else {
            // Default to just passing the options to the template
            this.el.innerHTML = template(options);
          }

          if (this.createLink) {
            this.addLink();
          } else if (this.createTitleLink) {
            this.addTitleLink();
          }

          return this;
        } catch (error) {
          console.log("Error rendering the CitationView: ", error);
          return this.clear();
        }
      },

      /**
       * Remove all HTML from the view.
       * @returns {CitationView} Returns the view.
       * @since 2.23.0
       */
      clear: function () {
        this.el.innerHTML = "";
        return this;
      },

      /**
       * Render a complete APA style citation.
       * @param {Object} options - The options to pass to the template.
       * @param {function} template - The template associated with this style,
       * or it's archive template if the object is archived and not indexed.
       * @param {number} [maxAuthors=20] - The maximum number of authors to
       * display. If there are more than this number of authors, then the
       * remaining authors will be replaced with an ellipsis. The default is 20
       * since that is the maximum that APA allows. Set to a falsy value to
       * display all authors.
       * @since 2.23.0
       */
      renderAPA: function (options, template, maxAuthors=20) {
        // Format the authors for display
        options.origin = this.CSLNamesToAPA(options.originArray, maxAuthors);
        this.el.innerHTML = template(options);
        // If there are citationMetadata, as well as an element in the current
        // template where they should be inserted, then show them inline.
        if (options.citationMetadata) {
          this.addCitationMetadata(options.citationMetadata);
        }
      },

      /**
       * Render an in-text APA style citation.
       * @param {Object} options - The options to pass to the template.
       * @param {function} template - The template associated with this style,
       * or it's archive template if the object is archived and not indexed.
       * @since 2.23.0
       */
      renderAPAInText: function (options, template) {
        options.origin = this.CSLNamesToAPAInText(options.originArray);
        this.el.innerHTML = template(options);
      },

      /**
       * Render a complete APA style citation with all authors listed.
       * @param {Object} options - The options to pass to the template.
       * @param {function} template - The template associated with this style,
       * or it's archive template if the object is archived and not indexed.
       * @since 2.26.0
       */
      renderAPAAllAuthors: function (options, template) {
        this.renderAPA(options, template, false);
      },

      /**
       * Render the list of in-text citations that cite the main citation. This
       * function will find the element in the template that is designated as
       * the container for the in-text citations and will render any citations
       * from the model's citationMetadata attribute.
       * @param {Object[]} citationMetadata - The citationMetadata to render.
       * See {@link CitationModel#defaults}.
       * @since 2.23.0
       */
      addCitationMetadata: function (citationMetadata) {
        const citationMetaEl = this.el.querySelector(
          "." + this.citationMetadataClass
        );
        if (citationMetaEl) {
          // Render a CitationView for each citationMetadata
          citationMetadata.forEach(function (cm, i) {
            const citationView = new CitationView({
              model: cm,
              style: this.style,
              context: "inText",
              createLink: true,
              openLinkInNewTab: true,
            });
            citationMetaEl.appendChild(citationView.render().el);
            // Put a comma after each citationMetadata except the last one
            if (i < citationMetadata.length - 1) {
              citationMetaEl.appendChild(document.createTextNode(", "));
            }
          }, this);
        }
      },

      /**
       * Make the entire citation a link to the view page or the source URL.
       * @since 2.23.0
       */
      addLink: function () {
        const url = this.model.getURL();
        if (!url) return;
        // Remove any existing links, but keep the content of each link in place
        const links = this.el.querySelectorAll("a");
        links.forEach((link) => {
          const content = link.innerHTML;
          link.outerHTML = content;
        });
        const id = this.model.getID();
        const target =
          url.startsWith("http") || this.openLinkInNewTab
            ? ' target="_blank"'
            : "";
        const dataId = id ? ` data-id="${id}" ` : "";
        const content = this.el.innerHTML;
        const aClass = "route-to-metadata";
        this.el.innerHTML = `<a class="${aClass}"${dataId}href="${url}"${target}>${content}</a>`;
      },

      /**
       * Make the title a link to the view page or the source URL.
       * @since 2.23.0
       */
      addTitleLink: function () {
        const url = this.model.getURL();
        if (!url) return;
        const titleEl = this.el.querySelector("." + this.titleClass);
        if (!titleEl) return;
        const target =
          url.startsWith("http") || this.openLinkInNewTab
            ? ' target="_blank"'
            : "";
        titleEl.innerHTML = `<a href="${url}"${target}>${titleEl.outerHTML}</a>`;
      },

      /**
       * Given a list of authors in CSL JSON, merge them all into a single
       * string for display in an APA citation.
       * @param {object[]} authors - An array of CSL JSON name objects
       * @returns {string} The formatted author string or an empty string if
       * there are no authors 
       * @param {number} [maxAuthors=20] - The maximum number of authors to
       * display. If there are more than this number of authors, then the
       * remaining authors will be replaced with an ellipsis. The default is 20
       * since that is the maximum that APA allows. Set to a falsy value to
       * display all authors.
       * @since 2.23.0
       */
      CSLNamesToAPA: function (authors, maxAuthors=20) {
        // Format authors as a proper APA style citation:
        if (!authors) return "";

        // authors = authors.map(this.CSLNameToAPA);
        // Uncomment the line above, and remove the line below, in order to
        // make the author names follow the APA format. For now, we are showing
        // full author names to avoid organization names getting mangled. See
        // https://github.com/NCEAS/metacatui/issues/2106
        authors = authors.map(this.CSLNameToFullNameStr);

        const numAuthors = authors.length;
        const lastAuthor = authors[numAuthors - 1];

        // Set maxAuthors to the number of authors if it is a falsy value.
        maxAuthors = maxAuthors || numAuthors;

        if (numAuthors === 1) return authors[0];
        // Two authors: Separate author names with a comma. Use the ampersand.
        if (numAuthors === 2) return authors.join(", & ");
        // Two to maxAuthors: commas separate author names, while the last
        // author name is preceded again by ampersand.
        if (numAuthors > 2 && numAuthors <= maxAuthors) {
          const authorsGrp1 = authors.slice(0, numAuthors - 1);
          return `${authorsGrp1.join(", ")}, & ${lastAuthor}`;
        }
        // More than maxAuthors: "After the first 19 authors’ names, use an
        // ellipsis in place of the remaining author names. Then, end with the
        // final author's name (do not place an ampersand before it). There
        // should be no more than twenty names in the citation in total."
        if (numAuthors > maxAuthors) {
          const authorsGrp1 = authors.slice(0, maxAuthors);
          return `${authorsGrp1.join(", ")}, ... ${lastAuthor}`;
        }
      },

      /**
       * Given one name object in CSL-JSON format, return the author's name in
       * the format required for a full APA citation: Last name first, followed
       * by author initials with a period after each initial. See
       * {@link EMLParty#toCSLJSON}
       * @param {object} cslJSON - A CSL-JSON name object
       * @since 2.23.0
       */
      CSLNameToAPA: function (cslJSON) {
        const {
          family,
          given,
          literal,
          "non-dropping-particle": nonDropPart,
        } = cslJSON;

        // Literal is the organization or position name
        if (!family && !given) return literal || "";

        let familyName = family;
        // If there is a non-dropping-particle, add it to the family name
        familyName =
          family && nonDropPart ? `${nonDropPart} ${family}` : family;

        // If there is no given name, just return the family name.
        if (!given) return familyName;

        // Handle full given names or just initials with or without periods.
        // Split on spaces and periods, then filter out empty strings.
        const initials = given
          .split(/[\s.]+/)
          .filter((str) => str.length > 0)
          .map((str) => str[0] + ".")
          .join("");

        // If there is no family name, just return the initials
        if (!familyName) return initials;

        // If there is a family name and initials, return the family name first,
        // followed by the initials
        return `${familyName}, ${initials}`;
      },

      /**
       * Given a list of authors in CSL JSON, merge them all into a single
       * string for display in an In-Text APA citation.
       * @param {object[]} authors - An array of CSL JSON name objects
       * @returns {string} The formatted author string or an empty string if
       * there are no authors
       * @since 2.23.0
       */
      CSLNamesToAPAInText: function (authors) {
        if (!authors || !authors.length) return "";
        // In the in-text citation provide the surname of the author. When there
        // are two authors, use the ampersand. When there are three or more
        // authors, list only the first author’s name followed by "et al."
        const nAuthors = authors.length;
        // Convert the authors to a string, either non-drop-particle + family
        // name, or literal
        const authorStr = authors.map((a) => {
          const { family, literal, "non-dropping-particle": ndp } = a;
          let name = family;
          name = family && ndp ? `${ndp} ${family}` : family;
          return name || literal;
        });
        if (nAuthors === 1) return authorStr[0];
        if (nAuthors === 2) return authorStr.join(" & ");
        return `${authorStr[0]} et al.`;
      },

      /**
       * Given a list of authors in CSL JSON, merge them all into a single
       * string where authors full names are separated by commas.
       * @param {object[]} authors - An array of CSL JSON name objects
       * @returns {string} The formatted author string or an empty string if
       * there are no authors
       * @since 2.23.0
       */
      CSLNameToFullNameStr: function (author) {
        if (!author) return "";
        const { given, family, literal, "non-dropping-particle": ndp } = author;
        let name = family;
        name = family && ndp ? `${ndp} ${family}` : family;
        name = name && given ? `${given} ${name}` : name;
        return name || literal;
      },
    }
  );

  return CitationView;
});