Source: src/js/views/metadata/EMLAttributeView.js

/* global define */
define([
  "underscore",
  "jquery",
  "backbone",
  "models/DataONEObject",
  "models/metadata/eml211/EMLAttribute",
  "models/metadata/eml211/EMLMeasurementScale",
  "views/metadata/EMLMeasurementScaleView",
  "views/metadata/EML211MissingValueCodesView",
  "text!templates/metadata/eml-attribute.html",
], function (
  _,
  $,
  Backbone,
  DataONEObject,
  EMLAttribute,
  EMLMeasurementScale,
  EMLMeasurementScaleView,
  EML211MissingValueCodesView,
  EMLAttributeTemplate
) {
  /**
   * @class EMLAttributeView
   * @classdesc An EMLAttributeView displays the info about one attribute in a
   * data object
   * @classcategory Views/Metadata
   * @screenshot views/metadata/EMLAttributeView.png
   * @extends Backbone.View
   */
  var EMLAttributeView = Backbone.View.extend(
    /** @lends EMLAttributeView.prototype */ {
      tagName: "div",

      /**
       * The className to add to the view container
       * @type {string}
       */
      className: "eml-attribute",

      /**
       * The HTML template for an attribute
       * @type {Underscore.template}
       */
      template: _.template(EMLAttributeTemplate),

      /**
       * The events this view will listen to and the associated function to
       * call.
       * @type {Object}
       */
      events: {
        "change .input": "updateModel",
        focusout: "showValidation",
        "keyup .error": "hideValidation",
        "click .radio": "hideValidation",
      },

      /**
       * Creates a new EMLAttributeView
       * @param {Object} options - A literal object with options to pass to the
       * view
       * @param {EMLAttribute} [options.model] - The EMLAttribute model to
       * display. If none is provided, an empty EMLAttribute will be created.
       * @param {boolean} [options.isNew=false] - Set to true if this is a new
       * attribute
       */
      initialize: function (options) {
        if (!options || typeof options != "object") options = {};
        this.isNew =
          options.isNew == true ? true : options.model ? false : true;
        this.model =
          options.model ||
          new EMLAttribute({ xmlID: DataONEObject.generateId() });
      },

      /**
       * Renders this view
       * @return {EMLAttributeView} A reference to this view
       */
      render: function () {
        var templateInfo = {
          title: this.model.get("attributeName")
            ? this.model.get("attributeName")
            : "Add New Attribute",
        };

        _.extend(templateInfo, this.model.toJSON());

        // Render the template
        var viewHTML = this.template(templateInfo);

        // Insert the template HTML
        this.$el.html(viewHTML);

        var measurementScaleModel = this.model.get("measurementScale");

        if (!this.model.get("measurementScale")) {
          // Create a new EMLMeasurementScale model if this is a new attribute
          measurementScaleModel = EMLMeasurementScale.getInstance();
        }

        // Save a reference to this EMLAttribute model
        measurementScaleModel.set("parentModel", this.model);

        // Create an EMLMeasurementScaleView for this attribute's measurement
        // scale
        var measurementScaleView = new EMLMeasurementScaleView({
          model: measurementScaleModel,
          parentView: this,
        });

        // Render the EMLMeasurementScaleView and insert it into this view
        measurementScaleView.render();
        this.$(".measurement-scale-container").append(measurementScaleView.el);
        this.measurementScaleView = measurementScaleView;

        // Create and insert a missing values view
        const MissingValueCodesView = new EML211MissingValueCodesView({
          collection: this.model.get("missingValueCodes"),
        });
        MissingValueCodesView.render();
        this.$(".missing-values-container").append(MissingValueCodesView.el);
        this.MissingValueCodesView = MissingValueCodesView;

        // Mark this view DOM as new if it is a new attribute
        if (this.isNew) {
          this.$el.addClass("new");
        }

        // Save a reference to this model's id in the DOM
        this.$el.attr("data-attribute-id", this.model.cid);
      },

      /**
       * After this view has been rendered, add the MeasurementTypeView and
       * render the MeasurementScaleView
       */
      postRender: function () {
        this.measurementScaleView.postRender();
        this.renderMeasurementTypeView();
      },

      /**
       * Render and insert the MeasurementTypeView for this view.
       *
       * This is separated out into its own method so it can be called from
       * `postRender()` which is called after the user switches to the
       * EntityView tab for this attribute. We do this to avoid loading as many
       * MeasurementTypeViews as there are Attributes which would get us rate
       * limited by BioPortal because every MeasurementTypeView hits BioPortal's
       * API on render.
       */
      renderMeasurementTypeView: function () {
        if (
          !(
            MetacatUI.appModel.get("enableMeasurementTypeView") &&
            MetacatUI.appModel.get("bioportalAPIKey")
          )
        ) {
          return;
        }

        var viewRef = this,
          containerEl = viewRef.$(".measurement-type-container");

        // Only insert a new view if we haven't already
        if (!containerEl.is(":empty")) {
          return;
        }

        // Dynamically require since this view is feature-flagged off by default
        // and requires an API key
        require(["views/metadata/EMLMeasurementTypeView"], function (
          EMLMeasurementTypeView
        ) {
          var view = new EMLMeasurementTypeView({
            model: viewRef.model,
          });

          view.render();
          containerEl.html(view.el);
        });
      },

      /**
       * Updates the model with the new value from the DOM element that was
       * changed.
       * @param {Event} e - The event that was triggered by the user
       */
      updateModel: function (e) {
        if (!e) return;

        var emlModel = this.model.get("parentModel"),
          tries = 0;

        while (emlModel.type !== "EML" && tries < 6) {
          emlModel = emlModel.get("parentModel");
          tries++;
        }

        var newValue = emlModel
            ? emlModel.cleanXMLText($(e.target).val())
            : $(e.target).val(),
          category = $(e.target).attr("data-category"),
          currentValue = this.model.get(category);

        // If the new value is just a string of space characters, then set it to
        // an empty string
        if (typeof newValue == "string" && !newValue.trim().length) {
          newValue = "";
        }

        // If the current value is an array...
        if (Array.isArray(currentValue)) {
          // Get the position of the updated DOM element
          var index = this.$(".input[data-category='" + category + "']").index(
            e.target
          );

          // If there is at least one value already in the array...
          if (currentValue.length > 0) {
            // If the new value is a falsey value, then don't' set it on the
            // model
            if (
              typeof newValue == "undefined" ||
              newValue === false ||
              newValue === null
            ) {
              // Remove one element at this index instead of inserting an empty
              // value
              var newArray = currentValue.splice(index, 1);

              // Set the new array on the model
              this.model.set(category, newArray);
            }
            // Otherwise, insert the value in the array at the calculated index
            else {
              currentValue[index] = newValue;
            }
          }
          // Otherwise if it's an empty array AND there is a value to set...
          else if (
            typeof newValue != "undefined" &&
            newValue !== false &&
            newValue !== null
          ) {
            // Push the new value into this array
            currentValue.push(newValue);
          }

          // Trigger a change on this model attribute
          this.model.trigger("change:" + category);
        }
        // If the value is not an array...
        else {
          // Check that there is an actual value here
          if (
            typeof newValue != "undefined" &&
            newValue !== false &&
            newValue !== null
          ) {
            this.model.set(category, newValue);
          }
        }
      },

      /**
       * Shows validation errors on this view
       */
      showValidation: function () {
        var view = this;

        setTimeout(function () {
          // If the user focused on another element in this view, don't do
          // anything
          if (_.contains($(document.activeElement).parents(), view.el)) return;

          // Reset the error messages and styling
          view.$el.removeClass("error");
          view.$(".error").removeClass("error");
          view.$(".notification").text("");

          if (!view.model.isValid()) {
            var errors = view.model.validationError;

            _.each(
              Object.keys(errors),
              function (attr) {
                view
                  .$(".input[data-category='" + attr + "']")
                  .addClass("error");
                view
                  .$(".radio [data-category='" + attr + "']")
                  .addClass("error");
                view
                  .$("[data-category='" + attr + "'] .notification")
                  .text(errors[attr])
                  .addClass("error");
              },
              view
            );

            view.$el.addClass("error");
          }

          // If the measurement scale model is not valid
          if (
            view.model.get("measurementScale") &&
            !view.model.get("measurementScale").isValid()
          ) {
            view.measurementScaleView.showValidation();
          }
        }, 200);
      },

      /**
       * Hides validation errors on this view
       * @param {Event} e - The event that was triggered by the user
       */
      hideValidation: function (e) {
        var input = $(e.target),
          category = input.attr("data-category");

        input.removeClass("error");

        this.$("[data-category='" + category + "'] .notification")
          .removeClass("error")
          .empty();
      },
    }
  );

  return EMLAttributeView;
});