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

/* global define */
define([
  "backbone",
  "models/metadata/eml211/EMLMissingValueCode",
  "collections/metadata/eml/EMLMissingValueCodes",
  "views/metadata/EML211MissingValueCodeView",
], function (
  Backbone,
  EMLMissingValueCode,
  EMLMissingValueCodes,
  EML211MissingValueCodeView
) {
  /**
   * @class EMLMissingValueCodesView
   * @classdesc An EMLMissingValueCodesView provides an editing interface for an EML
   * Missing Value Codes collection. For each missing value code, the view
   * provides two inputs, one of the code and one for the code explanation. Each
   * missing value code can be removed from the collection by clicking the
   * "Remove" button next to the code. A new row of inputs will automatically be
   * added to the view when the user starts typing in the last row of inputs.
   * @classcategory Views/Metadata
   * @screenshot views/metadata/EMLMissingValueCodesView.png
   * @extends Backbone.View
   * @since 2.26.0
   */
  var EMLMissingValueCodesView = Backbone.View.extend(
    /** @lends EMLMissingValueCodesView.prototype */ {
      tagName: "div",

      /**
       * The type of View this is
       * @type {string}
       */
      type: "EMLMissingValueCodesView",

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

      /**
       * The classes to add to the HTML elements in this view
       * @type {Object}
       * @property {string} title - The class to add to the title element
       * @property {string} description - The class to add to the description
       * paragraph element
       * @property {string} notification - The class to add to the validation
       * message container element
       * @property {string} rows - The class to add to the container element for
       * the missing value code rows
       */
      classes: {
        title: "",
        description: "subtle",
        notification: "notification",
        rows: "eml-missing-value-rows",
      },

      /**
       * User-facing text strings that will be displayed in this view.
       * @type {Object}
       * @property {string} title - The title text for this view
       * @property {string[]} description - The description text for this view.
       * Each string in the array will be rendered as a separate paragraph.
       */
      text: {
        title: "Missing Value Codes",
        description: [
          `Specify the symbols or codes used to denote missing or
          unavailable data in this attribute. Enter the symbol or number
          representing the missing data along with a brief description
          of why this code is used.`,
          `Examples: "-9999, Sensor down time" or "NA, record not available"`,
        ],
      },

      /**
       * Creates a new EMLMissingValueCodesView
       * @param {Object} options - A literal object with options to pass to the
       * view
       * @param {EMLAttribute} [options.collection] - The EMLMissingValueCodes
       * collection to render in this view
       */
      initialize: function (options) {
        if (!options || typeof options != "object") options = {};
        this.collection = options.collection || new EMLMissingValueCodes();
      },

      /**
       * Renders this view
       * @return {EMLMissingValueCodesView} A reference to this view
       */
      render: function () {
        if (!this.collection) {
          console.warn(
            `The EMLMissingValueCodesView requires a MissingValueCodes collection` +
              ` to render.`
          );
          return;
        }
        this.setListeners();
        this.el.innerHTML = "";
        this.el.setAttribute("data-category", "missingValueCodes");
        this.renderText();
        this.renderRows();

        return this;
      },

      /**
       * Add the title, description, and placeholder for a validation message.
       */
      renderText: function () {
        this.title = document.createElement("h5");
        this.title.innerHTML = this.text.title;
        this.el.appendChild(this.title);

        this.text.description.forEach((descText) => {
          this.description = document.createElement("p");
          this.description.classList.add(this.classes.description);
          this.description.innerHTML = descText;
          this.el.appendChild(this.description);
        });

        this.notification = document.createElement("p");
        this.notification.classList.add(this.classes.notification);
        this.el.appendChild(this.notification);
      },

      /**
       * Renders the rows for each missing value code in the collection, and
       * adds a new row for entry of a new missing value code.
       */
      renderRows: function () {
        // Create the div to hold each row
        this.rows = document.createElement("div");
        this.rows.classList.add(this.classes.rows);
        this.el.appendChild(this.rows);

        this.collection.each((model) => {
          this.addRow(model);
        });
        // For entry of new values
        this.addNewRow();
      },

      /**
       * Add a new, empty Missing Value Code model to the collection. This will
       * trigger the creation of a new row in the view.
       */
      addNewRow: function () {
        this.collection.add(new EMLMissingValueCode());
      },

      /**
       * Set listeners required for this view
       */
      setListeners: function () {
        this.removeListeners();
        // Add a row to the view when a model is added to the collection
        this.listenTo(this.collection, "add", this.addRow);
        // Make sure that removed models are removed from the view
        this.listenTo(this.collection, "remove", this.removeRow);
      },

      /**
       * Remove listeners that were previously set for this view
       */
      removeListeners: function () {
        this.stopListening(this.collection, "add");
        this.stopListening(this.collection, "remove");
      },

      /**
       * Tests is a model should be considered "new" for the purposes of
       * displaying it in the view. A "new" model is used to render a blank row
       * in the view for entry of a new missing value code. We consider it new
       * if it's the last in the collection and both attributes are blank.
       * @param {EMLMissingValueCode} model - The model to test
       * @return {boolean} Whether or not the model is new
       */
      modelIsNew: function (model) {
        if (!model || !model.collection) return false;
        const i = model.collection.indexOf(model);
        const isLast = i === model.collection.length - 1;
        return isLast && model.isEmpty();
      },

      /**
       * Creates a new row view for a missing value code model and inserts it
       * into this view at the end.
       * @param {EMLMissingValueCode} model - The model to create a row for
       * @returns {EML211MissingValueCodeView} The row view that was created
       */
      addRow: function (model) {
        if (!model instanceof EMLMissingValueCode) return;

        // New rows will not have a remove button until the user starts typing
        const isNew = this.modelIsNew(model);

        // Create and render the row view
        const rowView = new EML211MissingValueCodeView({
          model: model,
          isNew: isNew,
        }).render();

        // Add the model ID to the row view so we can match it to the model
        // Used by this.removeRow()
        rowView.el.setAttribute("data-model-id", model.cid);

        // Insert the row into the view
        this.rows.append(rowView.el);

        // If a user types in the last row, add a new row
        if (isNew) {
          this.listenToOnce(rowView, "change:isNew", this.addNewRow);
        }
      },

      /**
       * Removes a row view from this view
       * @param {EMLMissingValueCode} model - The model to remove a row for
       * @returns {EML211MissingValueCodeView} The row view that was removed
       */
      removeRow: function (model) {
        if (!model instanceof EMLMissingValueCode) return;
        const rowView = this.el.querySelector(`[data-model-id="${model.cid}"]`);
        if (rowView) {
          rowView.remove();
          return rowView;
        }
      },
    }
  );

  return EMLMissingValueCodesView;
});