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

define([
  "underscore",
  "jquery",
  "backbone",
  "models/DataONEObject",
  "models/metadata/eml211/EMLAttribute",
  "models/metadata/eml211/EMLMeasurementScale",
  "views/metadata/EMLMeasurementScaleView",
  "views/metadata/EML211MissingValueCodesView",
  "views/metadata/EMLMeasurementTypeView",
  "text!templates/metadata/eml-attribute.html",
], (
  _,
  $,
  Backbone,
  DataONEObject,
  EMLAttribute,
  EMLMeasurementScale,
  EMLMeasurementScaleView,
  EML211MissingValueCodesView,
  EMLMeasurementTypeView,
  EMLAttributeTemplate,
) => {
  /**
   * @class EMLAttributeView
   * @classdesc An EMLAttributeView displays the info about one attribute in a
   * data object
   * @classcategory Views/Metadata
   * @screenshot views/metadata/EMLAttributeView.png
   * @augments Backbone.View
   */
  const 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] - Set to true if this is a new
       * attribute
       */
      initialize(options = {}) {
        this.isNew = options.isNew === true ? true : !options.model;
        this.model =
          options.model ||
          new EMLAttribute({ xmlID: DataONEObject.generateId() });
      },

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

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

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

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

        let 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
        const 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);

        return this;
      },

      /**
       * After this view has been rendered, add the MeasurementTypeView and
       * render the MeasurementScaleView
       */
      postRender() {
        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() {
        if (
          !(
            MetacatUI.appModel.get("enableMeasurementTypeView") &&
            MetacatUI.appModel.get("bioportalAPIKey")
          )
        ) {
          return;
        }

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

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

        const 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(e) {
        if (!e) return;

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

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

        let newValue = emlModel
          ? emlModel.cleanXMLText($(e.target).val())
          : $(e.target).val();
        const category = $(e.target).attr("data-category");
        const 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)) {
          // Clone the value so that when we set it on the model, it triggers
          // the expected change events
          let newArray = [...currentValue];
          // Get the position of the updated DOM element
          const 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
              newArray = newArray.splice(index, 1);
            }
            // Otherwise, insert the value in the array at the calculated index
            else {
              newArray[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
            newArray.push(newValue);
          }
          this.model.set(category, newArray);
        }
        // If the value is not an array check that there is an actual value here
        else if (
          typeof newValue !== "undefined" &&
          newValue !== false &&
          newValue !== null
        ) {
          this.model.set(category, newValue);
        }

        this.model.set("isNew", false);
        this.isNew = false;
      },

      /**
       * Shows validation errors on this view
       */
      showValidation() {
        const view = this;

        setTimeout(() => {
          // 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()) {
            const errors = view.model.validationError;

            _.each(
              Object.keys(errors),
              (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(e) {
        const input = $(e.target);
        const category = input.attr("data-category");

        input.removeClass("error");

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

      /** Display the view */
      show() {
        this.$el.show();
      },

      /** Hide the view */
      hide() {
        this.$el.hide();
      },
    },
  );

  return EMLAttributeView;
});