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. The * `default` option is used when no format is specified in the options. * @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", }, }, }, /** * 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. * @since 2.23.0 */ renderAPA: function (options, template) { // Format the authors for display options.origin = this.CSLNamesToAPA(options.originArray); 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 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 = `${content}`; }, /** * 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 = `${titleEl.outerHTML}`; }, /** * 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 * @since 2.23.0 */ CSLNamesToAPA: function (authors) { // 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 maxAuthors = 20; const lastAuthor = authors[numAuthors - 1]; 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; });